不同模块机制,不同平台环境以及是否需要转译
问题背景
之前给webpack-cdn-plugin提PR时,遇到这样一个问题,入口文件使用了部分 ES6 特性,而作者不想直接提供转译版本,这就导致在低版本 Node.js 环境无法直接运行。
首先想到的办法是,能不能提供多个版本。但是package.json
中的main
只能支持单文件,无法提供多入口供使用者根据自身环境选择。
后来采用的做法是在入口文件中判断当前运行环境,使用动态require
来选择转译或非转译版本。等于说模块开发者需要判断使用者当前的运行环境,总感觉不太优雅。
前段时间刚好看到这一系列文章,里面针对这个问题给出了较为全面的解决方案。
多种模块格式版本
我们都知道模块格式包括了以下几种:
- AMD 浏览器端异步模块机制
- CJS Node.js同步模块机制,浏览器端想使用必须先使用类似 webpack 之类的工具编译成异步的
- ESM ES6提出的内置机制,支持同步和异步,部分浏览器已支持,Node.js 计划2018年实现
作为模块提供者,我们自然希望兼容更多模块格式,还好 UMD 提供了一系列兼容 AMD 和 CJS 的方案,例如支持在 Node.js 中写 AMD 标准的代码。这些模版范式为模块提供者提供了极大便利。
是时候说说 ESM 了。它提供了统一的import/export
,真正统一了浏览器和 Node.js 端的模块标准。而且配合 Webpack 和 Rollup 提供的 Tree-shaking 技术可以最大程度精简代码。那么问题来了,在 ESM 一统天下之前,如何提供使用 ESM 编写的代码给先进的打包工具,同时又不至于完全失去兼容性。换句话说,package.json
中真的只有main
这一个暴露模块入口文件的属性吗?
显然不是,来看看这两个属性吧:
- module,还处于提案阶段,提供使用 ESM 编写的版本
- browser,提供仅针对浏览器环境版本
对于打包工具来说,就不能只考虑main
入口了。
在 webpack 中,可以通过resolve.mainFields
来指定模块查找优先级。
而这个优先级又是根据目标环境确定的,例如针对浏览器环境,即target: 'web'
,默认的优先级为:["browser", "module", "main"]
。而服务端渲染中常用的target: 'node'
查找优先级为["module", "main"]
。可以看出,webpack 会优先使用 ESM 标准的代码。
看起来支持不同模块格式的问题解决了。
转译和非转译版本
除了支持不同的模块格式,代码中使用了新特性,是否需要转译也是一个问题。
Angular 提出在package.json
中新增es2015
属性,用来指定使用 ES6 的非转译代码入口。
当然,最好能参考 babel-preset-env 的做法,也根据运行环境决定是否需要使用转译版本的代码。
babel-preset-env
首先看看babel-preset-env
是如何使用的。针对浏览器环境:
"babel": {
"presets": [
[
"env",
{
"targets": {
"browsers": ["last 2 versions", "ie >= 7"]
}
}
]
]
}
针对 Node.js 环境:
"babel": {
"presets": [
[
"env",
{
"targets": {
"node": "current"
}
}
]
]
}
其他重要的参数包括:
- modules 默认CJS,不转译
false
- useBuiltIns 使用polyfill,注入类似
import "core-js/modules/es7.string.pad-start";
的代码
esnext
增加一个新的入口esnext
。直接提供支持 stage4 以上特性的代码,不使用转译,使用ESM:
{
···
"main": "main.js",
"esnext": {
"main": "main-esnext.js",
"browser": "browser-specific-main-esnext.js"
},
···
}
具体做法是:
- 对于模块开发者,提供非转译版本的
esnext
- 对于使用者,通过
resolve.mainFields
将esnext
加入,赋予最高优先级,同时告知 babel-loader 转译提供esnext
的代码。剩下的就交给babel-preset-env根据提供的配置环境自动引入需要的插件。
其他做法
- 全部转译,耗时但是配置简单。webpack 插件大多采用这种方式。
- 配置 babel-loader,只转译提供了 module 字段的依赖
- 根据文件后缀决定,
.js
不转译,.esm
转译
总结
作为模块提供者,我会采用esnext
的方式,即额外提供非转译版本。然后让使用者通过配置,根据自身运行环境选择使用不同的版本。