在生产中部署 ES2015+ 代码

时间:2022-12-30 07:07:45

大多数 Web 开发人员都喜欢编写具有所有最新语言特性的 JavaScript——async/await、类、箭头函数等。然而,尽管事实上所有现代浏览器都可以运行 ES2015+ 代码并原生支持我刚才提到的特性 , 大多数开发人员仍然将他们的代码转换为 ES5 并将其与 polyfills 捆绑在一起,以适应仍在使用旧版浏览器的一小部分用户。

这有点糟糕。 在理想情况下,我们不会写不必要的代码。

使用新的 JavaScript 和 DOM API,我们可以有条件地加载 polyfill,因为我们可以在运行时检测它们的支持。 但是对于新的 JavaScript 语法,这要复杂得多,因为任何未知的语法都会导致解析错误,然后所有代码都不会运行。

虽然我们目前没有一个好的解决方案来检测新语法的特性,但我们现在有办法检测基本的 ES2015 语法支持。

解决方案是 <script type="module">

大多数开发人员认为 <script type="module"> 是加载 ES 模块的方式(当然这是真的),但是 <script type="module"> 还有一个更直接和实用的用例——加载常规 JavaScript 具有 ES2015+ 特性并且知道浏览器可以处理的文件!

换句话说,每个支持 <script type="module"> 的浏览器也支持你所知道和喜爱的大部分 ES2015+ 特性。 例如:

  • 每个支持 <script type="module"> 的浏览器也支持 async/await
  • 每个支持 <script type="module"> 的浏览器也支持类。
  • 每个支持 <script type="module"> 的浏览器也支持箭头功能。
  • 每个支持 <script type="module"> 的浏览器也支持 fetch、Promises、Map、Set 等等!

剩下要做的唯一一件事就是为不支持 <script type="module"> 的浏览器提供回退。 幸运的是,如果我们当前正在生成代码的 ES5 版本,那么我们已经完成了这项工作。 我们现在只需要生成一个 ES2015+ 版本!

本文的其余部分解释了如何实现此技术,并讨论了发布 ES2015+ 代码的能力将如何改变我们编写模块的方式。


实现

如果你现在已经在使用像 webpackrollup 这样的模块打包器来生成你的 JavaScript,你应该继续这样做。

接下来,除了我们当前的捆绑包之外,我们将生成第二个捆绑包,就像第一个捆绑包一样; 唯一的区别是你不会一直转译到 ES5,也不需要包含遗留的 polyfill。

如果我们已经在使用 babel-preset-env(应该使用),则第二步非常简单。 你所要做的就是将你的浏览器列表更改为仅支持 <script type="module"> 的浏览器,Babel 将自动不应用它不需要的转换。

换句话说,它将输出 ES2015+ 代码而不是 ES5。

例如,如果我们正在使用 webpack 并且我们的主脚本入口点是 ./path/to/main.mjs,那么我们当前的 ES5 版本的配置可能看起来像这样(注意,我将这个包称为 main.mjs)。 es5.js 因为它是 ES5):

