nodeclub源码学习笔记

正式工作以后才知道,原来系统的去学习一门语言是多么的奢侈。本篇文章通过学习nodeclub项目的源码来学习nodejs的express框架开发。此篇文章建议在了解nodejs基本语法后再来观看,我反正是在菜鸟教程过了一遍nodejs有关的知识再来学习这个项目的。

1.nodeclub介绍

Nodeclub 是使用 Node.js + Express 框架和 MongoDB 开发的一个社区系统,是 cnodejs.org源码,算是一个基本的博客系统,包含文章发布,关注,评论等功能。这些功能可以说是任何一个网站的基础。那么我们从 nodeclub 里可以学到什么?

  1. 基本的架构
  2. 开发测试过程
  3. MVC 的设计
  4. middleware 的正确用法
  5. 如何设计 Mongodb schema
  6. 如何正确的使用 Mongoose
  7. 如何实现一个标签系统
  8. plugins? services ?
  9. 如何正确的使用 EJS helper
  10. 到底该怎样写路由, restful?
  11. 如何做基本的控制验证
  12. 如何发邮件
  13. session
  14. GitHub 用户登录
  15. 图片上传
  16. 消息发送

2.nodeclub中用到了哪些开源技术

下载nodeclub源码,打开package.json文件,找到里面的dependencies&devDependencies,可以看到这个项目所有的依赖。下面我将对这些模块一一做简单说明。

