评论

Hello! Bundling for node , webpack , rollup and esbuild

nodejs项目打包的初步介绍

序言

古有孔子曰: 茴香豆 的 字有种写法

本文主旨并不在于使用大量不同的打包工具,比较这玩意嘛,不在于多,而在于哪个可以使用更少的成本来达到我们的目的

本文代码运行环境如下

打包的项目示例以 expresskoa 为主

正文

先说一下,本篇文章打包的都是 nodejs 项目,不是前端页面

前端页面打包成 dist 部署,很好理解, spa csr 嘛

而 ssr 的 多入口打包服务端渲染客户端激活 也容易理解

那么为啥要打包 nodejs 项目呢? 有必要吗 ? 这取决与我们自身遇到的场景

普通部署场景

让我们从 nodejs 的部署开始讲起

先来一张非常有名的图

相信无论是前端还是 nodejs 开发人员,看到这张图都会 会心一笑

什么造成了这种原因?

原因在于强大又门槛低的 npm:

  • npm 包自身依赖可以层层依赖,深度非常高
  • npm 包作者不按照规范,发布时传了很多垃圾进去 , 现有的 prune 算法也无法做有效的清理
  • 更不用说 .bin/binary , 还有一些包在安装完成的 npm hook 里去下载大文件了(说的就是你 puppeteer )

这些都间接造成了 node_modules 又大又深,即使后来 npm 更新了,做了一个 flat 的结构,然而点开后,还是要滚很久(笑~)

我们平常部署nodejs项目:

  • 纯 cjs runtime , 直接在线上环境拉代码,yarn --production , 然后直接 node (docker同理)
  • ts nodejs , 本地调试 ts-node , 线上需要 yarndevDependenciesdependencies 都要安装进来,才可以 tsc

那么,我们干脆用前端的思路去进阶一下:

最基础的,就是使用 webpack/gulp 这类的去对 ts nodejs 做一层代码转化,同时整理相对应的资源

然而,这种添加的 fake compile time 大部分,还是以自己的项目为主,很少有会去打包 node_modules

这当然有考量,毕竟 node_modules 里的东西不可控, 除了 js ,里面还有很多其他语言的玩意和二进制文件

前端还好,除了 wasm , 其他基本都是 js, nodejs 包就丰富多了,cpp,py,rs,go 等等,简直就是个动物园,一旦破坏了目录结构,代码在 子进程 , 文件流 等等的路径没有转化,对应文件没有处理,就很容易产生意料不到的错误。

不过在部分 node_modules 可控的场景下,还是有必要对其进行打包

Serverless 场景

通常我们部署使用的是 layer + cloud function 的方式

通常,我们把 node_modules 打成 layer

自个的业务代码做成 cloud function , 并做一个关联绑定

在这样的场景下,就让我们的打包工具出厂,来帮助我们解决除开 builtin-modules 的第三方依赖了

webpack

先上一个 webpack tree shaking 的文档

Webpack Tree shaking info

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e. import and export. The name and concept have been popularized by the ES2015 module bundler rollup.

关键句 It relies on the static structure of ES2015 module syntax

这意味着 webpack 5.x 不对 cjs 模块做 tree shaking 了

简单的配置项在此

webpack 默认 nodejs inject:

大体上和前端方面类似 (调试方面,我都开了source-map, 可以直接在 vscode 编译前的源码里加断点)

rollup

Rollup 里提到了一句

Even though this algorithm is not restricted to ES modules, they make it much more efficient as they allow Rollup to treat all modules together as a big abstract syntax tree with shared bindings.

然而在 tree-shaking issues 里,我也没有找到 针对 cjs 比较好的 tree-shaking 方案

rollup 默认 nodejs inject:

从图上可见,所有的包,经过处理之后,都有 default 或者在 default

原因自然也是因为 rollup 主要的设计就是给 esm 用的

不过我自个在对应的配置项,比较喜欢 cjs 来写,而不是官网示例的 esm

这样可以方便使用 nodejs api 直接调试

配置项在此

esbuild

之前在 umivite 已经体验过这位 go 大神了

esbuild 默认 nodejs inject:

配置项在此

从图上看也是很有意思的:

var __esm = (fn, res) =>{
  return () => {
    return (fn && (res = fn(fn = 0)), res);
  } 
}
var __commonJS = (cb, mod) =>{
  return () => {
    return (mod || cb((mod = {exports: {}}).exports, mod), mod.exports);
  } 
} 

从生成出来的代码来看, 通过这两个方法对引入的模块,进行标记加闭包处理

  • esm => init_[module-name]
  • cjs => require_[module-name]

而产生的新的函数,则是真正的导入方法

同时,在代码中,假如使用 ES6 的方式导入的话,还会对模块做一次 __toModule 的处理

var __toModule = (module2) => {
  return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? {get: () => module2.default, enumerable: true} : {value: module2, enumerable: true})), module2);
};

相当于给包打扮打扮,说我就是 esm 包

而使用 cjs 导入,就没有这一层的步骤

值得一说的还有 esbuildimport() 的处理, 默认是做成 inline

// esbuild
var dynamic_exports = {}
if (condition) {
  Promise.resolve().then(() => (init_dynamic(), dynamic_exports)).then(function(m) {
    // do some thing
  });
}
// rollup 
if (condition) {
  Promise.resolve().then(function () { return require('./dynamic-[hash:8].js'); }).then(function (m) {
    // do some thing
  });
}

// webpack 
if (condition) {
  __webpack_require__.e(/*! import() */ "src_common_dynamic_js").then(__webpack_require__.bind(__webpack_require__, /*! ./dynamic.js */ "./src/common/dynamic.js")).then(function (,) {
    // do some thing
  })
}

当然这也毕竟,在 esbuild 的官方文档上的 Splitting 章节

Code splitting is still a work in progress. It currently only works with the esm output format. There is also a known ordering issue with import statements across code splitting chunks. You can follow the tracking issue for updates about this feature.

个人还是很看好这个项目的长期发展的。

Footer

在这个示例里 webpack , rollup , esbuild 配置项都非常简单

在这里为了阅读起来方便,也没有加 babel / ts 这些玩意和静态资源的处理

本文也只是初步介绍一下,这三样工具打包nodejs的方式,有兴趣的可以多交流一下。

附之前写的一篇 rollup 打包微信云开发的一篇文章:

抛砖引玉:一种改善微信云开发 , 开发者体验的思路

附录

最后一次编辑于  2021-04-09  
点赞 0
收藏
评论
登录 后发表内容