小马的大前端之路——Node.js初探

一次偶然的机会让我有幸跨越浏览器的鸿沟来真真切切的体验一次Node.js。

首先,我想说:“很荣幸在经历了2个月的努力,第一个Node.js项目落地了”。整个项目做下来,还是算比较顺畅的。

事情很简单:Node.js做的是接入层。

事出有因

前端的技术革新是日新月异的,前端工程化已经离不开Node.js。现在大多数的项目使用的是前后端分离的架构,后端提供借口前端通过接口数据进行数据渲染。但是现在前端的代码逻辑越来越复杂,场景也越来越多。这套架构是否适合所有的应用场景值得考虑了。大前端的出现,就是一种尝试吧。试图通过Node.js接入来应对各种应用场景。

架构图

不管是个人还是应该团队,技术革新是必须的。现在我们团队面临的问题就是如此,所以必须有人迈出这一步。而我也很幸运的成为第一个吃螃蟹的人。

始作俑者

不管什么技术,不管这么的优秀,它的运用与否都是要经过慎重考虑的。但,总不能都不用吧。那怎么办呢。找项目试点呗,线上项目运行的好好的肯定不能重构,而且人力紧张啊。只能找新项目了。刚巧,公司需要做新的项目,本以为按老路子前后分离做。可突然有一天…

组长说:“团队不是要进行技术选型吗?看这个项目使用Node.js做接入层可不可行?“。

经过慎重考虑,我回答说:“可以没问题。”。(管他3721,应了再说。😄)

借我老大的一句话:“技术这东西不落地,说了也白说”。

背景:其实团队对Node.js一直都保持着高度的关注,包括我。之前我一直都有在对Node.js的源码进行解读和研究。基础架构组也一直在进行Node.js技术框架进行调研,希望打造一套适用于团队开发的集成项目框架。

所以我相信:机会总是会照顾有准备的人的。

就这样我的Node.js之旅就开始了。

万事开头难

虽然我平时可能天天都会用Node.js跑命令,写各种npm包,甚至还写过一些自己的项目。但是要真正的用Node.js来真正开发项目还是有压力的。因为这种项目技术架构下要求我操心的东西变多了。平时的时候可能我只要写一些前端逻辑代码,做做前端工程化。但是这种架构下,要求我必须去学习和应用我不熟悉的东西。

我大致列了一些大的方向:

  • 1.Node.js接入层的总体架构是怎样的?
  • 2.前端技术用什么?
  • 3.前端工程化如何做?
  • 4.项目如何根据不同的环境(常有的环境:开发,测试,正式)运行?
  • 5.前端自动化怎么搞?
  • 6.单元测试?
  • 7.编码风格?
  • 8.Node.js如何和服务端对接?
  • 9.日志,上报,登录服务接入,权限校验等等我应该怎么做?
  • 10.项目如何发布上线?
  • 11.上线了如何保证服务稳定?
  • 12.如何debug问题?

可能还有很多很多需要处理的问题但是这已经可以看出一下端倪了。瞬间感觉我懂的只有冰山一角。代码码的再漂亮感觉也无力。要求的不再是单一的编码能力,而是大局观,思维角度的转变。

但不管怎样,新建git仓库开始搞呗。

如何得到一个合适的项目架构

这个确实是个问题,架构设计的合不合理。会影响到后期编码是否可以做到快速开发,还会影响后期的功能迭代和维护。

那么问题来了,我是预先设计还是预先编码?

这里我选择了先编码,然后重构。

背景:因为上文已经说过,基础架构组已经有一个简单的Node.js集成框架,它是不完整的,但是它够简单。也就是说我在这上面重构出自己的项目架构是完全没有问题的。

你可能会觉得还是要预先设计啊?

说的是侧重点不一样,侧重于编码实现,将这个项目跑起来,然后通过重构去寻找出合适的项目架构。

对于先编码还是设计这个问题我借用重构里面的是一句话:

“重构改变了预先设计的角色。如果没有重构,你就必须保证预先做出的设计是正确无误,这压力太大了。这意味着如果将来需要对原始设计做任何修改,代价都将非常昂贵。因此你需要把更多的精力放在预先设计上,以避免日后的修改。如果选择重构,问题的重点就转变了。你任然做预先设计,但是不必一定要找出证正确的解决方案,此刻的你只需要得到一个合理的解决方案就够了。“ –摘自《重构-改善既有代码的设计》

把一个简单的解决方法重构成一个灵活的解决方法有多难?答案是:“相当容易”。 –摘自《重构-改善既有代码的设计》

实在不明白我推荐你去看看《重构-改善既有代码的设计》这本书。

