原创声明:本文首发于公众号:前端琐话(qianduansuohua),欢迎关注css
前两天尤大在 vue 3.0 beta
直播中提到了一个 vite
的工具,并且还发推表示再也回不去 webpack
了, 还引来了 webpack
核心开发人员肖恩的搞笑回复, 那就让咱们一块儿来看一下 vite
到底有啥魔力?html
github:github.com/vitejs/vite前端
Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。vue
它主要具备如下特色:node
那废话少说,咱们先直接来试用一下。webpack
$ npm init vite-app <project-name> $ cd <project-name> $ npm install $ npm run dev 复制代码
咱们来看下生成的代码, 由于 vite
尝试尽量多地镜像 vue-cli
中的默认配置, 因此咱们会发现看上去和 vue-cli
生成的代码没有太大区别。git
├── index.html
├── package.json
├── public
│ └── favicon.ico
└── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ └── HelloWorld.vue
├── index.css
└── main.js
复制代码
那咱们看下入口 index.html 和 main.jsgithub
<!DOCTYPE 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 App</title> </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') 复制代码
发现主要的不一样在于多了这么个东西web
<script type="module" src="/src/main.js"></script> 复制代码
那下面咱们就来看下这是个啥?vue-cli
script module
是 ES 模块在浏览器端的实现,目前主流的浏览器都已经支持
其最大的特色是在浏览器端使用 export
、import
的方式导入和导出模块,在 script
标签里设置 type="module"
<script type="module"> import { createApp } from './main.js‘; createApp(); </script> 复制代码
浏览器会识别添加 type="module"
的 <script>
元素,浏览器会把这段内联 script 或者外链 script 认为是 ECMAScript
模块,浏览器将对其内部的 import
引用发起 http 请求获取模块内容。 在 main.js 里,咱们用 named export 导出 createApp
函数,在上面的 script 中能获取到该函数
// main.js export function createApp(){ console.log('create app!'); }; 复制代码
其实到这里,咱们基本能够理解 vite 宣称的几个特性了。
看到这里是否是会好奇那 vite
到底作了什么,咱们直接用浏览器的 ESM 不就行了,那咱们就来试试。
咱们在刚才生成的代码库里,不经过 npm run dev
来启动项目,直接经过浏览器打开 index.html, 会看到下面一个报错
在浏览器里使用 ES module 是使用 http 请求拿到模块,因此 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 (...args: any[]) => { if (optimizeDeps.auto !== false) { await require('../optimizer').optimizeDeps(config) } return listen(...args) }) as any return 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 都作了一层处理,其过程以下:
/@module/
在 /src/node/server/serverPluginModuleResolve.ts
里能够看到大概的处理逻辑是
上面咱们提到的是对普通 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, 有兴趣的能够去了解一下。