断断续续折腾了一周多的时间,终于实现了一个前后端分离开发的小项目(没有做UI,所以有点丑),代码可以点击这里访问到。
这篇文章就回顾一下整个项目的架构流程以及遇到的技术难点。
项目介绍
这是一个简单的购书平台,主要实现了以下功能:
- 注册
- 登陆/登出
- 添加书籍
- 查看书籍
- 添加购物车并且结算
后端基于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 routes app.js package.json
|
启动mongoDB
mongoDB的安装可以参考其官方文档,对于不同的系统环境,有不同的安装方法:
我的系统为Ubuntu 16.04,其安装方法可以看这里。
安装完成之后,可以通过以下命令来启动mongoDB服务器:
当然,mongodb自带一个JavaScript shell,常用的命令有以下几个:
1 2 3 4 5 6
| $ mongo $ show dbs $ db $ use <dbname> $ show collections $ db.test.users.find().pretty()
|
关于mongoDB更多的操作(CRUD),可以参考其官方教程
安装express中间件
express内置的中间件只有一个,那就是express.static()
,它负责在express应用中托管静态资源。
现在我们在项目中安装我们需要的几个第三方中间件:
1 2 3 4
| $ npm install mongoose --save $ npm install morgan --save $ npm install body-parser --save $ npm install express-session --save
|
以上几个中间件,都可以在这里找到,详细配置可以看其文档。
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') var path = require('path'); global.dbHelper = require('./common/dbHelper'); var routes = require('./routes/api') var app = express(); mongoose.connect('mongodb://localhost/test3') app.use(morgan('dev')); app.use(bodyParser.json()); app.use(session({ secret: 'fuckyou', resave: false, saveUninitialized: true, cookie: { maxAge: 1000*60*10 } })); app.use(express.static(path.join(__dirname, 'public'))); routes(app); 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(); }) require('./register')(app); require('./login')(app); require('./bookslist')(app); require('./cart')(app); require('./logout')(app); };
|
数据库设计
数据库我们选用了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
| 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'); for (var m in models) { var typeSchema = new Schema(models[m]); mongoose.model(m, typeSchema); } var _getModel = function (type) { return mongoose.model(type); }; 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) { var User = global.dbHelper.getModel('user'); app.route('/register') .post(function (req, res) { var postData = req.body; 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-resource和vue-router这两个插件。
vue-cli快速搭建应用
vue-cli是vue官方提供的一个脚手架工具,可以快速地帮我们构建项目,并且具有单文件Vue组件、热加载、保存时检查代码,单元测试等功能。
其使用方式如下:
1 2 3 4 5
| $ npm install -g vue-cli $ vue init webpack myProject $ cd myProject $ npm install $ npm run dev
|
我们可以点击这里来查看项目的架构。
官方的npm可以运行比较慢,建议换成淘宝的npm镜像,可以提高模块下载速度。以后就可以用cnpm
来替代npm
了(除了publish命令)。
插件安装
vue-resource可以通过XMLHttpRequest或者JSONP发起请求并且处理响应。
vue-router是vue.js的官方路由,可以将路由映射到各个组件。
现在,我们可以在项目中安装并使用它们:
1 2
| $ cnpm install vue-resource --save $ 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
| import {registerApi} from '../api' var postData = {username: this.user.username, password: this.user.password} this.$http.post(registerApi, postData).then(successCallBack, errorCallBack) 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 } }) 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
| 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.com
和http://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管理工具
以上不免有纰漏之处,仅作参考。