Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。 ——https://zhuanlan.zhihu.com/p/150083887?from_voters_page=true
做过vue项目的人都知道,当项目越变越大,或者变成多页面应用时,热更新打包速度奇慢无比,每次保存都要几分钟。
尤雨溪在B站直播提到他最近在做的这工具 vite[1] ,一个实验性的no build的vue开发服务器。(这个小工具可以支持热更新,且不用预编译)。它的特性有:
•基于浏览器原生JS module功能(补白阅读:在浏览器中使用javascript module(译)),因而有更快的冷启动和热更新,整体速度与模块数量无关(无论项目多大,都是O(1)复杂度)•没有打包的过程,源码直接传输给浏览器,使用原生的 <script module>
语法进行引入,开发服务器拦截请求和对需要转换的代码进行转换。•实现了真正的按需编译。打开哪个页面,就解析哪些模块。•生产环境提供了 vite build 脚本进行打包,它基于 rollup 进行打包
vite构建的简单过程可以看到如下:
此过程可以理解为“只解析,不打包”。理论上支持react等任意前端开发框架。作者甚至在社交网络直言:他以后再不想用 webpack 了。
但是,因为JS module是“现代浏览器”支持功能,对于远古浏览器是不支持的。因此,只建议在开发环境下使用。对于生产环境,还是只能走打包那套。
项目需求:基于现代浏览器实现一个mini版的vite工具。
npm init -y
在项目中新建一个index.html。通过script type="module"
引入main.js
文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vite-mini</title>
</head>
<body>
<!-- 单页面容器 -->
<div id="app"></div>
<script type="module" src="src/main.js"></script>
</body>
</html>
新建src/main.js,直接输入console.log('main.js')
,直接打开index.html,发现这种操作被同源策略阻止了。
Access to script at 'file:///Users/dangjingtao/Desktop/vite-min/src/main.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.
index.html:12 GET file:///Users/dangjingtao/Desktop/vite-min/src/main.js net::ERR_FAILED
所以还是得起服务器。此时可以选用koa启服务。
npm i koa -S
新建server.js
简单写一个服务:
// ./server.js
// 用最传统的方式
const fs = require('fs');
const path = require('path');
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
const { request: { url, query } } = ctx;
let content = '';
if (url == '/') {
content = fs.readFileSync('./index.html', 'UTF-8');
ctx.type = 'text/html';
} else if (url.endsWith('.js')) {
// 获取绝对路径
const p = path.resolve(__dirname, url.slice(1));
content = fs.readFileSync(p, 'UTF-8');
ctx.type = 'application/javascript';
} else {
}
ctx.body = content;
});
app.listen(3001, err => {
if (!err) {
console.log('server started..');
}
});
访问http://localhost:3001
就看到引入main.js的html了。现在验证之。
在src下新建log.js写一个模块服务:log
:
const {log} = console;
export default log;
然后在main.js引用它:
import log from './log.js';
log('aaa');
再次访问网址,发现log被导入进来了。显然,现代浏览器显然不需要打包了。
首先安装vue 3:
npm i vue@next -S
/@modules/vue
语法(rewriteImport)在做vue开发时,我们通常怎样使用的脚手架?
import { ref, watchEffect } from "vue"
console.log(ref)
let count = ref(0)
watchEffect(() => {
console.log("监测到数据变化")
})
我们在main.js写入上述代码,发现报错(只支持路径模式):
因此想要支持导入vue,首先要改造import xx from 'vue'
。正确应该是:
import xx from '/@modules/vue';
写一个匹配xx的方法,用来重写/@modules/vue
这样的代码:
const rewriteImport = (content) => {
return content.replace(/from ['"]([^'"]+)['"]/g, (s0, s1) => {
// 只改写需要去node_module找的
if (s1[0] !== '.' && s1[0] !== '/') {
return `from '/@modules/${s1}'`
}
return s0;
});
}
然后,修改js解析方法:
app.use(async ctx => {
const { request: { url, query } } = ctx;
let content = '';
if (url == '/') {
// 访问index.html
// ...
} else if (url.endsWith('.js')) {
// ...
// 1. 支持import xx from '/@modules/vue';
content = rewriteImport(content);
} else {
}
ctx.body = content;
});
然后再访问,发现代码被替换为想要的样子了:
小结:接下来只要有import语法的地方都需要调用这个方法“预编译”一下。
但是页面依然报错。因为服务器生成了一个http://localhost:3001/@modules/vue
的请求。
接下来便是拦截来自/@modules/
的路由,去找node_modules内的依赖——找依赖一般看的就是:
1.先去node_modules
文件夹下检索依赖名字2.找到对应依赖,看package.json
,找到里面的module
属性:
显然需要的就是这个文件。把它return 回去即可。
app.use(async ctx => {
const { request: { url, query } } = ctx;
let content = '';
if (url == '/') {
// 访问index.html
// ...
} else if (url.endsWith('.js')) {
// ...
// 1. 支持import xx from '/@modules/vue';
content = rewriteImport(content);
} else if (url.startsWith('/@modules/')) {
// 2. 去node_module找依赖
// prefix 是相关依赖在node_modules下的绝对路径。
const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
const module = require(prefix + '/package.json').module;
const p = path.resolve(prefix, module);
content = fs.readFileSync(p, 'UTF-8');
ctx.type = 'application/javascript';
content = rewriteImport(content);
} else {
}
ctx.body = content;
});
再跑一下,发现控制台已经多了很多依赖了。
访问http://localhost:3001
:
访问http://localhost:3001/@modules/vue
:
#### 2.2.3 屏蔽报错
各个请求是已经在network里发出去了。但是引入的vue作为前端代码依然还在报错。(报错原因:node环境全局变量process在浏览器端位undefined)。
这时可以用一个比较low的方式屏蔽错误。
app.use(async ctx => {
const { request: { url, query } } = ctx;
let content = '';
if (url == '/') {
// 访问index.html
content = fs.readFileSync('./index.html', 'UTF-8');
// 3.解决报错,把node环境参数发给前端:
content = content.replace('<script', `
<script>
window.process = {env:{NODE_ENV:'DEV'}}
</script>
<script
`);
ctx.type = 'text/html';
} else if (url.endsWith('.js')) {
// ...
// 1. 支持import xx from '/@modules/vue';
content = rewriteImport(content);
} else if (url.startsWith('/@modules/')) {
// 2. 去node_module找依赖
// prefix 是相关依赖在node_modules下的绝对路径。
// ..
} else {
}
ctx.body = content;
});
修改main.js,写一段代码:
import { ref, watchEffect } from "vue";
console.log(ref);
let count = ref(0);
watchEffect(() => {
console.log("监测到数据变化", count.value);
})
setInterval(() => {
count.value++;
}, 1000);
再次访问http://localhost:9001:
调用vue成功。
在vite中解析import语法用的是第三方模块
es-module-lexer
.vue
单文件Main.js作为全局的入口,通常是这么写的:
import { createApp } from "vue";
import App from './App.vue';
createApp(App).mount('#app');
应当允许引入App.vue,期望有里面有这些内容:
<template>
<div>
<h2>count: {{ count }}</h2>
<h2>double: {{ double }}</h2>
<button @click="add">add</button>
</div>
</template>
<script>
import { ref, computed } from "vue"
export default {
setup() {
const count = ref(1)
const add = () => {
count.value++
}
const double = computed(() => count.value * 2)
return { count, double, add }
},
}
</script>
解析vue文件的思路是:
•vue当中分为template和script两个标签。•把script拿出来解析js,把template拿出来解析模版。
观察vite的实现,发现vite是把style,script,template单独作为一个网络请求。借助vite的工具compiler-sfc
可以实现。
npm i @vue/compiler-sfc -S
在server.js中再加一个else if:当import语句以.vue
结尾时:
// ...
const compilerSfc = require('@vue/compiler-sfc');
// ...
app.use(async ctx => {
const { request: { url, query } } = ctx;
let content = '';
console.log(url)
if (url == '/') {
// 访问index.html
// ...
} else if (url.endsWith('.js')) {
// 支持import xx from '/@modules/vue';
// ...
} else if (url.startsWith('/@modules/')) {
// 去node_module找依赖
// ...
} else if (url.endsWith('.vue')) {
// 引入vue文件
const p = path.resolve(__dirname, url.slice(1));
let _content = compilerSfc.parse(fs.readFileSync(p, 'UTF-8'));
console.log(_content)
} else { }
ctx.body = content;
});
这时打印_content
值是:
{
descriptor: {
filename: 'component.vue',
source: '<template>\n' +
' <div>\n' +
// (略)...
'</script>\n',
template: {
type: 'template',
content: '\n' +
' <div>\n' +
' <h2>count: {{ count }}</h2>\n' +
' <h2>double: {{ double }}</h2>\n' +
' <button @click="add"></button>\n' +
' </div>\n',
loc: [Object],
attrs: {},
map: [Object]
},
script: {
type: 'script',
content: '\n' +
'import { ref, computed } from "vue"\n' +
'export default {\n' +
' setup() {\n' +
' const count = ref(1)\n' +
' const add = () => {\n' +
' count.value++\n' +
' }\n' +
' const double = computed(() => count.value * 2)\n' +
' return { count, double, add }\n' +
' },\n' +
'}\n',
loc: [Object],
attrs: {},
map: [Object]
},
scriptSetup: null,
styles: [],
customBlocks: []
},
errors: []
}
发现不但有解析的内容,包括script,template的内容都打印出来了。我们需要的就是这段js。
// 引入vue文件
const p = path.resolve(__dirname, url.slice(1));
let _content = compilerSfc.parse(fs.readFileSync(p, 'UTF-8'));
ctx.type = 'application/javascript';
content = rewriteImport(_content.descriptor.script.content)
.replace('export default', 'const __script = ');
再刷新,看到app.vue的网络请求实际上变成了script里的那段js:
App.vue还有template模板。需要把它抽离出来,解析为一个js,让它去生成html。
我们观察官方的vite项目,发现模板也是发起了一个请求,请求地址同样是App.vue,但是get参数不同(?type=template&t=时间戳
)。时间戳是做缓存用的,不需要,我们保留template即可。
再然后,发现这是一个请求,所以不能用endsWith
了,只能用indexOf。并且需要对type进行分类讨论。
拿到template -> 需要一个插件把template解析为render函数:
npm i @vue/compiler-dom -S
届时可以调用compileDom.compile
方法,抓取里面的html。
综上:
// ...
} else if (url.indexOf('.vue') > -1) {
// 引入vue文件,分离get请求参数
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
let _content = compilerSfc.parse(fs.readFileSync(p, 'UTF-8'));
if (!query.type) {
// 处理.vue中的script
ctx.type = 'application/javascript';
_content = rewriteImport(_content.descriptor.script.content)
.replace('export default', 'const __script = ');
content = `${_content}\n` +
`import { render as __render } from "${url}?type=template"\n` +
`__script.render = __render\n` +
`export default __script\n`;
} else if (query.type == 'template') {
// 处理.vue中的模板template
const template = _content.descriptor.template;
const render = compileDom.compile(template.content, { mode: 'module' }).code;
ctx.type = 'application/javascript';
content = rewriteImport(render);
}
}
再运行:
可见该部分内容已经被解析为js。
这时再运行:已经看到计数器了。
实际上你需要解析其它许多内容,比如style,sass,less,typescript等等。实际上就是原来webpack中各种loader的功能。
此处以样式为例。
在main.js中引入样式:
import { createApp } from "vue";
import App from './App.vue';
import style from 'App.css';
createApp(App).mount('#app');
src/App.css代码如下:
h2 {
color: red;
}
思路就是在app.use中继续加else if。
else if (url.endsWith('.css')) {
const p = path.resolve(__dirname, url.slice(1));
const _content = fs.readFileSync(p, 'UTF-8');
ctx.type = 'text/css';
content = _content;
}
再运行,你会发现,从ctx返回这段css是没卵用的。——思路还是返回一段js。通过js去创建全局的script标签。然后把样式塞进去。
else if (url.endsWith('.css')) {
const p = path.resolve(__dirname, url.slice(1));
let _content = fs.readFileSync(p, 'UTF-8');
_content =
`const css = '${_content.replace(/\n/g, '')}';\n` +
`let link = document.createElement('style');\n` +
`link.setAttribute('type','text/css');\n` +
`link.innerHTML = css;\n` +
`document.head.appendChild(link);\n` +
`export default css;`
ctx.type = 'application/javascript';
content = _content;
}
那么样式引入功能就实现了。所以你这里import的实际是一段js
你也许会说,那么多else if
已经很难看了。但在vite的真实实践中,这是通过中间件“链”起来的。中间件版可自行实现。此处不多赘述了。
[1]
vite: https://github.com/vuejs/vite
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有