2.1dependencies

  • async: async模块是为了解决嵌套金字塔,和异步流程控制而生。参考:传送门
  • bcryptjs: 该模块用于开发登录注册模块时对密码进行加密。资料参考:传送门
  • body-parser: body-parser是一个HTTP请求体解析中间件,使用这个模块可以解析JSON、Raw、文本、URL-encoded格式的请求体,Express框架中就是使用这个模块做为请求体解析中间件。传送门
  • bytes:
  • colors: 通过这个模块我们输出各种带颜色、方面区分或者更酷的日志以及 CLI 工具提示。传送门
  • compression:该模块用于压缩gzip文件,我们在Linux中经常会用到后缀为.gz的文件,它们就是GZIP格式的。 传送门
  • connect-busboy:文件上传模块,用来解析post请求,实现文件的上传。 传送门
  • connect-redis: 该模块用来连接redis,往往结合redis模块和express-session模块来实现session持久化解决方案。传送门
  • cookie-parser: 解析Cookie的工具。通过req.cookies可以取到传过来的cookie,并把它们转成对象。传送门
  • cors:跨域模块,只有 Web 才有跨域 CORS,移动端 iOS 与 Android 就没有,谁让 Web 能看源代码呢,如果想让XmlHttpRequest 按照自己意愿(域名、协议、端口)请求数据,那就需要跨域。传送门
  • csurf: csrf,跨站请求伪造,令牌模块,防止csurf攻击,该模块依赖express-session包。传送门
  • data2xml: 传送门
  • ejs-mate:ejs-mate 属于NodeJs Express 的一个母版页模块,可以方便的把页面公共部分放入ejs-mate设定的母版页内,实现代码公用。 传送门
  • eventproxy:一个控制并发的模块。 传送门
  • express:nodejs用于构建web服务的框架。
  • express-session: 用于实现session持久化的模块。传送门
  • helmet:给你的express应用加上帽子,即保护你的应用。 传送门
  • ioredis:
  • jpush-sdk:
  • loader-builder:
  • loader: Loader 可以理解为是模块和资源的转换器,它本身是一个函数,接受源文件作为参数,返回转换的结果。这样,我们就可以通过 require 来加载任何类型的模块或文件,比如 CoffeeScript、 JSX、 LESS 或图片。传送门
  • lodash:lodash是一个提供模块化、高性能和额外功能的实用工具库。 传送门
  • log4js: 日志管理模块。传送门
  • markdown-it:能够快速将.md文件转换为html文件的一个模块。传送门
  • memory-cache:缓存管理模块。 传送门
  • method-override:改模块主要用于form表单的处理,可以将表单的GET或者POST方法转化为PUT或者DELETE方法。传送门
  • moment:格式化时间模块,moment提供了一个函数,可用于将JavaScript的date对象包装到moment对象中。 传送门
  • mongoose: Mongoose是MongoDB的一个对象模型工具,是基于node-mongodb-native开发的MongoDB nodejs驱动,可以在异步的环境下执行。同时它也是针对MongoDB操作的一个对象模型库,封装了MongoDB对文档的的一些增删改查等常用方法,让NodeJS操作Mongodb数据库变得更加灵活简单。因为封装了对MongoDB对文档操作的常用处理方法,可以高效的操作mongodb,同时可以理解mongoose是一个简易版的orm ,提供了类似schema定义,hook、plugin、virtual、populate等机制,让NodeJS操作Mongodb数据库变得特别简单。传送门
  • multiline: 该模块可以帮助我们在js中实现多行文本。传送门
  • node-uuid:使用该模块可以生成唯一标识符。 传送门
  • nodemailer:该模块用来发送邮件。传送门
  • nodemailer-smtp-transport:与nodemailer类似,在之前的开发中往往和nodemailer配合,但现在凭借nodemailer一个包就可以实现邮件的发送了。
  • oneapm: oneAPM是一个平台,可以用来对nodejs的项目做测试和监控。 传送门
  • passport:该模块可用来做后台用户验证。传送门
  • passport-github:该模块用来做github登陆的鉴权。 传送门
  • pm2:网站发布工具模块,pm2 是一个带有负载均衡功能的Node应用的进程管理器。PM2 为 Node.js 的应用提供负载管理,保持应用程序永远在线,重新启动而无需停止服务,并提供应用的管理服务。当你要把你的独立代码利用全部的服务器上的所有CPU,并保证进程永远都活着,0秒的重载, PM2是完美的。 传送门
  • qn: 这个应该是和七牛云的相关衔接模块。
  • ready:
  • request:
  • response-time:
  • superagent:http模块superagent,它是一个强大并且可读性很好的轻量级ajaxAPI,是一个关于HTTP方面的一个库,而且它可以将链式写法玩的出神入化。 传送门
  • utility:该模块用来实现字符串加密,utility有两个很重要的方法,一个是sha1,一个是md5,通常使用他们对字符串进行加密处理。 传送门
  • validator: 顾名思义,用来验证字符串合法性的npm模块。传送门
  • xmlbuilder:用来创建xml文件的一个模块。 传送门
  • xss:

2.2devDependencies

  • 测试框架:mocha, should
  • 运行: forever
  • 请求模拟: supertest

3.目录结构

通过查看nodeclub的目录结构,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- api/
- bin/
- common/
- controllers/ 对应mvc模式的c层
- logs/ 记录项目运行的目录
- middlewares/ express中间件, 基本的auth, session 验证
- models/ 对应mvc的m层
- node_module/ 通过require下载的包放在这个文件夹下
- proxy/ 可以看做是对model处理的加工库
- public/ 对外资源访问路径,包括web端所用的js、css和图片资源
- test/ 测试文件,用于对项目功能的单元测试,文件都是.test.js形式
- views/ 对应mvc的v层
- api_route_v1.js
- app.js 应用入口
- config.js 应用配置文件,用来记录一些配置信息
- oneapm.js 对项目做测试和监控
- package.json
- web_router.js

通过目录结构可以发现,nodeclub 是以 express + mongodb + mongoose 作为基本框架的一个典型的 MVC 应用:

  • Model: 对应目录中的model目录。涉及到这层的技术有 mongoose orm。
  • View: 对应目录中的view目录。涉及到这层的还有ejs模板。
  • Controller: 对应目录中的controller目录,涉及到的目录还有middleware目录。

4.应用入口app.js

