Contents
  1. 1. 项目介绍
  2. 2. 后端环境搭建
    1. 2.1. 安装express
    2. 2.2. 启动mongoDB
    3. 2.3. 安装express中间件
    4. 2.4. app.js
    5. 2.5. 路由模块
    6. 2.6. 数据库设计
    7. 2.7. 添加路由
    8. 2.8. 测试
  3. 3. 前端环境搭建
    1. 3.1. vue-cli快速搭建应用
    2. 3.2. 插件安装
    3. 3.3. vue-resource基本使用
    4. 3.4. vue-router基本使用
  4. 4. 一些难点
    1. 4.1. 解决跨域
    2. 4.2. vue-resource发送带凭证的请求
    3. 4.3. vue-router: Cannot find element: #app
    4. 4.4. mongoose不能重复编译
    5. 4.5. 关于跨域的一点思考
    6. 4.6. 更改vue项目的build目录
  5. 5. 推荐工具

断断续续折腾了一周多的时间,终于实现了一个前后端分离开发的小项目(没有做UI,所以有点丑),代码可以点击这里访问到。

这篇文章就回顾一下整个项目的架构流程以及遇到的技术难点。

项目介绍

这是一个简单的购书平台,主要实现了以下功能:


  1. 注册
  2. 登陆/登出
  3. 添加书籍
  4. 查看书籍
  5. 添加购物车并且结算

后端基于node.js,使用了express框架,以及mongodb、mongoose、express-session等功能模块。

前端基于vue.js,使用了vue-resource、vue-router这两个插件。

为了模拟前后端分离开发的环境,后端运行在8888端口,而前端运行在8080端口。

前后端主要通过交换json数据来实现相关功能,总结来说,后端只负责提供数据(数据库读写),前端负责呈现数据

后端环境搭建

安装express

1
2
3
4
$ mkdir myapp
$ cd myapp
$ npm init
$ npm install express --save

它会为我们将根目录初始化以下:

1
2
3
4
5
common // 文件夹,存放可复用的工具模块
public // 文件夹,存放前端build的静态资源
routes // 文件夹,存放路由模块
app.js // 项目入口文件
package.json // 项目配置

启动mongoDB

mongoDB的安装可以参考其官方文档,对于不同的系统环境,有不同的安装方法:

我的系统为Ubuntu 16.04,其安装方法可以看这里

安装完成之后,可以通过以下命令来启动mongoDB服务器:

1
$ sudo mongod

当然,mongodb自带一个JavaScript shell,常用的命令有以下几个:

1
2
3
4
5
6
$ mongo // 进入shell
$ show dbs // 查看当前有有哪些数据库,默认只有一个test
$ db // db指向当前数据库
$ use <dbname> // 使db变量指向某个数据库
$ show collections // 查看当前数据库中的集合
$ db.test.users.find().pretty() // 查询test数据库中users集合中的所有文档,并且以较为规整的格式输出

关于mongoDB更多的操作(CRUD),可以参考其官方教程

安装express中间件

express内置的中间件只有一个,那就是express.static(),它负责在express应用中托管静态资源。

现在我们在项目中安装我们需要的几个第三方中间件:

1
2
3
4
$ npm install mongoose --save // 一个更便捷的操作mongoDB数据的对象模型工具
$ npm install morgan --save // 在控制台打印HTTP请求的记录,用于调试
$ npm install body-parser --save // 解析http请求体,并且覆盖到原来的req.body属性
$ npm install express-session --save // 创建会话,并将会话数据挂载到req.session,保存在服务端(除了connect.id)

以上几个中间件,都可以在这里找到,详细配置可以看其文档。

app.js

