webpack 之旅

发表于更新于阅读时长 16 分钟

Ah you think webpack is your ally?
You merely adopted the webpack. I was born in it, molded by it.

根据调查, Webpack 是当代前端界最流行的构建工具, 有着超过 60% 的市场占有率. 然而它的恶名几乎和它的流行一样被广为传播, 甚至有Webpack 配置工程师这样的玩笑. 本文将尽量降低理念和原理上的说明(因为它们很大程度上是不好的), 而着眼于实际的使用体验.

1. Why Webpack is necessary

It's not. 在前端工程化刚刚兴起的年代, 流行的是各种 task runner 来把资源转换成需要的形式, 诸如gruntgulp, 现在你仍然可以使用它们, 只不过可能会被人吐槽是过时的老古董而已. 而随着前端开发的复杂化和 NPM 的崛起, 开发者们终于认识到了通过script标签来引入依赖是多么的不可靠, 随之而来的则是各式各样的打包器, 诸如Browserify.

然而为什么大部分人还是选择了 Webpack 呢? 群聚效应是一个很好的解释. 特别地, 在程序业界尤其是开源界, 一个东西用的人越多, 那它的贡献者就越多, 周边设施就越完善, 使用体验也就越好. 当然 Webpack 也并非是天生就有这么多用户, 它的高可定制性击败了它的前辈打包器们, 而它丰富的插件使得用户可以不再使用额外的 task runner. 时至今日, 大部分的前端工具(框架, 语言)等等都需要支持 Webpack, 否则会失去很多用户. 这反过来也加强了 Webpack 的统治地位.

Webpack 的功能非常的丰富, 不仅提供开发服务器, 配上插件就有HMR功能, 甚至这个服务器还支持gzip. 它的可配置性也非常强, 甚至配置文件都不仅支持 JSON 和 JS, 还支持 JS 的两种方言 TypeScript 和 CoffeeScript, 甚至毫无道理地支持 JSX.

1.1 Why Webpack is bad

但是这并不能解释 Webpack 的恶名. 在我看来, Webpack 的问题和 Node 社区所面临的差不多: DRY 原则指导下的依赖爆炸. 如前所述, Webpack 以丰富的插件而闻名, 文档中甚至下了这样的结论

Plugins are the backbone of webpack

这样的心态导致了 Webpack 核心功能的极度孱弱, 没有(无论是官方还是第三方提供的)插件几乎不能满足任何项目的需求, 甚至产生了给插件用的插件这样不可思议的情形.

另一个恶劣的设计理念则是以 JS 核心. 尽管对于后端开发来说这并没有问题, 但是前端显然可以复用一部分 HTML 资源机制, 而不是所有资源都需要在 JS 中导入. 这又带来了一个新的问题: 如何将这些导入的文件再解析成单独的文件. 为了解决这个问题, 我们自然还需要使用更多的插件.

2. How to use Webpack

光是抱怨并不能让 Webpack 变得好用, 不如我们还是讨论一下妥善地配置 Webpack, 解决这个问题 once and for all. 前文中提到 Webpack 支持多种配置语言, 在此我选择 TypeScript, 不仅是因为这是现在最流行的选择, 在复杂的配置中有类型提示的帮助也非常有用. 本文中的所有代码我都放在这个repo中.

2.1 准备工作

使用你喜欢的包管理器(当然我推荐 yarn)安装必要的依赖: 基础的webpack webpack-cli webpack-dev-server, 以及为了 TypeScript 所准备的typescript ts-node以及相关的 typing. 然后初始化各种配置文件, 记得要把 tsconfig 中的 module 一项写成 commonjs 否则 nodejs 不认.

然后再利用 npm script 来方便调试. 我建议至少写入两项:"build": "webpack"以及"serve": "webpack-dev-server".此时我们可以开始撰写 Webpack 配置了.

2.2 第 0 个问题: 如何决定 mode