所以我将侧重点放在了预先编码上,让后在整个项目demo跑起来之后再去寻找合适的架构。一个合理的架构体系就是把代码放到它应该出现的位置上去。代码是具有流失性的,就好比一个房间从来不整理的话,就会变的脏乱不堪。重构就是将代码再次整理将它放回原位。

目录图

技术框架选型考虑

技术框架的选择会影响着项目的总体架构,编码,产出效益,以及后期人员维护的成本。

首先我想说:“不管前端还是后端用什么框架我觉得还是要站在团队的角度上去考虑这个问题,毕竟这不是个人的项目。总不能说我不在就没人能维护这个项目吧”。

Node.js后端

koa2。为什么没有使用koa或者express等框架,或者为什么团队不自己开发。

Node.js v8LTS 已经快要来临。koa已经升级到了koa2版本,没有必要再用旧的express太老了。koa2在这两年已经锋芒毕露,现阶段团队没有必要花费很多的人力去搞一套自己的框架,可以转变思维在koa2的基础上做一个集成的适合团队项目使用的框架。

基于这个基础架构团队使用koa2作为主框架使用在现阶段是最合适的。特别是在Node.js v7.6+ 原生支持了asyncawait语法。

前端框架

jQuery的王朝已经渐渐被瓦解。angular.js,react和vue三足鼎立的时代已经到来。再次基于团队的现状,选择了最有优势的angular.js v1.x。

在这里我并没有说其他框架不好的意思,完全是基于团队现状的考虑,以及当前框架是否可以帮助我高效的完成开发的一种考虑。假如有一天我觉得angular.js已经不适合现阶段项目开发需求,我会义不容辞的提出我的疑问。

比如:项目需要我们考虑加速页面渲染时,要考虑服务器渲染;服务器压力山大时,考虑前后端分离。同构作为最合适的编码方式react和vue都是不错的选择。

框架没有对与错,只有合不合适。

webpack2 作为当红炸子鸡,我也是优先考虑的。至于为什么没有选webpack3嘛。。。

其实是这样的,我也有实际的去使用webpack3来做过测试,就是这个项目。我的衡量标准就是压缩要比现在的要小。最后没有达到预期效果所以没有进行合并。

gulp 工作流处理,没毛病。这里可能会有的让人疑惑,为什么使用了webpack2 还要使用gulp?为什么2个都要用?

其实对于这2个组件,它们没有绝对的对立关系。在这里它们是相辅相成的。

总的前端框架:angular.js v1.x + webpack2 + gulp。

babel用来编译前端代码。

项目使用的主要框架,如图:

主要框架图

前端工程化

项目的总体架构和前端技术框架的选型势必会对前端工程化产生深远的影响。前端代码放到哪里,webpack打包如何做,产出文件放到哪里。gulp需要做哪些事情,多还是少,烦不烦琐。这种种问题都会对你项目的架构做出挑战。这也就是我为什么先编码然后通过重构来调整项目架构的原因之一。假如你预先就把项目的总体架构规定死了,那么后期你的编码就会想尽办法的去套这个项目架构,写出来的代码可想而知——一定是不尽人意的。

那么第一个问题就来了。

自己编写的anglaur.js部分的源码放到哪里

对于这个问题,在使用Node.js开发初期,我就对基础的架构做出了建议:前端源码不能放到服务器静态资源目录。只有打包后的文件才会放到静态资源文件目录,除非该文件可以直访问。

这就意味着,我需要寻找一个文件目录来放置前端源代码。最合理的位置就是于服务器目录平级放置。

webpack

通过webpack的编译打包,将文件保存到静态资源目录。我这里把所以和代码相关的打包和编译任务都交给了webpack,其中还包含公共文件的提取,版本控制,压缩,以及模版文件注入。

webpack

如何进行版本控制

版本控制用的比较多的就2种:基于文件和基于hash。

基于文件就好比,每次打包的时候都会生成不同文件名的文件。有利于在线上跑多个版本的功能。

基于hash就意味着线上这个功能的文件永远就只有一个,无法进行全量灰度。

这里有个问题就是:基于文件的版本控制,难点就在于打包后的.js.css文件名是不可控的,所以,并不能把引入的js或css文件路径写死在html模版文件里面。所以通过webpack打包的时候,我需要指定模版文件是哪一个,通过webpack的模版文件注入插件完成js或css文件路径的引入。

其它方式;通过在webpack打包完成之后,将返回值种的hash参数保存下来。这样也可以完成基于文件的版本控制。

gulp的工作流

gulp结合webpack的应用如鱼得水,webpack打包任务是gulp任务流里最重要的一环。考虑到打包编译,都交给webpack做了。那gulp所要做的就是保证前端各个任务正确的执行。包括何时执行webpack打包,完成打包以后做什么。

gulp

前端自动化

