前言
目前 Vite 3.0 已经正式发布,也被称为是下一代前端构建工具。
Vite 作为构建工具最基础的功能就是在入口文件 index.html 中引入相关的内容。
原生 ESM 语法
总所周知,原生 ESM 语法<script type="module" src="...">是 script 标签支持 type 属性值为 module,这样可以通过模块的方式引入 JavaScript 的代码。Vite 使用的就是这种方式来引入 JavaScript 的代码。
构建 mini-vite 服务
使用 express 来构建服务,初始化工程。 代码仓地址mini-vite: vite 基础版 (gitee.com)
响应 html 文件
index.html 文件准备。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.js"></script>
</body>
</html>
app.get("/", (_, res) => {
let content = fs.readFileSync(
path.resolve(__dirname, "./src/index.html"),
"utf-8"
)
// window下process缺失会导致报错,所以动态设置process
content = content.replace(
"<script",
`<script>
window.process = {
env: {
NODE_ENV: 'dev'
}
}
</script>
<script`
)
res.type("html").send(content)
})
响应 js 文件
index.js 文件准备。
import { createApp } from "vue"
import App from "/src/app.vue"
import "/src/index.css"
createApp(App).mount("#app")
app.get("/*.js", (req, res) => {
let content = fs.readFileSync(
path.resolve(__dirname, req.url.slice(1)),
"utf-8"
)
res.type("application/javascript").send(rewriteImport(content))
})
// 替换第三方库并发送请求 from 'vue' => from '/@modules/vue'
function rewriteImport(content) {
return content.replace(/ from ['|"]([^'"]+)['|"]/g, (s0, s1) => {
if (s1[0] !== "." && s1[0] !== "/") {
return ` from '/@modules/${s1}'`
} else {
return s0
}
})
}
响应第三方库
app.get("/@modules/*", (req, res) => {
const prefix = path.resolve(
__dirname,
"node_modules",
req.url.replace("/@modules/", "")
)
const module = require(prefix + "/package.json").module
let content = fs.readFileSync(path.resolve(prefix, module), "utf-8")
res.type("application/javascript").send(rewriteImport(content))
})
响应 Vue 单文件组件
app.vue 文件准备。
<template>
<div>{{ count }}</div>
<button type="button" @click="add">count++</button>
</template>
<script>
import { ref } from "vue"
export default {
setup() {
const count = ref(0)
const add = () => {
count.value++
}
return { count, add }
},
}
</script>
<style>
button {
font-size: 16px;
}
</style>
<style>
button {
font-size: 20px;
}
</style>
// .vue单文件编译 => complier-sfc => template + script + styles
const complierSfc = require("@vue/compiler-sfc")
const complierDom = require("@vue/compiler-dom")
app.get("/*.vue", (req, res) => {
const p = path.resolve(__dirname, req.url.split("?")[0].slice(1))
const { descriptor } = complierSfc.parse(fs.readFileSync(p, "utf-8"))
let body = ""
if (!req.query.type) {
body = `
${rewriteImport(
descriptor.script.content.replace(
"export default",
"const __script = "
)
)}
import { render as __render } from "${req.url}?type=template"
__script.render = __render
export default __script
`
if (descriptor.styles.length > 0) {
body += resolveStyle(
descriptor.styles.map((item) => item.content).join("")
)
}
} else {
// template => complier-dom => render函数
const { template } = descriptor
const render = complierDom.compile(template.content, { mode: "module" })
body = rewriteImport(render.code)
}
res.type("application/javascript").send(body)
})
// 处理 style
function resolveStyle(content) {
return `
let link = document.createElement('style')
link.setAttribute('type','text/css')
document.head.appendChild(link)
link.innerHTML = "${content.replace(/\s/g, "")}"
`
}
响应 css 文件
index.css 文件准备。
div {
color: skyblue;
}
app.get("/*.css", (req, res) => {
const file = fs.readFileSync(
path.resolve(__dirname, req.url.slice(1)),
"utf-8"
)
res.type("application/javascript").send(resolveStyle(file))
})