在使用 Webpack 的过程中, 我们常常会在不同的情景下使用它, 最常见的两种便是开发环境和生产环境. 因此, 我们常常需要使用代码决定是否要使用某些插件和配置. 这将会是我们遇到的第 0 个问题: 如何在 Webpack 配置中知道此时 Webpack 使用的是什么模式. 而且你将会发现, 就像你将要遇到的其他问题一样, 虽然有一大堆解决方法, 但其中没有那一种是完美无缺的.

无论只是看了webpack --help的输出还是大致看了一眼文档, 你都会知道应该传入 mode 参数. 如果你有一点点 Node.js 基础, 你就会知道应该用process.env.NODE_ENV访问. 但是真的是如此吗? 显然并不是. 这个参数只会设置被打包的代码中的环境变量, 而不是 webpack.config.js 中的, 因此process.env.NODE_ENV的值是 undefined. 这可真是出人意料.

那该怎么办呢? Webpack 文档中给出了几种解决方案. 一种是使用webpack-merge, 此时我们可以把开发和生产环境的 Webpack 配置分开并且复用共同代码, 无论是后者继承前者还是两者都继承同一个基础配置. 不得不承认, 这种解决方案看起来很不错, 也在业界很常用. 然而这不仅给我们将要有一大堆依赖的项目又增加了一个依赖, 而且还让这个已经有了一大堆配置文件的项目又多了一两个文件.

另一个文档中给出的方案则是使用环境变量, 就像普通的 Node.js 项目一样设置process.env.NODE_ENV. 这个做法带来的问题则是缺乏跨 shell 兼容性, 如果要顾虑兼容性的话则要cross-env, 换言之, 又一个依赖. 我并不反对安装依赖, 但是考虑到我们要安装一大堆依赖, 我们必须明智地选择每一个依赖.

还有一种方法则是导出函数而不是对象作为 Webpack 配置, 此时第一个参数即为env选项传入的参数. 为了我以及读者的理智考虑, 我决定还是不要采用这种做法, 以避免太多大括号和缩进.

当然并不是没有猴一些的方法. 显而易见, mode 只不过是一个参数, 和其他的参数并没有什么不同. 因此我们可以手动 parse 参数列表来获取传入的 mode. 参数列表中找出 mode, 并且传给 mode 选项

const modeArg: string | undefined = process.argv.filter(str => str.startsWith('--mode'))[0]
const mode = modeArg !== undefined ? modeArg.split('=')[1].trim() : 'development'
const devMode = mode !== 'production'

const config: webpack.Configuration = {
  mode: devMode ? 'development' : 'production' // 以防用户传入不合法的 mode
}

2.3 如何生成 HTML

如前所述, Webpack 有着以 JS 为核心的设计, 因此在默认状态下它并不会输出 HTML, 而是希望你手写一个 HTML, 在其中手动引入对应的 script. 这当然不好, 很多人都意识到了这一点, 于是就有了html-webpack-plugin. 它的作用是根据指定的 HTML 生成最后会成为网页入口的 HTML, 并且自动引入生成的所有 script. 我还使用了script-ext-html-webpack-plugin, 来为所有 script 标签加上 defer 属性, 来加快页面的载入.

但这并不够, 为了修正 Webpack 这一莫名其妙的理念, 我们还需要html-loader. 这样就免除了我们在 JS 中导入所有资源的麻烦. 可想而知, 这两个插件的功能有一定重叠, 比如两者都提供压缩功能, 至于具体在哪个插件中完成就见仁见智了.

此时为了避免太多的缩进, 我决定使用一个单独的变量 rules 存储所有 loader 的规则, 并用 ES6 提供的方便的对象字面量简写来把它传入 modules 选项. 此外为了能让 HTML 中引入的资源能被正确的生成, 其中较小的能被嵌入到 HTML 中, 我们还需要其他的 loader.