神圣的入口文件,几乎每个项目都会有一个 entry,对于了解一个应用熟悉入口逻辑很重要。 下面将分步看看nodeclub 的 app.js 做了什么:

4.1require(./config)

通过该语句导入config.js的配置文件,后续通过config.xxx的语法即可调用config文件中的属性。该文件主要用于应用相关配置的设置, 主要分为:

  • 1.应用全局数据配置
  • 2.数据库连接配置
  • 3.session,auth 相关配置
  • 4.rss配置
  • 5.mail配置
  • 6.第三方连接相关配置, github, weibo

配置文件也是了解应用的一个好地方, 在 config.default.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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// debug 为 true 时,用于本地调试
debug: true,

get mini_assets() { return !this.debug; }, // 是否启用静态文件的合并压缩,详见视图中的Loader

name: 'Nodeclub', // 社区名字
description: 'CNode:Node.js专业中文社区', // 社区的描述
keywords: 'nodejs, node, express, connect, socket.io',

// 添加到 html head 中的信息
site_headers: [
'<meta name="author" content="EDP@TAOBAO" />'
],
site_logo: '/public/images/cnodejs_light.svg', // default is `name`
site_icon: '/public/images/cnode_icon_32.png', // 默认没有 favicon, 这里填写网址
// 右上角的导航区
site_navs: [
// 格式 [ path, title, [target=''] ]
[ '/about', '关于' ]
],
// cdn host,如 http://cnodejs.qiniudn.com
site_static_host: '', // 静态文件存储域名
// 社区的域名
host: 'localhost',
// 默认的Google tracker ID,自有站点请修改,申请地址:http://www.google.com/analytics/
google_tracker_id: '',
// 默认的cnzz tracker ID,自有站点请修改
cnzz_tracker_id: '',

// mongodb 配置
db: 'mongodb://127.0.0.1/node_club_dev',

// redis 配置,默认是本地
redis_host: '127.0.0.1',
redis_port: 6379,
redis_db: 0,
redis_password: '',

session_secret: 'node_club_secret', // 务必修改
auth_cookie_name: 'node_club',

// 程序运行的端口
port: 3000,

// 话题列表显示的话题数量
list_topic_count: 20,

// RSS配置
rss: {
title: 'CNode:Node.js专业中文社区',
link: 'http://cnodejs.org',
language: 'zh-cn',
description: 'CNode:Node.js专业中文社区',
//最多获取的RSS Item数量
max_rss_items: 50
},

log_dir: path.join(__dirname, 'logs'),

// 邮箱配置
mail_opts: {
host: 'smtp.126.com',
port: 25,
auth: {
user: 'club@126.com',
pass: 'club'
},
ignoreTLS: true,
},

//weibo app key
weibo_key: 10000000,
weibo_id: 'your_weibo_id',

// admin 可删除话题,编辑标签。把 user_login_name 换成你的登录名
admins: { user_login_name: true },

// github 登陆的配置
GITHUB_OAUTH: {
clientID: 'your GITHUB_CLIENT_ID',
clientSecret: 'your GITHUB_CLIENT_SECRET',
callbackURL: 'http://cnodejs.org/auth/github/callback'
},
// 是否允许直接注册(否则只能走 github 的方式)
allow_sign_up: true,

// oneapm 是个用来监控网站性能的服务
oneapm_key: '',

// 下面两个配置都是文件上传的配置

// 7牛的access信息,用于文件上传
qn_access: {
accessKey: 'your access key',
secretKey: 'your secret key',
bucket: 'your bucket name',
origin: 'http://your qiniu domain',
// 如果vps在国外,请使用 http://up.qiniug.com/ ,这是七牛的国际节点
// 如果在国内,此项请留空
uploadURL: 'http://xxxxxxxx',
},

// 文件上传配置
// 注:如果填写 qn_access,则会上传到 7牛,以下配置无效
upload: {
path: path.join(__dirname, 'public/upload/'),
url: '/public/upload/'
},

