在接触 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,如下:
值得注意的是,在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 文件;
这里是通过 express.static 指定了静态文件获取的路径,在 test/ 下(默认获取index.html);
const root = process.cwd();
app.use(express.static(root))
vueMiddleware中间件
中间件主要针对请求的不同类型的文件,进行了处理:
(另外,这里使用了 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,再将依赖模块名替换;
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下的文件列表:
总结
原理:
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缓存存储请求的文件结果,下次再请求时,直接返回缓存结果,提升效率;