上个月我们神马这边第一次尝试了用Node.js写项目: 动效平台 ,前端工程师负责从前到后的所有流程。当然,这是一个内部项目,目前还是积累Node.js方面的知识,为以后迎接更大的项目做准备。
我们做的东西是类似于 codepen
或 jsbin
这样的东西。
做改内部系统的目的主要基于以下考虑:
为上面要重复造轮子呢?我认为有以下两点:
每一个组件所属分类,而分类又所属在产品线(神马、书旗)下。每一个组件也有其对应的历史版本。
项目架构是由明智完成,工程化这边使用了 fis
来编译js和css。使用 bower
管理前端类库。web框架基于 express
,使用 log4js
来管理输出的日志。模板使用 ejs
,因为我们前端模板使用 utc
模板,所以减少了学习成本。使用 Promise
来处理异步回调。
整体上,项目使用MVC架构,分为以下几层:
orm2
除了以上几个层外,还有工具库,里面包含了文件的操作,相对路径的获得、SASS的编译、XSS的过滤等。
项目前端使用Angular.js来组织代码。前端支持使用scss来书写样式,通过ajax来编译。下面介绍以下后端各层的核心代码和层与层之间的交互。
路由层主要是处理不同的路由,我们路由层依赖控制层的代码。同事也放了一些自定义的中间件。
module.exports = function(app) { // 静态资源目录 app.use('/favicon.ico', express.static(path.join(path.dirname(require.main.filename), '/public/favicon.ico'))); app.use('/public/upload', function(req, res, next) { res.setHeader('Content-Type', 'application/octet-stream'); next(); }); app.use('/public', express.static(path.join(path.dirname(require.main.filename), '/public'))); // 首页 app.get('/', PageController.redirectWelcome, ComponentController.renderIndexPage); // 编辑组件页面 app.get('/component/edit/:componentID', PageController.redirectWelcome, ComponentController.renderEditPage); //...
我们将文件上传到静态目录 public
下的 upload
里,所以响应的时候得价格头部内容:以文件的方式下载而非在浏览器直接打开。
一些依赖产品线路由我们加了个中间件验证,若无产品线则跳到欢迎页让其选择产品线。
控制层使我们核心业务处理,根据不同的情况来调用不同的数据处理层,比如编辑一个组件,如果没有修改组件,那么不新增组件历史版本,若修改则新增,等等。
下面试新增组件方法:
//创建组件、组件项 function createComponent(data, files) { //组件 var component = new Component(data.name, data.categoryID, 'userid', data.remarks, data.productLineID); //用户ID后期通过session给值 //历史版本 var componentHistory = new ComponentHistory(component.componentID, data.html, data.js, data.css, 'userid', data.updateContent); //用户ID后期通过session给值 //首先保存到数据然,然后再保存到文件中 return Promise.all([ ComponentDAL.createComponent(component), ComponentHistoryDAL.createComponentHistory(componentHistory), saveFile({ files : files, componentID : component.componentID }) ]); }
所有异步操作完成后,我们会将数据传给view层来渲染,这样就完成了
V→C→M→C→V
这样一个MVC处理思想。下面数渲染的逻辑
create: function(req, res) { var data = req.body, files = req.files; data.productLineID = req.cookies.productLineID; //当组件存储完成、文件上传完成,才响应 createComponent(data, files).then(function(result) { //渲染页面 res.redirect('/component/edit/' + result[2]); }).catch(function(e) { console.error(e); res.redirect('error'); }); },
我们使用ORM数据库来建表,但是我感觉如果想在数据库大量优化则第一次使用ORM来建表,优化那些不必在是ORM负责。
var ComponentTable = db.define('component', Component.getType(),{ cache : false });
这边遇到了一个坑,他默认有对缓冲,这样如果是调用他的get方法的话,修改数据库并不能实时的展示,必须重启Node应用才可以,直接执行SQL无影响。索性将其cache关闭。
框架支持自动建表,剩下了不少麻烦
//同步表 ComponentTable.sync();
当然这个方法是异步方法,但不可能项目刚启动就有对数据库的操作,搜易偷了个懒这样写。其实是有问题的,但不知如何解。
操作数据库的方法我就不一一写出了,我写了个 orm的Demo 便于参考,官网上也很详细。
对于复杂sql我们只能手写。以下是一个比较复杂的SQL:
SELECT componentHistory.componentHistoryID, componentHistory.html, componentHistory.js, componentHistory.css, component.componentID, component.categoryID, component.name, component.remarks FROM (SELECT componentHistoryID, componentID, html, js, css FROM componentHistory WHERE componentHistory.componentHistoryID = ?) componentHistory inner join component ON componentHistory.componentID = component.componentID AND component.status=1
框架也是支持直接调用,但这样使用Mysql驱动来写,也是不能跨数据库,这也是一个弱点。
db.driver.execQuery(getComponentHistoryByComponentHistoryIDSQL, [componentHistoryID], function(err, data) { if(err) { console.error(err); reject(err); }else { resolve(data); } });
查询出得数据库是一维数据,还需要我们组装才能给前端,我们减少了一层业务逻辑层,组装还是写在了数据处理层中。
下面有一个例子:
function formatCategories(arr) { var resultObj = {}; for(var i = 0; i < arr.length; i++) { if(arr[i]['categoryID'] in resultObj) { resultObj[arr[i]['categoryID']]['example'].push({ 'componentID' : arr[i]['componentID'], 'componentName' : arr[i]['componentName'] }); }else { resultObj[arr[i]['categoryID']] = { 'id' : arr[i]['categoryID'], 'category' : arr[i]['categoryName'], 'example' : [ { 'componentID' : arr[i]['componentID'], 'componentName' : arr[i]['componentName'] }] }; } } var result = []; for(var key in resultObj) { result.push(resultObj[key]); } return result; }
模型层为类的定义,因为orm建表需要知道每一个类的类型,所以还需为其提供:
var uuid = require('node-uuid'), ModelBase = require('./ModelBase'), util = require('util'); var Component = function(name, categoryID, userID, remarks, productLineID) { var now = new Date(); this.componentID = uuid.v4(); this.categoryID = categoryID; this.name = name; //组件名称 this.userID = userID; //创建人ID this.remarks = remarks || ''; this.createTime = now; this.modifyTime = now; this.status = 1; this.productLineID = productLineID; ModelBase.call(this); }; Component.getType = function() { return { componentID : String, categoryID : String, productLineID : String, name : String, userID : String, createTime : { type : 'date', time : true }, remarks : String, status : Number } }; //继承原型方法 util.inherits(Component, ModelBase); module.exports = Component;
当然,框架支持建立索引、默认值等:
var Person = db.define('person', { id: {type: 'serial', key: true}, id_num: {type: 'text', unique: true, required: true}, name: {type: 'text'}, sex: {type: 'enum', values: ['m', 'f'], defaultValue: 'm'}, age: {type: 'number', defaultValue: 1} });
这个项目马上就要完成了,我觉得我们接下来要准备的又以下几点:
这是我们前端第一次负责整个项目,从前到后,使用Node.js来编写服务的代码,所以其中还是有几块不足的地方:
项目还算比较顺利的到了尾声,想起我们一起去阿里拌面吃过的拉条子和撸过的串,还是比较怀念的。这只是我们的第一步,虽然目前Topic只有我一个人在杭州,沟通起来是有些成本,但是还是希望能和大家一起进步!朝着全栈的路上,猛进!