炎热的七月,透着一点雨水,就这么来临。半年就如此过去了,看了不少内容,但是想写成博客的却越来越少,可能是人懒了。 最近不少工程化的新秀如后浪般出现,虽然不至于动摇 webpack 这个巨浪,只是对行业也有很深刻的影响,觉得蛮有意思,这里介绍一下:
esbuild
esbuild 做的事情很简单,打包压缩,没有其他的复杂功能,目前也没有其他的插件系统,倒是 esbuild 本身更像一个插件,有点像 webpack 刚出来那会的情况。
esbuild 最大特点就是快,飞快,其本身采用 Go 语言实现,加上高并发的特色,在打包压缩的路上,一骑绝尘。官方数据,和正常的 webpack 相比,在打包方面提高了 100+ 倍以上,这对于需要代码更新后立刻发版到线上的项目而言,超级有意义,这不就是大家一直追求的快速构建嘛。
在构建项目的时候,基本都可以看到这一幕,打包到最后,本以为要结束了,结果进度条一直在 90% 左右的位置,一动不动,尤其是项目大了之后。其实这个最后的过程,是代码丑化、压缩以及 tree-shaking 的过程。代码压缩这部分,在以前的 webpack,是 UglifyjsWebpackPlugin 来处理的,后来内置到 webpack 里面,再后来,由于 uglify-js 不支持 es6,改用 terser 作为 webpack 内置的默认打包压缩工具。即便如此,业务小的时候还好,上来后,打包的时间会非常长。
本地尝试
按照文档思索着建一个最小的 demo,来看看速度如何。按照首页的提示,采用如下内容,分别用 webpack 和 esbuild 来打包:
// 业务内容
import * as React from "react";
import * as ReactDOM from "react-dom";
ReactDOM.render(<h1>Hello, world!</h1>, document.getElementById("root"));
// webpack 配置,只是对jsx采用babel打包,同时还要配置babel的基本配置
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
],
},
};
// esbuild 指令内容,由于采用首页的方式一直报错,最后根据错误,改为如下指令
esbuild --bundle main.jsx --outdir=dist --minify --sourcemap
采用 esbuild 的时候,可以明显感觉到速度飞快,基本上 半秒不到就打包好了,而 webpack 嗯。。。三四秒的样子,速度还是很明显的,可能是因为项目小,没有 100 倍的感觉,但是 esbuild 基本上不用等。只是看看打包的体积,发现 esbuild 的体积比 webpack 的大三倍。这难道是时间换体积?经过排查是 process.env.NODE_ENV
的问题,esbuild 的版本里面包含了 development 和 production 两个模式的内容。官方文档有提示到:
Note that the double quotes around "production" are important because the replacement should be a string, not an identifier. The outer single quotes are for escaping the double quotes in Bash but may not be necessary in other shells.
process.env.NODE_ENV
变量需要配置,并且不能省略 "production"
的引号,只是在 json 里面,添加引号一直无法正常使用,去掉引号会导致无法识别变量,最后采用 api 的方式构建,如下
const { build } = require("esbuild");
build({
entryPoints: ["./main.jsx"],
outdir: "dist",
minify: true,
bundle: true,
sourcemap: true,
define: {
"process.env.NODE_ENV": '"production"',
},
}).catch(() => process.exit(1));
需要注意的是,define 里面的 key-value 结构的 value 不能是对象,不支持嵌套的 key。最后会打包有如下效果:
// 原本的 process.env.NODE_ENV 会被替换,development的内容会被设置为null
if (true) {
checkDCE();
module.exports = require_react_dom_production_min();
} else {
module.exports = null;
}
最后回头一看发现和 webpack 打包的体积居然是一模一样的,esbuild 大了 0.5k 不到。另外有个有趣的现象,如果把 bundle 配置去掉,包的内容,真的只有上面的 react 的业务代码。
想看看 esbuild 的源码,专门学了一下 go 语言,发现还是蛮简单(可能是学比较基础)。只是三脚猫功夫直接看源码,还是云里雾里的,也就放弃了。
esbuild-webpack-plugin
看到 umi 里面支持 esbuild,具体可以看看 esbuild-webpack-plugin
的代码。结构是一个典型的 webpack 的插件,通过 esbuild 的 transform 这个 api 来实现打包,可以看看下面的配置:
const transform = async () =>
await ESBuildPlugin.service.transform(source, {
...this.options,
minify: true,
sourcemap: !!devtool,
sourcefile: file,
});
官方介绍到,如果需要重复调用 esbuild 的 api,最好是实例化 esbuild,达到复用的方式,也就是采用 transform 这个 api。
可以看到上面的代码,采用的配置只是 minify 而已,没有对 bundle 处理,按照作者的介绍
esbuild 有两个功能,bundler 和 minifier。bundler 的功能和 babel 以及 webpack 相比肯定差很多,直接上风险太大;而 minifier 倒是可以试试,在 webpack 和 babel 产物的基础上做一次压缩,坑应该相对较少。
这样确实不错,让 esbuild 做最专业的事情,同时可以继续使用生态丰富的 webpack,而压缩则是 esbuild,作者说到: 试验性功能,可能有坑,但效果拔群,具体的时间效果也不对比了,送上传送门。效果只是减少 1/3,估计是 webpack 本身其他操作占用了时间。
这个插件有配合 umi 的部分,但也可以用到其他 webapck 项目里面。具体是要配置 optimization.minimizer
如下:
optimization: {
minimizer: [new (require("esbuild-webpack-plugin").default)()];
}
正常的 webpack 会采用 terser 作为内置的默认压缩工具,这里面改为 Esbuild 就可以了。
ES Module
上面的 esbuild,可以说很好的解决了生产模式的压缩疼点,提高打包速度,但是开发环境呢?能用上 esbuild 吗?当然也是可以的,只是最优解并非如此。
有一次,看到一个线上地址 https://iconsvg.xyz/ 的页面,打开控制台一看发现居然是采用 ES Module 的形式,如下图。
现在的浏览器基本已经支持 ES 模块化了,直接模块化有什么不可以?直接用在生产环境会有很多问题,比如请求加载数量,比如兼容性,那对于开发环境呢?如 vite 和 snowpack 这样的工具已经就是 bundleless 的工具,在开发环境上采用 ES Module 的方式实现快速热更新。
对于 ES Module,目前文件扩展名为 .js 结构,有推荐采用 .mjs 后缀,可以更清晰的表明是个模块,由于兼容问题,现在采用 .js 后缀就可以了。
应用的时候要采用下面的格式,来声明这个脚本是一个模块:
<script type="module" src="main.mjs"></script>
如果没有声明 type="module"
浏览器会提示 Uncaught SyntaxError: Cannot use import statement outside a module
错误。
vite
vite 在开发环境通过解析文件返回到浏览器,不会有打包过程。这样当修改项目某个文件的时候,只会向浏览器发送更新该文件的请求,而不会去处理别的文件,最终打包的速度项目大小没有关系,可以很大提高开发环境热更新效率。需要注意的是 vite 在生产环境采用 rollup 打包。
开发服务器劫持
vite 在开发环境的定位和 webpack-dev-server 是有点像的,都是作为一个开发服务器,响应客户端的请求。先看看 demo 上具体的效果,官方直接提供一个 create-vite-app 项目作为起步脚手架模板,上面提供 vue 到 react 的模板,采用 template-vue 模板,启动的时间非常快,基本上按下回车差不多就跑起来了。可以看看下图:
几秒钟的时间,项目就启动完毕了,对比一下 vue-cli 3,差不多要 10s 的样子,当然也是由于业务体积的问题,少量的业务,webpack 自然是非常快的(复杂的例子,就没有了,因为 vite 支持的是 vue-next,老项目用的是 vue 2 可能支持力度不好,无法迁移)。
通过控制台可以看一下,发起的请求:
前面是 vite 加载过程,后面是 vue-cli 3 的项目,可以明显看到 vite 是直接请求了 .vue
文件以及 vue.js 文件,而 vue-cli 3 则是请求打包好后的开发文件,只是前图的 vite 里面明明是一个 App.vue
文件为什么会请求三次呢?这里就要说一下 vite 作为开发服务器对网络的劫持作用。
vite 里面会启动一个 koa 服务器,采用中间件的方式对请求的文件劫持,结构如下
// 省略部分代码
const resolvedPlugins = [
moduleRewritePlugin,
htmlRewritePlugin,
moduleResolvePlugin,
proxyPlugin,
serviceWorkerPlugin,
hmrPlugin,
vuePlugin,
cssPlugin,
esbuildPlugin,
jsonPlugin,
assetPathPlugin,
serveStaticPlugin,
];
插件的配置从查找模块、模块路径重写到 vue、css 等资源的处理,客户端请求什么内容,就由专门的中间件处理。比如入口,请求 http://localhost:3000/
返回的是 index.html
,但是结果如下:
中间的 script 部分是和原 index.html
不一样的。额外加载 hmr 文件,正是上面 vite 请求网络图里面的 hmr 请求,同时还注入了全局的环境变量 process.env.NODE_ENV
,可以看一下是如何实现的:
// htmlRewritePlugin 的内容,下面是注入的代码
const devInjectionCode =
`\n<script type="module">\n` +
`import "${hmrClientPublicPath}"\n` +
`window.__DEV__ = true\n` +
`window.process = { env: ${JSON.stringify({
...env,
NODE_ENV: mode,
BASE_URL: "/",
})}}\n` +
`</script>\n`;
async function rewriteHtml(importer: string, html: string) {
// 省略缓存以及script标签替换内容
return injectScriptToHtml(html, devInjectionCode);
}
app.use(async (ctx, next) => {
await next();
if (ctx.status === 304) {
return;
}
if (ctx.response.is("html") && ctx.body) {
// 省略部分代码
ctx.body = await rewriteHtml(importer, html);
return;
}
});
除了 html 的特殊处理外,vite 还会对 import { createApp } from "vue"
这样的 import 语句重写路径为 import { createApp } from "/@modules/vue.js"
,前者的路径客户端是无法正常找到的,通过重写 @modules
vite 可以明白这是一个第三方模块包的请求,对于这些 node_modules 的包可以做一系列的优化,后面会介绍到。
vue 文件处理
对于 vue 单文件的处理,首个文件的访问路径还是源于 main.js 的正常 import,但是到了 vite,.vue 文件则会被 vuePlugin 处理,毕竟浏览器无法识别 .vue 文件,需要解析再返回给浏览器。先看看原始代码 App.vue:
// 原始文件
<template>
<div class="hehe">522215{{ a }}</div>
</template>
<script>
export default {
name: "App",
data() {
return {
a: 123
};
}
};
</script>
<style scoped>
.hehe {
background: red;
}
</style>
拦截后输出的文件
截图是返回的 App.vue 文件,可以看到原始的 .vue 文件变成一个 js 文件,也就是上图的代码。上图仅保留了原 App.vue 里面的 script 部分,渲染模板 template 以及样式 style 在 script 部分里通过 import 的方式引入,一个 .vue 文件拆分成三个。于是就有了左边 network 里面请求的 App.vue?type=style&index=0
和 App.vue?type=template
。拆分成三个请求,每个请求各司其责,比如更新 template 的时候,就发送新的 template 文件到客户端,避免一次修改三个文件:script、template 和 style 混在一起发送,可以说很巧妙。
vuePlugin 里面的实现,更多的是对请求路径的参数判断,如果参数 type 为 undefined(就是 script)、template 以及 style,都分别处理,同时在 script 的时候,如果文件是 typescript,还会采用 esbuild 的 transform API 来编译代码。
三个请求的由来,其实可以追朔到 vue 对 sfc 文件的解析,在 sfc 单文件处理的模块里面,会通过 ast 的方式将文件拆分成,script、template 和 style 三个模式,自然 vite 里面应该按照这三个模式更新 vue 是最合理的。
热更新机制
上面截图以及代码部分可以看到 hmr 的字样,hmr 则是代表热更新的部分。热更新分为两部分,一部分在客户端,一部分在开发服务器。客户端的主要热更新的代码,在 html 访问的时候,已经通过 import "${hmrClientPublicPath}"
这样的方式加载,而 vite 也会通过 chokidar 来监听访问过的文件,当文件变化的时候,会通过 websocket 来通知客户端,再由客户端请求具体的更新代码。
// 客户端主要代码
socket.addEventListener("message", async ({ data }) => {
switch (type) {
case "connected":
console.log(`[vite] connected.`);
break;
case "vue-reload":
// 重新加载vue
case "vue-rerender":
// vue 组件重新渲染
case "style-update":
// 样式更新
case "style-remove":
// 移除样式
case "js-update":
// js更新,react项目更新依赖这个
case "custom":
// 自定义的,目前没有用到好像
case "full-reload":
// 整个页面重新加载,
}
});
客户端对 vue-rerender 的指令,在加载文件后,会直接调用 vue-next 里面的热更新的函数:
// @vue/runtime-core > hmr > rerender 代码
function rerender(id: string, newRender?: Function) {
const record = map.get(id)
if (!record) return
// Array.from creates a snapshot which avoids the set being mutated during
// updates
Array.from(record).forEach(instance => {
if (newRender) {
instance.render = newRender as InternalRenderFunction
}
instance.renderCache = []
// this flag forces child components with slot content to update
isHmrUpdating = true
instance.update()
isHmrUpdating = false
})
}
可以看到这里将新的 render 注入,也就是 template 解析后生成的渲染函数,再调用实例的 update 方法,而这个 update 方法是,vue-next 里面渲染组件的主要入口,采用 effect 的方式。
服务端监听本地文件的变化。在 vue 的中间件里面,会对更新后的文件发送对应的指令,这里提一下重新加载 vue 文件和重新渲染 vue 组件的处理的方式不同。
// descriptor 是 vue-sfc 里面通过 ast 分析出来的描述器
if (!isEqualBlock(descriptor.script, prevDescriptor.script)) {
return sendReload();
}
if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
needRerender = true;
}
可以看到如果前后脚本不一致会重新加载,而如果只是模板不一样,则只会重新渲染组件。这里可以看到是对 vue 的处理,那如果是 react 项目呢?
react 项目处理
在上面的代码里面,我们经常可以看到 vue 的影子,比如 vue 的中间件,vue 的客户端的热更新代码,而对于 react 是需要特殊的配置的,这里我们看看 react 项目的配置时候需要的插件:
// @ts-check
const reactPlugin = require("vite-plugin-react");
/**
* @type { import('vite').UserConfig }
*/
const config = {
jsx: "react",
plugins: [reactPlugin],
};
module.exports = config;
在 vite.config.js 里面需要按照如上配置,而之前的 vue-next 则是什么都不用写。可以明显感觉到 vite 里面 vue-next 是一等生,毕竟连客户端的热更新代码,都用到 vue 的热更新部分。。。。
通过 vite-plugin-react 可以向 vite 项目提供更多的中间件,这个也是类似于 vue 的中间件,只是一个是内置,一个第三方包来配置。通过劫持 html 返回自己的运行时更新代码 react-refresh 部分以及 vite 的 hmr 客户端代码。
// vite-plugin-react 里面代码
module.exports = {
resolvers: [resolver],
configureServer: reactRefreshServerPlugin,
transforms: [reactRefreshTransform],
};
// vite 里面处理插件的 transforms 的方法
app.use(async (ctx, next) => {
await next();
const { path, query } = ctx;
let code: string | null = null;
for (const t of transforms) {
if (t.test(path, query)) {
ctx.type = "js";
if (ctx.body) {
code = code || (await readBody(ctx.body));
if (code) {
ctx.body = await t.transform(
code,
isImportRequest(ctx),
false,
path,
query
);
code = ctx.body;
}
}
}
}
});
reactRefreshServerPlugin 会先服务器添加中间件,当访问 html 代码的时候,则会注入基本的全局代码;transforms 则会在 vite 开发服务器搭建的时候,通过 transforms 方式添加中间件,对 jsx/tsx 文件处理,注入以下关键代码。
const header = `
import RefreshRuntime from "${runtimePublicPath}";
let prevRefreshReg;
let prevRefreshSig;
if (!window.__vite_plugin_react_preamble_installed__) {
throw new Error(
"vite-plugin-react can't detect preamble. Something is wrong. See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201"
);
}
if (import.meta.hot) {
prevRefreshReg = window.$RefreshReg$;
prevRefreshSig = window.$RefreshSig$;
window.$RefreshReg$ = (type, id) => {
RefreshRuntime.register(type, ${JSON.stringify(path)} + " " + id)
};
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
}`.replace(/[\n]+/gm, "");
const footer = `
if (import.meta.hot) {
window.$RefreshReg$ = prevRefreshReg;
window.$RefreshSig$ = prevRefreshSig;
import.meta.hot.accept();
RefreshRuntime.performReactRefresh();
}`;
上面是注入的代码,header 和 footer。可以看出来来,主要注入的部分是热更新相关的。其中有个很特别的地方 import.meta.hot
,这个是 vite 特有的标记;
For manual HMR, an API is provided via import.meta.hot. For a module to self-accept, use import.meta.hot.accept:
export const count = 1;
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
console.log("updated: count is now ", newModule.count);
});
}
这是 import.meta.hot
的用法,对于正常的需要热更新的代码文件,可以通过 import.meta.hot
这个条件语句判断。当内容更新的时候,加载内容,并执行下面 accept
的回调,至于回调里面如何处理,则需要自己控制了。react 采用的则是 import.meta.hot
的方式,更新的方式,当然是通过 react-refresh 来。
上面客户端热更新方式里面,有一种是 js-update,当 jsx 文件更新的时候,会通知到客户端执行 js-upload,并最终加载新的 jsx 文件,当然同时也包含上面的添加的代码。
在 js-update 里面,会分析服务端下发文件的 id 路径,而加载哪些文件,则是根据这个 id 路径来判断的,通过分析 id 的所有依赖,依次加载。这些依赖的来源,并不是 webpack 打包时候分析的 import 的包,而是需要用户调用 accept 或者 acceptDeps 显示添加的,以及依赖更新后的回调函数。比如下面的方式:
import { foo } from "./foo.js";
foo();
if (import.meta.hot) {
import.meta.hot.acceptDeps("./foo.js", (newFoo) => {
// the callback receives the updated './foo.js' module
newFoo.foo();
});
// Can also accept an array of dep modules:
import.meta.hot.acceptDeps(
["./foo.js", "./bar.js"],
([newFooModule, newBarModule]) => {
// the callback receives the updated modules in an Array
}
);
}
官网介绍的这种方式,通过 acceptDeps 指明依赖的路径,当文件变化的时候(指的是自身或者 import 进来的文件),会加载 acceptDeps 中的文件,执行对应的回调。如果不需要指出具体的依赖,比如像 react 的方式,采用 import.meta.hot.accept()
,表明是自身的更新,或者是自身 import 的文件的更新,重新加载本身,也就是 jsx 文件就好了。
回到 react 身上,采用 import.meta.hot.accept()
的方式加 react-fresh 的热更新,好像不是最稳妥的,毕竟每次修改,都要重新加载一次文件,再去更新,没有 vue 来得优雅。当然还有就是不像 sfc 那样需要拆分成三个文件。
vite 启动
前面介绍了 vite 的拦截,vue 和 react 的处理,但是在一开始的时候会解析 package.json 中的文件,对 dependencies 中的包缓存到 node_modules/.vite_opt_cache
里面,不管项目中有没有遇到。多次访问的时候,缓存可以提高访问速度,比如对 vue-next 访问速度的提高。
snowpack
snowpack 和 vite 都是优秀 ES Module 加载方案,发力的领域也是开发环境。vite 文档介绍到,项目依赖关系的处理是受到 snowpack 的启发,在开发环境上都是会启动一个开发服务器,并且解析返回速度也是类似的。可以看出来 vite 有不少方面是借鉴了 snowpack 。
当然 vite 有自己特色的部分,比如 热更新,可以做到深入到 vue 的热更新机制,以及调用 react 的热更新,当然 vite 里面 vue 是第一公民。snowpack 不同于 vite 的地方在于,其构建的时候,支持 webpack 和 Parcel 等,这样无疑对开发者更加友好。
这里很好的介绍了 snowpack 构建的 O(1) 的过程,基本上每次文件更新都小于 50ms。well,现在 webpack 5 也做了很多优化,本地开发没有这么不堪了。上图也适用于 vite,两者都是 ES Module 级别的构建。
snowpack 的劫持
snowpack 和 vite 很不一样,vite 使用 koa 中间件的方式,对不同的文件处理,snowpack 没有中间件的概念,没有 koa 甚至是 express,采用 http-proxy、http 和 http2 来搭建开服服务器。
先看看网络加载情况
可以看出在 vite 里面 App.vue 被拆分成三个文件加载,而这里,只是分成两个文件,app.js 包含 script 和 template, app.css.proxy.js 则是 style 部分。
snowpack 采用外部插件来解析 vue 的方式,比如 vue 项目里面的配置:
// snowpack.config.json 里面的配置
"extends": "@snowpack/app-scripts-vue"
// @snowpack/app-scripts-vue 里面的配置
const scripts = {
"mount:public": "mount public --to /",
"mount:src": "mount src --to /_dist_",
};
module.exports = {
scripts,
plugins: ["@snowpack/plugin-vue", "@snowpack/plugin-dotenv"],
installOptions: {},
devOptions: {},
};
// @snowpack/plugin-vue 里面的配置
module.exports = function plugin(config, pluginOptions) {
return {
defaultBuildScript: "build:vue",
async build({ contents, filePath }) {
// 采用 @vue/compiler-sfc 里面的parse来编译 sfc 文件,和 vite 的编译是一样的。
}
}
}
如上面的结构,通过加载插件里面的 build 方法,实现对 sfc 文件的解析,中间过程比 vite 要复杂一些,vite 的中间件体系很直观,而 snowpack 则是通过不断的分析 config.scripts
里面的配置(通过不断的调用 fs.stat
判断),来得到正确的文件路径以及对应的解析方式,比如 _dist_/App.js
最后会转换为 src/App.vue
,并采用上述的 @snowpack/plugin-vue
的 build 方法加载 src/App.vue
,得到打包后的 script/template 组成的部分,以及 css 内容。发送到客户端并作缓存处理后。
build 方法里面会通过 parse 编译 sfc 文件,得到的 descriptor 和 vite 的差不多,包含 script、template 和 style 三个部分,其中 script 部分的代码会和 tempalte 的代码合并也就是后面 App.js 的主体。 style 作为单独的部分不会立刻发送到客户端,而是先做本地缓存里面。
css 部分会有如下处理方式
// snowpack dev.js wrapResponse
if (responseFileExt === ".js") {
code = wrapImportMeta({ code, env: true, hmr: isHmr, config });
}
if (responseFileExt === ".js" && cssResource) {
code =
`import './${path.basename(reqPath).replace(/.js$/, ".css.proxy.js")}';\n` +
code;
}
可以看到在 App.js 里面添加 css 的 import 部分,这个和 vite 类似,只是 css 文件后缀采用 css.proxy.js 标识,而 vite 采用 type=style
的方式来区分。
另外 snowpack 对 html 的处理,会有一个 isRoute
变量来判断,并注入热更新等代码;
热更新机制
分为两套代码,客户端代码和开发服务器的代码,其中客户端的代码没有 vite 种类复杂
socket.addEventListener("message", ({ data: _data }) => {
if (!_data) {
return;
}
const data = JSON.parse(_data);
debug("message", data);
if (data.type === "reload") {
reload();
return;
}
if (data.type !== "update") {
return;
}
runModuleAccept(data.url)
.then((ok) => {
if (!ok) {
reload();
}
})
.catch((err) => {
console.error(err);
reload();
});
});
可以看出 snowpack 只有 reload 和 update 模式,没有 vite 那样复杂,但是其 js 部分更新逻辑是基本一致的,并且有很相同的 import.meta.hot
方式以及 import.meta.hot.accept
功能。基本和 vite 差不多,这里就不介绍了,当然 snowpack 不用判断 import.meta.hot
是不是在 if 条件语句里面。
snowpack 没有像 vite 那样在客户端采用 vue 的热更新。
snowpack 启动的时候,也会对依赖进行分析,不同的是它会将依赖放在 node_modules/.cache/snowpack/dev
下面。node_modules 包的请求路径也会被改写为 web_modules/vue.js
这样的特殊标记。
webpack
在 snowpack 还可以使用 webpack,官方专门维护了 @snowpack/plugin-webpack
插件,和上面的 @snowpack/plugin-vue
一样都归属于插件范畴,在解析文件的时候会用到,提供一个 build 方法,并且最后通过 webpack 打包文件。snowpack 提供了一些默认配置,比如 babel、MiniCssExtractPlugin 这些。如果要扩展的话采用以下的方式配置,和 vue.config.js 的方式蛮像的。
// snowpack.config.js
module.exports = {
plugins: [
[
"@snowpack/plugin-webpack",
{
extendConfig: (config) => {
config.plugins.push(/* ... */);
return config;
},
},
],
],
};
这里 webpack 的处理方式蛮奇怪的,会将打包好之后的文件,手动注入到 html 里面,而不是采用默认的方式,可能是没有 index.html?可能也是受限于 snowpack 和 webpack 的结合?具体的也就没有深入研究了,感兴趣的可以看看。
总结
上面介绍了三款最近流行的打包工具,esbuild 用于生产环境,vite 和 snowpack 主要用于开发环境。esbuild 打包压缩速度远超同行,也被用于 vite 和 snowpack 里面,作为 JavaScript 文件和 Typescript 文件降级和编译的工具,esbuild 如果要用于生产的话,可以考虑使用 esbuild-webpack-plugin
,仅仅作为压缩工具,效率也能提高不少。
vite 里面有不少借鉴 snowpack 的部分,当然也有自己特别的方式,比如中间件的结构,比如客户端更精准的热更新,当然和 snowpack 一样支持 webpack 更好了,只是目前看来难度不大?两者都可以用在生产,目前看来 vite 采用 rollup 打包,离主流 webpack 有点远,而 snowpack 支持 webpack 所以友好度更高。当然 vite 有尤大佬参与,自然不太一样。
本文还有不少源码没有深入介绍到,只是做一个稍微浅的解读,感兴趣的可以继续深入研究,如果能理解 esbuild 的 go 语言的源码就更好了。