const rules: webpack.RuleSetRule[] = [
  {
    test: /\.(html)$/,
    loader: 'html-loader'
  },
  {
    test: /\.(png|jpg|gif|svg|webp)$/,
    loader: 'url-loader',
    options: {
      limit: 1024
    }
  }
]
// ...
modules: {
  rules
}

并且第一次写入插件选项

plugins: [
  new HtmlWebpackPlugin({
    template: 'src/index.html'
  }),
  new ScriptExtHtmlWebpackPlugin({
    defaultAttribute: 'defer'
  })
]

2.4 如何生成 CSS

我曾经写过一篇文章讲述CSS 预处理器以及如何选择它们. 如果你没有耐心读的话, 我可以直接说结论: 用 Scss.

把 Scss 整合到 Webpack 中应当并不困难, 实践上也并不困难, 只不过又需要引入一大堆依赖. 我们需要用sass-loader把 Scss 转换成 CSS, 用css-loader识别引入的 CSS 文件, 用style-loader生成 CSS 并注入到 HTML 中. 此时规则大体上长这样, 别忘了给每个 loader 都传入 sourceMap 选项

{
    test: /\.scss$/,
    use: [
        {
            loader: 'style-loader',
            options: {
                sourceMap: true
            }
        },
        {
            loader: 'css-loader',
            options: {
                sourceMap: true
            }
        },
        {
            loader: 'sass-loader',
            options: {
                sourceMap: true
            }
        }
    ]
}

既然我们都以及搞的这么复杂了, 不如把 postcss 也弄进来, 这样就可以按照需要支持的浏览器来生成对应的 CSS. 在sass-loader后面加上postcss-loader. 我们还需要cssnano在生产模式下进行压缩.

{
    loader: 'postcss-loader',
    options: {
        sourceMap: 'inline',
        plugins: [
            require('postcss-preset-env')(),
            devMode || require('cssnano')()
        ].filter(notBoolean)
    }
}

注意这里使用了 inline source map, 因为否则style-loader会生成 href 为 blob 的样式, 而 Firefox 不能正确识别其中的 source map. 此时可以把style-loader的 source map 选项去除.

然后在package.json的"browserslist"键中写入值, 我推荐

"browserslist": [
    "last 2 major versions and >1%",
    "not ie <= 11",
    "not op_mini all"
]

在生产环境中, 我们常常希望能生成一个单独的 CSS 文件而不是在 JS 中生成. 显然 Webpack 生态圈中有这样的插件, 让我们把它 加入 CSS loader 的第一项

devMode ? 'style-loader' : MiniCssExtractPlugin.loader,

其实我们并不需要css-loader, 但是mini-css-extract-plugin必须要在css-loader之后才能正常运作.

那么我们应当如何导入这些样式文件呢? 常见的做法是在 JS 中导入它们, 但是有些时候想要像传统前端那样有一些全局样式的话, 可以考虑把全局样式表加入到 entry 选项中, 就像这样

entry: {
        main: [__dirname + '/src/main.ts', __dirname + '/src/styles.scss'],
}

这样 Webpack 就会在全局中导入这个样式表. 这样做的问题在于会生成一个空的模块导入, 但绝大多数时候问题都不大.

2.5 如何生成 JS

终于来到了最复杂也是最重要的部分 JavaScript 了. 关于怎样写 JS, 不同的人有不同的口味, 不使用任何新特性的 JS, 紧跟标准更新的 JS, 快要没人用的 Coffeescript, 抑或是酷炫的 ReasonML/Scala/Haskell/Elm 等等. 我还是选择 TypeScript, 因为这是最被广泛使用也是和普通 JS 差别最小的生成到 JS 的语言.

显而易见我们需要安装typescript, 为了保证写出高质量的代码, 我们还需要安装tslint(如果运气好的话, 在不久的将来我们可以迁移到eslint). 那么我们要用什么东西让 Webpack 编译 TS 呢? 最符合直觉的选项自然是ts-loader, 如果你曾经研究过这个问题的话你可能会说awesome-typescript-loader(可惜这个项目已经停止开发了).

