速度与激情之 Vite 初体验
共 10553字,需浏览 22分钟
·
2021-02-17 13:01
梁晓莹,一只喜欢游泳&&读书的猪猪女孩。
大家最近学习 Vue3 学废了吗?尤雨溪尤大大马不停蹄地又给大家送来了专门为 Vue3 打造的开发利器 — Vite。你是否在开发过程中使用 Webpack 觉得不那么丝滑,是否等待启动编译可以喝好几口热水?本文将带领大家简单了解 Vite 的基本知识和作用,让我们更好的开启 Vue3 开发之旅~ 首先,学习 Vite 之前得至少有 2 部分的知识储备:1)掌握 ES Modules 特性 2)了解 Http2 标准,限于篇幅,这里就不过多赘述啦~
一、问题来源
1.1 Webpack 槽点
如果应用比较复杂,那么使用 Webpack 的开发过程就相对没有那么舒适。
- Webpack Dev Server 冷启动时间会比较长
- Webpack HMR 热更新的反应速度比较慢
1.2 回顾 Webpack 初衷
【之前技术环境】我们使用 Webpack 打包应用代码,最后生成一个 bundle.js,主要有两个原因:
- 浏览器环境并不很好的来支持模块化
- 零散的模块文件会产生大量的 HTTP 请求
1.3 思考现在
bundle 太大,要采用各种 Code Splitting,压缩代码,去除的插件,提取的第三方库,so tired~ 【当前技术环境】是否能解决 Webpack 当时的难点?thinking~~
二、解决思路
2.1 ES Module
随着浏览器的对 ES 标准支持的逐渐完善,第一个问题已经慢慢不存在了。现阶段绝大多数浏览器都是支持 ES Modules 的。
其最大的特点是在浏览器端使用 export import 的方式导入和导出模块,在 script 标签里写 type="module"
,然后使用 ES Module。
// 当 html 里嵌入 ES module 的 script 标签时候,浏览器会发起 http 请求,请求 http server 托管的 main.js ;
// index.html
变成了
import HelloWorld from '/src/components/HelloWorld.vue'
const __script = {
name: 'App',
components: {
HelloWorld
}
}
import "/src/App.vue?type=style&index=0"
import {render as __render} from "/src/App.vue?type=template"
__script.render = __render
__script.__hmrId = "/src/App.vue"
typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(__script.__hmrId, __script)
__script.__file = "/Users/liangxiaoying/myfile/wy-project/vite-demo/src/App.vue"
export default __script
这样就把原本一个 .vue
的文件拆成了三个请求(分别对应 script、style 和 template) ,浏览器会先收到包含 script 逻辑的 App.vue 的响应,然后解析到 template 和 style 的路径后,会再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。
实际上在看到这个思路之后,对于其他的类型文件的处理几乎都是类似的逻辑,根据请求的不同文件类型,做出不同的编译处理。实际上 Vite 就是在按需加载的基础上通过拦截请求实现了实时按需编译
2.5 HTTP 2
零散模块文件在HTTP 1.x 确实会产生大量的 HTTP 请求,而大量的 HTTP 请求在浏览器端就会并发请求资源的问题;但是这些问题随着HTTP 2的出现,也就不复存在了。 why?
HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8 个的 TCP 链接请求限制;HTTP 2 则可以使用多路复用,代替原来的序列和阻塞机制。所有请求都是通过一个 TCP 连接并发完成。
三、三大作用
即 Vite 的 3 大核心功能:Static Server + HMR + Compile
3.1 快速的冷启动
社区:比如可以借助各种 cli :vue-cli、create-react-app 等等
当我们对比使用 vue-cli-service serve 的时候,会有明显感觉。因为 Webpack Dev Server 在启动时,需要先 build—遍,而 build 的过程是需要耗费很多时间的。而 Vite 则完全不同,当我们执行 Vite serve 时(npm run dev),内部直接启动了 Web Server,并不会先编译所有的代码文件。那仅仅是启动 Web Server,速度上自然就蹭蹭蹭的 up↑。那么及时请求的编译呢?关于支持 JSX, TSX,Typescript 编译到原生 JS —— Vite 引入了EsBuild,是使用 Go 写的,直接编译为 Native 代码,性能要比 TSC 好二三十倍,所以就不用担心啦~ 当然也会用上缓存,具体这里暂时不扩展。
3.2 即时的热模块更新
社区:Webpack HMR 等
热更新的时候,Vite 只需要立即编译当前所修改的文件即可,所以 响应速度非常快。而 Webpack 修改某个文件过后,会自动以这个文件为入口重写 build—次,所有的涉及到的依赖也都会被加载一遍,所以反应速度会慢很多。
3.3 真正的按需编译
社区:需要开发者自行在代码中引入其他插件 impor('xx.js')
实现 dynamic-import;如@babel/plugin-syntax-dynamic-import
但是像 Webpack 这类工具的做法是将所有模块提前编译、打包进 bundle 里,换句话说,不管模块是否会被执行,都要被编译和打包到 bundle 里。随着项目越来越大打包后的 bundle 也越来越大,打包的速度自然也就越来越慢。
Vite 利用现代浏览器原生支持 ESM 特性,省略了对模块的打包。
对于需要编译的文件,Vite 采用的是另外一种模式:即时编译。也就是说,只有具体去请求某个文件时才会编译这个文件。所以,这种「即时编译」的好处主要体现在:按需编译。
四、核心思路
4.1 初始启动静态服务
初始执行命令 npm run dev --> 实际就是启动了 /src/node/server/index.ts 如上文提到启动了一个 koa server, 该文件还使用了 chokidar 库创建一个 watcher,来监听文件变动:
export function createServer(config: ServerConfig): Server {
// 启动静态 server:
const app = new Koa()
const server = resolveServer(config, app.callback())
......
const listen = server.listen.bind(server)
server.listen = (async (port: number, ...args: any[]) => {
...
}) as any
// 其中关键 1:使用 chokidar 对文件进行递归监听:监听到文件变动可对不同模块进行相应处理
const watcher = chokidar.watch(root, {
ignored: ['**/node_modules/**', '**/.git/**'],
...
}) as HMRWatcher
// 其中关键 2:执行各类插件
const resolvedPlugins = [
// rewrite and source map plugins take highest priority and should be run
// after all other middlewares have finished
sourceMapPlugin,
moduleRewritePlugin,
htmlRewritePlugin, // 处理 html 文件
// user plugins
...toArray(configureServer),
envPlugin,
moduleResolvePlugin,
proxyPlugin,
clientPlugin, // 输出客户端执行代码
hmrPlugin, // 处理热模块更新
...(transforms.length || Object.keys(vueCustomBlockTransforms).length
? [
createServerTransformPlugin(
transforms,
vueCustomBlockTransforms,
resolver
)
]
: []),
vuePlugin, // 处理单文件组件
cssPlugin, // 处理样式文件
enableEsbuild ? esbuildPlugin : null,
jsonPlugin,
assetPathPlugin,
webWorkerPlugin,
wasmPlugin,
serveStaticPlugin
]
resolvedPlugins.forEach((m) => m && m(context))
}
4.2 监听消息,拦截部分请求
我们可以看到初始第一个请求如下:那么这个文件哪里来的?这就是经过 clientPlugin 【/src/node/server/serverPluginClient.ts】处理输出的:
export const clientPublicPath = `/vite/client` // 当前的文件名称
const legacyPublicPath = '/vite/hmr' // 历史版本的名称
...
export const clientPlugin: ServerPlugin = ({ app, config }) => {
// clientCode 替换配置的信息,用于最后 body 输出:
const clientCode = fs
.readFileSync(clientFilePath, 'utf-8')
.replace(`__MODE__`, JSON.stringify(config.mode || 'development'))
...
app.use(async (ctx, next) => {
// 请求路径是/vite/client,返回响应:200,响应文本是处理好的 clientCode
if (ctx.path === clientPublicPath) {
// 设置 socket 配置信息
let socketPort: number | string = ctx.port
...
if (config.hmr && typeof config.hmr === 'object') {
// hmr option 有最高优先级
...
}
ctx.type = 'js'
ctx.status = 200
// 返回整合好的 body
ctx.body = clientCode.replace(`__HMR_PORT__`, JSON.stringify(socketPort))
} else {
if (ctx.path === legacyPublicPath) { // 历史版本 /vite/hmr
console.error('xxxx')
}
return next()
}
})
}
请求/vite/client 实际就是 /src/client/client.ts 文件,即返回 body = clientCode = client.ts 文件内容;那么它做啥了呢???使用 websoket 处理消息,快速编译,达到实时热更新:
const socketProtocol =
__HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws')
const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`
// 启动 websocket 通信,可实时处理消息,实现 HMR
const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')
...
监听消息:
socket.addEventListener('message', async ({ data }) => {
const payload = JSON.parse(data) as HMRPayload | MultiUpdatePayload
handleMessage(payload)
})
处理消息:
async function handleMessage(payload) {
const { path, changeSrcPath, timestamp } = payload;
switch (payload.type) {
case 'connected': // socket 连接成功
console.log(`[vite] connected.`);
break;
case 'vue-reload': // 组件重新加载
queueUpdate(import(`${path}?t=${timestamp}`)
.catch((err) => warnFailedFetch(err, path))
.then((m) => () => {
__VUE_HMR_RUNTIME__.reload(path, m.default);
console.log(`[vite] ${path} reloaded.`);
}));
break;
case 'vue-rerender': // 组件重新渲染
const templatePath = `${path}?type=template`;
import(`${templatePath}&t=${timestamp}`).then((m) => {
__VUE_HMR_RUNTIME__.rerender(path, m.render);
console.log(`[vite] ${path} template updated.`);
});
break;
case 'style-update': // 样式更新
// check if this is referenced in html via
const el = document.querySelector(`link[href*='${path}']`);
if (el) {
el.setAttribute('href', `${path}${path.includes('?') ? '&' : '?'}t=${timestamp}`);
break;
}
// imported CSS
const importQuery = path.includes('?') ? '&import' : '?import';
await import(`${path}${importQuery}&t=${timestamp}`);
console.log(`[vite] ${path} updated.`);
break;
case 'style-remove': // 样式移除
removeStyle(payload.id);
break;
case 'js-update': // js 更新
queueUpdate(updateModule(path, changeSrcPath, timestamp));
break;
case 'custom': // 自定义更新
const cbs = customUpdateMap.get(payload.id);
if (cbs) {
cbs.forEach((cb) => cb(payload.customData));
}
break;
case 'full-reload': // 网页重刷新
if (path.endsWith('.html')) {
...
} else {
location.reload();
}
}
}
咦?那设立了 message 监听,那 message 又是谁发出来的呢?
4.3 不同插件,监听文件变化,返回消息
例如:cssPlugin 【/src/node/server/serverPluginCss.ts】
// 处理 css 文件,监听 css 文件变动
export const cssPlugin: ServerPlugin = ({ root, app, watcher, resolver }) => {
// 输出 css 请求的响应模板
export function codegenCss(
id: string,
css: string,
modules?: Record
): string {
let code =
`import { updateStyle } from "${clientPublicPath}"\n` +
`const css = ${JSON.stringify(css)}\n` +
`updateStyle(${JSON.stringify(id)}, css)\n`
if (modules) {
code += dataToEsm(modules, { namedExports: true })
} else {
code += `export default css`
}
return code
}
app.use(async (ctx, next) => {
await next()
// 处理 .css 的 imports
...
const id = JSON.stringify(hash_sum(ctx.path))
if (isImportRequest(ctx)) {
const { css, modules } = await processCss(root, ctx)
ctx.type = 'js'
// 用`?import`去重写 css 文件为一个 js 模块,插入 style 标记,链接到实际原始 url
ctx.body = codegenCss(id, css, modules)
}
})
watcher.on('change', (filePath) => {
// 筛出 css 文件,更新 css 请求文件
if (文件更新) {
watcher.send({ // 发送消息
type: 'style-update',
path: publicPath,
changeSrcPath: publicPath,
timestamp: Date.now()
})
}
})
}
4.4 逻辑小结
将当前项目目录作为静态文件服务器的根目录 拦截部分文件请求 处理代码中 import node_modules 中的模块 b 处理 Vue 单文件组件(SFC)的编译 通过 WebSocket 实现 HMR
五、Snowpack VS Vite
同:
底层原理:Snowpack v2 和 Vite 均提供基于浏览器原生 ES 模块导入的开发服务器; 冷启动快速:在开发反馈速度方面,两个项目都具有相似的性能特征; 开箱即用:避免各种 Loader 和 Plugin 的配置。
Vite 默认情况下支持更多的选择加入功能-例如 TypeScript transpilation、CSS import、CSS Modules 和 postcss 支持(需要单独安装所对应的编译器) 都是现成的,无需配置;snowpack 也是支持 JSX、TypeScript、React、Preact、CSS Modules 等构建,非默认;
插件:支持很多自定义插件;Vite 关于这部分的官方文档还没有。
异:
演变:Snowpack 最初不提供 HMR 支持,但在 v2 添加了它,从而使两个项目的范围更加接近。Vite 最初就是参考了 snowpack v1; 双方在基于 ESM 的 HMR 上合作过,尝试建立统一的 api ESM-HMR API 规范, 但因为底层不同还是会略微不同; 使用:Vite 当前暂时只能给 Vue 3.x.使用+react 等部分模板, 对 vue 支持更棒👍;snowpack 没限制; 生产打包:Vite 用 rollup,打包体积更小(rollupInputOptions:定义 rollup 的插件功能);snowpack 用 parcel/webpack;- 决定了开发者生产个性化配置的方案不一样; 偏向:Vue 支持是 Vite 中的一级功能。例如,Vite 提供了一个更细粒度的 HMR 与 Vue 的集成,并且对构建配置进行了微调,以生成最高效的 bundle; 文档完善性:
Vitejs 优点是尤雨溪出品,可能和 Vue3 生态更好的融合。缺点是文档不完善。目前 star 13.7k; Snowpack 优点是更加成熟,有成熟的 v1 和已经发布正式版的 v2, 支持 react, Vue, preact, svelte 等各类应用,文档也更加完善。目前 star 14.4k。
so。。。如何选择?:=> 选 Vite:
喜欢用 Vue,那么 Vite 提供更好支持; 诉求是打包 bundle 体积小 ,那么 Vue 使用 rollup👌。
=> 选 Snowpack:
不喜欢用 Vue,不用 vue-cli,喜欢 react 等; 大的 team 想要使用各类插件 plugin,想要清晰的文档📚等; 对 Webpack 比较用的惯,想要开发模式不要 bundle 打包,更快速👀。
本篇文章主要是带领 Vue 开发爱好者学习尤大对于按需编译等方面的想法和新思路,助力童鞋们的高效开发,减少学习路径。期待 Vite 不仅能成为 vue 的配套工具,还能在未来形成更成熟的社区方案,推进技术进步!!!