module.exports = {
  entry: './path/to/main.mjs',
  output: {
    filename: 'main.es5.js',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [{
      test: /\.m?js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['env', {
              modules: false,
              useBuiltIns: true,
              targets: {
                browsers: [
                  '> 1%',
                  'last 2 versions',
                  'Firefox ESR',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};

要制作一个现代的 ES2015+ 版本,我们所要做的就是进行第二个配置并将您的目标环境设置为仅包括支持 <script type="module"> 的浏览器。 它可能看起来像这样(注意,我在这里使用 .mjs 扩展名,因为它是一个模块):

module.exports = {
  entry: './path/to/main.mjs',
  output: {
    filename: 'main.mjs',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [{
      test: /\.m?js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['env', {
              modules: false,
              useBuiltIns: true,
              targets: {
                browsers: [
                  'Chrome >= 60',
                  'Safari >= 10.1',
                  'iOS >= 10.3',
                  'Firefox >= 54',
                  'Edge >= 15',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};

运行时,这两个配置将输出两个可用于生产的 JavaScript 文件:

  • main.mjs(语法为 ES2015+)
  • main.es5.js(语法为 ES5)

下一步是更新我们的 HTML 以在支持模块的浏览器中有条件地加载 ES2015+ 包。 我们可以使用 <script type="module"><script nomodule> 的组合来做到这一点:

<!-- Browsers with ES module support load this file. -->
<script type="module" src="main.mjs"></script>

<!-- Older browsers load this file (and module-supporting -->
<!-- browsers know *not* to load this file). -->
<script nomodule src="main.es5.js"></script>

注意 :我们已经更新了本文中的示例,以对我作为模块加载的任何文件使用 .mjs 文件扩展名。 由于这种做法相对较新,如果我不指出在使用它时可能遇到的一些问题,那我们就是失职了:

  • 我们的 Web 服务器需要配置为使用 Content-Type 标头 text/javascript 提供 .mjs 文件。 如果我们的浏览器无法加载 .mjs 文件,这可能就是原因。
  • 如果我们使用 Webpack 和 babel-loader 来捆绑 JavaScript,我们可能已经复制/粘贴了一些仅转译 .js 文件的配置代码。 将配置中的正则表达式从 /\.js$/ 更改为 /\.m?js$/ 应该可以解决我们的问题。
  • 较旧的 webpack 版本不会为 .mjs 文件创建 sourcemap,但自 webpack 4.19.1 以来,此问题已得到修复。

重要注意事项

在大多数情况下,这种技术“有效”,但在实施这种策略之前,有一些关于如何加载模块的细节很重要,需要注意:

  1. 模块像 <script defer> 一样加载,这意味着它们在文档被解析之前不会被执行。 如果我们的某些代码需要在此之前运行,最好将该代码拆分出来并单独加载。
  2. 模块总是在严格模式下运行代码,因此如果出于任何原因您的任何代码需要在严格模式之外运行,则必须单独加载它。
  3. 模块对待* var 和函数声明的方式与脚本不同。 例如,在脚本中 var foo = 'bar'function foo() {…} 可以从 window.foo 访问,但在模块中情况并非如此。 确保我们不依赖代码中的这种行为。

警告 ! Safari 10 不支持 nomodule 属性,但我们可以通过在使用任何 <script nomodule> 标记之前在 HTML 中内联一段 JavaScript 片段来解决这个问题。 (注意:这已在 Safari 11 中修复)。


是时候开始将我们的模块发布为 ES2015 了

目前该技术的主要问题是大多数模块作者不发布其源代码的 ES2015+ 版本,他们发布转译后的 ES5 版本。

现在可以部署 ES2015+ 代码了,是时候改变它了。

我完全理解这对不久的将来提出了许多挑战。 今天大多数构建工具都会发布文档,推荐假定所有模块都是 ES5 的配置。 这意味着如果模块作者开始将 ES2015+ 源代码发布到 npm,他们可能会破坏一些用户的构建并且通常会引起混淆。

问题是大多数使用 Babel 的开发人员将其配置为不在 node_modules 中转换任何内容,但是如果模块是使用 ES2015+ 源代码发布的,这就是一个问题。 幸运的是,修复很容易。 我们只需从构建配置中删除 node_modules 排除项:

rules: [
  {
    test: /\.m?js$/,
    exclude: /node_modules/, // Remove this line
    use: {
      loader: 'babel-loader',
      options: {
        presets: ['env']
      }
    }
  }
]

不利的一面是,如果像 Babel 这样的工具除了本地依赖项之外还必须开始转译 node_modules 中的依赖项,那么构建速度会变慢。 幸运的是,这个问题可以在一定程度上通过持久的本地缓存在工具级别得到解决。

不管我们在 ES2015+ 成为新的模块发布标准的道路上可能会遇到什么坎坷,我认为这是一场值得一战的斗争。 如果我们作为模块作者,只将我们代码的 ES5 版本发布到 npm,我们就会将臃肿和缓慢的代码强加给我们的用户。

通过发布 ES2015,我们给了开发者一个选择,最终让每个人都受益。