file_limit: '1MB',

// 版块
tabs: [
['share', '分享'],
['ask', '问答'],
['job', '招聘'],
],

// 极光推送
jpush: {
appKey: 'YourAccessKeyyyyyyyyyyyy',
masterSecret: 'YourSecretKeyyyyyyyyyyyyy',
isDebug: false,
},

create_post_per_day: 1000, // 每个用户一天可以发的主题数
create_reply_per_day: 1000, // 每个用户一天可以发的评论数
create_user_per_ip: 1000,
visit_per_day: 1000, // 每个 ip 每天能访问的次数

当数据过多的时候,我们可以将配置文件可以放在一个 config 的文件夹下面,用多个文件的方式来整理。比如运营数据配置和其他数据配置分开,因为很有可能需要做一个小的工具来让非技术人员配置相关参数。这时候可以用一个 index.js 作为 facade,相当于一个大的 node module。

4.2require(‘./models’)

model目录对应MVC模式的m层,目录下面有个index.js文件,所以require('./models')相当于require('./models/index'),index 相当于一个模型的 facade, 做的事情分别是:

  • 1.connect mongodb
  • 2.require 各个 model 模块
  • 3.exports 所有的 model

简而言之就是初始化了应用 model 层,模型使用 orm 框架 mogoose 来写,了解 mogoose 过后, models 部分的代码也就是秒懂了。我说的只是代码,literaly, 一个项目的核心就是 model 的设计,以前做过的任何项目都是一样, 数据库 table 的设计好坏直接影响应用的开发以及性能。

model目录下的文件意思为:

  • base_model.js 基础模型
  • index.js 相当于一个模型的facade,作用上面说到过
  • message.js 消息模型,对于一个 blog 来说, 基本的只有回复消息, 这里加了关注和@消息。
  • reply.js 回复模型
  • topic.js 话题模型
  • topic_collect.js 话题集合模型
  • user.js 用户模型

4.3require(‘./middlewares’)

express 的基础是 middleware,或者说 express 的基础是 connect,connect 的基础是 middleware。middleware 模式在 professional nodejs 中有一个专门的章节来讲解。何为 middleware 呢? middleware 模式 相当于一个加工流水线(大家叫 middleware stack),每一个 middleware 相当于一个加工步骤,当出现一个 http 请求的时候,http 请求会挨着每个 middleware 执行下去。

express 里处理一个请求的过程基本上就是请求通过 middleware stack 的过程: * -> middleware stack -> 路由 -> controllers -> errorhandlering

middleware 怎样做到的, 异步的方法呢? middleware 使用 promise 的方式来处理异步,所有每个 middleware 都有三个参数 req, res, next, 对于异步的情况, 必须要调用 next() 方法。不然后续的 middleware 就无法执行。 ps: debug 的时候没调用 next() 还不会报错,一定注意。

4.3.1auth.js

auth.js exports 出来的函数全部都是中间件,从变量名就完全清楚的知道到底在做什么了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//-- 需要admin权限
exports.adminRequired = function (req, res, next) {}

//-- 需要有用户
exports.userRequired = function (req, res, next) {}

//-- 需要有用户并登录
exports.signinRequired = function (req, res, next) {
if (!req.session.user) {
res.render('notify/notify', {error: '未登入用户不能发布话题。'});
return;
}
next();
}

//-- 屏蔽用户 -_-
exports.blockUser = function (req, res, next) {}

这里其实就可以看到中间件的作用了,我们以前写 php 的时候每次都需要判断用户是否登录, 没登陆 redirect 到 index.php ,只不过这里的方式是通过中间件来处理。明白这里什么意思,其他的中间件模块也就秒懂了。

4.4 require(‘./web_router.js’)和require(‘./api_router_v1.js’)

