写在前面不知不觉,我已经写了一年Node.js了。与最初的demo、本地工具等不同,今年的线上业务是用Node.js写的。从最开始的Node.js同构直出,到最近的Node接入层,算是Node开发入门。目前,我自己维护着组内大部分流传下来的Node服务,包括内部系统和在线服务。新增的后台服务也尽可能使用Node开发。这篇文章是关于我最近的一些小总结和思考。本文不会深入讲解Node.js本身的特性、架构等。我没有编写过Node扩展或库,对Node.js的理解不够深入。为什么用Node对我来说,对团队来说,Node适合的原因其实很简单:开发速度快。熟悉JS的前端同学可以快速上手,节省成本。选择一个http服务器库启动一个服务器,选择合适的中间件,匹配请求路由,使用ORM库链接数据库,根据情况进行增删改查。Node的适用场景Node.js采用事件驱动、非阻塞I/O模型使其轻量高效。该模型允许Node.js避免因需要等待输入或输出(数据库、文件系统、Web服务器...)响应而造成的CPU时间损失。因此,Node.js适合用于高并发、I/O密集、业务逻辑量少的场景。对应平时的具体业务,如果是内部系统,大部分只是需要对某个数据库进行增删改查,那么服务器端直接就是Node.js的穿梭。对于线上业务,如果流量不大,业务逻辑简单,服务端也完全可以使用Node.js。对于流量大、复杂度高的项目,一般采用Node.js作为接入层,由后台同学负责实现服务。如下图:也是写JS,Node.js有什么区别?Node.js主要是面向数据的,接受请求后返回具体的数据。这就是两者在商业路径上的区别。真正的区别其实在于商业模式(businessmodel,这是我自己想到的一个词)。直接用图片表达出来。在开发页面时,每个用户的浏览器都有一份JS代码。如果代码在某些情况下崩溃,只会影响当前用户,不会影响其他用户。用户刷新后即可恢复。在Node.js中,不开启多进程,所有的用户请求都会进入同一个JS代码,只有一个线程在执行这段JS代码。如果用户请求出错,Node.js进程挂了,服务器直接挂了。虽然可能有进程守卫,挂起的进程会被重启,但是在用户请求量大的情况下,会频繁触发错误,可能会出现服务端一直挂着重启的情况,影响用户经验。产生影响。以上可能是Node.js开发和前端JS开发最大的区别。Node.js开发注意事项用户访问Node.js服务时,如果某个请求卡住,服务长时间无法返回结果,或者逻辑错误导致服务挂掉,都会带来大尺度经验问题。服务器端的目标是快速可靠地返回数据。缓存由于Node.js不擅长处理复杂的逻辑(JavaScript本身执行效率低),如果要使用Node.js作为访问层,应该避免复杂的逻辑。如果要快速处理数据并返回,一个关键点:使用缓存。比如直接用Node做React同构,renderToStringAPI可以说是比较重逻辑。如果页面复杂度高,每次请求都会完整的执行renderToString,这样会耗费很长时间执行代码,增加响应时间,降低服务的吞吐量。这个时候缓存就很重要了。实现缓存的主要方式:内存缓存。可以使用Map、WeakMap、WeakRef等实现,参考下面简单的示例代码:constcache=newMap();router.get('/getContent',async(req,res)=>{constid=req.query.id;//命中缓存if(cache.get(id)){returnres.send(cache.get(id));}//请求数据consstrsp=awaitrpc.get(id);//经过复杂的操作,处理数据constcontent=process(rsp);//设置缓存cache.set(id,content);returnres.send(content);});在使用缓存的时候,有一个很重要的问题:如何更新内存缓存。最简单的方法之一就是设置一个定时器,周期性的删除缓存,下次请求来的时候重新设置缓存。在上面的代码中,加入如下代码:setTimeout(function(){cache.clear();},1000*60);//每分钟删除一次缓存如果server端完全由Node实现,则需要使用Node端直接连接数据库,当对数据时效性要求不是太高,流量不是太大的时候,可以使用上面类似的模型,如下图。这样可以减轻数据库的压力,加快Node.js的响应速度。另外,还需要注意内存缓存的大小。如果不断往缓存中写入新的数据,内存就会越来越大,最终爆裂。考虑使用LRU(最近最少使用)算法进行缓存。开辟一块内存专门作为缓存区。当缓存大小达到上限时,最旧的未使用缓存将被驱逐。内存缓存会随着进程的重启而彻底失效。当后台业务复杂,接入层流量和数据量较大时,可以采用如下架构,使用独立的内存缓存服务。Node访问层直接从缓存服务中取数据,后台服务直接更新缓存服务。当然,上图的架构是最简单的情况。在现实中,需要考虑分布式缓存和缓存一致性的问题。这是另一个话题。错误处理由于Node.js语言的特性,Node服务相对容易出错。一旦出错,影响就是服务不可用。因此,处理错误非常重要。处理错误,最常用的是trycatch。但是trycatch无法捕获异步错误。在Node.js中,异步操作非常普遍,异步操作主要暴露回调函数中的错误。看一个例子:constreadFile=function(path){returnnewPromise((resolve,reject)=>{fs.readFile(path,(err,data)=>{if(err){throwerr;//catch抓不到错误,这个和Node的eventloop有关//reject(err);//catch可以捕获}resolve(data);});});}router.get('/xxx',asyncfunction(req,res){try{constres=awaitreadFile('xxx');...}catch(e){//捕获错误处理...res.send(500);}});上面代码中,readFile抛出的错误无法被catch捕获。如果我们将throwerr替换为Promise.reject(err),则可以在catch中捕获错误。我们可以Promise所有的异步操作,然后统一使用async、try、catch来处理错误。然而,总会有错过的地方。这时候可以使用进程捕获全局错误,防止进程直接退出,导致后续请求挂掉。示例代码:process.on('uncaughtException',(err)=>{console.error(`${err.message}\n${err.stack}`);});process.on('unhandledRejection',(reason,p)=>{console.error(`UnhandledRejectionat:Promise${p}reason:`,reason);});要捕获Node.js中的错误,您还可以使用域模块。现在这个模块已经不推荐了,我也没有在项目中实践过,这里就不展开了。Node.js近几年推出的async_hooks模块,目前还处于实验阶段,不建议在线上环境直接使用。做好进程守护,开启多进程,及时修复错误告警,制定良好的编码规范,使用合适的框架,提高Node服务的效率和稳定性。写在后面,本文总结了一年多Node.js开发的实战总结。Node.js的开发不同于前端网页的开发,侧重点也不一样。太久没有正式开发Node.js了,有些点还没有深入了解。这篇文章只是一些经验。