但是我不会这么做

2.5.1 使用 babel-loader

我相信本文的读者早就已经知道 babel 是干什么的, 而 babel-loader 只不过是把这项任务交给 Webpack 做. 信息通畅的读者可能也会知道 babel 在preset-typescipt的帮助下已经能被用来转译 TS 代码, 但是, 为什么?

和你可能已经看到的信息并不一样, 的确 babel 转译时不会做类型检查, 而是脱去类型提示, 但是 tsc 其实也提供这个功能, 只要调用tsc.transpileModule即可. 我也不会使用任何 TS 不支持的 JS 新特性, 因为这样会让 TS 的最大意义 —— 类型检查 —— 失效. 那么使用 babel 的好处在哪里呢?

  • 使用者更多, 整个 JS 生态圈中, 只要进行了工程化的几乎都在使用 babel. 相应地, 使用 babel-loader 的人也相当多, 因此一旦出了问题, 它能受到更多的关注, 解决起来也应当更快
  • 默认提供缓存功能, 因此编译确实更快
  • 周边生态更发达
  • 支持按照 browserslist 转译, polyfill 粒度更细
  • 一个小作用是 TS 并不支持转译 Vue 的 TSX 语法, 因此使用 Vue 时你总需要 babel

但是显然使用 babel 并非没有缺点

  • 显然需要新增一大堆依赖
  • TSC 和 babel 的根本设计差异在于 TSC 默认包含 stage-3 的特性, 而 babel 默认只包含 stage-4, 因此需要引入插件来维持维护两者行为的同步, 这是巨大的维护成本
  • 还有一些次要问题, 比如 TC39 把 class property 从[[SET]]语义改成了[[DEFINE]]语义, TSC 并不会改变之前的行为, babel (的某个插件)却改了; 相似的, 装饰器这一特性也一样
  • 如前所述, babel 没有类型检查, 这就要求我们必须使用`fork-ts-checker-webpack-plugin`

希望读者能依照自身的情况, 做出正确的选择.

2.5.2 具体怎么做

不说了, 直接贴代码

// const pragma = 'h'

{
    test: /\.tsx?$/,
    loader: 'babel-loader',
    options: {
        cacheDirectory: __dirname + '/.cache',
        parserOpts: { strictMode: true },
        presets: [['@babel/preset-env', { modules: false }]],
        plugins: [
            '@babel/plugin-transform-typescript',
            // [
            //     '@babel/plugin-transform-typescript',
            //     {
            //         isTSX: true,
            //         jsxPragma: pragma
            //     }
            // ],
            [
                '@babel/plugin-proposal-object-rest-spread',
                {
                    loose: true,
                    useBuiltIns: true
                }
            ],
            ['@babel/plugin-proposal-class-properties', { loose: true }]
            // use these when in need
            // ['@babel/plugin-proposal-decorators', { legacy: true }],
            // ['@babel/plugin-transform-react-jsx', { pragma }]
        ]
    },
    include: /src/
}

可以看到为了正确转译 TS, 需要做大量的工作. 值得一提的是尽管 object spread 已经正式进入 ECMAScript 标准, 但是 babel 默认的转译结果是辅助函数而非Object.assign, 为了修正这一行为我们仍需手动配置@babel/plugin-proposal-object-rest-spread

2.5.3 转译到什么

看起来这不是一个问题, 是吗? 最符合直觉的答案就是按照 browserslist 里的内容. 但是显然, 转译并非没有代价. 代价来源于两个方面: 用表达力更低的旧版本 JS 表达新的语法结构需要更多代码, 以及必须要引入的 polyfill. 那么问题来了, 代价应当由谁来支付? 在这里介绍两种做法.

