vite的优势
- 由于webpack每次打包都是根据整个依赖图谱,重新打包成对应的bunlde文件,当项目大时,打包效率比较低。虽然也有更新但效果不明显。
- vite使用的是es6的import方式,运行时按需引入对应的代码。所以只需要加载一部分文件的数据,效率高。
只要设置type="module"声明了script的js,就可以使用import的方式导入其他js
下面main.js 就可以正常的使用import
<script type="module" src="/src/main.js"></script>
main.js
import { createApp } from "vue"; //这里可以用import 写法
import App from './App.vue'//这里可以用import 写法
createApp(App).mount('#app')
迷你vite
原理
通过起一个服务器,把部分代码转化成单独的请求。 如
- 遇到vue 改变成读当前node_moudles里面的vue
- 遇到css 转化成新一个js请求,并动态生成style插入到html
- 遇到template ,转化成新一个js请求,并在服务端实时转化成render方法
实现
代码结构
- index.html
- main.js
- App.vue
- index.css
index.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>
main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
const app = createApp(App)
app.mount('#app')
App.vue
<template>
<p>--{{val}}--</p>
<button @click="add">测试+1</button>
</template>
<script>
import {ref} from 'vue';
export default {
name: 'HelloWorld',
data() {
return {
val: 0
}
},
methods: {
add() {
this.val++
}
}
}
</script>
<style>
p {
color: green;
}
</style>
index.css
html {
color : red;
}
我们看到主要要解决的是
- import的路径问题:识别
from 'vue'里面的vue 具体是从哪里来的 - vue文件识别问题:
App.vue文件的解析,js不能识别和加载 - css文件识别问题:解析
index.css,js不能直接识别和加载
import的路径问题
启动一个koa服务,用于转化当前请求的文件地址
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服务后,请求http://127.0.0.1:10086返回index.html的内容
这里看到 main.js报错
修改koa代码 ,让他能正常返回 main.js 和 vue
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文件的时候,变成多个不同的import请求,通过url?type=xxx 区分
- 如:url?type=template,就做template解析处理
- 如:url?type=style,就做css解析处理
我们使用@vue/compiler-sfc 来处理vue文件。
compiler-dom 处理<template>的转化
const compilerSfc = require('@vue/compiler-sfc') // .vue
const compilerDom = require('@vue/compiler-dom') // 模板<template>处理
...
else if(url.indexOf('.vue')>-1){ //发现是.vue单文件组件,判断 1.无参数生成:template + 当前script输出 2.有参数template 把template转成render函数
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
// 使用vue自导的compile框架 解析单文件组件,等价于vue-loader
const {descriptor} = compilerSfc.parse(fs.readFileSync(p,'utf-8'))
let body = ''
if(!query.type){ // 1.无参数生成:template + 当前script输出 , 此版本只支持传统的options 写法 即 export default { data() {},methods: {} }
ctx.type = 'application/javascript'
//template + 当前script输出
body = body + `
${rewriteImport(descriptor.script.content.replace('export default ','const __script = '))}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script;
`
ctx.body = body
}else if(query.type==='template'){ // 2.有参数template 把template转成render函数
const template = descriptor.template
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请求
请求App.vue返还内容
请求/App.vue?type=template返还内容
css文件识别
识别template中的style
- 修改.vue的文件逻辑处理,新增style的处理,满足条件加入${url}?type=style
- 并通过
query.type==='style'判断style的请求场景
else if(url.indexOf('.vue')>-1){ //发现是.vue单文件组件,判断 1.无参数生成:template + 当前script输出 2.有参数template 把template转成render函数
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
// 使用vue自导的compile框架 解析单文件组件,等价于vue-loader
const {descriptor} = compilerSfc.parse(fs.readFileSync(p,'utf-8'))
let body = ''
if(!query.type){ // 1.无参数生成:template + 当前script输出 , 此版本只支持传统的options 写法 即 export default { data() {},methods: {} }
ctx.type = 'application/javascript'
if(descriptor.styles && descriptor.styles.length > 0) { //如果有style ,也输出 import url?type=style
body = body + ` import "${url}?type=style"`
}
//template + 当前script输出
body = body + `
${rewriteImport(descriptor.script.content.replace('export default ','const __script = '))}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script;
`
ctx.body = body
}else if(query.type==='template'){ // 2.有参数template 把template转成render函数
const template = descriptor.template
const render = compilerDom.compile(template.content, {mode:"module"}).code
ctx.type = 'application/javascript'
ctx.body = rewriteImport(render) // 继续递归处理import内容
}else if(query.type==='style'){ // 3.有参数style 把template转成render函数
body = body + `
let link = document.createElement('style')
link.setAttribute('type', 'text/css')
document.head.appendChild(link)
let css2 = \`${descriptor.styles.map(o => o.content).join('\n')}\`
link.innerHTML = css2
`
ctx.type = 'application/javascript'
ctx.body = body
}
}
识别.css文件
识别.css后缀,然后通过创建一个<style>标签插入css到html里面
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
}
完整的请求逻辑
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const compilerSfc = require('@vue/compiler-sfc') // .vue
const compilerDom = require('@vue/compiler-dom') // 模板<template>处理
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) // 递归调用
}else if(url.indexOf('.vue')>-1){ //发现是.vue单文件组件,判断 1.无参数生成:template + 当前script输出 2.有参数template 把template转成render函数
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
// 使用vue自导的compile框架 解析单文件组件,等价于vue-loader
const {descriptor} = compilerSfc.parse(fs.readFileSync(p,'utf-8'))
let body = ''
if(!query.type){ // 1.无参数生成:template + 当前script输出 , 此版本只支持传统的options 写法 即 export default { data() {},methods: {} }
ctx.type = 'application/javascript'
if(descriptor.styles && descriptor.styles.length > 0) { //如果有style ,也输出 import url?type=style
body = body + ` import "${url}?type=style"`
}
//template + 当前script输出
body = body + `
${rewriteImport(descriptor.script.content.replace('export default ','const __script = '))}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script;
`
ctx.body = body
}else if(query.type==='template'){ // 2.有参数template 把template转成render函数
const template = descriptor.template
const render = compilerDom.compile(template.content, {mode:"module"}).code
ctx.type = 'application/javascript'
ctx.body = rewriteImport(render) // 继续递归处理import内容
}else if(query.type==='style'){ // 3.有参数style 把template转成render函数
body = body + `
let link = document.createElement('style')
link.setAttribute('type', 'text/css')
document.head.appendChild(link)
let css2 = \`${descriptor.styles.map(o => o.content).join('\n')}\`
link.innerHTML = css2
`
ctx.type = 'application/javascript'
ctx.body = body
}
} else if(url.endsWith('.css')){ // 如果是css 生成style标签插入到html
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
`
ctx.type = 'application/javascript'
ctx.body = content
}
})
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('服务启动')
})
最终请求效果
扩展-热更新
通过WebSocket 实时通知客户端更新页面数据
const WebSocket = require('ws');
const chokidar = require('chokidar');
....
const server = app.listen(10086, ()=>{
console.log('服务启动')
})
const wss = new WebSocket.Server({ server });
// 向所有客户端发送刷新通知
function broadcast(message) {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// 使用 chokidar 监听文件
chokidar.watch(['./*.js', './*.vue'], {
ignored: /node_modules/,
persistent: true,
usePolling: true,
interval: 100, // 设置轮询间隔,单位是毫秒
}).on('change', (path) => {
console.log(`File ${path} has been changed`);
// 执行重载或其他操作
});
wss.on('connection', ws => {
ws.on('message', message => console.log('received:', message));
});
main.js添加监听
const socket = new WebSocket('ws://localhost:10086');
socket.onmessage = (event) => {
if (event.data === 'reload') {
location.reload();
}
};