为什么我从 Webpack 转向了 Snowpack?

2,018 阅读9分钟

Webpack 有何劣势?

很长一段时间里,Webpack 几乎成为了前端代码打包的标准,它拥有灵活的打包配置、可拓展的插件机制、大量官方提供的插件用以支持不同类型的文件。Webpack 通过模块之间的依赖关系顺藤摸瓜,将所有类型的文件例如 JavaScript、CSS、图片、字体文件等都包含进来,构建了一个依赖关系图,最后将他们打包进可以在浏览器中运行的 bundle 里。

image.png

Webpack 是一个很优秀很强大的打包工具,但是其陡峭的学习路线、复杂的配置也一直为人所诟病。更重要的是,当我们修改了某个文件的代码时,即使只是一个很小的改动,Webpack 都需要重新构建、重新打包整个项目。尤其随着项目的复杂度上升,项目变得越来越庞大,Webpack 打包也越来越慢,我们宝贵的开发时间就在改代码——保存——等待浏览器页面刷——新——……的循环中浪费掉了。

时间倒回到 2014 年,Webpack 横空出世,彼时 ES modules 还不存在。随着 Webpack 的流行,我们脑海里逐渐形成了一个根深蒂固的观念,即:项目代码需要一个现代打包工具来构建我们的应用。但事实并非一直如此。如今,随着现代浏览器对 ES modules 的广泛支持,我们终于有了别的选择——Vite 或者 Snowpack。