Angular 在这个PR之后的做法把所有的 polyfill 在 es6-polyfill 中引入, 然后把生成的 script 加上 nomodule 属性, 这样较新的浏览器就不会加载或是执行这个标签里的代码. 这一做法是被这样的事实所支撑的: 我们通常需要支持两类浏览器, 一类是功能陈旧并且长时间不会被更新的, 一类是功能完全且会被快速更新的. 因此我们只需要对前者做 polyfill 即可. 这个做法并非完美无缺: 显然第一条代价仍然需要所有用户支付; polyfill 不是按需引入, 旧版本浏览器的用户必须支付比必要更多的代价(不过大部分时候我们并不关心他们). 一个小问题是 babel 转译 async 时会使用regenerator-runtime, 而它会被当作 polyfill 引入(严格意义上说来这是 es7-polyfill)因此不会被新版浏览器执行, 这会使得 async 在新版浏览器中也无法运行. 万幸我们只需要babel-plugin-transform-async-to-promises就可以修正这一行为. 另一个小问题是 safari 10 尽管支持大部分新特性, 却会忽视 nomodule 属性, 因此仍会下载那 100 多 kb 的 script, 不过我相信这是仍然坚持使用旧版本的 safari 的用户的责任.

为了完成这一目的, 我们需要在 entry 中多加一项

polyfills: __dirname + '/src/polyfills.ts'

然后再利用一下script-ext-html-webpack-plugin

new ScriptExtHtmlWebpackPlugin({
  defaultAttribute: 'defer',
  sync: 'polyfills',
  custom: {
    test: 'polyfills',
    attribute: 'nomodule'
  }
})

另一种做法则要激进的多. Vue cli以及插件webpack-babel-multi-target-plugin使用了这样的做法: 按照两种配置生成针对 modern 和 legacy 浏览器的代码, 使用type=module和 nomodule 来在不同的浏览器中使用不同的代码. 这样的话新浏览器的用户就完全不必为兼容性支付任何一点代价(或许除了那个 script 标签的几个字节). 但是这个做法的问题则更严重. 这个做法的本质是利用 Webpack 可以接受多个配置的特性, 运行两个 Webpack 来生成两份代码. 这会使得一些插件无效, 其中最为严重的是前文中提过的mini-css-extract-plugin以及html-webpack-plugin. 尽管通过一些复杂的 hack 能让他们跑起来, 这种做法的稳定性仍然非常令人生疑. 这种做法会带来的一个愚蠢的后果是特定版本的 Edge(好在它已经死了)中会把有type=module的 script 下载两次, 而这常常是非常严重的代价.

总之, 这个问题目前仍然没有妥善的解决方案, 希望读者能和本文会提到的其他问题一样, 做出明智的选择.

2.6 开发配置

我们终于把核心部分搭好了, 但我们的开发体验仍然不够好. 首先我们需要搭好开发服务器, 这很容易:

devServer: {
        before(_, server) {
            // watch index.html changes
            (server as any)._watch(__dirname + '/src/index.html')
        },
        clientLogLevel: 'warning',
        hot: true
    },

等等这是什么? 没错, 这里我们要解决另一个大问题: 模块热替换. 顾名思义, 这个东西的作用是让你在开发的时候不需要每次写点什么就要刷新一次页面, 更改了代码之后大部分状态都会被保留. 这在调整 CSS 的时候尤其有用. 但是为了让它正确运行也必须做出很多努力.

我们需要在入口文件中加入

if (module.hot) {
  console.log('HMR updated')
  module.hot.accept()
}

HMR 的 log 非常烦人, 又没有合适的选项把它调整到可以接受的地步, 所以这里我们把它关了, 换之以我们自己的 log.

那么之前的那个 before 回调又是干什么的呢? 我们为了生成正确的 HTML 使用了html-webpack-plugin, 它会依照模板生成正确的 HTML. 这里就出现了问题: devServer 并不能知道新的 HTML 被生成, 因为生成的 HTML 此时仅仅存在于这个插件所管理的内存中. 所以对模板的任何更改都不会触发 devServer 的更新. 因此我们需要调用 _watch 这个私有函数, 告诉 devServer 必须观察 HTML 模板, 一旦这个文件更新就要刷新页面(HTML 的 HMR 不仅复杂而且没什么意义).