express 的世界里另外一个很重要的就是route, Node.js 启动的是服务, 监听了某一端口, 接受 http or https or socket 请求, 那 url 中像 /index.php?blabla 这一串的存在怎么处理呢, express 的 router 功能就可以帮我们解析。

MVC 中如何将一个请求和 controller 联系起来呢, router 就是这样的纽带

1
2
3
4
5
6
//--get, post 请求
app.get('/signin', sign.showLogin);
app.post('/signin', sign.login);
//--使用中间件
app.get('/signup', configMiddleware.github, passport.authenticate('github'));
app.post('/:topic_id/reply', auth.userRequired, limit.postInterval, reply.add);

router 是了解一个应用最佳的地方,一个请求如何处理, 到相应的 controller 去看就知道了。

4.5initialization

experess initialize: app.js 中其他大多部分就是express的初始化了, 初始化流程如下:

  • 1.配置上传 upload_dir
  • 2.模板引擎设置
  • 3.express 通用中间件设置
  • 4.pasport 中间件
  • 5.自定义中间件
    • 1.auth_user
    • 2.block_user
    • 3.staticfile: upload
    • 4.staticfile: user_data
  • 6.csrf
  • 7.errorhandler
  • 8.set view cache

配置的顺序很重要, 中间件的执行顺序是按照定义顺序来执行的, 如果一个中间件依赖另外的中间件, 而自己先执行了, 这种情况就会错误。 常见的问题就是session配置, 一定要记得配置 session 中间件的时候, 要先配置 cookieParser。

4.5.1session 设置

这个步骤在 initialize 里边已经有了, 不过再单独讲一下, nodeclub 使用的是 connect-mongo 来作为 session 的存储.

1
2
3
4
5
6
7
8
//--cookieParser一定要在前面, 因为session的设置依赖cookie
app.use(express.cookieParser());
app.use(express.session({
secret: config.session_secret,
store: new MongoStore({
db: config.db_name,
}),
}));

4.5.2view helpers

使用过 ejs 的肯定知道, ejs 里边 view helper 设置很简单, 就像赋值变量一样。 当对于一些通用的 helper 可以这样设置:

1
2
3
4
5
6
app.helpers({
config: config,
Loader: Loader,
assets: assets
});
app.dynamicHelpers(require('./common/render_helpers'));

4.5.3github pasport initialize

1
2
3
4
5
6
7
8
// github oauth
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
done(null, user);
});
passport.use(new GitHubStrategy(config.GITHUB_OAUTH, githubStrategyMiddleware));

5.用户注册

user 是每个应用都会处理的基本, 注册登录登出, 看看 nodeclub 做了哪些事情:

1.路由:

1
2
3
4
5
6
7
8
9
10
//--设置能否直接注册, 不能的话通过github注册
if (config.allow_sign_up) {
app.get('/signup', sign.showSignup);
app.post('/signup', sign.signup);
} else {
app.get('/signup', configMiddleware.github, passport.authenticate('github'));
}
app.post('/signout', sign.signout);
app.get('/signin', sign.showLogin);
app.post('/signin', sign.login);

2.controller & model:sign.signup:

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
sanitize = validator.sanitize;
check = validator.check;
exports.signup = function (req, res, next) {
//--xss 消毒
var name = sanitize(req.body.name).trim();
name = sanitize(name).xss();
...
//--validations
try {
check(name, '用户名只能使用0-9,a-z,A-Z。').isAlphanumeric();
} catch (e) {
res.render('sign/signup', {error: e.message, name: name, email: email});
return;
}
...
//--用用户名登录或者email登录
query = {'$or': [{'loginname': loginname}, {'email': email}]}
User.getUserByQuery(query, {}, function(){
...
pass = md5(pass);
...
User.newAndSave(name, loginname, pass, email, avatar_url, false, function (err) {
...
// 发送激活邮件
mail.sendActiveMail(email, md5(email + config.session_secret), name);
res.render('sign/signup', {
success: '欢迎加入 ' + config.name + '!我们已给您的注册邮箱发送了一封邮件,请点击里面的链接来激活您的帐号。'
});
})
})
}

