vite是一个开发构建工具,开发过程中它利用浏览器native ES Module特性按需导入源码,预打包依赖。
特点: 启动快,更新快
一、按需加载
工作原理
1.浏览器利用es module imports,关键变化是index.html中的入口导入方式(开发阶段需要考虑浏览器兼容)
所以,开发阶段不需要进行打包操作
本地文件使用 type=module,不支持本地路径,需要开启一个本地serve
2.第三方依赖模块预打包,并将导入地址修改为相对地址,从而统一所有请求为相对地址
预打包:第三方依赖提前打包存在node_modules中,程序启动时,直接从node_modules中去下载(浏览器只能加载相对路径)
二、解析
vite需要根据请求资源类型的不同做不同的解析工作,解析的最后结果是js对象
比如app.vue 文件
import HelloWorld from "./components/HelloWorld.vue";
解析为
const _script = {
name: "App",
components: {
HelloWorld
}
}
// template 部分转换为了一个模板请求,解析结果是一个渲染函数
import {render as _render} from "src/App.vue?type=template"
// 将解析得到的render函数设置到组件配置对象上
_script.render=_render
_script._hmrId="src/App.vue""
_script._file="/Users/...../src/App.vue"
vue文件 在服务端提前被vite编译之后,直接输出给浏览器,直接使用
三、vite手写实现
1、基础实现
基于koa框架,创建node服务器
基本配置:
// 一个node服务器,相当于devServer 基于koa
const Koa = require("koa");
const app = new Koa();
const fs = require("fs");
const path = require("path");
const compilerSfc = require("@vue/compiler-sfc"); // 编译器,得到一个组件配置对象,解析vue文件
const compilerDom = require("@vue/compiler-dom"); // 编译模板
app.use(async (ctx) => {
// 处理文件解析
});
// 设置端口监听
app.listen(4002, () => {
console.log("vite start");
});
2、解析html
利用fs对文件进行读取
// 首页
ctx.type = "text/html";
ctx.body = fs.readFileSync("./index.html", "utf8");
3、解析js
跟html解析区别在意文本类型的不同
// 响应js请求
const p = path.join(__dirname, url);
ctx.type = "text/javascript";
ctx.body = fs.readFileSync(p, "utf-8");
4、第三方库的支持,比如vue
我们在代码里一般这样引入
import { createApp, h } from “vue”;
这里引入会报错
表示期望路径是绝对路径或者相对路径
第三方的依赖我们都会安装在node_modules中,所以需要去node_modules中去寻找对应的js文件,因此我们需要对路径进行重写
比如vue,我们可以给它加一个别名----/@module/vue,
一个改写函数
// 重写导入,让引用的第三方文件,变成相对地址
function rewriteImport(content) {
return content.replace(/ from ['|"](.*)['|"]/g, function (s0, s1) {
// s0 - 匹配字符串
// s1 - 分组内容
// 看看是不是相对地址
if (s1.startsWith(".") || s1.startsWith("../") || s1.startsWith("/")) {
// 不处理
return s0;
} else {
// 是外部文件
return ` from '/@module/${s1}'`;
}
});
}
名称改写好之后,我们要去node_modules里面对应的js文件
比如vue,可以先看一下vue的位置
dist/vue.runtime.esm-bundler.js 是vue文件真正的路径
所以我们需要把
import XX from “vue” 变成 … from “baseurl + /node_moudle/dist/vue.runtime.esm-bundler.js”
// 获取模块名称,就是@module/ 后面的内容
const moduleName = url.replace("/@module/", "");
// 在node_module 中找到对应的模块
const prefix = path.join(__dirname, "../node_modules", moduleName);
// 要加载文件的地址
const module = require(prefix + "/package.json").module;
const filePath = path.join(prefix, module);
const ret = fs.readFileSync(filePath, "utf-8");
ctx.type = "text/javascript";
ctx.body = rewriteImport(ret);
看一下处理后的路径地址,对filePath的打印
如果要保证运行,还需要在index.html添加
// 这是vue的第三方包中的某个配置,这里相当于欺骗了浏览器
<script>
window.process = {
env: {
NODE_ENV: 'dev'
}
}
</script>
5、单文件组件支持,.vue文件(sfc)
实现思路:*.vue文件 => template 模板 => render函数
- *.vue文件 => template 模板 需要借助 compiler-sfc
这里主要提取js代码,生产render函数 - template => render函数 需要借助 compiler-dom
// 读取vue文件的内容
const p = path.join(__dirname, url.split("?")[0]);
const ret = compilerSfc.parse(fs.readFileSync(p, "utf-8")); // 将vue文件中的块进行解析,得到一个ast
if (!query.type) {
// 没有type说明是sfc(单文件组件)
// 解析sfc,处理内部的js
console.log(ret);
// 获取脚本内容
const scriptContent = ret.descriptor.script.content;
ctx.type = "text/javascript";
ctx.body = `
${rewriteImport(scriptContent)}
// template解析转换为另一个请求单独做
import { render as __render } from '${url}?type=template'
// __script.render = __render
export default __render
`;
} else if (query.type === "template") {
// 2.template => render 函数
const tpl = ret.descriptor.template.content;
// 编译为包含render的模块
const render = compilerDom.compile(tpl, { mode: "module" }).code;
ctx.type = "text/javascript";
ctx.body = rewriteImport(render);
}
对ret进行打印,看一下script和template
对其中的content部分进行处理
6、处理.css文件
思路:创建style便签,将css文件的内容进行读取,添加到style标签中
const p = path.join(__dirname, url.split("?")[0]);
const file = fs.readFileSync(p, "utf-8");
// css 转化为js代码
// 利用js创建style标签
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;
四、vite插件
vite插件可以扩展vite能力,比如解析用户自定义的文件输入,在打包代码前转义代码,或者查找第三方模块
1、 插件钩子
开发时,vite dev server 创建一个插件容器,按照Rollup调用创建钩子的规则请求各个钩子函数
在服务启动时调用一次:
optipns 替换或操控rollup选项(只在打包时有用,开发时为空)
buildStart 开始创建(只是一个信号)
每次有模块请求时都会调用:
resolveId 创建自定义确认函数,常用语句定位第三方依赖
load 创建自定义加载函数,可用于返回自定义的内容
transform 可用于装换已加载的模块内容
在服务器关闭时调用一次:
buildEnd
closeBundle
vite 特有钩子
config:修改Vite配置(可以配置别名)
configResolved :vite配置确认
configureServer:用于配置dev server
transformIndexHtml: 用于转换宿主页(可以注入或者删除内容)
handleHotUpdate:自定义HMR更新时调用
2、钩子调用顺序
config → configResolved → optipns → configureServer → buildStart → transform → load → resolveId → transformIndexHtml → vite dev server
3、插件顺序
别名处理 Alias
用户插件执行(如果设置 enforce : ‘pre’)
Vite 核心插件(plugin-vue)
用户插件执行(如果未设置 enforce)
Vite 构建插件
用户插件执行(如果设置 enforce : ‘post’)
Vite 构建后置插件
4、实现一个mock服务器—vite-plugin-mock
实现思路:
给开发服务器实例(concent)配置一个中间件,这个中间件可以存储用户配置接口映射信息,并提前处理输入请求,如果请求的url和路由表匹配则接管,按用户配置的handler返回结果
import path from "path";
let mockRouteMap = {};
function matchRoute(req) {
let url = req.url;
let method = req.method.toLowerCase();
let routeList = mockRouteMap[method];
return routeList && routeList.find((item) => item.path === url);
}
// 默认导出的插件工厂函数
export default function (options = {}) {
// 获取mock文件入口,默认是index
options.entry = options.entry || "./mock/index.js";
// 转换为绝对路径
if (!path.isAbsolute(options.entry)) {
options.entry = path.resolve(process.cwd(), options.entry);
}
// 返回的插件
return {
configureServer: function ({ app }) {
// 定义路由表
const mockObj = require(options.entry);
// 创建路由表
createRoute(mockObj);
// 定义中间件:路由匹配
const middleware = (req, res, next) => {
// 1.执行匹配过程
let route = matchRoute(req);
//2. 存在匹配,是一个mock请求
if (route) {
console.log("mock req", route.method, route.path);
res.send = send;
route.handler(req, res);
} else {
next();
}
};
// 最终目标,给app注册一个中间件
app.use(middleware);
},
};
}
function createRoute(mockConfList) {
mockConfList.forEach((mockConf) => {
let method = mockConf.method || "get";
let path = mockConf.url;
let handler = mockConf.response;
// 路由对象
let route = { path, method: method.toLowerCase(), handler };
if (!mockRouteMap[method]) {
mockRouteMap[method] = [];
}
console.log("create mock api");
// 存入映射对象中
mockRouteMap[method].push(route);
});
}
// 实现一个send方法
function send(body) {
let chunk = JSON.stringify(body);
if (chunk) {
chunk = Buffer.from(chunk, "utf-8");
this.setHeader("Content-Length", chunk.length);
}
this.setHeader("Content-Type", "application/json");
this.statusCode = 200;
this.end(chunk, "utf8");
}