Babel 还是 Node 开发的“必需品”吗?
现在做 Node 开发还需要“麻烦”的 Babel 吗?毋庸置疑,Babel 曾经对构建和开发 Node.js 应用程序有过很大的影响,但随着 Node.js 的原生功能不断强大,Babel 或许也不再是 Node 开发的“必需品”。本文将主要介绍关于如何在 Node 开发中摆脱 Babel 的方法。
如果你是 Node.js 资深开发人员,乃至涉足了 React 或 Vue.js 等前端库,那么不用说你很有可能跟 Babel 打过交道。 Babel 最初曾是 Reddit 上的一个不起眼的项目,但现在已经发展得如此壮大,甚至从根本上改变了我们构建和开发 Node.js 应用程序的方式。
很难准确地形容 Babel 的影响力到底有多大,因为现在它被拆分成了许多小包的形式,但只要看看 npm 的 @Babel/core 包就足见一斑了(提示:它一周的下载量差不多有 800 万次,而 React 才不过 500 万而已!)。
Babel 的确取得了惊人的成就,但它也在某些方面很让人胃疼。首先,现在你得在你的应用程序或库中引入一套构建系统。虽然这本身没那么可怕,但这样做确实带来了许多额外的复杂性和问题:你有没有同时打包兼容 ES 和 ES20XX 版本的库呢?你想要输出到 ECMAScript 规范的哪一个“阶段”?此外我个人觉得最典型的例子就是,你当前的工具集怎样与它配合呢(调试之类的事情)?
当然,我们不能忘了我们的源映射(source maps)老朋友,我们可以用它智能地从已转换的代码逆向到源代码上。如果你正在同时为浏览器和 Node.js 构建,那么事情就更复杂了,因为你还必须为浏览器打包一个版本——啊,好麻烦!
但我要说的是,也许你根本就用不着 Babel。以前只有 Babel 才有的许多酷炫的玩意儿现在都成了 Node.js 的原生功能,也就是说你可以省掉许多依赖项和构建步骤,甚至用不着第三方系统帮你做自动编译了。
认真读完这篇文章后,我希望大家能和我达成共识,看到 Node 开发的“复兴”时代就要来临了——我们不再需要什么构建系统,自然也用不着 Babel!
摆脱 Babel 的第一步:处理模块
JavaScript 开发中比较让人头疼的一部分就是它的模块系统。有些人可能不太熟悉这块,比如说你可能会在 Web 上看到很多这样的语法:
复制代码
exportconstdouble =(number) =>number*2; exportconstsquare =(number) =>number*number;
但要在 Node 中运行上面的代码却不加任何类型的 Babel-ifying(或标志),就会出现以下错误:
复制代码
export const double = (number) => number * 2; ^^^^^^ SyntaxError: Unexpected token export
有很多年经历的开发人员可能会回想起 requirejs 和 commonjs 语法流行的时代,彼时和我们现在处理 commonjs 和 ECMAScript 模块语法的方式非常相似。
但是如果你在用 Node 比较新的版本——哪怕是 8.0 也行——那么无需任何转换或 Babel 也能用 ECMAScript 模块。你只需使用 –experimental-modules 开关启动你的应用程序即可:
复制代码
node--experimental-modulesmy-app.mjs
当然,最关键的是要注意——至少在版本 8 和 10 中——你的文件必须用 mjs 这个扩展名,表明它们是 ECMAScript 模块而不是 CommonJS。在 Node 12 中情况要好得多,只需将一个新属性附加到你的应用程序(或库)的 pacakge.json 即可:
复制代码
//package.json { "name":"my-application", "type":"module"//RequiredforECMASCript modules }
在 Node.js 12 及更高版本上使用 type 方法时,它还有一个额外的好处,就是加载的所有依赖项都支持 ECMAScript 模块。因此随着越来越多的库迁移到“原生”JavaScript,你用不着再担心当不同的库打包不同的模块系统时如何处理 import 或 require 了。
可以在 Node 的文档站点上阅读 更多信息 。
摆脱 Babel 的第二步:使用现代化的异步控制流程
如果你一直在愉快地使用 Node.js 中更现代化的异步控制流方法(名为 Promise 和搭配它们的 async/await),一个好消息是它们自 Node 8 以来就获得了原生支持!
良好的控制流(特别是针对并行发出请求等操作)是编写快速且可维护的 Node 应用程序的关键所在。要在 Node 8 中使用像 Promise 或 await 这样的东西,其实你什么都用不着准备:
复制代码
// log.js asyncfunctiondelayedLogger(...messages){ returnnewPromise((resolve) =>{ setImmediate(()=>{ console.debug(...messages); resolve(true); }); }); } asyncfunctiondoLogs(){ delayedLogger('2. Then I run next!'); console.log('1. I run first!'); awaitdelayedLogger('3. Now I run third because I "await"'); console.log('4. And I run last!'); } doLogs();
下面这个例子现在很容易就能实现:
复制代码
nodelog.js
用不着特殊的开关,也不用更新你的 package.json——直接就能搞定!不仅如此,你甚至可以使用这些原生 Promise 尝试捕获未捕获的异常,万一你的应用程序出现问题也能即时发现:
复制代码
process.on('unhandledRejection', (reason, promise) => { console.log('Unhandled Rejection at:', promise,'\nMessage:', reason); }); asyncfunctionwillThrowErrors(){ returnnewPromise(functionshouldBeCaught(resolve, reject){ reject('I should be caught and handled with!'); }); } willThrowErrors();
虽说这很好用,但如果我们需要深入了解异步调用堆栈并查看抛出的内容和背后的机制,有时就会很让人头疼。要启用异步堆栈跟踪,你需要升级到 Node 12 并对特定版本使用 –async-stack-traces 开关。
成功启用后,你就可以更容易地推断出错误的来源,并找出问题的根源所在。举个例子,像下面这样的程序很难看出它到底是怎么出错的:
复制代码
// app.js asyncfunctionsleep(num){ returnnewPromise((resolve) =>{ setTimeout(resolve, num); }); } asyncfunctionexecute(){ awaitsleep(10); awaitstepOne(); } asyncfunctionstepOne(){ awaitsleep(10); awaitstepTwo(); } asyncfunctionstepTwo(){ awaitsleep(10); awaitstepThree(); } asyncfunctionstepThree(){ awaitsleep(10); thrownewError('Oops'); } execute() .then(()=>console.log('success')) .catch((error) =>console.error(error.stack));
在 Node 10 中运行它将返回以下跟踪:
复制代码
$ node temp.js --async-stack-traces Error:Oops at stepThree (/Users/joelgriffith/Desktop/app.js:24:11)
如果我们切换到 Node 12 上就能获得更好的输出,可以清楚地看到调用的结构:
复制代码
$ node temp.js --async-stack-traces Error:Oops at stepThree (/Users/joelgriffith/Desktop/temp.js:24:11) at async stepTwo (/Users/joelgriffith/Desktop/temp.js:19:5) at async stepOne (/Users/joelgriffith/Desktop/temp.js:14:5) at async execute (/Users/joelgriffith/Desktop/temp.js:9:5)
摆脱 Babel 第三步:留下语法糖!
Babel 的一大好处就是它从 ES6 开始这么多年积累的一大堆出色的语法糖。有了这些便利,我们就能用更易读和更简洁的方式执行常用操作。但更让我高兴的是,自 Node 的第 6 版以来,大多数语法糖都能直接用了。
我最喜欢的例子之一是解构赋值。这个小捷径做出的下面这种效果更容易理解,并且不需要任何构建系统就能在 Node 中正常运作:
复制代码
constletters = ['a','b','c']; const[a, b, c] = letters; console.log(a, b, c);
如果你只关心第三个元素,那么下面这种代码也能用,就是看起来有点难看。
复制代码
conststuff = ['boring','boring','interesting']; const[,, interesting] = stuff; console.log(interesting);
提到语法糖,对象解构也是开箱即用的:
复制代码
constperson = { name:'Joel', occupation:'Engineer', }; constpersonWithHobbies = { ...person, hobbies: ['music','hacking'], }; console.log(personWithHobbies);
不过对象解构需要 Node 8 以上版本,而数组解构最早获得支持的版本是 Node 6。
最后,现在 Node 6 及以上版本完整支持默认参数了(这是这个语言以前缺少的重要功能)。它会省去程序中(以及 Babel 的转换输出)的大量 typeof 检查,因此你可以执行以下操作:
复制代码
functionmessageLogger(message,level= 'debug>'){ console.log(level, message); } messageLogger('Coolitworks!'); messageLogger('Andthisalsoworks', 'error>');
其实 Node 中能用的东西太多了,上面举的这些例子也不过是皮毛而已:此外模板字面量、反引号(多行字符串),胖箭头,甚至是 class 关键字都准备好了。
别急,还有呢!
摆脱不必要的依赖项是提高应用程序安全性和可维护性的一个好办法。你不再需要依赖外部维护的软件,不需要等待生态系统进化,于是就能更快地前进。除此之外,移除 Babel 后你实际上也在部署更易读的代码。
例如,Babel 有时会在程序文件的开头注入大量的 polyfill。虽然这些帮助程序在大多数情况下完全无害,但它可能会把新手或不熟悉这种代码的人们绕糊涂了。这条规律一般来说没错:如果新手会被某件事物弄糊涂,那么它可能就不应该加到你的项目里。
如果有人正在使用你的软件包,想要确定问题是来自你的代码还是来自转换器注入的帮助程序就更复杂了。如果你的最终输出中注入的代码没那么多,你也能更好地理解正在构建的程序底层是怎样的结构。
不管是 Babel 也好,其他依赖项也罢,要采用或者抛弃它们都要考虑一点,那就是责任。不管什么时候,只要你引入了没有亲自阅读或了解过的代码,都可能会出一些意料之外的问题。比如说依赖关系太复杂导致 npm install 速度缓慢,比如说模块需要在线打猴子补丁而导致程序启动缓慢,又比如说出现问题却没能正确报告……为了避免这些麻烦,不用 Babel 这样的包可能是最好的。
引入新的模块或构建过程不单单是我们个人的事情,更是我们团队和项目要解决的问题,所以我希望你们更多地把它当作是一种责任(维护它、升级它,并意识到使用它的后果) ,而不仅仅是当成随手拿来用的工具。
最后,为什么你可能还是要用 Babel 呢
虽然 Node 已经进步了这么多,但有时你可能还是要用 Babel 才行。如果你想体验规范中“最新和最好的”那部分,那么 Babel 是你唯一的选择。如果你想无需改动整个构建管道就使用 TypeScript,那么用 Babel 也能做到。
有时 Babel 的代码实际上比 Node 原生方法更快。通常来说这是源于 Node 维护者必须处理的一些边缘情况,这些情况 Babel 不一定要考虑。再过几年,我相信 Node 的性能优势会覆盖所有层面,但是新功能往往会比用户手里的实现慢很多。
最后,如果你需要向 Web 浏览器交付代码,那么在可预见的未来你可能还得继续使用 Babel。像 React 这样的库以及其他用来实现或增强语言的库总归需要一种方法来转换为浏览器可理解的代码。
但如果你的用户群主要使用的是现代化的浏览器,那么放弃构建系统就是利大于弊的,能显著缩小程序体积。这不仅能加快页面加载,而且还能显著提升性能表现,因为哪怕额外的 1KB 内容也可能花费大量时间来处理,毕竟每个字节在执行之前都需要解析和验证!
我希望本文能帮助你编写更好、更快、更安全的 Node.js 应用程序——而且不用 Babel 也能写出很多功能来!