前言
我们知道webpack是根据文件依赖生成依赖树,并将资源打包后放到内存来提供服务,而vite则可以秒级启动项目,那它是如何做到的呢,今天我们来一探究竟。
vue-dev-server
原理
import Vue from 'vue'
import App from './test.vue'
import './index.css'
new Vue({
render: h => h(App)
}).$mount('#app')
熟悉ES6的同学应该知道,浏览器对于type="module"类型的脚本中的import会发起一个http请求,但是浏览器并不认识Vue,也不认识*.vue文件,对于*.css等类型的资源也不认识,那怎么办呢?
解决方案
看vue-dev-server文档,其中提到下面几点:
- 原生ES module直接导入,无需build
- 拦截
*.vue请求并实时编译- 有ES module的库直接从CDN获取
*.js文件中导入的npm包,即时重写路径以指向本地(本地即node_modules里)
由此我们也可以看出,对于npm包需要重写路径指向本地node_modules,对于js文件需要对里面的import进行处理,对于vue文件则需要即时编译,对于css则动态创建style标签。
实施
后台启动一个express服务器,拦截前端请求
- js
const recast = require('recast')
const isPkg = require('validate-npm-package-name')
function transformModuleImports(code) {
const ast = recast.parse(code)
recast.types.visit(ast, {
visitImportDeclaration(path) {
const source = path.node.source.value
if (!/^\.\/?/.test(source) && isPkg(source)) {
path.node.source = recast.types.builders.literal(`/__modules/${source}`)
}
this.traverse(path)
}
})
return recast.print(ast).code
}
exports.transformModuleImports = transformModuleImports
对于js里的npm包会加上/__modules前缀(比如import Vue from "/__modules/vue"),后续拦截从本地node_modules里找
- npm包
if (req.path.startsWith('/__modules/')) {
const key = parseUrl(req).pathname
const pkg = req.path.replace(/^\/__modules\//, '')
let out = await tryCache(key, false) // Do not outdate modules
if (!out) {
out = (await loadPkg(pkg)).toString()
cacheData(key, out, false) // Do not outdate modules
}
send(res, out, 'application/javascript')
}
---------------------------------------------------------------
const fs = require('fs')
const path = require('path')
const readFile = require('util').promisify(fs.readFile)
async function loadPkg(pkg) {
if (pkg === 'vue') {
const dir = path.dirname(require.resolve('vue'))
const filepath = path.join(dir, 'vue.esm.browser.js')
return readFile(filepath)
}
}
exports.loadPkg = loadPkg
对于npm包需要到node_modules下vue/package.json中拿到modules字段值的值,并读取其中的文件内容
- vue文件
async function bundleSFC (req) {
const { filepath, source, updateTime } = await readSource(req)
const descriptorResult = compiler.compileToDescriptor(filepath, source)
const assembledResult = vueCompiler.assemble(compiler, filepath, {
...descriptorResult,
script: injectSourceMapToScript(descriptorResult.script),
styles: injectSourceMapsToStyles(descriptorResult.styles)
})
return { ...assembledResult, updateTime }
}
对于vue文件,需要@vue/component-compiler进行即时编译为浏览器支持的js
- css文件
if(url.endsWith('.css')){
const p = path.resolve(__dirname,url.slice(1))
const file = fs.readFileSync(p,'utf-8')
const content = `
const css = "${file.replace(/\n/g,'')}"
let link = document.createElement('style')
link.setAttribute('type', 'text/css')
document.head.appendChild(link)
link.innerHTML = css
export default css
`
ctx.type = 'application/javascript'
ctx.body = content
}
热更新
为了实现HMR需要在客户端和服务端各启动一个websocket,服务端通过chokidar监听文件变化并通知客户端 server:
const watcher = chokidar.watch(appRoot, {
ignored: ['**/node_modules/**', '**/.git/**'],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true,
});
ws.send({
type: 'update',
updates: [
{
type: 'js-update',
timestamp,
path: getShortName(file, appRoot),
acceptedPath: getShortName(file, appRoot),
},
],
});
客户端收到消息重新获取变更的文件 client:
async function handleMessage(payload: any) {
switch (payload.type) {
case 'connected':
console.log(`[vite] connected.`);
setInterval(() => socket.send('ping'), 30000);
break;
case 'update':
payload.updates.forEach((update: Update) => {
if (update.type === 'js-update') {
fetchUpdate(update);
}
});
break;
}
}
预构建
对于分散的模块,为了减少http请求数量,vite通过 预构建合并成一个模块,从而只会有一个请求。已预构建的依赖请求使用 HTTP 头 max-age=31536000, immutable 进行强缓存。
总结
vite对比webpack确实有更好的开发体验,充分利用了浏览器对es6新特性的支持,当然其中也不乏esbuild解析js时的高性能,通过对vite工作原理的简单分析,我们对整个vue项目怎么在浏览器里呈现有了更感性的认识。