相信点进来看文章的都对vite有一定的了解,下面我们一起来实现一个简易版vite,来加深我们对vite的理解!(PS:建议 clone 项目下来对着看)
完整代码
开始之前我们先来 预览效果,启动了vite服务,用官方vite的react示例代码作为演示,修改 jsx 和 css 都进行了热更新。
0. 先搭建基础服务
首先,需要有一个服务让我们去访问项目,新建一个 index.js 文件,用 koa 创建一个http服务。
// index.js
import Koa from 'koa';
const app = new Koa();
// 运行
(function createServer() {
app.listen(12345, () => {
console.log(`listening:12345 port!!!`);
});
})();
ok, 服务建好。
下面我们来创建 ws 服务。因为我们需要hmr, 当你的项目文件发生改变后,通过 ws 去通知客户端:“喂,你的文件发生改变了,该去更新了!”。
// index.js
import { WebSocketServer } from 'ws';
function createWs() {
const Websocket = new WebSocketServer({ port: 5000 });
Websocket.on('connection', function connection(ws) {
console.log('ws is connected!!');
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
});
}
ok,很简单是是吧。
1. hmr
开发怎么少得了 hmr 呢?
这里的 hmr 主要做一个通知工作,通过 chokidar 去监听文件的变化,然后通过 ws 通知客户端去进行更新操作,所以更新操作是在客户端进行,下面会讲到。
// 发起热更新
const launchHmr = (ws, config) => {
// 监听到文件变化,处理文件
chokidar.watch(resolveOnRoot('src')).on('change', (changePath) => {
changePath = changePath.replace(__dirname, '');
console.log(`${changePath} has changed!!`);
const cssObj = {};
if (changePath.endsWith('.css')) {
cssObj.css = fs.readFileSync(resolveOnRoot(changePath)).toString();
}
// 将更新类型、文件路径、样式内容传给客户端
ws.send(
JSON.stringify({
type: 'change',
changePath,
...cssObj,
}),
);
});
};
2. 中间件
vite源码里面有14个中间件,用来处理请求,转换资源,处理配置、日志等。我这个简易版不会包含这么多,我主要写了3个,分别是 baseMiddleware 、indexHtmlMiddleware、transformMiddleware,下面进行逐个讲解。
- baseMiddleware 处理请求资源的地址,用来去掉请求时带上的参数,挂载在ctx对象上面
// 处理地址
export const baseMiddleware = (ctx, next) => {
const requestUrl = ctx.request.url.split('?')[0];
ctx.requestUrl = requestUrl;
next();
};
- indexHtmlMiddleware 处理入口文件
因为我们手写 vite 运行起来访问的是 localhost: port, 所以这里默认为请求 / 为 html入口资源。(PS:这里没有其他掺杂路由,这个简单的列子单纯让你加深对 vite 的理解)
说一说 下面的代码,当用户请求入口资源时候,服务端会找到入口文件,读取文件内容,在文件中插入处理样式(client/style.js)、处理websocket (client/ws.js)和react热更新(react-refresh)这几个资源,再返回处理后的 html 资源,资源具体干了什么下面会说,莫急。
export const indexHtmlMiddleware = (ctx, next) => {
if (ctx.requestUrl === '/') {
// 根路径返回模版 HTML 文件
const html = fs.readFileSync(`${__dirname}/index.html`, 'utf-8');
const importHandleStyle =
'';
const importHandleWs =
'';
const preambleCode = `
`;
ctx.type = 'text/html';
ctx.body = `${preambleCode}${importHandleStyle}${importHandleWs}${html}`;
}
next();
};
- transformMiddleware 处理非html资源,既然上面处理了入口html资源,当然也要处理别的资源啦。
可以看到下面代码是通过判断资源请求的后缀或前缀,对各种资源进行处理。分别包含 jsx、node_modules资源、css、svg、vite客户端 资源。为方便大家看, 我这里已经将方法单独抽了出来,大家可以看看 utils.js 文件。传送门
简单说说每个处理资源的方法
① handleJsx: 用 esbuild 对jsx进行转换,之后再通过封装 react-refresh插件对jsx文件插入热更新内容(参考 @vite/plugin-react-refresh)
② handleNodeModules: 处理第三方资源,通过 enbuild 对第三方资源进行打包,打包后放到缓存目录 node_modules/.cvite,下次访问就直接取 node_modules/.cvite中的资源。
③ handleCss:读取样式文件内容,通过标签方式插入样式,插入样式时候记录。
④ handleSvg:转base64。
⑤ 最后一个通过判断/@vite/client,找到客户端资源的路径,返回对应的请求资源。
export const transformMiddleware = (ctx, next) => {
const { requestUrl } = ctx;
if (requestUrl === '/') {
} else if (requestUrl.endsWith('.jsx')) {
handleJsx(ctx, requestUrl);
} else if (requestUrl.startsWith('/@modules/')) {
handleNodeModules(ctx, requestUrl);
} else if (requestUrl.endsWith('.css')) {
handleCss(ctx, requestUrl);
} else if (requestUrl.endsWith('.svg')) {
handleSvg(ctx, requestUrl);
} else if (requestUrl.startsWith('/@vite/client')) {
ctx.type = 'application/javascript';
const filePath = requestUrl.replace('/@vite/client', 'vite/client');
ctx.body = fs.readFileSync(resolveOnRoot(filePath), 'utf8');
} else {
}
next();
};
好了,说完中间件了,最终我们启动方法为
// 运行
(function createServer() {
createWs();
app.listen(12345, () => {
console.log(`listening:${12345} port!!!`);
});
app.use(baseMiddleware);
app.use(indexHtmlMiddleware);
app.use(transformMiddleware);
})();
3. 再说hmr
接着,我们再补充刚才没有细讲的热更新。
刚才说了,服务端通过 ws 去通知客户端进行热更新,客户端收到指令后会执行 handleFile 方法。
export function handleFile(path, css) {
const { JSX, CSS, SVG, NODE_MODULE } = FILE_TYPE;
if (checkFileType(path) === CSS) {
// 处理样式
globalUpdateStyle(path, css);
} else if (checkFileType(path) === JSX) {
// 处理jsx
import(`${path}?${timeParam()}`);
}
- css热更新:上面说了,样式是通过 style标签 插入的,那我们需要维护一个 Map
window.sheetsMap = new Map();
, 在插入样式同时,我们去设置 Mapwindow.sheetsMap.set(id, style);
文件路径作为 id,css内容作为 style。
这样,当客户端收到更新 css 的指令后,就会调用globalUpdateStyle(path, css);
去更新样式资源,实现样式热更新。而 globalUpdateStyle方法在 /@vite/client/style.js 文件里面,这个我们在执行 indexHtmlMiddleware 中间件已经加载了该资源的。
export const globalUpdateStyle = (id, content) => {
let style = window.sheetsMap.get(id);
if (style) {
style.innerHTML = content;
} else {
style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.innerHTML = content;
document.head.appendChild(style);
}
window.sheetsMap.set(id, style);
};
- react热更新:react热更新用了
react-fresh
,在看 vite 源码时候,发现 vite.config.js 文件里面使用了 @vitejs/plugin-react-refresh,而 vite 在处理 react jsx 时候会调用 transform 方法处理文件。
下面简单说下,transform 做了些什么。
① 用 babel 和 react-refresh/babel 插件对代码进行转换
// 转换前
function Text1() {
return Test Component1;
}
export default Text1;
// 转换后
function Text1() {
return /* @__PURE__ */React.createElement("div", null, "Test Component1");
}
_c = Text1;
export default Text1;
var _c;
// 帮我们注册需要热更新的组件
$RefreshReg$(_c, "Text1");
② 在文件头部插入提供注册热更新的方法。
const header = `
...
window.$RefreshReg$ = (type, id) => {
RefreshRuntime.register(type, ${JSON.stringify(id)} + " " + id)
};
...
`;
③ 在文件尾部插入刷新组件的方法。
const footer = `
...
window.__vite_plugin_react_timeout = setTimeout(() => {
window.__vite_plugin_react_timeout = 0;
RefreshRuntime.performReactRefresh();
}, 30);
...
`;
ok,处理完毕,当客户端收到更新 jsx 的指令后,会重新 import 对应的 jsx 文件,加上时间戳参数,确保是新的,然后文件执行RefreshRuntime.performReactRefresh();
进行刷新。
4.锦上添花
主线流程就说完了,你可以做些锦上添花的内容,例如实现读取vite.config.json
文件的配置,尝试ssr,资源缓存等等让他看起来更高级。