迷你vite

379 阅读3分钟

在接触 vite 构建工具时,偶然了解到 vue-dev-server 的存在,少量的代码,但映入眼球的理念却十分吸引我,对学习vite也有一定的帮助,因此对其做了总结。

源码仓库地址

导语:想象一下,您可以在浏览器中本地导入Vue单文件组件...无需构建步骤。

PS:这是一个概念仓库;

入口

clone仓库后老办法,从 package.json 入手,这里启动了一个node本地服务;

"scripts": {
    "test": "cd test && node ../bin/vue-dev-server.js"
},

调试

cd test

这里将启动路径放在了 test文件夹 下,一共有三个文件,index.html、test.vue、main.js,如下: 1639552432-1987-61b995b03087b-417792.png

值得注意的是,在index.html中,利用<script>标签的 type="module" 模块化特性,将 main.js 文件通过import引入;

vue-dev-server.js

然后通过node启动了 vue-dev-server.js ,代码并不复杂,主要是 启动了3000端口服务 ,在请求到来时,会经过一个 vueMiddleware中间件

#!/usr/bin/env node

const { vueMiddleware } = require('../middleware')
//...
app.use(vueMiddleware())
//...
app.listen(3000, () => {
  console.log('server running at <http://localhost:3000>')
})

访问3000端口

可以看到返回了一个html,该文件内容,就是 test/index.html 文件; 1639552559-9951-61b9962ff2f46-385802.png

这里是通过 express.static 指定了静态文件获取的路径,在 test/ 下(默认获取index.html);

const root = process.cwd();

app.use(express.static(root))

vueMiddleware中间件

中间件主要针对请求的不同类型的文件,进行了处理: 1639552612-283-61b99664451bb-81382.png (另外,这里使用了 LRU缓存 去缓存文件处理结果,提升效率;)

let cache
if (options.cache) {
    const LRU = require('lru-cache')
    cache = new LRU({
        max: 500,
        length: function (n, key) { return n * 2 + key.length }
    })
}

1、import './main.js'文件,修改依赖模块的引用方式,方便请求识别依赖模块加载相应的文件;

import Vue from 'vue'
==>
import Vue from "/__modules/vue"

// 读取文件内容
const result = await readSource(req);
// 转化文件内容
out = transformModuleImports(result.source);
// tips:这里将文件类型设置为:application/javascript,这样浏览器可以识别并执行文件;(下同)
send(res, out, 'application/javascript');

可以看到这里是通过将文件内容转化为AST,再将依赖模块名替换; 1639552723-1525-61b996d3253f7-353166.png

2、import Vue from "/__modules/vue" 文件,加载本地node_modules下的文件;

if (pkg === 'vue') {
	const dir = path.dirname(require.resolve('vue'))
	const filepath = path.join(dir, 'vue.esm.browser.js')
	return readFile(filepath)
}

3、import App from './test.vue' 文件;
在服务端,将本地的vue文件,编译过后返回给浏览器执行;
这里将文件类型设置为:application/javascript,这样浏览器可以识别并执行文件;

const vueCompiler = require('@vue/component-compiler')

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 }

可以观察下浏览器Sources下的文件列表: 1639552851-7799-61b99753be6a7-728065.png

总结

原理:
1、通过 \<script type="module"\> 来支持原生import语法导入文件;
2、import './main.js' 时,服务端识别 .js后缀,将文件内容的 import vue from 'vue' 引用修改为 import vue from '/__modules/vue',然后返回文件内容,指定 Content-Type 为 application/javascript;
3、当 import vue from '/__modules/vue' 时,服务端识别到 /__modules/ 模块加载,将读取node_modules 下的文件并且返回;
4、import请求 .vue 组件时,服务端将 .vue组件 进行编译,从服务端返回时,指定 Content-Type 为 application/javascript,这样文件虽然是 .vue 后缀,却可以执行其中的代码;

2、3、4点可以归纳为:在服务端接受到对文件的请求时,再对文件进行处理(转换、编译等...),返回时 指定 Content-Type 为 application/javascript,浏览器识别为可执行文件从而执行代码;

5、通过 LRU缓存 存储请求的文件结果,下次再请求时,直接返回缓存结果,提升效率;