I don’t need to say more about what Vite is. Friends who have used Vue
must know that this article will use a very simple beggar version by handwriting Vite
to learn about Vite
basic implementation principle of Vite
to the earliest version of ---b64ccf79500695fcf2ab9aeda905ba1e--- ( vite-1.0.0-rc.5
version, Vue
version 3.0.0-rc.10
) It is already the version of 3.x
, why not directly refer to the latest version, because it is difficult to understand the source code of this relatively complete tool as soon as it comes up, anyway, the author can't, so we can start from the earliest Version to spy on the principle, friends with strong ability can ignore~
This article will be divided into two parts, the first one mainly discusses how to successfully run the project, and the second one mainly discusses hot updates.
Front-end test project
The front-end test project structure is as follows:
Vue
是Options Api
,不css
语言、 ts
js
语言, So it is a very simple project, our goal is very simple, is to write a Vite
service to make this project run!
Build basic services
vite
The basic structure of the service is as follows:
First let's start a service, HTTP
application framework we use 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);
The next thing we need to do is to intercept various types of requests for different processing.
intercept html
The entry address of the project access is http://localhost:3000/index.html
, so the first request received is the html
file request, we temporarily return html
the content of the file :
// 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('')
}
});
Now the access page is definitely still blank, because the request initiated by the page main.js
has not been processed yet. The content of main.js
is as follows:
Intercept js requests
main.js
request needs to be processed a little, because the browser does not support naked import, so we need to convert the naked import statement, convert import xxx from 'xxx'
to import xxx from '/@module/xxx'
, and then intercept the /@module
request, obtain the module to be imported from node_modules
and return it.
To resolve import statements we use 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);
// ...
}
});
The result of parsing is:
The parsing result is an array, the first item is also an array representing imported data, and the second item represents export, main.js
no, so it is empty. s
、 e
位置, ss
、 se
位置。
Next, we check that when the import source is not .
or /
, it is converted to the form of /@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());
}
});
Modification js
string We use magic-string , from this simple example you should be able to see the magic of it, that is, even if the string has changed, the index calculated from the original string is used It's still correct to modify it, because the index is still relative to the original string.
You can see that vue
has been successfully modified to /@module/vue
.
Then we need to intercept /@module
request:
// 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);
}
})
We first obtained the package.json
file of the package, the purpose is to find its entry file, and then read and use esbuild to convert, of course Vue
Yes ES模块
However, some packages may not be available, so they are handled directly and uniformly.
intercept css requests
css
There are two kinds of requests, one is from the link
tag, the other is from the import
89bade3b1df1377b5116ae877738ed97---tag, ---8a0f034834c74bca9 css
link
css
请求我们直接返回css
即可, import
的css
的, ES模块
只支持js
, so we need to convert it to js
type. The main logic is to manually insert css
into the page, so we need to handle these two requests separately.
In order to distinguish import
request, let's modify the code of the previous interception js
, and add ?import
query parameters to each import source:
// ...
// 遍历导入语句
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`);
}
});
//...
Don't forget to modify the place where you intercept /@module
:
// ...
let pkg = removeQuery(req.url.slice(9));// 从/@module/vue?import中解析出vue
// ...
// 去除url的查询参数
const removeQuery = (url) => {
return url.split("?")[0];
};
In this way, requests for import
will have a flag:
Then process the css
request separately according to this flag:
// 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);
};
If it is import
imported css
then convert it to a response of type js
and provide a create style
The method of the page is executed immediately, then this css
will be inserted into the page. Generally, this method will be injected into the page in advance.
If it is link
labeled css
request to directly return css
.
Intercept vue requests
最后,就是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);
}
})
Then parse the three parts separately, the template
and css
parts will be converted into a import
request.
Process js
section
// ...
const { compileScript, rewriteDefault } = require("@vue/compiler-sfc");
let code = "";
// 处理js部分
let script = compileScript(descriptor);
if (script) {
code += rewriteDefault(script.content, "__script");
}
The rewriteDefault
method is used to convert export default
into a new variable definition so that we can inject more data like:
// 转换前
let js = `
export default {
data() {
return {}
}
}
`
// 转换后
let js = `
const __script = {
data() {
return {}
}
}
`
//然后可以给__script添加更多属性,最后再手动添加到导出即可
js += `\n__script.xxx = xxx`
js += `\nexport default __script`
Processing template
section
// ...
// 处理模板
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
dfa6605766e6cee7056b595c291eb4a7---语句, render
函数挂载到__script
上, type=template
request, returns the compilation result of the template.
Processing style
section
// ...
// 处理样式
if (descriptor.styles) {
descriptor.styles.forEach((s, i) => {
const styleRequest = removeQuery(req.url) + `?type=style&index=${i}`;
code += `\nimport ${JSON.stringify(styleRequest)}`
})
}
Like templates, styles are also converted into a single request.
Finally export __script
and return the data:
// ...
// 导出
code += `\nexport default __script`;
res.setHeader("Content-Type", typeAlias.js);
res.statusCode = 200;
res.end(code);
__script
其实就是Vue
的组件选项对象,模板部分编译的结果就是组件render
,相当于把js
and template sections into a complete component options object.
Handling template requests
When Vue
single file request url
exists type=template
parameter, we compile the template and return:
// 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);
};
Handling style requests
The style is the same as the previous style request we intercepted, and it also needs to be converted into js
and then manually inserted into the page:
// 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;
}
})
The logic of the style conversion to js
is used in two places, so we can extract it into a function:
// 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
`;
};
Fix single file bare import issue
The js
part in a single file can also import modules, so there will also be a problem of bare import. The processing method of bare import was introduced earlier, that is to replace the import source first, so the single file js
After the partial parsing, we also need to perform a replacement operation. We first extract the replacement logic into a public method:
// 处理裸导入
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();
};
Then process it immediately after compiling the js
part:
// 处理js部分
let script = compileScript(descriptor);
if (script) {
let scriptContent = await parseBareImport(script.content);// ++
code += rewriteDefault(scriptContent, "__script");
}
In addition, there will also be a bare import Vue
in the compiled template code, which also needs to be processed:
// 处理模板请求
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;
}
Handling static files
App.vue
Introduced two pictures:
The result after compilation is:
ES模块
can only import js
files, so the import of static files, the response result also needs to be 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
The imported static file processing is very simple, just export the url
string of the static file as the default.
In this way, we will receive two requests for static files:
For simplicity, we consider static files that do not match any of the above rules, and use serve-static to serve static files:
// 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)));
The middleware of the static file service is placed at the end, so that the routes that are not matched will come here. The effect of this step is as follows:
You can see that the page has been loaded.
In the next article, we will introduce the implementation of hot updates, See you later~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。