另一种做法(也是非常流行的create-react-app所用的)是结合 contentBase 和 watchContentBase 这个选项. contentBase 的作用是凡是 devServer 没有的东西都去这里找, 而 watchContentBase 的作用则是在 contentBase 变化时通知 devServer 更新. 显然我们只需要把 HTML 放在 contentBase(常常是资源文件夹)中再令 watchContentBase 为 true 即可

这两种做法, 一个使用了私有 API, 一个破坏了文件结构, 希望读者能在权衡这两者的利弊, 做出明智的选择. 当然不要忘记把 HMR 插件加入插件列表, 但要记得只在 dev 模式下, 否则会报错

;[devMode && new webpack.HotModuleReplacementPlugin()].filter(notBoolean)

另一个小问题是我们之前使用 babel, 因此没有类型检查, 这很好修正, 只需加上fork-ts-checker-webpack-plugin即可. 别忘了加上 TSLint 支持.

最后要处理的问题就是 source map. 得益于之前的妥善配置, 现在所有的模块都能生成对应的 source map, 但我们要怎样让它们生成最终可用于 debug 的 source map 呢? 看起来只要对 devtool 选项传入相应值即可. 然而这个选项可以接受的值有十三种之多.

不要惊慌, 我们可以用下表来分析它们是怎样组成的

词缀 特性 eval-source source-map
- 用 eval 包裹代码, 蕴含 inline. 有较大运行性能损失 基础版, 不会忽视子模块的 map
inline 把 map 写在代码中 x o
cheap 只记录源代码的行号, 省略列号 生成最快 较快
cheap-module cheap 且不会忽视子模块的 map 重建较快 速度普通
inline-cheap inline 和 cheap 的结合, 无视子模块的 map x 速度较快
inline-cheap-module 前三者的结合 x 速度普通
hidden 不在代码中引用 map x 可在生产环境中使用
nosources map 中不包含源代码, 只有位置 x 可在生产环境中使用

另外有两种不常用的配置是 none 和 eval, 分别是不生成 source map 和仅把代码用 eval 包裹.

考虑到我们对子模块(TS)的应用, 那么在开发环境中应当用的是inline-cheap-module-source-map或是cheap-module-eval-source-map, 而在生产环境中我则推荐nosources-source-map. 希望读者能做出明智的选择.

2.7 生产配置

让我们先解决一些简单的问题. 每次 build 时, 我们常常不希望之前的 build 干扰我们, 所以需要加上 clean-webpack-plugin. 为了尊重我们引入的第三方库, 我们还需要license-webpack-plugin, 但是又不希望它被加入到我们生成的 JS 中, 因此需要关闭 perChunkOutput, 这样所有的许可证都会被集中到一个文本文件里.

剩下的大问题就是如何让我们的代码能被浏览器缓存.

最基础的, 让我们配置一下 output

output: {
    path: __dirname + '/dist',
    filename: devMode ? '[name].js' : '[name].[contenthash].js'
}

注意 Webpack 支持三种 hash, 他们分别是[hash], 每次构建时产生, 同次构建内共享;[chunkhash], 同一个 entry 的文件共享一个 hash; 以及[contenthash], 用生成的每个文件的内容计算 hash. 显然此处我们需要用第三个. 注意 output 设置还可以设置 chunkFilename, 它将作用于非入口文件(比如后面提到的共同文件), 但因为一个愚蠢的bug, 最好还是不要使用它.

好了, 别忘记给mini-css-extract-plugin也传入chunkFilename: 'styles.[contenthash].css. 另一个需要特殊处理的东西是资源, 我们通常不希望他们的 URL 中含有 hash, 因为如果用户想要把他们下载下来的话, 文件名就会乱掉. 所以我们要给 url-loader 传入name: devMode ? '[name].[ext]' : 'assets/[name].[ext]?[hash].

