前言
前两天尤大在 vue 3.0 beta
直播中提到了一个 vite
的工具,而且还发推表示再也回不去 webpack
了, 还引来了 webpack
核心开发人员肖恩的搞笑回复, 那就让我们一起来看一下 vite
到底有啥魔力?
![a8997b3d790bc937b52bac1f40fb505b.png](https://img-blog.csdnimg.cn/img_convert/a8997b3d790bc937b52bac1f40fb505b.png)
什么是 Vite?
github:https://github.com/vitejs/vite
Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。
它主要具有以下特点:
- 快速的冷启动
- 即时的模块热更新
- 真正的按需编译
那废话少说,我们先直接来试用一下。
$ npm init vite-app
$ cd
$ npm install
$ npm run dev
我们来看下生成的代码, 因为 vite
尝试尽可能多地镜像 vue-cli
中的默认配置, 所以我们会发现看上去和 vue-cli
生成的代码没有太大区别。
├── index.html
├── package.json
├── public
│ └── favicon.ico
└── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ └── HelloWorld.vue
├── index.css
└── main.js
那我们看下入口 index.html 和 main.js
html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite Apptitle>
head>
<body>
<div id="app">div>
<script type="module" src="/src/main.js">script>
body>
html>
// main.js
// 只是引用的是最新的 vue3 语法,其余没有啥不同
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
发现主要的不同在于多了这么个东西
<script type="module" src="/src/main.js">script>
那下面我们就来看下这是个啥?
原理
ESM
script module
是 ES 模块在浏览器端的实现,目前主流的浏览器都已经支持
![8247d8241c15ba03a775c5801ba9f61e.png](https://img-blog.csdnimg.cn/img_convert/8247d8241c15ba03a775c5801ba9f61e.png)
其最大的特点是在浏览器端使用 export
、import
的方式导入和导出模块,在 script
标签里设置 type="module"
<script type="module">
import { createApp } from './main.js‘;
createApp();
script>
浏览器会识别添加 type="module"
的 元素,浏览器会把这段内联 script 或者外链 script 认为是
ECMAScript
模块,浏览器将对其内部的 import
引用发起 http 请求获取模块内容。在 main.js 里,我们用 named export 导出 createApp
函数,在上面的 script 中能获取到该函数
// main.js
export function createApp(){
console.log('create app!');
};
其实到这里,我们基本可以理解 vite 宣称的几个特性了。
- webpack 之类的打包工具为了在浏览器里加载各模块,会借助胶水代码用来组装各模块,比如 webpack 使用 map 存放模块 id 和路径,使用 webpack_require 方法获取模块导出,vite 利用浏览器原生支持模块化导入这一特性,省略了对模块的组装,也就不需要生成 bundle,所以 冷启动是非常快的
- 打包工具会将各模块提前打包进 bundle 里,但打包的过程是静态的——不管某个模块的代码是否执行到,这个模块都要打包到 bundle 里,这样的坏处就是随着项目越来越大打包后的 bundle 也越来越大。而 ESM 天生就是按需加载的,只有 import 的时候才会去按需加载
看到这里是不是会好奇那 vite
到底做了什么,我们直接用浏览器的 ESM 不就好了,那我们就来试试。
Vite 运行
提供 web server
我们在刚才生成的代码库里,不通过 npm run dev
来启动项目,直接通过浏览器打开 index.html, 会看到下面一个报错
![f57ebb15c7150cfe52eef8976ff8ae19.png](https://img-blog.csdnimg.cn/img_convert/f57ebb15c7150cfe52eef8976ff8ae19.png)
在浏览器里使用 ES module 是使用 http 请求拿到模块,所以 vite 的一个任务就是启动一个 web server 去代理这些模块,vite 里是借用了 koa 来启动了一个服务
export function createServer(config: ServerConfig): Server {
// ...
const app = new Koa()const server = resolveServer(config, app.callback())// ...const listen = server.listen.bind(server)
server.listen = (async (...args: any[]) => {if (optimizeDeps.auto !== false) {await require('../optimizer').optimizeDeps(config)
}return listen(...args)
}) as anyreturn server
}
模块解析
那我们就在本地起一个静态服务,再来打开一下 index.html 来看下
大概意思是说,找不到模块 vue,"/", "./", or "../"开头的 import 路径,才是合法的。
import vue from 'vue'
也就是说浏览器中的 ESM 是获取不到导入的模块内容的,平时我们写代码,如果不是引用相对路径的模块,而是引用 node_modules
的模块,都是直接 import xxx from 'xxx'
,由 Webpack
等工具来帮我们找这个模块的具体路径进行打包。但是浏览器不知道你项目里有 node_modules
,它只能通过相对路径或者绝对路径去寻找模块。
那这就引出了 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 中间件里获取请求 body
- 通过 es-module-lexer 解析资源 ast 拿到 import 的内容
- 判断 import 的资源是否是绝对路径,绝对视为 npm 模块
- 返回处理后的资源路径:"vue" => "/@modules/vue"
支持 /@module/
在 /src/node/server/serverPluginModuleResolve.ts
里可以看到大概的处理逻辑是
- 在 koa 中间件里获取请求 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>#app {font-family: Avenir, Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;text-align: center;color: #2c3e50;margin-top: 60px;
}style>
变成了
import HelloWorld from '/src/components/HelloWorld.vue';
const __script = {
name: 'App',
components: {
HelloWorld,
},
};
import "/src/App.vue?type=style&index=0&t=1592811240845"
import {render as __render} from "/src/App.vue?type=template&t=1592811240845"
__script.render = __render
__script.__hmrId = "/src/App.vue"
__script.__file = "/Users/wang/qdcares/test/vite-demo/src/App.vue"
export default __script
这样就把原本一个 .vue
的文件拆成了三个请求(分别对应 script、style 和template) ,浏览器会先收到包含 script 逻辑的 App.vue 的响应,然后解析到 template 和 style 的路径后,会再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。
// App.vue?type=style
import { updateStyle } from "/vite/hmr"
const css = "\n#app {\n font-family: Avenir, Helvetica, Arial, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-align: center;\n color: #2c3e50;\n margin-top: 60px;\n}\n"
updateStyle("7ac74a55-0", css)
export default css
// App.vue?type=template
import {createVNode as _createVNode, resolveComponent as _resolveComponent, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue.js"
const _hoisted_1 = /*#__PURE__*/
_createVNode("img", {
alt: "Vue logo",
src: "/src/assets/logo.png"
}, null, -1 /* HOISTED */
)
export function render(_ctx, _cache) {
const _component_HelloWorld = _resolveComponent("HelloWorld")
return (_openBlock(),
_createBlock(_Fragment, null, [_hoisted_1, _createVNode(_component_HelloWorld, {
msg: "Hello Vue 3.0 + Vite"
})], 64 /* STABLE_FRAGMENT */
))
}
实际上在看到这个思路之后,对于其他的不同类型的文件处理几乎都是类似的逻辑,根据请求的不同文件类型,做出不同的编译处理。
实际上 vite 就是在按需加载的基础上通过拦截请求实现了实时按需编译
后语
到这里我们实际上就基本了解了 vite
的原理,虽然在目前的生态下,完全替代 webpack 还不可能,但毕竟是一种的新的解决方案的探索。而实际上,除了 vite
, 社区里类似的方案还有 snowpack, 有兴趣的可以去了解一下。