这里的自动化可能与你在别的地方所说的自动化可能有分歧。这里的前端自动化主要指的是在前端代码如何完成自动化打包编译。其实项目中可以进行自动化的流程有很多,我在项目里接入的是jenkins,主要用来自动完成前端打包编译,然后通过zip命令对webpack打包编译后的所有文件进行打包成.zip文件。因为打包后的文件不入库。

这里有疑惑是正常的。首先为什么不把weboack打包后生成的文件纳入git版本库?

道理很简单,git版本库里面的任意一个文件产生变化,就会有下一个版本号产生。webpack每次打包编译就势必会产生文件变化,如果把打包文件纳入版本库就必须提交文件,从而产生版本号。也就是说我本地提交一次代码到git库后,jenkins会进行打包,然后打包文件又必须提交回git库,这样就相当于每次提交代码否会产生2次提交记录(一次我自己的提交,一次jenkins完成自动化打包后的提交。)。所以为了不让jenkins完成打包后向git代码库提交文件,所要做的就是把webpack打包后产生的文件都移除版本库。

但问题没有这么简单,webpack打包不纳入版本库,发布的时候,这些webpack打包后产生文件怎么发布。这里解决方案就是通过把所有和webpack打包相关的文件用zip命令打包成一个${commitId}.zip包(commitId 是git每次提交参数的可以通过bash获取:commitId=$(git rev-parse HEAD))。这样发布的时候就可以通过commitId找到${commitId}.zip这个压缩包,然后解压它到指定位置即可。

为什么有2个打包任务?

第一次是webpack打包,前端代码需要打包编译。第二次是文件打包,发布需要,原因很就是webpack打包文件不入库的解决方法。

所以要求团队中必须会搭建并且有使用过jenkins,这个工具对团队的帮助是非常大的,预先打包文件并缓存,比在发布项目的时候再进行打包要好很多。可以预先发现打包问题及时进行补救,以免发布时打包出现问题而影响发布进度和线上项目的正常运行。

jenkins

git仓库支持添加hooks。所以可以在git库里添加触发事件。让jenkins自动完成打包。

假如有一天,我需要写单元测试的时候,也可以试着让jenkins帮我跑自动化测试了。这算是我回答了单元测试的问题吗?哈哈哈哈哈哈哈。。。。。。

前端问题基本解决了,现在问题抛到了服务端。

Node.js服务端运行环境配置

写个项目,要跑起来很简单,我的项目入口文件是server/index.js。通过执行如下命令就可以启动:

1
node server/index.js

但有时候,环境并没有我想的那么简单。因为项目需要针对不同的环境运行,所以必需对不同的运行环境使用不同的配置文件。这样就需要我在启动Node.js服务的时候,必须携带不同的参数。所以要求我在编码的时候尽可能的做到环境参数的配置化——牵涉到与执行环境有关的参数尽量进行配置化。

启动

Node.js接入层服务的接入,权限的校验

其实对于一个小白来说,很担心的是我如何才能在Node.js里面往真正的服务器发起request请求。我项目站点的登录服务鉴权如何去做,以及用户登录了,有没有权限去访问都是个问题。

http服务的接入

通过http模块发起requset请求。其实开始的时候我也是一脸茫然的,如何在接入层请求后端服务,可想而知这是之前作为前端的我从来没有考虑过的。现在回想起来就那么回事。有些事情想着可能很复杂,真正的做起来就好像有种:山重水复疑无路,柳暗花明又一春。的感觉。

服务接入

Node.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
exports.example = async (ctx)=>{
let options = {
port: 80,
hostname: 'www.test.com',
method:'GET',
path:'/api/getuser?token=document.cookie.token'
};
let getData = function (){
return new Promise((resolve , reject)=>{
let request = http.request(options , (socket)=>{
let data = '';
console.log('status: ' , socket.statusCode , socket.headers);
socket.on('data' , (chunk)=>{
data += chunk;
});
socket.on('end' , ()=>{
console.log('server call back get data: ' , data);
return resolve(data);
});
socket.on('error' , (e)=>{
return reject(data);
});
});
request.end();
});
}
ctx.body = await getData();
}

这里我没有考虑https的方式,因为https是建立在SSL/TLS之上的,也就是说,需要有私钥和公钥和CA证书才行。CA证书虽说可以自己颁发但还是得本机自行安装才有效。对https自己颁发CA证书感兴趣的可以看看这篇文章:HTTPS自签发CA证书

后端服务器(PHP/JAVA…)需要做的就是根据请求参数是否合法已经齐全,然后验证调用者是否有权限使用该功能。这样的案例比比皆是,比如使用第三方服务。

小到Number校验

