Index:
- 功能需求分析 && DB model design
- Architecture design
- Project directory architecture]
- Major module explain
- 工程化
项目Github源码
此文章只讲一些比较重要的内容 具体的实现请查看源码
个人认为这是一个写的很优雅的后端项目
<a id="title01"></a>
<p> </p>
功能需求分析 && DB model design
从上图中 设计了官网的所有功能 有:
- columns: case studies(home也整合在此column)、 careers、 news 这四个 column
- post management
- member management
- User
- API
以及 Database 的 schema
<a id="title02"></a>
<p> </p>
Architecture design
Platform:
项目基于 Node.js platform
Web Framework:
使用 Express
View:
使用 ejs (embeded javascript) 因为页面内容并不复杂 所以使用传统的服务器端渲染后端页面
使用 semantic ui 作为前端UI框架 虽然这框架体积很大 只兼容高级浏览器 但UI模块丰富 适合公司内部系统使用
Storage:
Content Database 由于 content 没有复杂的 relationship 所以使用了便于操作 性能优越的 Mongodb NoSQL
Session Database 使用读写性能很高的 Redis
<a id="title03"></a>
<p> </p>
Project directory architecture
以上是工程目录 接下来依次向下讲解核心文件夹
controllers:
此文件夹创建了许多 Data class 以 Object-Oriented Programming 的形式来操作数据对象 操作 data model
For example:
lib
此文件夹创建了许多 helper functions 方便编写逻辑 middleware/
文件夹中创建了两个用来检测用户权限 与 读取数据的 middleware
models
此文件夹创建了许多 Data models 因为本项目主要使用 Mongodb
所以使用了 Mongoose Database framework
来轻松的操作 mongodb
public
此文件夹作为后端的 Static resources directory 用户上传的文件也会存放在此文件夹下的 uploads/ 文件夹下
routes
此文件夹根据需求 创建了许多 router 文件 我们会主要使用此文件夹下的 routes.js 这个 major router 来组织其他的 router 然后提供 routes.js 给 app.js 来使用
views
此文件夹存放着服务器端的页面 template 文件 用来提供给 ejs 引擎去渲染成HTML页面
common/ 文件夹中放置着页面内公共的部分 以及 head foot 两个 ejs 文件 分别放置着 link 以及 scripts
<a id="title04"></a>
<p> </p>
Major module explain
接下来会讲解一些这个项目中的核心模块的思路
创建文章
创建文章一共有四个栏目 其中 home 跟 case studies 栏目合并成一个栏目
所有文章的基本结构都是相同的 标题 正文 发帖时间.. 不同栏目会有不同栏目特殊的字段 我们会为这些不同种类的文章 创建不同的 controller 使用这些 controller 去控制文章数据
因为文章的基本结构都是相同的 所以我们会创建一个 Entry.js 在 controllers/ 文件夹 来作为所有类型文章的 base class
For example:
<p> </p>
创建文章 的 Router 方面 由于各栏目创建文章的程序不是很复杂 所以我们将 创建文章功能 集合到了一个 submit
route 中 通过统一的 endpoint 来操作数据
我们使用 formidable middleware 来获取用户 form 传输过来的 data 使用这些 data 来创建文章
创建文章 我们不使用 modelHelper module 来获取对应的 data model 因为 在创建文章时会调用 Entry instance 的 save method 在 save method 中 已经包含了对应的 data model 用来存储文章
通过 request.body 中的 entry_type field 在创建 文章 instance 的时候 来选择对应的 entry class
exports.submit = function (app) {
return function (req, res, next) {
// specified upload dir
var uploadDir = app.get('root') + '/uploads';
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);
// handle incoming form data
var form = new formidable.IncomingForm();
form.uploadDir = uploadDir;
form.maxFieldsSize = 10000 * 1024 * 1024;
// parse request body data
form.parse(req, function (err, fields, files) {
if (err) return next(err);
//console.dir(fields);
//console.dir(files);
// rename file
for (var i in files) {
// create union file name
if (i.indexOf('entry_') >= 0 && files[i].size) {
console.log(i);
files[i].name = new Date().getTime() + Math.random().toFixed(5)*100000 + '.' + files[i].type.split('/')[1];
fs.rename(files[i].path, form.uploadDir + '/' + files[i].name);
}
}
var entry = {
type: fields.entry_type,
date: new Date(fields.entry_date),
title: fields.entry_title,
body: fields.entry_body
};
// column match via entry_type field
switch (fields.entry_type) {
case 'case':
entry.homeThumbSrc = '/uploads/' + files.entry_home.name;
entry.homeThumbMobileSrc = '/uploads/' + files.entry_home_mobile.name;
entry.caseStudiesThumbSrc = '/uploads/' + files.entry_case.name;
entry.caseStudiesThumbMobileSrc = '/uploads/' + files.entry_case_mobile.name;
entry.homeBlockColor = fields.entry_color;
entry.homeBtnColor = fields.entry_btn_color;
entry.order = fields.entry_order;
entry.pushHome =false;
entry = new Case(entry);
break;
case 'career':
entry = new Career(entry);
break;
case 'news':
entry = new News(entry);
break;
default:
return next(new Error('Invalid entry type'));
}
entry.save(function (err) {
if (err) return next(err);
console.log('entry saved');
res.status(201);
res.redirect('/entry?state=201');
});
});
}
};
更新文章
更新文章也跟创建文章的组织方式相同 所有栏目的文章更新功能 共用一个 router 通过统一的 endpoint 来操作数据
通过 request param 的 column 值 使用(using modelHelper module)对应的 entry model 来执行更新操作
modelHelper
通过 request path 中的 column 来获取对应的 column data model
在更新文章 删除文章之类的文章操作中 会用到此 helper function
eg: 以下 command 会使用 case 的 data model 来删除指定的 id 56fde6b0bc56ea0057b6707d 此条数据
$ curl http://localhost:4000/entry/56fde6b0bc56ea0057b6707d?column=case
modelHelper module 有 history功能 它会记录你上次使用 modelHepler 的 column
如果你使用 moduleHelper 时 request param 中不包含 column 信息的话 它会使用 history中记录的 column 或者 默认使用 case column 如果 history不存在
exports.delete = function (req, res, next) {
// Is valid _id object format?
var id = req.param('id');
id = id ? id.match(/^[0-9a-fA-F]{24}$/) : '';
if (!id) {
res.status(400);
res.send({message: 'Invalid entry id'});
return;
}
// Using model helper just passing req object to this function
modelHelper(req, function (err, model) {
if (err) return next(err);
Entry.delete(model, id, function (err, entry) {
// remove logic
});
});
};
tagsHelper
标签切换 根据当前的 request path 以及 request body 中的 column 使用不同参数的标签
module.exports = function (path, column, context) {
context.topic = '';
context.tags = [
{ url: '?column=case', text: 'Case'},
{ url: '?column=career', text: 'Career'},
{ url: '?column=news', text: 'News'}
];
// fill the url via path
context.tags.map(function (tag) {
return tag.url = path + tag.url;
});
// get context's topic
context.tags.forEach(function (tag, index, arr) {
if (tag.text.toLowerCase() == column) {
return context.topic = arr.splice(index, 1)[0];
}
});
return context;
};
usage:
exports.form = function (app) {
return function (req, res, next) {
var column = req.param('column');
var context = {};
// init page tags
context = tagsHelper(req.path, column, context);
req.session.entryColumnHistory = column;
res.status(200);
res.render('page', context);
}
};
<a id="title05"></a>
<p> </p>
工程化
Node.js 提供了基于 environment variable 来运行的系统 所以我们通过设置 NODE_ENV 这个 environment variable 来运行不同环境下的 app.js 程序
$ NODE_ENV=development node app.js
$ NODE_ENV=production node app.js
在 app.js
中 通过启动时设置的 NODE_ENV environment variable 来执行不同的 运行配置
if (environment == 'production') {
app.set('path', 'admin.sisobrand.com:4000');
}
if (environment == 'development') {
app.set('path', 'localhost:4000');
}
服务器端页面API在不同环境下的使用
通过给 页面公共部分 添加一个 input:hidden 标签 来保存当前的 API path
eg: ejs模板引擎来实现上述功能
<!-- global variables -->
<% if (globalVariables) { %>
<input type="hidden" name="gv_path" value="<%=globalVariables.path%>"/>
<%}%>
<!-- end global variables -->