(如果对模块规范还不是很清晰,可以参考我的另一篇文章:CommonJS和ES6 Module 模块规范原理浅析

Snowpack 是如何工作的

当改动某个文件时,以 Webpack、Parcel 为代表的传统 JavaScript 构建工具需要重新构建、打包整个项目的代码,等在浏览器看到这次改动时,需要一定的时间。而 Snowpack 在开发过程中不会打包文件,每个文件只会构建一次,然后永久缓存。当一个文件发生改动时,Snowpack 只会重新构建这个文件,所以浏览器会立即更新。

image.png

当然,Snowpack 也支持生产环境构建时打包文件,官方提供了 Webpack 和 Rollup 插件(截止 Snowpack v3.8.2 时 Rollup 插件还在支持中)。由于 Snowpack 会事先构建和处理,所以不需要再为打包器提供复杂的打包配置。

总结来说 Snowpack 的优势是:

  1. 快,并且不会随着项目体积的增大而变慢;
  2. 开发模式不打包,生产模式可以打包并优化。

Snowpack 默认支持支持 JSX 和 TS 源码,还可以通过插件来集成其他工具:TypeScript、Babel、Vue、Svelte、PostCSS、Sass 等。

不打包的开发模式(Unbundled Development)

也就是说在开发过程中,Snowpack 将一个个文件各自独立地传输到浏览器中。这些文件可以使用 Babel、TypeScript、Sass 等工具构建后,独立加载到浏览器中。能这样做都归功于 ESM 的 import 和 export 语法。任何时候改动某个文件,Snowpack 只会重新构建那个文件。利用 ESM 来进行 unbundled development 的能力带来了几乎即刻的单文件构建,只花费 10 ~ 25 毫秒就能在浏览器加载并更新。

与此相对的就是打包的开发模式。现今几乎所有流行的 JavaScript 构建工具都是针对打包的开发模式的,每次变更、保存后,在浏览器中可以看到之前,必须和所有剩下的代码一起重新打包。这会给开发工作流中引入额外的工作,增加了复杂度。但既然 ESM 现在早已被广泛支持,这种方式就不需要了。

和传统的打包的开发模式相比,不打包的开发模式有以下几个好处:

  • 单个文件的构建是很快的;
  • 单个文件的构建结果是确定的;
  • 单个文件更容易 debug;
  • 项目大小不会影响 dev 速度;
  • 独立的文件能更好地缓存。

最后一点十分关键:每个文件独立构建并无限期缓存。只要某个文件没有发生变更,就永远不会被再次构建,浏览器也永远不需要再一次下载该文件。这就是不打包的开发模式真正的厉害之处。

Snowpack 的 dev server 只有在浏览器请求一个文件时,才会构建这个文件。这就意味着,Snowpack 可以瞬间(50 毫秒内)启动,然后慢慢扩展到无限大的项目,而不会降低速度。相反,30 秒以上的启动时间在传统模块打包器构建大型项目时很常见。

Snowpack 提供了一个快速创建项目的脚手架工具 Create Snowpack App (CSA)(node 版本需要 v14.17.0 及以上,这里有个坑:相关 issue),默认启用 HMR。还为各大前端框架提供了一些官方的项目创建模板,例如创建一个 React 项目就可以使用以下模板:

npx create-snowpack-app react-snowpack --template @snowpack/app-template-minimal

此外也可以通过插件启用 Fast Refresh。Fast Refresh 是一个框架相关的 HMR 增强,它用改变时保留组件状态的方法应用单文件更新。Fast Refresh 让开发更快速,尤其在开发弹出窗口和其他通常需要点击重新打开或重新访问的二级视图状态时。Fast Refresh 可以通过以下插件自动启用:

node_modules 的处理

通常我们在代码中这样引入一个 npm 包:

import xxx from 'some-package';

一方面,'some-package' 没有文件扩展名,是一个无效的 URL,浏览器无法识别;另一方面 npm 包发布时通常会使用 CommonJS 模块规范,因此如果不经过构建处理过程是无法在浏览器上直接运行的。就算项目本身是用浏览器原生支持的 ESM 写的,可以直接在浏览器中运行,但只要引入任何一个 npm 包就会打回到打包的开发模式。

Snowpack 是这么做的:相比起只为了这一种需求就打包整个项目,Snowpack 会单独处理依赖。看看Snowpack 是如何处理的:

node_modules/react/**/*     -> http://localhost:3000/web_modules/react.js
node_modules/react-dom/**/* -> http://localhost:3000/web_modules/react-dom.js
  1. Snowpack 扫描项目中所有用到的包;
  2. Snowpack 从 node_modules 目录中读取这些已安装的依赖;
  3. Snowpack 分别将每个依赖打包成一个单个的 js 文件。例如 reactreact-dom 就会分别被转为 react.jsreact-dom.js
  4. 每个处理后的文件可以直接运行在浏览器中,通过 ESM 的 import 导入。
  5. 由于这些依赖很少改变,Snowpack 很少需要重新构建这些依赖。

等 Snowpack 构建完依赖,任何 npm 包就可以直接在浏览器中运行,而不需要借助任何模块打包器的能力。

<!-- This runs directly in the browser with `snowpack dev` -->
<body>
  <script type="module">
    import React from 'react';
    console.log(React);
  </script>
</body>

生产打包

Snowpack 最初的理念就是:你使用一个打包器应该是因为你想用,而不是你需要用。Snowpack 将打包看做一个可选的生产环境优化,也就是说完全可以跳过复杂的打包。

image.png

默认情况下,snowpack build 会使用和 dev 命令相同的 unbundle 的方式,将代码打包到 build 文件夹中,这对大多数项目来说是可以的。但是 Snowpack 同样也会支持打包和优化,例如:老版本浏览器支持、代码压缩、代码分割、tree-shaking、消除 dead code,以及其他性能优化。

传统的模块打包器通常需要很多配置,但是 Snowpack 只需要一行插件,不需要配置。能到做这样,是因为 Snowpack 在发送给打包器之前会先进行构建,所以打包器绝不会看到诸如 JSX, TS, Svelte, Vue 之类的源代码。打包器插件只需要关心 HTML, CSS, 和 JS 就好了。

// Bundlers plugins are pre-configured to work with Snowpack apps.
// No config required! You just need to install the plugin first.
{
  "plugins": [["@snowpack/plugin-webpack"]]
}

Snowpack 的打包优化有两种方式:内置的 esbuild 和插件(Webpack, Rollup或任何其他的你想用的)

方式一:内置 esbuild

Snowpack 最近(截止本文发表时,版本为 v3.8.2)在 esbuild 的助力下发布了一个内置优化器。使用这个内置优化器,可以打包、转译、最小化你的生产构建,比 Webpack 或 Rollup 快 10 ~ 100 倍。

但是 esbuild 目前还没有准备好为生产环境所用,所以当前只推荐用作小型项目里。

// snowpack.config.mjs
// Example: Using Snowpack's built-in bundling support
export default {
  optimize: {
    bundle: true,
    minify: true,
    target: 'es2018',
  },
};

所有支持的打包优化的接口如下:

export interface OptimizeOptions {
  entrypoints: 'auto' | string[] | ((options: {files: string[]}) => string[]);
  preload: boolean;
  bundle: boolean;
  loader?: {[ext: string]: Loader};
  sourcemap: boolean | 'external' | 'inline' | 'both';
  splitting: boolean;
  treeshake: boolean;
  manifest: boolean;
  minify: boolean;
  target: 'es2020' | 'es2019' | 'es2018' | 'es2017';
}

方式二:插件

Snowpack 通过插件支持以下流行的模块打包器

现阶段,在内置优化支持尚未成熟之前,推荐使用 @snowpack/plugin-webpack

Streaming Imports

Snowpack v3.0 引进了一个新的特性叫做 Streaming Imports。也就是按需获取 imported packages,在开发和构建时,通过 Snowpack 管理你的前端项目,可以不必安装仅仅是工具作用的包,甚至连 npm、yarn、pnpm 都可以一起丢掉。

可以通过以下配置启用:

// snowpack.config.mjs
export default {
  packageOptions: {
    source: 'remote',
  },
};

这个配置告诉 Snowpack:从 Skypack CDN 来远程获取你的 imports,而不是在本地构建他们。

当你启用了 streaming import 并且运行 snow dev,本地的 Server 将会开始从https://pkg.snowpack.dev 远程拉取所有的 imports。例如:import "preact" 在你的项目中将会变成类似 import "https://pkg.snowpack.dev/preact"。Snowpack 会缓存返回结果以便未来或离线使用。

Snowpack 的 dev server 已经完成了转译的工作,所以你的源文件将仍旧包含 import "preact" 的语句。当你运行 snowpack build 后,build 目录生成的文件将会包含 import '../_snowpack/pkg/preact.js';

pkg.snowpack.dev 是 Snowpack 基于 Skypack 的 ESM Package CDN。每个 npm 包都是 ESM 的形式,任何老版本的非 ESM 的包都会被 CDN 转为 ESM。

比起传统的“npm install + 本地构建”有以下几个好处:

  • 快速:依赖跳过 install + build 步骤,直接从 ESM CDN 中加载预构建成 ESM 的依赖,并且依赖可以缓存到本地以备离线使用。
  • 安全:由于 ESM packages 会被预构建,所以不会有机会 在你的机器上运行代码,而是只会在浏览器沙盒中运行。
  • 简单:ESM packages 被 Snowpack 管理,前端项目不再需要 Node.js,甚至如果你想的话,也可以完全扔掉 npm CLI。
  • 不会影响最终构建:streaming imports 仍然会和剩下的代码一起编译、打包到最终的代码中,跟其他方式打包的代码别无二致。

同时会使用 snowpack.deps.json 进行版本管理,它类似 package.json 和 package-lock.json 的结合。