如何实现一个简易版vite

相信点进来看文章的都对vite有一定的了解,下面我们一起来实现一个简易版vite,来加深我们对vite的理解!(PS:建议 clone 项目下来对着看)

完整代码

开始之前我们先来 预览效果,启动了vite服务,用官方vite的react示例代码作为演示,修改 jsxcss 都进行了热更新。

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个,分别是 baseMiddlewareindexHtmlMiddlewaretransformMiddleware,下面进行逐个讲解。

  • 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资源,当然也要处理别的资源啦。
    可以看到下面代码是通过判断资源请求的后缀或前缀,对各种资源进行处理。分别包含 jsxnode_modules资源csssvgvite客户端 资源。为方便大家看, 我这里已经将方法单独抽了出来,大家可以看看 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();, 在插入样式同时,我们去设置 Map window.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 做了些什么。
    ① 用 babelreact-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,资源缓存等等让他看起来更高级。

你可能感兴趣的:(如何实现一个简易版vite)