Vite是什么就不必笔者多说了,用过Vue
的敌人必定都晓得,本文会通过手写一个非常简单的乞丐版Vite
来理解一下Vite
的根本实现原理,参考的是Vite
最早的版本(vite-1.0.0-rc.5
版本,Vue
版本为3.0.0-rc.10
)实现的,当初曾经是3.x
的版本了,为什么不间接参考最新的版本呢,因为一上来就看这种比较完善的工具源码比拟难看懂,反正笔者不行,所以咱们能够先从最早的版本来窥探一下原理,能力强的敌人能够疏忽~
本文会分为高低两篇,上篇次要探讨如何胜利运行我的项目,下篇次要探讨热更新。
前端测试项目
前端测试项目构造如下:
Vue
组件应用的是Options Api
,不波及到css
预处理语言、ts
等js
语言,所以是一个非常简单的我的项目,咱们的指标很简略,就是要写一个Vite
服务让这个我的项目能运行起来!
搭建根本服务
vite
服务的根本构造如下:
首先让咱们来起个服务,HTTP
利用框架咱们应用connect:
// app.js
const connect = require("connect");
const http = require("http");
const app = connect();
app.use(function (req, res) {
res.end("Hello from Connect!\n");
});
http.createServer(app).listen(3000);
接下来咱们须要做的就是拦挡各种类型的申请来进行不同的解决。
拦挡html
我的项目拜访的入口地址是http://localhost:3000/index.html
,所以接到的第一个申请就是html
文件的申请,咱们临时间接返回html
文件的内容即可:
// app.js
const path = require("path");
const fs = require("fs");
const basePath = path.join("../test/");
const typeAlias = {
js: "application/javascript",
css: "text/css",
html: "text/html",
json: "application/json",
};
app.use(function (req, res) {
// 提供html页面
if (req.url === "/index.html") {
let html = fs.readFileSync(path.join(basePath, "index.html"), "utf-8");
res.setHeader("Content-Type", typeAlias.html);
res.statusCode = 200;
res.end(html);
} else {
res.end('')
}
});
当初拜访页面必定还是一片空白,因为页面发动的main.js
的申请咱们还没有解决,main.js
的内容如下:
拦挡js申请
main.js
申请须要做一点解决,因为浏览器是不反对裸导入的,所以咱们要转换一下裸导入的语句,将import xxx from 'xxx'
转换为import xxx from '/@module/xxx'
,而后再拦挡/@module
申请,从node_modules
里获取要导入的模块进行返回。
解析导入语句咱们应用es-module-lexer:
// app.js
const { init, parse: parseEsModule } = require("es-module-lexer");
app.use(async function (req, res) {
if (/\.js\??[^.]*$/.test(req.url)) {
// js申请
let js = fs.readFileSync(path.join(basePath, req.url), "utf-8");
await init;
let parseResult = parseEsModule(js);
// ...
}
});
解析的后果为:
解析后果为一个数组,第一项也是个数组代表导入的数据,第二项代表导出,main.js
没有,所以是空的。s
、e
代表导入起源的起止地位,ss
、se
代表整个导入语句的起止地位。
接下来咱们查看当导入起源不是.
或/
结尾的就转换为/@module/xxx
的模式:
// app.js
const MagicString = require("magic-string");
app.use(async function (req, res) {
if (/\.js\??[^.]*$/.test(req.url)) {
// js申请
let js = fs.readFileSync(path.join(basePath, req.url), "utf-8");
await init;
let parseResult = parseEsModule(js);
let s = new MagicString(js);
// 遍历导入语句
parseResult[0].forEach((item) => {
// 不是裸导入则替换
if (item.n[0] !== "." && item.n[0] !== "/") {
s.overwrite(item.s, item.e, `/@module/${item.n}`);
}
});
res.setHeader("Content-Type", typeAlias.js);
res.statusCode = 200;
res.end(s.toString());
}
});
批改js
字符串咱们应用了magic-string,从这个简略的示例上你应该能发现它的魔法之处,就是即便字符串曾经变了,但应用原始字符串计算出来的索引批改它也还是正确的,因为索引还是绝对于原始字符串。
能够看到vue
曾经胜利被批改成/@module/vue
了。
紧接着咱们须要拦挡一下/@module
申请:
// app.js
const { buildSync } = require("esbuild");
app.use(async function (req, res) {
if (/^\/@module\//.test(req.url)) {
// 拦挡/@module申请
let pkg = req.url.slice(9);
// 获取该模块的package.json
let pkgJson = JSON.parse(
fs.readFileSync(
path.join(basePath, "node_modules", pkg, "package.json"),
"utf8"
)
);
// 找出该模块的入口文件
let entry = pkgJson.module || pkgJson.main;
// 应用esbuild编译
let outfile = path.join(`./esbuild/${pkg}.js`);
buildSync({
entryPoints: [path.join(basePath, "node_modules", pkg, entry)],
format: "esm",
bundle: true,
outfile,
});
let js = fs.readFileSync(outfile, "utf8");
res.setHeader("Content-Type", typeAlias.js);
res.statusCode = 200;
res.end(js);
}
})
咱们先获取了包的package.json
文件,目标是找出它的入口文件,而后读取并应用esbuild进行转换,当然Vue
是有ES模块
的产物的,然而可能有的包没有,所以间接就对立解决了。
拦挡css申请
css
申请有两种,一种来源于link
标签,一种来源于import
形式,link
标签的css
申请咱们间接返回css
即可,然而import
的css
间接返回是不行的,ES模块
只反对js
,所以咱们须要转成js
类型,次要逻辑就是手动把css
插入页面,所以这两种申请咱们须要离开解决。
为了能辨别import
申请,咱们批改一下后面拦挡js
的代码,把每个导入起源都加上?import
查问参数:
// ...
// 遍历导入语句
parseResult[0].forEach((item) => {
// 不是裸导入则替换
if (item.n[0] !== "." && item.n[0] !== "/") {
s.overwrite(item.s, item.e, `/@module/${item.n}?import`);
} else {
s.overwrite(item.s, item.e, `${item.n}?import`);
}
});
//...
拦挡/@module
的中央也别忘了批改:
// ...
let pkg = removeQuery(req.url.slice(9));// 从/@module/vue?import中解析出vue
// ...
// 去除url的查问参数
const removeQuery = (url) => {
return url.split("?")[0];
};
这样import
的申请就都会带上一个标记:
而后依据这个标记来别离解决css
申请:
// app.js
app.use(async function (req, res) {
if (/\.css\??[^.]*$/.test(req.url)) {
// 拦挡css申请
let cssRes = fs.readFileSync(
path.join(basePath, req.url.split("?")[0]),
"utf-8"
);
if (checkQueryExist(req.url, "import")) {
// import申请,返回js文件
cssRes = `
const insertStyle = (css) => {
let el = document.createElement('style')
el.setAttribute('type', 'text/css')
el.innerHTML = css
document.head.appendChild(el)
}
insertStyle(\`${cssRes}\`)
export default insertStyle
`;
res.setHeader("Content-Type", typeAlias.js);
} else {
// link申请,返回css文件
res.setHeader("Content-Type", typeAlias.css);
}
res.statusCode = 200;
res.end(cssRes);
}
})
// 判断url的某个query名是否存在
const checkQueryExist = (url, key) => {
return new URL(path.resolve(basePath, url)).searchParams.has(key);
};
如果是import
导入的css
那么就把它转换为js
类型的响应,而后提供一个创立style
标签并插入到页面的办法,并且立刻执行,那么这个css
就会被插入到页面中,个别这个办法会被提前注入页面。
如果是link
标签的css
申请间接返回css
即可。
拦挡vue申请
最初,就是解决Vue
单文件的申请了,这个会略微简单一点,解决Vue
单文件咱们应用@vue/compiler-sfc
的3.0.0-rc.10
版本,首先须要把Vue
单文件的template
、js
、style
三局部解析进去:
// app.js
const { parse: parseVue } = require("@vue/compiler-sfc");
app.use(async function (req, res) {
if (/\.vue\??[^.]*$/.test(req.url)) {
// Vue单文件
let vue = fs.readFileSync(
path.join(basePath, removeQuery(req.url)),
"utf-8"
);
let { descriptor } = parseVue(vue);
}
})
而后再别离解析三局部,template
和css
局部会转换成一个import
申请。
解决js
局部
// ...
const { compileScript, rewriteDefault } = require("@vue/compiler-sfc");
let code = "";
// 解决js局部
let script = compileScript(descriptor);
if (script) {
code += rewriteDefault(script.content, "__script");
}
rewriteDefault
办法用于将export default
转换为一个新的变量定义,这样咱们能够注入更多数据,比方:
// 转换前
let js = `
export default {
data() {
return {}
}
}
`
// 转换后
let js = `
const __script = {
data() {
return {}
}
}
`
//而后能够给__script增加更多属性,最初再手动增加到导出即可
js += `\n__script.xxx = xxx`
js += `\nexport default __script`
解决template
局部
// ...
// 解决模板
if (descriptor.template) {
let templateRequest = removeQuery(req.url) + `?type=template`;
code += `\nimport { render as __render } from ${JSON.stringify(
templateRequest
)}`;
code += `\n__script.render = __render`;
}
将模板转换成了一个import
语句,而后获取导入的render
函数挂载到__script
上,前面咱们会拦挡这个type=template
的申请,返回模板的编译后果。
解决style
局部
// ...
// 解决款式
if (descriptor.styles) {
descriptor.styles.forEach((s, i) => {
const styleRequest = removeQuery(req.url) + `?type=style&index=${i}`;
code += `\nimport ${JSON.stringify(styleRequest)}`
})
}
和模板一样,款式也转换成了一个独自的申请。
最初导出__script
并返回数据:
// ...
// 导出
code += `\nexport default __script`;
res.setHeader("Content-Type", typeAlias.js);
res.statusCode = 200;
res.end(code);
能够看到__script
其实就是一个Vue
的组件选项对象,模板局部编译的后果就是组件的渲染函数render
,相当于把js
和模板局部组合成一个残缺的组件选项对象。
解决模板申请
当Vue
单文件的申请url
存在type=template
参数,咱们就编译一下模板而后返回:
// app.js
const { compileTemplate } = require("@vue/compiler-sfc");
app.use(async function (req, res) {
if (/\.vue\??[^.]*$/.test(req.url)) {
// vue单文件
// 解决模板申请
if (getQuery(req.url, "type") === "template") {
// 编译模板为渲染函数
code = compileTemplate({
source: descriptor.template.content,
}).code;
res.setHeader("Content-Type", typeAlias.js);
res.statusCode = 200;
res.end(code);
return;
}
// ...
}
})
// 获取url的某个query值
const getQuery = (url, key) => {
return new URL(path.resolve(basePath, url)).searchParams.get(key);
};
解决款式申请
款式和后面咱们拦挡款式申请一样,也须要转换成js
而后手动插入到页面:
// app.js
const { compileTemplate } = require("@vue/compiler-sfc");
app.use(async function (req, res) {
if (/\.vue\??[^.]*$/.test(req.url)) {
// vue单文件
}
// 解决款式申请
if (getQuery(req.url, "type") === "style") {
// 获取款式块索引
let index = getQuery(req.url, "index");
let styleContent = descriptor.styles[index].content;
code = `
const insertStyle = (css) => {
let el = document.createElement('style')
el.setAttribute('type', 'text/css')
el.innerHTML = css
document.head.appendChild(el)
}
insertStyle(\`${styleContent}\`)
export default insertStyle
`;
res.setHeader("Content-Type", typeAlias.js);
res.statusCode = 200;
res.end(code);
return;
}
})
款式转换为js
的这个逻辑因为有两个中央用到了,所以咱们能够提取成一个函数:
// app.js
// css to js
const cssToJs = (css) => {
return `
const insertStyle = (css) => {
let el = document.createElement('style')
el.setAttribute('type', 'text/css')
el.innerHTML = css
document.head.appendChild(el)
}
insertStyle(\`${css}\`)
export default insertStyle
`;
};
修复单文件的裸导入问题
单文件内的js
局部也能够导入模块,所以也会存在裸导入的问题,后面介绍了裸导入的解决办法,那就是先替换导入起源,所以单文件的js
局部解析进去当前咱们也须要进行一个替换操作,咱们先把替换的逻辑提取成一个公共办法:
// 解决裸导入
const parseBareImport = async (js) => {
await init;
let parseResult = parseEsModule(js);
let s = new MagicString(js);
// 遍历导入语句
parseResult[0].forEach((item) => {
// 不是裸导入则替换
if (item.n[0] !== "." && item.n[0] !== "/") {
s.overwrite(item.s, item.e, `/@module/${item.n}?import`);
} else {
s.overwrite(item.s, item.e, `${item.n}?import`);
}
});
return s.toString();
};
而后编译完js
局部后立刻解决一下:
// 解决js局部
let script = compileScript(descriptor);
if (script) {
let scriptContent = await parseBareImport(script.content);// ++
code += rewriteDefault(scriptContent, "__script");
}
另外,编译后的模板局部代码也会存在一个裸导入Vue
,也须要解决一下:
// 解决模板申请
if (
new URL(path.resolve(basePath, req.url)).searchParams.get("type") ===
"template"
) {
code = compileTemplate({
source: descriptor.template.content,
}).code;
code = await parseBareImport(code);// ++
res.setHeader("Content-Type", typeAlias.js);
res.statusCode = 200;
res.end(code);
return;
}
解决动态文件
App.vue
外面引入了两张图片:
编译后的后果为:
ES模块
只能导入js
文件,所以动态文件的导入,响应后果也须要是js
:
// vite/app.js
app.use(async function (req, res) {
if (isStaticAsset(req.url) && checkQueryExist(req.url, "import")) {
// import导入的动态文件
res.setHeader("Content-Type", typeAlias.js);
res.statusCode = 200;
res.end(`export default ${JSON.stringify(removeQuery(req.url))}`);
}
})
// 查看是否是动态文件
const imageRE = /\.(png|jpe?g|gif|svg|ico|webp)(\?.*)?$/;
const mediaRE = /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/;
const fontsRE = /\.(woff2?|eot|ttf|otf)(\?.*)?$/i;
const isStaticAsset = (file) => {
return imageRE.test(file) || mediaRE.test(file) || fontsRE.test(file);
};
import
导入的动态文件解决很简略,间接把动态文件的url
字符串作为默认导出即可。
这样咱们又会收到两个动态文件的申请:
简略起见,没有匹配到以上任何规定的咱们都认为是动态文件,应用serve-static来提供动态文件服务即可:
// vite/app.js
const serveStatic = require("serve-static");
app.use(async function (req, res, next) {
if (xxx) {
// xxx
} else if (xxx) {
// xxx
// ...
} else {
next();// ++
}
})
// 动态文件服务
app.use(serveStatic(path.join(basePath, "public")));
app.use(serveStatic(path.join(basePath)));
动态文件服务的中间件放到最初,这样没有匹配到的路由就会走到这里,到这一步成果如下:
能够看到页面曾经被加载进去。
下一篇咱们会介绍一下热更新的实现,See you later~
发表回复