我曾经在 10 个技巧,2017 年成为更好的 Node 开发者 介绍了10个Node.js的技巧和技术。这篇文章评述的内容是在那基础之上进一步延伸出来的10个最佳实践,帮助你将Node技能提高到新的层次。下面是本文涵盖的内容:
使用npm脚本——停止使用bash脚本,npm脚本和Node能更好地组织脚本。比如,npm run build、start和test。当开发者考虑新项目的时候,npm脚本似乎是唯一值得相信的东西。
使用env变量——利用process.env.NODE_ENV,它可以设置为development或者production。某些框架也会使用这个变量,所以请按惯例使用它。
理解事件循环——setImmediate()并非立即执行,nextTick()也不一定就是下一个。使用setImmediate()或者setTimeout()会将CPU密集型任务放在下一个事件循环周期进行。
使用功能继承——在调试代码以及理解原型继承或类的时候,要避免使用过多脑力陷入盲目的思想斗争。像伟大的Node贡献者们那样使用功能继承就好。
使用恰当的名称——给文章起个有意义的名称。同时请不要大写文件名,并在需要的时候添加连字符。大写的文本名不会让人觉得奇怪,但可能 在跨平台时发生问题 。
考虑不使用JavaScript——可怜的ES6/7经过6年的会议商讨终于诞生的时候,我们已经有更好的 JavaScript,CoffeScript。如果你愿意高效产出代码,并且不想再纠结var/const/let、分号、class和其它主题,那就用CoffeeScript。
提供原生代码——在使用转译器的项目中,直接提交原生代码(构建结果)可以让你的项目不需要 构建直接运行。
使用gzip——唔!npm i compression -S和理性的日志——不要太多也不要太依赖环境。npm i morgan -S。
扩大规模——从使用Node进行开发的第一天开始就应该考虑集群和无状态服务。使用pm2或者strongloop的集群控制。
缓存请求 ——从你的Node服务榨取最大性能,把它们放在像 Nginx 这样的静态文件服务器后面,并像 Varnish Cache 和 DNS 那样对请求进行缓存。
下面我们依次来看看上面提到的每一条。
现在npm脚本几乎已经成为一个标准,它被用来进行构建、测试以及最重要的启动应用。这是一个Node开发者遇到一个新Node项目的时候首先要看的地方。有些人( 1 , 2 , 3 , 4 )甚至已经抛弃了Grunt、Gulp,转用更低级更可依赖的npm脚本。我完全能够理解他们的观点。考虑到npm脚本有前后钩子,你可以使它实现非常复杂的自动化:
"scripts": { "preinstall": "node prepare.js", "postintall": "node clean.js", "build": "webpack", "postbuild": "node index.js", "postversion": "npm publish"}
通常在进行前端开发的时候,你可能会运行两个或者更多的监控进程以实时重新构建代码。比如,一个 webpack进程,一个nodemon进程。因为第一个命令不会产生提示,你可以用&&来运行它们。不过有一个方便的模块,叫做 concurrently ,它能同时启动多个进程。
同时,应该在项目内安装命令行的开发工具,比如 webpack、nodemon、gulp、Mocha等,以避免产生 冲突。你可以在bash/zsh的profile中指定像./node_modules/.bin/mocha这样的路径(PATH!):
export PATH="./node_modules/.bin:$PATH"
在一个项目的早期阶段,利用环境变量,可以确保不泄露敏感信息,并且能从一开始就正确构建代码。而且,很多库和框架(我知道其确切的表达)会将信息放在 NODE_ENV 这样的环境变量中修改。把它设置成 production。并把它设置你要的 MONGO_URI 和 API_KEY 的值。你可以创建一个 shell 文件(例如:start.sh),并且添加到 .gitignore:
NODE_ENV=production MONGO_URL=mongo://localhost:27017/accounts API_KEY=lolz nodemon index.js
Nodemon 也有一个配置文件,你可以设置你自己的环境变量( 例子 ):
{ "env": { "NODE_ENV": "production", "MONGO_URL": "mongo://localhost:27017/accounts" }}
强大而聪明的 事件循环 之所以能让Node如此快速和高效是因为它充分利用原本浪费在等待输入和输出任务的所有的时间。 因此,Node非常适合于优化I/O密集型系统。
如果你需要执行某些CPU密集型(例如,计算、密码的哈希码或者数据压缩),那么除了为这些CPU任务启动新的进程之外,你可能想要探索如何使用setImmediate()或者setTimeout()来延期执行任务——它们回调函数中的代码将在下一个事件循环周期继续运行。 注意喽!nextTick()工作在同一个周期,与字面含义相反。
这是一幅来自工作于事件循环上的Bert Belder的图。它非常清楚的展示了事件循环是如何工作的!
JavaScript支持原型继承,即某些对象从其它对象继承。ES6中也加入了 class 运算符。不过它明显比功能继承更复杂。多数Node大量指出后者更为简洁。它通过一个简单的函数工厂模式实现,不需要使用原型、new或者this。当你更新原型的时候不会产生副作用(即导致所有实例改变)因为在功能性继承中每个对象自己有方法拷贝。
看看下面来自 TJ Holowaychuk 的代码。TJ Holowaychuk 是一个天才,作品有 Express、Mocha、Connect、Superagent以及 其它很多Node模块。Express就使用功能性继承 ( 完整源代码 ):
exports = module.exports = createApplication;// ...function createApplication() { var app = function(req, res, next) { app.handle(req, res, next); }; mixin(app, EventEmitter.prototype, false); mixin(app, proto, false); app.request = { __proto__: req, app: app }; app.response = { __proto__: res, app: app }; app.init(); return app;}
客观的说,核心Node模块使用了很多原型继承。如果你也使用这种模式,一定要搞清楚它如何运作。你可以看看这里的 JavaScript 继承模式 。
这一点是显而易见的。好的名字具有文档的作用。你更喜欢哪一个?
const dexter = require('morgan') // ... app.use(dexter('dev')) // When is the next season?
如果只看 app.use(),我不知道 dexter 在干啥。如果使用更有意义的名称会怎么样:
const logger = require('morgan') // ... app.use(logger('dev')) // Aha!
同样的,文件名也必须正确地反映其内部的代码是做什么用的。看看Node的lib目录( GitHub 链接 ),这里所有代码都与平台绑定,你会看到清楚明白的文件/模块名称(即使你对 所有 模块都不熟悉):
events.js fs.js http.js https.js module.js net.js os.js path.js process.js punycode.js querystring.js
内部模块与代码中的方法和变量一样,使用下划线标记(_debugger.js、_http_agent.js、_http_client.js)。这会让开发者警觉:这是一个内部接口,如果你想使用它就得全靠自己——不要因为它被重构甚至被删除而产生抱怨。
嗯?你刚刚读对了吗?但是究竟是什么意思呢?是的。这是正确的。即使使用ES6和ES2016/ES7增加的两个功能,JavaScript仍然有它的quirk。除了JavaScript,还有很多其他可供您或您的团队通过极少设置中获益的选择。根据你的专业水平和应用程序的性质,你可能会选择更好的 TypeScript 或 Flow 提供强大打印功能。另一种极端情况,也存在像 Elm 和 ClojureScript 这种纯虚函数式语言。 CoffeeScript 是另一个伟大的和富有争议的测试选择。你也可以了解下 Dart 2.0 。
当你需要的仅仅是几个宏的话(宏允许使用构建你需要的语言),而不是一个完整的新语言,那么可考虑下 Sweet.js ,它几乎是这么做的 - 允许你编写用于生成代码的代码。
如果你选了非JavaScript路径,请依然包含你所编译的代码,因为一些开发人员可能不太了解你所使用的语言并正确地构建它。例如,VS Code是最大的TypeScript项目之一,也许在Angular 2之后,Code开始使用TypeScript来对Node的核心模块进行类型化。在VS Code repo( 链接 )的vscode/src/vs/base/node/中,你可以看到熟悉的模块名称,例如crypto、process等,但是都包含ts扩展名。在repo中还有其他ts后缀的文件。但是,他们还包括使用原生JavaScript代码的vscode/build。
Express 是个伟大而且成熟的框架。它的光彩来自允许大量其它模块对其行为进行配置。这样说来,你需要知道常用的中件间以及如何使用它。为什么不看看 我的 Express 作弊表 。其中列有我一个主要的中间件。比如,npm i compression -S 会因为减少反应而降低下载速度。logger('tiny')或logger('common')分别会提供更少(dev)或更多(prod)日志。
Node因为其非阻塞式I/O而在异步方面非常伟大,它只需要简间的代码就能保持异步特性,因为就只有一个线程。这使得有机会早期,甚至在写第一行代码的时候,就开始扩大规模。Node 有核心 cluster 模块,它 让你在纵向扩展时不会遇到太多问题。不过,使用像 pm2 或者 StrongLoop 的 集群控制 这样的工具可能是更好的方法。
例如,下面演示如何使用 pm2:
npm i -g pm2
然后你可以对同一个服务启动 4 个实例:
pm2 start server.js -i 4
对于 Docker,pm2 版本2+ 提供了 pm2-docker。所以 Dockerfile 可以这样:
# ... RUN npm install pm2 -g CMD ["pm2-docker", "app.js"]
官方的 Alpine Linux pm2 映像在 Docker Hub 。
这是一条DevOps最佳实践,它允许你从Node实例中获得更多的帮助信息(你可以通过pm2或其他类似的获得更多,见上文)。方法是让Node服务器做app类似的请求、处理数据和执行业务逻辑,并将流量负载到另一个Web服务器(如Apache httpd或Nginx)的静态文件上。 再次,你可能使用Docker进行配置:
FROM nginx COPY nginx.conf /etc/nginx/nginx.conf
我喜欢使用Docker组合来配置多个容器(nginx、Node、Redis、MongoDB)。示例如下:
web: build: ./app volumes: - "./app:/src/app" ports: - "3030:3000" links: - "db:redis" command: pm2-docker app/server.js nginx: restart: always build: ./nginx/ ports: - "80:80" volumes: - /www/public volumes_from: - web links: - web:web db: image: redis
在如今这个开源软件的时代,你没有借口不去学习那些已经开源并且可信的、测试过的源码。你不必在自己的圈子里得到这些(源码)。学无止境,并且我坚信很快我们就从我们将要经历的失败和成功中获得不同的且最好的练习。这是可以保证的。
最后我想写一写关于软件正在如何吞并这个世界以及JavaScript正在如何吞并软件的...它们和每年出现的标准一样的版本,数不胜数的npm模型,工具和会议...但是相反只要是我提过一句的我还是会完成的。
我发现好多人都在追赶下一代的新框架或者语言。这是典型的新鲜对象综合症。他们每周学习新的库以及每个月学习新的框架。他们强迫自己依赖Twitter、Reddit、Hacker News和JS Weekly。他们在JavaScript的世界里频繁使用这些来拖延(自己创造自己的东西)。他们的公共GitHub历史是空空如也的。
学习新的东西是好事情,但是不要混淆了实际的“建筑原料”。那些事物、那些支付你薪水的才是实际的“建筑物”。停止过度的制造。你不是在制造下一个Facebook。(不论是)Promises vs。(还是)generator vs。异步等待对我来说都是有争议的,因为等到有人在讨论中回答了线程的问题的(时候),我已经写好了我的回调(函数)(并且使用CoffeeScript来以比ES5/6/7快2倍的速度完成(任务))
最后,最好的练习就是实践,最最好的就是去掌握最本质的(知识)。阅读源码,在代码中尝试新的东西并且最重要的就是你自己写大量的代码。现在,在这个点上,停止阅读,去实际操作吧!