安装完中间件之后,我们就开始写我们的项目入口文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 引入第三方中间件
var express = require('express');
var mongoose = require('mongoose');
var morgan = require('morgan');
var bodyParser = require('body-parser');
var session = require('express-session')
// node内置模块
var path = require('path');
// 自定义模块
global.dbHelper = require('./common/dbHelper'); // 用来操作数据库中的集合
var routes = require('./routes/api') // 用来操作路由
var app = express();
// 调用mongoose,连接数据库test3
mongoose.connect('mongodb://localhost/test3')
// 调用morgan中间件,在控制台打印http请求记录
app.use(morgan('dev'));
// 调用body-parser中间件,将传来的json数据解析为对象,并覆盖到req.body
app.use(bodyParser.json());
// 调用express-session中间件,connect.id过期时间设置为10分钟
app.use(session({
secret: 'fuckyou',
resave: false,
saveUninitialized: true,
cookie: {
maxAge: 1000*60*10
}
}));
// 调用express.static中间件,指定静态资源目录,默认为pubic下的index.html
app.use(express.static(path.join(__dirname, 'public')));
// 调用路由模块,处理不同请求,并且将app作为参数传入
routes(app);
// 监听8888端口
app.listen(8888);

路由模块

我们将路由模块都放置在routes文件夹下,并且由api.js统一管理,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = function (app) {
// 因为前后端分离开发,需要处理跨域,所以对所有请求均设置响应头
app.use(function (req, res, next) {
res.setHeader('Access-Control-Allow-Origin', 'http://192.168.0.101:8080');
res.setHeader('Access-Control-Allow-Headers', 'content-type');
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
// 如果是预请求,就直接终结请求-响应循环
app.options('*', function (req, res, next) {
res.end();
})
// 输出的api有以下5个,为了方便统一管理,我们都统一在这里
require('./register')(app); // 处理注册页(/register)发来的请求
require('./login')(app); // 处理登录页(/login)发来的请求
require('./bookslist')(app); // 处理图书列表页(/home)发来的请求
require('./cart')(app); // 处理购物车页(/cart)发来的请求
require('./logout')(app); // 处理登出(/logout)请求
};

数据库设计

数据库我们选用了MongoDB,它是一个开源的NoSQL数据库,相比于MySQL那样的关系型数据库,它更加适合在数据规模很大、事务性不强的场合下使用。同时,它也是一个对象数据库,没有表、行等概念,只有集合和文档的概念,数据格式为JSON

mongoose是一个针对MongoDB操作的对象模型库,它封装了MongoDB对文档的一些增删改查等常用方法。可以点击这里查看基础示例。

我们将对数据库的Schema定义在common/models.js文件中,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义并导出数据库中几个集合的模型(Schema)
// 注意:Schema对应的数据集合会默认加上s,即books,users,carts
module.exports = {
book: {
name: String,
description: String,
price: Number
},
user: {
username: String,
password: String
},
cart: {
username: String,
bookname: String,
number: Number,
price: Number,
status: Boolean
}
};

并且将操作Schma的公共方法封装在了common/dbHelper.js文件中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var models = require('./models');
// 将models中定义的所有模式(Schema)编译为模型(Model)
for (var m in models) {
var typeSchema = new Schema(models[m]);
mongoose.model(m, typeSchema);
}
// 返回编译后的对应type的模型(该模型可直接对数据库增删改查)
var _getModel = function (type) {
return mongoose.model(type);
};
// 将getModel方法暴露出去
module.exports = {
getModel: function (type) {
return _getModel(type);
}
};

添加路由

我们拿register.js来举例,它用来专门处理前端注册(register)页面发起的post请求,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module.exports = function (app) {
// 获取User的Model,用于对集合users的增删改查
var User = global.dbHelper.getModel('user');
app.route('/register')
.post(function (req, res) {
var postData = req.body;
// 先从集合users中检索是否存在该用户名
User.findOne({username: postData.username}, function (err, doc) {
if (doc) {
// 如果用户名已经存在
res.status(500).send('用户名已经存在');
}else {
// 如果用户名不存在,那么就该用户数据保存至数据库
User.create({
username: postData.username,
password: postData.password
}, function (err, doc) {
if (err) {
res.status(500).send('服务端保存数据出错');
} else {
res.status(200).send('注册成功');
}
});
}
});
});
};

测试

写完路径/register对应的路由模块后,我们通过firefox的一款插件HttpRequester来模拟一下前端的post请求,模拟结果如下:

然后,我们再进入mongdb shell,查看是否将user信息存入数据库:

数据保存成功!其他路由模块也是一样的实现原理,这里就不一一写出。

至此,我们已经可以正确地响应前端的各种http请求,后端的开发流程大致就是这样,它主要负责提供api,向前端发送数据。

前端环境搭建

前端的搭建基于vue.js,是一个小型的单页应用,并且使用了vue-resourcevue-router这两个插件。

vue-cli快速搭建应用

vue-cli是vue官方提供的一个脚手架工具,可以快速地帮我们构建项目,并且具有单文件Vue组件、热加载、保存时检查代码,单元测试等功能。

其使用方式如下:

1
2
3
4
5
$ npm install -g vue-cli // 全局安装vue-cli
$ vue init webpack myProject // 创建一个基于webpack模板的新项目myProject
$ cd myProject
$ npm install // 安装依赖
$ npm run dev // 启动项目,可在本地8080端口访问

我们可以点击这里来查看项目的架构。

官方的npm可以运行比较慢,建议换成淘宝的npm镜像,可以提高模块下载速度。以后就可以用cnpm来替代npm了(除了publish命令)。

插件安装

vue-resource可以通过XMLHttpRequest或者JSONP发起请求并且处理响应。
vue-router是vue.js的官方路由,可以将路由映射到各个组件。

现在,我们可以在项目中安装并使用它们:

1
2
$ cnpm install vue-resource --save // cnpm是淘宝npm镜像
$ cnpm install vue-router --save

然后我们在项目入口文件中使用他们:

1
2
3
4
5
6
// 导入插件
import VueResource from 'vue-resource'
import VueRouter from 'vue-router'
// 全局注入
Vue.use('VueResource')
Vue.use('VueRouter')

vue-resource基本使用

现在我们将实现Register.vue组件,其功能就是让用户输入用户名和密码,然后通过发送POST请求来实现注册功能。

假设我们通过vue的双向绑定,获取了用户注册信息user,那么我们可以通过vue-resource提交数据,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 导入要提交的地址registerApi,这里为http://192.168.0.101:8888/register
import {registerApi} from '../api'
// 要提交的数据
var postData = {username: this.user.username, password: this.user.password}
// 方法一:使用当前实例的$http来提交数据,它返回一个Promise对象
this.$http.post(registerApi, postData).then(successCallBack, errorCallBack)
// 方法二:使用当前实例的$resource来提交数据,同样返回一个Promise对象
var resource = this.$resource(registerApi)
resource.save(registerApi, postData).then(successCallBack, errorCallBack)

更加详细的CURD操作,可以看这里

vue-router基本使用

vue-router的主要作用就是将路由映射到各个组件

现在假设我们有两个组件:Register.vue,Login.vue,那么我们可以这样使用vue-router。

main.js中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建一个路由器实例,可以传入配置参数
var router = new VueRouter()
// 定义路由规则,每条路由规则映射到一个组件
router.map({
'/register': {
component: Register
},
'/login': {
component: Login
}
})
// 启动应用
// 路由器会创建一个App实例,并且挂载到选择符#app的元素上
router.start(App, '#app')

完成以上工作后,我们就可以在App.vue组件上使用vue-router来进行导航:

1
2
3
4
5
6
7
8
9
<div id="app">
<!-- 使用指令 v-link 进行导航 -->
<ul>
<li v-link = "{path: '/register'}">注册</li>
<li v-link = "{path: '/login'}">登陆</li>
</ul>
<!-- 路由外链 -->
<router-view></router-view>
</div>

关于vue-router更详细的使用,可以点击这里

一些难点

在写这个小项目过程中踩过一些,有的真的是很基础的错误,但是我有时不得不花费大量的时间来发现并改正,所以我将它们总结在这里。

解决跨域

完成上面的任务后,我们还不能实现前后端的分离开发,因为还存在跨域问题。

使用CORS解决跨域问题在我之前的博客中总结过,可以在这里查看。

这里我们也是通过CORS来解决的跨域,代码之前已经呈现,这里再写出核心代码:

1
2
3
4
5
6
7
8
9
10
app.use(function (req, res, next) {
// 之前设置为了星号,但是因为要发送带凭证的请求,必须要指定域名,详见下一个小结
res.setHeader('Access-Control-Allow-Origin', 'http://192.168.0.101:8080');
res.setHeader('Access-Control-Allow-Headers', 'content-type');
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
app.options('*', function (req, res, next) {
res.end();
});

vue-resource发送带凭证的请求

后台使用了express-session中间件来管理会话,它的工作原理如下:


当客户通过浏览器(前端)第一次请求服务器(后端)时,服务器会在http的响应头中,通过set-cookie字段来给浏览器端设置一个默认叫做connect.sid的cookie,用作该用户的身份标识

此后,浏览器每次对服务器端发起http请求时,都会自动带上这个cookie。服务端在接收到这个connect.sid字段后,会判断其是否过期(maxAge),以及是否正确,并且对客户端做出不同的响应。


在这个项目中,当某个客户发来请求时,浏览器端需要根据请求头中是否有connect.sid来判定用户登陆与否,所以,我们的请求必须要带上cookie。即要发送带凭证的请求

而在vue-resource中,解决方式如下:

1
2
3
// main.js
// 发送带凭证的请求,因为每次请求都要带上cookie中的connect.id
Vue.http.options.credentials = true

vue-router: Cannot find element: #app

在使用vue-router时,遇到这个错误提示,其由main.js中的以下代码引起:

1
router.start(App, '#app')

以上代码可以理解为: 将App组件,挂载到#app元素上

而错误很明确,即找不到一个id为#app的元素

所以,我们需要对vue-cli给我们自动生成的index.html文件作出修改:<app></app>改为<div id="app"></app>

mongoose不能重复编译

使用mongoose编写dbHelper.js时,出现了错误:Cannot overwrite ‘users’ model once compiled.

谷歌一番之后,发现原因:重复对某个一模式(Schma)进行了编译

这里有一个解决方式,做了很详细的说明。

关于跨域的一点思考

在这个demo的开发过程中成功实现了简单的前后端分离,那是因为前后端都运行在本地,只有有端口号不同。这导致后端的8888端口是可以通过响应头中的set-cookie字段来向前端的8080端口写入connect.sid这个cookie的

但是如果前后端运行在完全不同的两个域,比如http://www.abc.comhttp://www.123.com,那这样的情况下,跨域就有些复杂了。

更改vue项目的build目录

由于前后端在同一个主机,所以在没有解决跨域之前,我是通过将前端build目录配置为后端的public文件夹下的,这样,我每次修改完前端代码之后,直接npm run build,就会直接build到后端,这样,直接直接在后端运行项目了。

那么如何更改bulid的出口呢?

打开config/index.js,修改如下两行代码:

1
2
index: path.resolve(__dirname, '../../myapp/public/index.html'),
assetsRoot: path.resolve(__dirname, '../../myapp/public'),

当然,这样的效率是不高的,如果能解决跨域,不建议这么做。

推荐工具

  • HttpRequester —— 火狐浏览器的一个插件,可以方便地发起各种http请求
  • showDoc —— 一个在线生成api文档的工具
  • rap —— 阿里在用的一个可视化接口管理工具
  • Apizza —— 极客专属的api管理工具

以上不免有纰漏之处,仅作参考。

Contents
  1. 1. 项目介绍
  2. 2. 后端环境搭建
    1. 2.1. 安装express
    2. 2.2. 启动mongoDB
    3. 2.3. 安装express中间件
    4. 2.4. app.js
    5. 2.5. 路由模块
    6. 2.6. 数据库设计
    7. 2.7. 添加路由
    8. 2.8. 测试
  3. 3. 前端环境搭建
    1. 3.1. vue-cli快速搭建应用
    2. 3.2. 插件安装
    3. 3.3. vue-resource基本使用
    4. 3.4. vue-router基本使用
  4. 4. 一些难点
    1. 4.1. 解决跨域
    2. 4.2. vue-resource发送带凭证的请求
    3. 4.3. vue-router: Cannot find element: #app
    4. 4.4. mongoose不能重复编译
    5. 4.5. 关于跨域的一点思考
    6. 4.6. 更改vue项目的build目录
  5. 5. 推荐工具