赞
踩
梁晓莹,一只喜欢游泳&&读书的猪猪女孩。
大家最近学习 Vue3 学废了吗?尤雨溪尤大大马不停蹄地又给大家送来了专门为 Vue3 打造的开发利器 — Vite。你是否在开发过程中使用 Webpack 觉得不那么丝滑,是否等待启动编译可以喝好几口热水?本文将带领大家简单了解 Vite 的基本知识和作用,让我们更好的开启 Vue3 开发之旅~ 首先,学习 Vite 之前得至少有 2 部分的知识储备:1)掌握 ES Modules 特性 2)了解 Http2 标准,限于篇幅,这里就不过多赘述啦~
如果应用比较复杂,那么使用 Webpack 的开发过程就相对没有那么舒适。
- - Webpack Dev Server 冷启动时间会比较长
- - Webpack HMR 热更新的反应速度比较慢
【之前技术环境】我们使用 Webpack 打包应用代码,最后生成一个 bundle.js,主要有两个原因:
- - 浏览器环境并不很好的来支持模块化
- - 零散的模块文件会产生大量的 HTTP 请求
bundle 太大,要采用各种 Code Splitting,压缩代码,去除的插件,提取的第三方库,so tired~ 【当前技术环境】是否能解决 Webpack 当时的难点?thinking~~
随着浏览器的对 ES 标准支持的逐渐完善,第一个问题已经慢慢不存在了。现阶段绝大多数浏览器都是支持 ES Modules 的。
其最大的特点是在浏览器端使用 export import 的方式导入和导出模块,在 script 标签里写 type="module"
,然后使用 ES Module。
- // 当 html 里嵌入 ES module 的 script 标签时候,浏览器会发起 http 请求,请求 http server 托管的 main.js ;
- // index.html
- <script type="module" src="/src/main.js"></script>
-
-
- // 使用 export 导出模块, import 导入模块:
- // main.js
- import { createApp } from 'vue'
- import App from './App.vue'
- import './index.css'
-
- createApp(App).mount('#app')
直接访问 index.html,报错:在浏览器里使用 ES module 是使用 http 请求拿到模块的,所以 file 协议的请求不允许。
那我们就在本地起一个静态服务,再来打开一下 index.html 来看下报错:找不到模块 vue;原因:"/", "./", or "../"开头的 import 相对/绝对路径,才是合法的。
import vue from 'vue'
也就是说浏览器中的 ESM 是获取不到导入的模块内容的。平时我们写代码,如果不是引用相对路径的模块,而是引用 node_modules
的模块,都是直接 import xxx from 'xxx'
,由 Webpack
等工具来帮我们处理 js 间的相互依赖关系,找这个模块的具体路径进行打包,但是浏览器不知道你项目里有 node_modules
,它只能通过相对路径或者绝对路径去寻找模块。
那咋办???所以 Vite 的一个任务就是启动一个 web server 去代理这些模块,Vite 这里是借用了 koa 来启动了一个服务
- export function createServer(config: ServerConfig): Server {
- // ...
- const app = new Koa<State, Context>()
- const server = resolveServer(config, app.callback())
-
- // ...
- const listen = server.listen.bind(server)
- server.listen = (async (port: number, ...args: any[]) => {
- if (optimizeDeps.auto !== false) {
- await require('../optimizer').optimizeDeps(config)
- }
- return listen(port, ...args)
- }) as any
-
- server.once('listening', () => {
- context.port = (server.address() as AddressInfo).port
- })
-
- return server
- }