接下来我们还要充分利用 optimization 选项. 具体说来则是

optimization: {
    runtimeChunk: 'single',
    splitChunks: {
        chunks: 'all',
        minChunks: 2,
        cacheGroups: {
            vendor: {
                test({ resource }, chunks) {
                    const onlyByPolyfill =
                        chunks.length === 1 && chunks[0].name === 'polyfills'
                    return /node_modules/.test(resource) && !onlyByPolyfill
                },
                name: 'vendors',
                chunks: 'all',
                minChunks: 1,
                minSize: 8192
            }
        }
    }
}

对于多个入口的项目来说, 每个入口都需要 Webpack 提供的运行环境来导入导出模块. runtimeChunk的作用是只生成单独一份运行环境, 避免重复的代码.

后面对split-chunks-plugin的配置稍稍有些复杂, 但并非多余. 首先我们要通过chunks: 'all'来使得无论时同步还是异步导入的模块都会被加入优化, 然后我们确保只有一个模块被至少两个入口引用时才会被单独打包. 后面的部分则是为了第三方模块. 在日常开发中, 我们经常会进入这样的情景: 第三方依赖在开发伊始就被确定下来, 此后除了安全问题机会不会有变动; 而业务逻辑则会经常变动. 因此更改业务逻辑导致缓存失效时, 用户所需的流量会因为第三方模块单独打包而显著减少. 但我们还有一点小问题: 我们的 polyfill 文件也很少变动, 而里面引入的 polyfill 我们不会希望它被打包到 vendors 中, 所以需要进行一些特殊的配置.

这样最终生成的文件结构将是

文件 作用
assets/.. 所有资源文件
index.html 用户访问根 URL 时的入口
styles.css 项目里所有的 CSS 的打包
runtime.js Webpack 运行环境
vendors.js 所有第三方模块的打包
polyfills.js polyfills 的集合
main.js 业务逻辑等等应用的主题

剩下的就都是一些小问题了. 如果要使用 CDN , 只需要配置 publicPath 即可. Webpack 4 会自动进行摇树优化, 这意味着只有被用到的模块中被导入的部分才会被打包. 令人遗憾的是, 目前 Webpack 只支持对 ES6 模块静态导入的优化, 无论是动态导入还是 Commonjs 都不支持.

3. Why Webpack is not necessary

如果你已经读完了这么一大堆字, 我相信你已经对 Webpack 的生态, 它的优点和缺点都已经充分了解了. 你可能会产生这样的怀疑, 难道前端圈就没有一个使用方便又功能强大的打包器吗? 并不能说没有, 它的名字叫parcel

3.1 奋斗了 18 年,才和你坐在一起喝咖啡

显然这里不会再有一篇复杂的文章详细讲解如何配置 parcel, 你也不需要. 你要做的只是安装parcel-bundler, 然后运行parcel src/index.html, 等待 parcel 自动安装完所有依赖, 所有的配置就都完成了.

pikachu

听起来很不可思议是吗? 然而事实的确是这样. 尽管不能完全复制前述 Webpack 的功能, 诸如嵌入小资源, 分离业务逻辑和第三方模块这两个次要功能不被支持, 然而我们却有了更为强大的摇树优化--虽然对动态导入仍然无效, 但是 Commonjs 模块也能被优化. 和缺失的一点小功能比起来, 这简直是巨大改进.

3.2 我们应该使用 parcel 吗?

尽管 parcel 这一项目更像是 proof-of-concept 而非严肃的项目, 开发者的态度也着实令人生疑. 看到 github issue 区的画风

screen

我相信大部分人会很难严肃对待这个项目. 仍然, 它带来的巨大优势非常诱人, 很多网站诸如 digital ocean 也已经把它用在了实际生产中. 希望读者能和前面遇到的所有其他问题一样, 做出明智的选择.

© 2016 - 2023Austaras Devas