如何实现一个简易版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 =
      '<script type="module" src="/@vite/client/style.js"></script>';
    const importHandleWs =
      '<script type="module" src="/@vite/client/ws.js"></script>';
    const preambleCode = `
    <script type="module">
      import RefreshRuntime from "/@modules/react-refresh"
      RefreshRuntime.injectIntoGlobalHook(window)
      window.$RefreshReg$ = () => {}
      window.$RefreshSig$ = () => (type) => type
      window.__vite_plugin_react_preamble_installed__ = true
    </script>`;
    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 <div>Test Component1</div>;
}
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,资源缓存等等让他看起来更高级。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容