不同模块机制,不同平台环境以及是否需要转译

问题背景

之前给webpack-cdn-plugin提PR时,遇到这样一个问题,入口文件使用了部分 ES6 特性,而作者不想直接提供转译版本,这就导致在低版本 Node.js 环境无法直接运行。

首先想到的办法是,能不能提供多个版本。但是package.json中的main只能支持单文件,无法提供多入口供使用者根据自身环境选择。

后来采用的做法是在入口文件中判断当前运行环境,使用动态require来选择转译或非转译版本。等于说模块开发者需要判断使用者当前的运行环境,总感觉不太优雅。

前段时间刚好看到这一系列文章,里面针对这个问题给出了较为全面的解决方案。

多种模块格式版本

我们都知道模块格式包括了以下几种:

作为模块提供者,我们自然希望兼容更多模块格式,还好 UMD 提供了一系列兼容 AMD 和 CJS 的方案,例如支持在 Node.js 中写 AMD 标准的代码。这些模版范式为模块提供者提供了极大便利。

是时候说说 ESM 了。它提供了统一的import/export,真正统一了浏览器和 Node.js 端的模块标准。而且配合 Webpack 和 Rollup 提供的 Tree-shaking 技术可以最大程度精简代码。那么问题来了,在 ESM 一统天下之前,如何提供使用 ESM 编写的代码给先进的打包工具,同时又不至于完全失去兼容性。换句话说,package.json中真的只有main这一个暴露模块入口文件的属性吗?

显然不是,来看看这两个属性吧:

对于打包工具来说,就不能只考虑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"
                }
            }
        ]
    ]
}

其他重要的参数包括:

esnext

增加一个新的入口esnext。直接提供支持 stage4 以上特性的代码,不使用转译,使用ESM:

{
    ···
    "main": "main.js",
    "esnext": {
        "main": "main-esnext.js",
        "browser": "browser-specific-main-esnext.js"
    },
    ···
}

具体做法是:

其他做法

  1. 全部转译,耗时但是配置简单。webpack 插件大多采用这种方式。
  2. 配置 babel-loader,只转译提供了 module 字段的依赖
  3. 根据文件后缀决定,.js不转译,.esm转译

总结

作为模块提供者,我会采用esnext的方式,即额外提供非转译版本。然后让使用者通过配置,根据自身运行环境选择使用不同的版本。