有可能最简单的参数校验都不知道如何校验。这跟javascript语言以及前端的思维方式有关。我开始的时候也是这样,感觉写起代码来怪怪的。

其实这是一个简单的例子,在前端检验一个Number类型的值是不是有效,我一般是通过:

1
num = typeof num === 'number' && num === num && num !== Infinity ? num : 0;

这种思路和逻辑放在前端完全是没有问题的,但是在Node.js接入层这么写感觉很尴尬。所以要转变我的思维方式:

1
num = Number.isFinite(num) ? num : 0;

小到参数的校验,我都要认真的考虑。是时候改变自己的思维方式了,考虑使用JavaScript原生的方式处理会比自己写好很多。

权限的校验

我并不希望所有的用户都能访问这个项目,即使他已经登录了也不行。这就是我要解决的问题。

权限

权限管理在这里就显得极其重要了。最好的方式就是把权限相关的功能进行服务化。


使命感觉才刚刚开始!!!!!

项目的部署上线

可以说我对项目部署和运维基本上是没有经验。但是有一点就是项目上线后的可用率是必须要保证的。不能因为一点小问题,就让服务挂掉,然后还要人屁颠屁颠的重新手动重启吧。也不能说服务器断电了,重启后也要手动启动吧。这一些列的问题都是必须解决的。

pm2

很高效的开发完成了项目后,其实项目的真正使命才要刚刚开始,如何保证服务在线上稳定的运行,保证高可用率。这就需要借助其它组件来完成了。使用pm2管理确实是个好的方案。

  1. 首先通过npm install -g pm2进行安装。

  2. 安装完成了之后,就可以在项目中进行pm2相关配置。

案例:

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
//test.config.js
'use strict';
//pm2配置文件
module.exports = {
apps:[{
name : 'test',
script: './server/index.js',//应用入口
cwd: './',
instances : 1,
watch : ['server'],
env: {
'NODE_ENV': 'development',
},
env_production: {
'NODE_ENV': 'production',
},
exec_mode : 'cluster',
source_map_support : true,
max_memory_restart : '1G',
//日志地址
error_file : '/data/logs/pm2/test_error.log',
out_file : '/data/logs/pm2/test_access.log',
listen_timeout : 8000,
kill_timeout : 2000,
restart_delay : 10000, //异常情况
max_restarts : 10
}]
};

  1. 然后就可以通过命令启动:
1
pm2 start test.config.js

nginx

Nginx 是俄罗斯人编写的十分轻量级的 HTTP 服务器,Nginx,它的发音为“engine X”,是一个高性能的HTTP和反向代理服务器。nginx配置也是必不可少的,80端口就一个,所以我需要nginx进行转发。

例如下面的案例:

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
upstream test_upstream {
server 127.0.0.1:6666;
keepalive 64;
}
server{
listen 80;
server_name www.test.com;
client_max_body_size 10M;
index index.html index.htm;
error_log /data/nginx/log/error_www.test.com.log;
access_log /data/nginx/log/access_www.test.com.log combined;
location / {
proxy_store off;
proxy_redirect off;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Remote-Host $remote_addr;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://test_upstream/;
proxy_read_timeout 60s;
}
}

项目启动的端口是本机的6666端口,但是我不可能说访问www.test.com的时候后面还带着端口号吧。这个时候就是nginx发挥作用的时候,访问域名不带端口默认使用80端口,由nginx做反向代理到我服务6666端口。

这里有一点post请求时client_max_body_size参数的设定直接会影响data的大小。

日志,上报,运营维护

项目的健康与否,都会在日志和上报中体现。我只需要每天看看日志,看看视图就可以对当天项目的运行情况做一个大致的了解。如果没有这些辅助的功能,两眼一抹黑,发生啥事都不知道。

编码风格

编码风格方面遵循eslint的语法标准。使用了最新的async/awaitimport语法。

编码

debug代码

Node.js已经支持在chrome中直接调试Node.js代码,只要在启动项目的时候添加--inspact参数。

1
node --inspect server/index.js

debug

复制上面红框的url链接到chrome里面打开,然后点击start后,再访问页面,需要暂停的时候可以点击stop,进行代码分析。

总结

作为一个初学者,我只能说Node.js在做接入层上,确实是可以做到如鱼得水,关键点就是契机。抛开Node.js接入层,前端的工程化是完全可以做的。但是服务器同构渲染是没有办法做到的,除非与后端同学配合;使用Node.js接入层,那么前端在处理一些棘手的问题时就会游刃有余,而且后端服务会得到更深一层的保护,不至于说后端服务直面攻击,因为多了一层Node.js接入层在前面。

如果你正在考虑要不要使用Node.js,我是无法给出答案的。

原创:Jin

原文链接:https://futu.im/posts/2017-07-26-first-time-use-node.js