vite的优势
- 由于webpack每次打包都是根据整个依赖图谱,重新打包成对应的bunlde文件,当项目大时,打包效率比较低。虽然也有更新但效果不明显。
- vite使用的是es6的import方式,运行时按需引入对应的代码。所以只需要加载一部分文件的数据,效率高。
只要设置type="module"声明了script的js,就可以使用import的方式导入其他js
下面main.js 就可以正常的使用import
js
<script type="module" src="/src/main.js"></script>
main.js
js
import { createApp } from "vue"; //这里可以用import 写法
import App from './App.vue'//这里可以用import 写法
createApp(App)
.mount('#app')
迷你vite实现原理
下面是main.js的代码
js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
const app = createApp(App)
app.mount('#app')
App.vue
js
<template>
--{{val}}--
</template>
<script>
const val = Vue.ref("oooo")
</script>
我们看到主要要解决的是
- import的路径问题:识别
from 'vue'
里面的vue 具体是从哪里来的 - vue文件识别问题:
App.vue
文件的解析,js不能识别和加载 - css文件识别问题:解析
index.css
,js不能直接识别和加载
import的路径问题
启动一个koa服务,用于转化当前请求的文件地址
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
if(url=='/'){
ctx.type="text/html"
let content = fs.readFileSync('./index.html','utf-8')
ctx.body = content
}
})
app.listen(10086, ()=>{
console.log('服务启动')
})
我们一般启动一个vue服务后,index.html返回的内容是
html
<!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>xxxx</title>
</head>
<body>
<h1>xxxxx</h1>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
修改koa代码 ,让他能正常返回html的内容
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
if(url=='/'){ // 返回首页html内容
ctx.type="text/html"
let content = fs.readFileSync('./index.html','utf-8')
ctx.body = content
}else if(url.endsWith('.js')){ //返回js路径 既访问的文件是: <script type="module" src="/src/main.js"></script>的 main.js
// js文件
const p = path.resolve(__dirname,url.slice(1))
ctx.type = 'application/javascript'
const content = fs.readFileSync(p,'utf-8')
ctx.body = rewriteImport(content) // 替换路径
}else if(url.startsWith('/@modules/')){ // 处理node_module
const prefix = path.resolve(__dirname,'node_modules',url.replace('/@modules/',''))
const module = require(prefix+'/package.json').module
const p = path.resolve(prefix,module)
const ret = fs.readFileSync(p,'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(ret) // 递归调用
}
})
function rewriteImport(content){ //此方法用于 替换请求文件的路径 改成/@modules下面
return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0,s1){
// . ../ /开头的,都是相对路径
if(s1[0]!=='.'&& s1[1]!=='/'){
return ` from '/@modules/${s1}'`
}else{
return s0
}
})
}
app.listen(10086, ()=>{
console.log('服务启动')
})
请求的index.html
请求的main.js
请求的vue
vue3又会根据自己的import继续引入其他vue3运行所需要的框架
- runtime-dom
- runtime-core
- shared
- reactivity
vue文件识别
.vue中的三种类型
<template>
<scirpt>
<style>
原理就是把请求vue文件的时候,变成3个不同的import请求,通过url?type=xxx 区分
- 如:url?type=template,就做template解析处理
- 如:url?type=style,就做css解析处理
- 如:url?type=js,就做js解析处理
我们使用@vue/compiler-sfc 来处理vue文件。
compiler-dom 处理<template>
的转化
js
const compilerSfc = require('@vue/compiler-sfc') // .vue
const compilerDom = require('@vue/compiler-dom') // 模板<template>处理
...
else if(url.indexOf('.vue')>-1){ //发现是.vue文件,把他转化为url?type=template 的二次请求
// vue单文件组件
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
// 使用vue自导的compile框架 解析单文件组件,等价于vue-loader
const {descriptor} = compilerSfc.parse(fs.readFileSync(p,'utf-8'))
if(!query.type){
ctx.type = 'application/javascript'
//descriptor.script.content 输出的是template内容
console.log("vue come in",descriptor)
ctx.body = `
${rewriteImport(descriptor.script.content.replace('export default ','const __script = '))}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script
`
console.log("ctx.body",ctx.body)
}else if(query.type==='template'){ // 这里处理 url?type=template的逻辑
// 模板内容
const template = descriptor.template
// 要在server端吧compiler做了
const render = compilerDom.compile(template.content, {mode:"module"}).code
ctx.type = 'application/javascript'
ctx.body = rewriteImport(render) // 继续递归处理import内容
}
}
使用sfc转化后的App.vue,里面多了一个template的转化后的/App.vue?type=template
请求
css文件识别
识别.css后缀,然后通过创建一个<style>
标签插入css到html里面
js
else if(url.endsWith('.css')){
// console.log("css come in")
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
}
扩展-热更新
通过WebSocket 实时通知客户端更新页面数据
js
export function watch() {
const watcher = chokidar.watch(appRoot, {
ignored: ['**/node_modules/**', '**/.git/**'],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true,
});
watcher;
return watcher;
}
export function handleHMRUpdate(opts: { file: string; ws: any }) {
const { file, ws } = opts;
const shortFile = getShortName(file, appRoot);
const timestamp = Date.now();
console.log(`[file change] ${chalk.dim(shortFile)}`);
let updates;
if (shortFile.endsWith('.css')) {
updates = [
{
type: 'js-update',
timestamp,
path: `/${shortFile}`,
acceptedPath: `/${shortFile}`,
},
];
}
ws.send({
type: 'update',
updates,
});
}
实时处理更新的数据
js
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;
}
}