那这就引出了 Vite 的一个实现核心 - 拦截浏览器对模块的请求并返回处理后的结果我们来看下 Vite 是怎么处理的?
/@module/
前缀通过工程下的 main.js 和开发环境下的实际加载的 main.js 对比,发现 main.js 内容发生了改变,由
- import { createApp } from 'vue'
- import App from './App.vue'
- import './index.css'
-
- createApp(App).mount('#app')
-
变成了
- import { createApp } from '/@modules/vue.js'
- import App from '/src/App.vue'
- import '/src/index.css?import'
-
- createApp(App).mount('#app')
-
为了解决 import xxx from 'xxx'
报错的问题,Vite 对这种资源路径做了一个统一的处理,加一个/@module/
前缀。我们在 src/node/server/serverPluginModuleRewrite.ts
源码这个 koa 中间件里可以看到 Vite 对 import 都做了一层处理,其过程如下:
在 koa 中间件里获取请求 ctx.body
通过 es-module-lexer 解析资源 ast 拿到 import 的内容
判断 import 的资源是否是绝对路径,绝对视为 npm 模块
rewriteImports 返回处理后的资源路径:"vue" => "/@modules/vue"
如何支持 /@module/?
在 /src/node/server/serverPluginModuleResolve.ts
里可以看到大概的处理逻辑是
在 koa 中间件里获取请求 ctx.body
判断路径是否以 /@module/ 开头,如果是取出包名
去 node_module 里找到这个库,基于 package.json 返回对应的内容
上面我们提到的是对普通 js module 的处理,那对于其他文件,比如 vue
、css
、ts
等是如何处理的呢?我们以 vue 文件为例来看一下,在 Webpack 里我们是使用的 vue-loader 对单文件组件进行编译,实际上 Vite 同样的是拦截了对模块的请求并执行了一个实时编译。通过工程下的 App.vue 和开发环境下的实际加载的 App.vue 对比,发现内容发生了改变 原本的 App.vue
- <template>
- <img alt="Vue logo" src="./assets/logo.png" />
- <HelloWorld msg="Hello Vue 3.0 + Vite" />
- </template>
-
- <script>
- import HelloWorld from './components/HelloWorld.vue'
-
- export default {
- name: 'App',
- components: {
- HelloWorld
- }
- }
- </script>
- <style>
- body {
- background: #fff;
- }
- </style>

变成了
- 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 就是在按需加载的基础上通过拦截请求实现了实时按需编译
零散模块文件在HTTP 1.x 确实会产生大量的 HTTP 请求,而大量的 HTTP 请求在浏览器端就会并发请求资源的问题;但是这些问题随着HTTP 2的出现,也就不复存在了。
why?
HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8 个的 TCP 链接请求限制;HTTP 2 则可以使用多路复用,代替原来的序列和阻塞机制。所有请求都是通过一个 TCP 连接并发完成。
即 Vite 的 3 大核心功能:Static Server + HMR + Compile
社区:比如可以借助各种 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 好二三十倍,所以就不用担心啦~ 当然也会用上缓存,具体这里暂时不扩展。
社区:Webpack HMR 等
热更新的时候,Vite 只需要立即编译当前所修改的文件即可,所以 响应速度非常快。而 Webpack 修改某个文件过后,会自动以这个文件为入口重写 build—次,所有的涉及到的依赖也都会被加载一遍,所以反应速度会慢很多。
社区:需要开发者自行在代码中引入其他插件 impor('xx.js')
实现 dynamic-import;如@babel/plugin-syntax-dynamic-import
但是像 Webpack 这类工具的做法是将所有模块提前编译、打包进 bundle 里,换句话说,不管模块是否会被执行,都要被编译和打包到 bundle 里。随着项目越来越大打包后的 bundle 也越来越大,打包的速度自然也就越来越慢。
Vite 利用现代浏览器原生支持 ESM 特性,省略了对模块的打包。
对于需要编译的文件,Vite 采用的是另外一种模式:即时编译。也就是说,只有具体去请求某个文件时才会编译这个文件。所以,这种「即时编译」的好处主要体现在:按需编译。
初始执行命令 npm run dev --> 实际就是启动了 /src/node/server/index.ts 如上文提到启动了一个 koa server, 该文件还使用了 chokidar 库创建一个 watcher,来监听文件变动:
- export function createServer(config: ServerConfig): Server {
- // 启动静态 server:
- const app = new Koa<State, Context>()
- 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))
- }

我们可以看到初始第一个请求如下:那么这个文件哪里来的?这就是经过 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 <link>
- 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 又是谁发出来的呢?
例如: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, string>
- ): 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()
- })
- }
- })
-
- }

将当前项目目录作为静态文件服务器的根目录
拦截部分文件请求
处理代码中 import node_modules 中的模块 b
处理 Vue 单文件组件(SFC)的编译
通过 WebSocket 实现 HMR
同:
底层原理: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 的配套工具,还能在未来形成更成熟的社区方案,推进技术进步!!!
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
欢迎加我微信「qianyu443033099」拉你进技术群,长期交流学习...
关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。
点个在看支持我吧,转发就更好了
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。