6.mongoose的使用

一个应用通常会遇到这样的情景, 一个页面需要的数据包括, 文章列表, 评论列表,用户数据,广告数据, other stuff… 问题是每个都是异步的, 怎么办。 user 数据获取过后的 callback 调用文章列表获取, 文章列表获取的 callback 调用评论列表的获取… 这样就太蛋疼了。 nodeclub 使用了 eventproxy 模块优雅的解决这样的问题:

1
2
3
4
5
6
render = function(){}
var proxy = EventProxy.create('tags', 'topics', 'hot_topics', 'stars', 'tops', 'no_reply_topics', 'pages', render);
proxy.fail(next);
Tag.getAllTags(proxy.done('tags'));
Topic.getTopicsByQuery(query, options, proxy.done('topics'));
User.getUsersByQuery({ is_star: true }, { limit: 5 }, proxy.done('stars'));

当然异步处理的方法有很多:

  1. 基于事件的:eventProxy
  2. 基于promise的:Async.js Q.js, when.js
  3. 基于编译的:continuation, wind
  4. 基于语言语法的:yield, livescript

7.消息

原先以为有动态的消息推送, 有队列处理, 但是没有

在 Sublime text 里边全局搜索 sendReply2Message 会发现是在 controller/reply.js 里边调用的, 也就是说,消息是直接触发的。

8.开发

8.1测试

一个项目必定离不开测试, nodeclub基于mocha BDD测试框架, 一切的前提假设至少能看懂jasmine或者mocha或者任何一个BDD风格的测试代码。

所有的测试文件都在test文件夹下,文件名以test.js结尾,打开即看到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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var app = require('../app');
describe('app.js', function () {
//--before, 执行it的前面会执行
before(function (done) {
//--done, 异步方法
app.listen(3001, done);
});
after(function () {
app.close();
});
it('should / status 200', function (done) {
//--使用 app.request()就可以模拟请求了? 这个api哪里来的, 求解释?
app.request().get('/').end(function (res) {
res.should.status(200);
done();
});
});
});
//--按理说应该是可以正常运行了但是我一直出现这个错误:
//--connect ADDRNOTAVAIL 知道的求解释
//--我尝试用supertest直接测试, 但是也是一直timeout, mocha
//--里边加大timeout时间, 结果就是一直没反应。

//--分析原因, express版本问题, nodeclub中express的版本还是2.x, 所以才会有
//--app.request(), app.close()这些api
//--第二个原因, 到supertest官网, 发现人家都已经转战到superagent项目了, 于是我写了下面这个测试脚本, 可以通过了
var express = require('express');
var should = require('should');
var path = require('path');
var superagent = require('superagent');
var app = express()
app.get('/user', function(req, res, next) {
res.send(200, {
name: 'tobi'
})
})
describe('myapp.js', function() {
this.timeout(5000)
before(function(done) {
app.listen(21, done);
})
after(function() {
// app.close()
})
it('should /status 200', function(done) {
agent = superagent.agent()
agent.get('http://localhost:21/user').end(function(err, res) {
console.log(err, res)
res.should.have.status(200);
res.text.should.include('tobi');
return done();
});
})
})

8.2运行

nodejs 是单线程应用, 如果我们用 node 命令来运行我们的应用, 当出现一个小错误, 它就挂了。 然后没有然后了。 避免这种问题的方法有如下工具:

  1. forever
  2. nodemon
  3. supervisor nodeclub 使用 forever 来运行项目, 使用这类工具的好处就是, 当有代码改动过后, 会自动的重启应用。 不必每次自己去运行 node *.js

总的来说看起来还是很吃力的,需要学习的还有很多,希望自己快速成长,早日成为团队里能够独当一面的大侠吧。接下来学习这个教程:nodejs学习笔记

记得扫一扫领一下红包再走哦