什么是 Vite
官网上已经说明啦:下一代前端开发与构建工具,提高开发者的开发体验
分析
首先看一下空项目里都有些什么吧,项目的根目录下会有个 index.html
,叫宿主页,页面内会有一个 script
标签
这里使用浏览器新支持的 type="module"
,这样写有一个好处,就是在 main.js
中可以以 ESModule
的方式编写,且不需要打包工具进行打包,浏览器会发送相应请求,去寻找 vue
以及 ./App.vue
,如下如所示
等待加载完这两个资源之后,在进行 createApp()
创建应用程序,在页面上显示
这样的处理方法有以下好处
- 在开发阶段不需要打包,这样不管我的项目有多大,加载的速度快
- 按需加载,用到相关的模块就去加载相关的模块,不相关的不加载,提高速度
需要知道的知识点
- 浏览器只知道相对地址,它根据当前的
index.html
的地址,去发送请求,获取资源
- 浏览器只认识
html
、js
、 css
,如果给我一个vue
文件,浏览器是不知道怎么处理,所以 vite
需要额外去处理 vue
文件,(也就是把 vue
转化成 js
)其他的文件格式同理,把特殊的文件类型进行解析和转换。
思路
vite
相当于一个服务器,处理一下宿主文件,以及 vue
文件等
目标
自己实现一个基础的 vite
,包括一下几个功能
- 返回宿主页面
- 处理裸模块
- 处理单文件组件(SFC)
开发
先写一个 Node 服务
返回宿主页面
我们需要有个宿主页面,来当我们的单页面的宿主
之后,当我们请求 /
时,就需要返回宿主页,那我们可以用 fs
模块
但是这样也不对,因为请求的 main
也是 html
了,那是因为我们没写路由
写完路由之后如下,main.js
会找不到,之后再来处理 js
文件
处理 js 文件
大概的处理方式就是找到文件之后,返回即可
演示如下
这里需要说明一下,ctx.type
默认是 text/plain
也就是纯文本,我们需要设置成 application/javascript
,告诉浏览器这是个 js
代码
path.join
的使用与否,不影响结果,不过输出的处理后的内容的话,还是能看出区别的,使用 path.join
拼接的路径会更规范一些
裸模块的路径重写
浏览器是不认识绝对路径的,只知道相对路径
比方说入口文件中的 'vue‘
,它就是一个裸模块,浏览器发送请求,是找不到这个文件的,需要 vite
来做一下处理,帮我们找到这个模块然后返回
其中,我们需要处理一下导入的文字,在前面加上些标志,说明她是裸模块,此时需要一个函数来批量处理这些东西
处理完之后,浏览器会发送相关依赖的请求,从而请求资源
之后浏览器就可以请求资源了
1 2 3 4 5 6 7 8 9 10 11 12
| function rewriteImport(content) { return content.replace(/ from ['"](.*)['"]/g, function (s1, s2) { const startsWithResult = s2.startsWith("/") || s2.startsWith("./") || s2.startsWith("../"); if (startsWithResult === true) { return s1; } else { return ` from '/@modules/${s2}'`; } }); }
|
加载裸模块
大概的处理逻辑就是这样的
先到 package.json
中找到真正的入口位置路径,然后 fs
一下,返回
process 报错
如果遇到如下报错,那么需要在页面上声明一下变量
模拟一下
可以稍微查看一下报错的位置
因为这些代码里有些是 node
的代码,而浏览器环境中是没有 process
变量的,所以我们需要在页面上设置一下
1 2 3 4 5 6 7 8 9
| <script> window.process = { env: { NODE_ENV: "dev", }, }; </script>
|
1 2 3 4 5 6
| import { createApp, h } from "vue";
createApp(h("div", "我是内容")).mount("#app");
|
解析 sfc
就是解析单文件的 app.vue
主要是处理 app.vue
这个请求,然后把他变成 js
代码
思路还是一样的,只不过这里引入个模块 compiler-sfc
,此模块是专门分析 vue
文件,把他转化成 js
对象
1
| const compilerSFC = require("@vue/compiler-sfc");
|
打印一下分析后的内容,如下图所示
我们需要先拿到组件中的 script
的内容,然后返回,其中 template
转换成 渲染函数
需要另外 一个请求去处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const p = path.join(__dirname, url.split("?")[0]); const content = fs.readFileSync(p, "utf-8"); const compileContent = compilerSFC.parse(content); const { type } = query; if (!type) { const { content: scriptContent } = compileContent.descriptor.script; const script = scriptContent.replace( "export default ", "const __script = " ); ctx.type = "application/javascript"; ctx.body = ` ${rewriteImport(script)} import { render as __render } from '${url}?type=template' // 请求 html 获取,render 函数 __script.render = __render // 将 render 函数 挂载到 脚本变量 中 export default __script // 导出脚本变量 `;
|
模板编译
模板编译有另外一个模块去处理 @vue/compiler-dom
,它可以将 template
转换成渲染函数
1 2 3 4 5 6 7 8 9 10
| else if (type === "template") { const { content: templateContent } = compileContent.descriptor.template; const render = compilerDOM.compile(templateContent, { mode: "module", }).code; console.log("render:>>", render); ctx.type = "application/javascript"; ctx.body = rewriteImport(render); } }
|
整体代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| const Koa = require("koa"); const fs = require("fs"); const path = require("path"); const compilerSFC = require("@vue/compiler-sfc"); const compilerDOM = require("@vue/compiler-dom");
const app = new Koa();
app.use(async (ctx) => { const { url, query } = ctx; if (url === "/") { ctx.type = "text/html"; ctx.body = fs.readFileSync(path.join(__dirname, "index.html"), "utf-8"); } else if (url.endsWith(".js")) { const p = path.join(__dirname, url); ctx.type = "application/javascript"; ctx.body = rewriteImport(fs.readFileSync(p, "utf-8")); } else if (url.startsWith("/@modules/")) { const moduleName = url.replace("/@modules/", ""); const p = path.join(__dirname, "node_modules", moduleName); const modulePath = require(path.join(p, "/package.json")).module; const realPath = path.join(p, modulePath); ctx.type = "application/javascript"; ctx.body = rewriteImport(fs.readFileSync(realPath, "utf-8")); } else if (url.indexOf(".vue") > -1) { const p = path.join(__dirname, url.split("?")[0]); const content = fs.readFileSync(p, "utf-8"); const compileContent = compilerSFC.parse(content); const { type } = query; if (!type) { const { content: scriptContent } = compileContent.descriptor.script; const script = scriptContent.replace( "export default ", "const __script = " ); ctx.type = "application/javascript"; ctx.body = ` ${rewriteImport(script)} import { render as __render } from '${url}?type=template' // 请求 html 获取,render 函数 __script.render = __render // 将 render 函数 挂载到 脚本变量 中 export default __script // 导出脚本变量 `; } else if (type === "template") { const { content: templateContent } = compileContent.descriptor.template; const render = compilerDOM.compile(templateContent, { mode: "module", }).code; console.log("render:>>", render); ctx.type = "application/javascript"; ctx.body = rewriteImport(render); } } });
function rewriteImport(content) { return content.replace(/ from ['"](.*)['"]/g, function (s1, s2) { const startsWithResult = s2.startsWith("/") || s2.startsWith("./") || s2.startsWith("../"); if (startsWithResult === true) { return s1; } else { return ` from "/@modules/${s2}"`; } }); }
app.listen(3000);
|
gitee 地址
仓库地址