使用Node.js完成的第一个项目的实践总结

2021-07-03 12:04

阅读:623

[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. $ npm install   

这时你会发现,项目的根目录下多了一个node_modules文件夹,那里面就是从npm远程库里下载的模块然后“安装”到你的项目中的。
现在,你就可以在你的项目中应用你依赖的这些modules了。你可以通过require关键字来使用他们。比如,

[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. require("eventproxy");  

Node.js 模块加载机制

node.js的模块加载基于CommonJS规范。
在Node.js中,将模块分为两大类:
(1)原生模块
原生模块在Node.js源代码编译的时候编译进了二进制执行文件,加载速度最快。 
(2)文件模块
node.js依赖modulepath(模块路径)来加载module,而modulepath的生成规则主要是从当前文件目录开始查找node_modules文件夹,然后依次进入父目录查找父目录下的node_modules目录直至到根目录下得node_modules目录。所以在require的时候,如果带上module的路径,则按照该路径查找,如果没有就按照上面的node_modules文件夹向上追溯查找,如果都没有找到,则抛出异常。

自动化部署

项目环境的构建、部署都是自动化的。
我们假设项目最终会发布在任意版本的Ubuntuserver上。在安装Git的前提下,通过如下命令去clone项目到本地:

[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. git clone git://github.com/yanghua/FixedAssetManager_Server.git  
项目doc下有四个shell文件:
  • node_install_ubuntu.sh - 在ubuntuserver上安装node.js的脚本
  • node-canvas-install_ubuntu.sh - 在Ubuntuserver上安装node-canvas的脚本
  • mysql_install_ubuntu.sh - 在Ubuntu server上安装mysql的脚本
  • dispatch.sh - 部署项目的脚本

将它们copy到当前用户的home目录下,依次执行即可。整个过程几乎实现了无需人为干涉的“自动化”。

pm2 - 线上监控管理工具

 
pm2是非常优秀工具,它提供对基于node.js的项目运行托管服务。它基于命令行界面,提供很多特性:
  1. 内置的负载均衡器(使用nodecluster module)
  2. 以守护进程运行
  3. 0s(不间断)重启
  4. 为ubuntu/ CentOS 提供启动脚本
  5. 关闭不稳定的进程(避免无限死循环)
  6. 基于控制台监控
  7. HTTP API
  8. 远程控制以及实时监控接口
pm2使用nodecluster构建一个内置的负载均衡器。部署多个app的实例来达到分流的目的以减轻单app处理的压力。
 

异常监控与邮件推送

 

node.js 到处都是异步调用。常用的try/catch同步捕获异常并处理的方式,在这里不起作用了。这是因为很多callback已经离开了当时try的上下文,导致无法获取异常产生的堆栈信息。基于这个问题,我们对异常处理的模式按类型进行区分处理:

(1)http请求异常
这种异常Express就可以进行处理。如果是非法请求,在路由的时候,对未匹配的请求进行统一处理:

[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. app.get("*", others.fourofour);   

(2)业务异常

这种异常通常不会影响到程序的运行,我们以不同的异常代码返回给前端或者终端,来给调用端友好的提示。

(3)应用程序级别的异常或必须处理的错误

这种情况下,应用程序可能没有办法处理异常,也有可能由应用程序抛出。对于这种应用程序级别的异常。我们用两种方式来catch:

[1]利用Express提供的应用程序的异常处理机制:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. app.error(function(err, req, res, next) {                                             
  2.         mailServie.sendMail({                                                     
  3.           subject : "FixedAssetManager_Server[App Error]",                    
  4.           text    : err.message + "\n" + err.stack + "\n" + err.toString()            
  5.         });                                                                       
  6.         if (err instanceof PageNotFoundError) {                               
  7.             res.render("errors/404");                                             
  8.         } else if (err instanceof ServerError) {                                      
  9.             res.render("errors/500");                                             
  10.         }                                                                         
  11.     });  

[2]应用程序已经无法响应处理了,则利用node.js提供的,对于进程级别的异常处理方式:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. process.on("uncaughtException", function (err) {                                      
  2.         mailServie.sendMail({                                                     
  3.             subject : "FixedAssetManager_Server[App Error]",                      
  4.             text    : err.message + "\n" + err.stack + "\n" + err.toString()          
  5.         });                                                                       
  6.     });   
这两种应用程序级别的异常,优先级都比较高,因此我们采用了邮件通知的策略,来辅助开发人员进行快速定位并修复。
 

静态资源优化:压缩合并与缓存

web应用中对于资源的定义大致分为:静态资源、动态资源两种。动态资源通常是可变的,需要进行相应处理的,而静态资源在线上通常都是不会变的。常见的静态资源有:javascript文件、css文件、图片文件等。对于这些静态文件,我们通过设置过期时间来进行缓存。而对于文本文件,由于浏览器的解析行为,对他们进行合并或者压缩都不会产生影响。
这里需要提到我们在组件中介绍的Loader。在项目刚被clone下来的时候,需要先执行makebuild来对项目进行初始化。在初始化的过程中,Loader会对项目的views文件夹中的文件进行扫描。它通常会扫描html界面:查找类似于如下的片段:

[html] view plain copy
 
 
 
 print?技术分享技术分享
  1.                                                                       
  2.     
  3.       .css("/public/libs/bootstrap/css/bootstrap.min.css")                        
  4.       .css("/public/stylesheets/login.css")                                       
  5.       .done(assets)                                                               
  6.       %>                                                                       
  7.                                                                      
  8.     
  9.       .js("/public/libs/jquery/jquery-1.10.2.min.js")                             
  10.       .js(‘/public/libs/bootstrap/js/bootstrap.min.js‘)                           
  11.       .js("/public/libs/CryptoJS_v3.1.2/rollups/sha256.js")                       
  12.       .js("/public/libs/js/login.js")                                                 
  13.       .done(assets)                                                               
  14.     %>  
去查找.css()以及.js()方法中的这些路径参数,并对这些文件进行合并或压缩混淆(这取决于配置),来产生一份assets.json(资产列表)文件。这里面定义了一些键值对,键为上面代码段中Loader()方法后面跟的参数,值为具体合并后文件的路径。这样,Loader会根据配置来判断加载类型。如果当前为开发模式或者为debug模式,则调用.js()/.css(),基于其中的路径参数来生成。如果为发布模式,则根据.done()里的assets键值对,结合Loader()的参数,来输出合并后的文件。以减少前端http请求数量以及总大小。对publich文件夹下的文件,设置静态文件缓存:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. //config for production env                                                       
  2. app.configure("production", function () {                                             
  3.     app.use(‘/public‘, express.static(staticDir, { maxAge: maxAge }));                
  4.     app.use(express.errorHandler());                                              
  5.     app.set(‘view cache‘, true);                                                      
  6. });  
因为设置了缓存,在重新改动这些静态文件再发布的时候,如果缓存时间太长,则客户端的静态文件可能不会被替换。对于这个问题,Loader通过在文件的后缀追加一个版本号来作为参数。这样,当重新生成assets.json的时候每个文件会产生新的版本号,浏览器会请求“新文件”替换掉客户端老的缓存文件。
 

Restful风格的URL

Restful以“Resource”为核心概念,认为URL是用来表示一种资源。而不应该表示一个动作或者其他的东西。而动作,比如“CRUD”正好对应http的四个method:get/post/put/delete。本项目中,我们大部分的URL以Restful风格为主,但没有严格贯彻执行。

前端内容模板化、组件化

前端我们采用的是ejs的模板来构建,它很好得实现了html的片段化、组件化。有一个基础的模板,别的都只是一块html片段。它们在服务端完成组合、解析,生成完整的html流输出到客户端。
这样的开发模式,使得前端代码的划分比较清晰,组件化也使得代码的复用变得更容易。

makefile

在项目初始化的过程中,我们使用makefile文件来使得一些动作自动化运行。比如我们之前提到过的构建assets.json来合并文件的动作,就是通过执行makebuild文件来完成的。

增强的Debug模块

目前,Node.js还没有很强大的调试工具。常用的辅助诊断方式就是打log。但繁多的日志输出,混杂在http log里实在是不方便判断。我们在项目中使用了debug module来进行debug,他支持对log加不同颜色的key word并且还支持timestamp。你在一大堆日志中,一眼就足以区分是从哪个module或者组件输出的。我们在项目中对不同的layer应用不同的关键字:

[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. var debug4Ctrller   = require("debug")("controller");  
  2. var debug4Proxy     = require("debug")("proxy");  
  3. var debug4Lib       = require("debug")("lib");  
  4. var debug4Test      = require("debug")("test");  
  5. var debug4Other     = require("debug")("other");  

将其置为全局:

[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. global.debugCtrller = debug4Ctrller;  
  2. global.debugProxy   = debug4Proxy;  
  3. global.debugLib     = debug4Lib;  
  4. global.debugTest    = debug4Test;  
  5. global.debugOther   = debug4Other;  
这样你在controller层的log就可以以如下方式log:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. debugCtrller("XXX %s", "YYY");  

这样在Terminal中,输出的log会按照不同的颜色进行区分,辨别性明显增强:

 
 
 

一切都可自动化——Grunt

 
Grunt是Javascript任务运行器。
 

为什么需要任务运行器?

 
对于需要反复重复的任务,例如压缩、编译、单元测试、代码检查等,自动化工具可以减轻你的劳动,简化你的工作。
 

为什么使用Grunt?

 
Grunt 有庞大的生态圈,并且每天都在增长。你可以自由地选择数以百计的插件,帮助你自动化地处理任务。
 
用Grunt构建现有项目
1:全局安装grunt命令行接口:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. npm install -g grunt-cli  

2:在项目的根目录下新建一个Gruntfile.js文件,该文件为grunt的配置、初始化文件
3:在packaget.json文件的devDependencies项中,添加grunt核心依赖以及需要的插件依赖:
 
注如果不想手动添加这些依赖,可以直接打开Terminal,在项目根路径下运行:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. npm install grunt --save-dev      

依赖会被自动写入package.json的devDependencies项中。

关于Gruntfile的编写规则,详细请查看,Gruntjs中文文档
目前项目中用到的几个gruntplugin:
  • jshint: 用于对JS语法进行强制检查
  • csslint: 用于对css语法进行强制检查
  • uglify: 用于压缩项目文件
Gruntfile配置的写法非常灵活并且随意,支持对文件进行正则匹配等特性。
 

统一的代码风格

 
对于多人协作的项目,代码风格显得尤为重要。对于代码风格的统一,我们采用软硬结合的方式。软指的是自动化格式工具;硬指的是强制检查工具。
 

自动格式化工具

目前项目中,两人都采用SublimeText作为代码编辑器。借助,SublimeText多如牛毛的插件,可以简化很多重复性工作,带来显著的效率提升。这里介绍几个格式化工具:
  • alignment:等号对齐排版插件
  • JSFormat:JS代码格式化排版工具
  • HTML-CSS-JS Prettify 代码格式化工具
对于2,3两个插件,选择他们的原因是:他们开放了代码风格的定义规则,而不是强制应用它们自己的规则。你可以任意定义你想让它格式化的代码风格,比如:
 

你只需要在项目的根目录下,创建一个.jsbeautifyrc文件,里面对缩进,空格等进行定义即可覆盖默认配置。这非常方便那些已经习惯了自己有一套代码风格的人使用这些插件。

更难能可贵的是,对于一个项目你可以有多个.jsbeautifyrc文件进行配置。他们的优先级取决于这些配置文件靠近待格式化文件的程度(某种意义上就是这些配置文件在目录层次的深度)。这非常切换我们的需求:因为node项目前后端都是js。对于后端我们采用的是4空格缩进,对于前端JS我们采用的2空格缩进。那么我们只需要在前端JS文件夹下,新建一个新的.jsbeautifyrc配置文件,copy上面的配置,然后将indent_size修改为2即可。
 

强制检查工具

自动格式化工具只是一种“效率工具”,不足以形成“强制规定”。这里我们辅以代码检查工具,来强制要求代码风格、语法规范。

检查工具在GruntSection已经列出,在commit代码之前,必须运行检查,并确保没有任何Warnning跟Error。
 

Express 2.x to 3.x 接口适配与调整

1、ejs 服务端模板的调整:
原先的模板配置方式是采用在html文件,设置:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. ‘layout‘) -%>  

来标识一个component会套用某个模板,而在3.x中ejs模板引擎,改为采用middleware的方式使用:(需要安装一个module:express-partials)
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. app.use(require(‘express-partials‘)());  

与此同时,3.x专门提供了一个设置引擎的接口:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. app.engine(‘html‘, require(‘ejs‘).renderFile);  

替代了原先2.x的:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. app.register(‘.html‘, require(‘ejs‘));  

2、处理错误的方式改变:

2.x处理错误有专门的一个API:

[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. app.error(function(err, req, res, next) {                                             
  2.     //error logic handle                                                      
  3. };  

 

3.x退而采用middleware的方式来处理:

[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. app.use(function(err, req, res, next) {                                           
  2.     //error logic handle                                                      
  3. }  

更多express2.x to 3.x的改变,可以看看官方给出的变更列表
 

从EventProxy 到 async 切换

eventproxy是淘宝前端团队开发的一个node.js事件处理代理。用于辅助开放人员组织代码的执行顺序,对于很多需要干预执行顺序与过程的代码,避免了node.js深层嵌套的callback模式。

async跟eventproxy出于同样的目的。但在API的设计模式上有所差异。async的API的风格偏向于“整合”,Eventproxy偏向于“拆分”。
 

Mongodb for node.js:mongoose的使用

 

model的定义

MongoDB里数据的集合称之为collection。而每个collection都有一个schema与之对应,可以简单的理解为是对其数据的定义(类型与结构)。

对应到mongoose里,一个schema是一个model,形如:
 
 
好处:mongodb直接存储了json文件,mongoose内部完成了对model数据对象的mapping,并自动继承了一些基本操作接口。这样的model是真正意义上的“充血模型”,而不是很多编译型语音内构建的“贫血模型”式的model。
 

给每个层定义索引文件

 

程序设计的一个重要指标:模块性。在c/c++里有头文件,在面向对象语言里有pagckage/namespace的概念。他们的目的之一就是提升模块性,降低耦合度。

在node.js中,我们也可以采用类似c/c++的headfile的模式,以层为单位。将对外可见的以文件为单位的module以一个独立的文件对外开放(通常我们称其为index.js文件)。形如:
 
 
对于任何文件内的访问权限跟可见性,我们都可以采用exports关键字予以控制。
 

supertest 模拟 http request 测试

supertest是一个用于模拟http request的module,可借助其进行web功能测试。

它提供了基于描述的API链式调用,可以非常容易得模拟http请求测试,形如:
 
 

密码采用混入salt值的方式进行加密

密码只是采用hash方式进行“加密”,还是相当不安全的。随着现在计算能力的增强以及字典规模的扩大,简单的md5已经非常不安全。一旦被拖库,密码很容易就会被破解。关于密码的问题,除了采用(SSL加密数据传输链路)一直都没有非常成熟的解决方案。所以,问题就退而求其次转变为如何提升破解难度的问题。而在密码中混入salt,是一直非常经济而有效的方式。这里我们处理用户身份认证的方式是:

入库之后的加密密码 = sha3 (md5 (passwrod) + salt)

其中:salt的计算方式为:sha256(userName)
 

解决linux解压缩包中文乱码

 

在windows上打包的zip压缩包,在ubuntu上解压缩后,凡是文件名含有中文的都出现了乱码。产生这一问题的原因是:在windows上压缩文件,通常采用系统默认编码(通常是gbk或gb2312),而传到Linux上去,在linux上通常都默认采用的utf8编码,所以需要进行解码。

项目中有需要在服务器上对上传上来的zip压缩包解压缩的步骤,默认调用的是shell命令(unzip命令)。网上很多提供的解决方案是通过提供"-O"参数,显示指定编码。但未能成功,因为在现在版本的unzip里,该参数已经失效了。通过unzip几经折腾,还是无法解决乱码问题,于是转而采用7z来进行解压缩,并显式指定英文环境ASCII编码(通过LANG=C),示例:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. LANG=C 7z e `source zip file path` -o`target path`    

其中参数“e”表示释放所有文件到目标路径(递归所有压缩包中的子文件夹),参数“-O”指定解压到的路径(注意参数-O跟输出路径中间无空格)。

这一步只是完成了解压,文件名这时还是乱码的。此时需要linux上专门的转码工具——convmv来进行编码转换!

如果该命令不存在,可以先apt-getinstall一下,然后运行如下命令:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. convmv -f cp936 -t utf8 -r --notest -- `target path`/*  

该命令的意思是:对`targetpath`下的所有文件,从cp936编码转换成utf8编码,其中的--notest表示不进行测试,直接转换。如果你想确保转码安全,可以先进行测试,看是否会产生乱码,然后再进行转码,测试命令如下:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. convmv -f cp936 -t utf8 -r -- *  

这是你只要关注文件名是否会产生乱码,无需关注目录名,如果没有乱码,就可以安全转换了。

如果文件内容有乱码,可以借助如下命令对文件进行转码:
[javascript] view plain copy
 
 
 
 print?技术分享技术分享
  1. iconv -f cp936 -t utf8 -o output.txt input.txt  

备注:我这边遇到的是,在一个压缩包内部有文件夹的情况下,在windows压缩,在ubuntu下解压缩产生了中文乱码。但如果只是选中几个文件直接压缩,在ubuntu下不做转码,中文照常显示。
 

源码 & 总结 & TODO

由于边摸索边构建,在代码规范性上还有待进一步重构。同时,对CNode官方社区的开源代码表示感谢,它真的让我们学到了许多。
本文所述项目开源在github


评论


亲,登录后才可以留言!