Vite 原理浅析及应用

2022-04-01 16:14:30 浏览数 (1)

前言

Tips: 如果大家想直接看重点可以跳过前言,这里将介绍一下为什么我会有这次分享,也就是本次分享的背景以及目的。

为了方便大家快速寻找到自己需要的知识点,列出以下大纲

  • 前言
  • 什么是 Vite ?
  • Vite 原理浅析
  • Vite 应用及实践
  • 个人对 Vite 的一些想法

#背景

本次的分享仅限于参考,学习,并不保证在前端高速发展,日新月异的情况下有没有过时。如有不对的点欢迎各位大佬指出,有什么不足的地方也请大家担待。只是一些实践过后的想法和总结,拿出来分享一下,欢迎探讨。

  • 19 年想法,20 年实现,21 年可用于生产
  • 解决开发痛点
  • 实践并应用于项目
#想法 => 实现 => 生产

尤大在 19 年就已经有了 Vite 的想法,只不过那个时候还在忙 Vue3 的重构,并没有太多的时间来具体实现,之后 Vue3 几乎成型后,尤大也就开始研究 Vite 的开发了,在 20 年的时候就发了微博、知乎、推特等社交账号说明,并将源码上传至 Github 。

到目前为止(2021 年 08 月),Vite 已经完全可以用于生产环境,已经成熟。

#解决开发痛点

本次升级 Vite 的最初的目的就是要改善开发的痛点,那么我们系统的痛点是什么呢?

HMR,好,什么是 HMR?就是我们在开发过程中代码的热更新。为什么说这个是开发时候的痛点呢?

举个例子:

代码语言:javascript复制
// ...
// 打印一些变量
console.log(variable);
// ...

如上,我修改项目中一处代码,保存后。页面将在 6s 后给我反馈。。也就是说,无论我改了什么代码?什么内容,只要触发了热更新,我将浪费 6s 的时间去等待。久而久之......人生有多少个 6s ?

好,这就是为什么我要解决这个问题的点。我先说一下目前升级前后的对比数据。

Vite

Webpack

提升倍数

启动项目

0.5s

33s

66 倍

首屏

19s

36s

1.9 倍

HMR

0.2s

6s

30 倍

这个是目前升级前后的对比数据,大概的。所以这是解决开发痛点的原因! 而且随着项目的越来越大,模块越来越多,组件越来越多,就会从 6s => 7s => 8s => ...人生 over

我相信最开始这个系统的 HMR 也会在 1s 以内的。

#实践并应用于项目

其实这个不光是说说,我在 Q2 就跟组内成员提过说要提升一下开发效率,用 Vite 作为开发构建工具,来实际提升。当然现在也应用于项目上了。文章后面会有本次升级的经验以及一些报错和解决的方式,大家可以用作参考。这也是本次分享的目的!

#大纲

  • 什么是 Vite ?
  • Vite 原理浅析
  • Vite 应用及实践
  • 个人对 Vite 的一些想法

#什么是 Vite ?

#下一代前端开发与构建工具

  • 极速的服务启动
  • 轻量快速的热重载
  • 丰富的功能
  • 通用的插件
#极速的服务启动

为什么是极速的服务启动,其实你可以理解为只是启动了一个本地服务器,你可以想象一下自己启动一个 node 服务器是有多么的快?node index.js 之后回车是不是就已经开启了一个服务器? 其实 Vite 也是这样的,它只是启动了一个 node 服务器而已,只不过在第一次启动之前会有一个预编译的过程,可能会出现几秒的启动速度(取决于你的项目需要预编译的包多少)。

所以 Vite 在启动服务器的时候是非常极速的。

#轻量快速的热重载

Vite 实现了一套基于 ESM 模块的 HMR ,通过 websocket 来实现。

它会将你的所有文件添加一个 watcher ,来监听你的文件变动,实现热重载。

快速的热重载如何体现?类似 Webpack 进行热更新时,会将你的所有文件重新打包一次,来实现热更新,而 vite 是只重载你更改的那个文件,通过 HTTP 来重新发送请求即可实现,所以是快速的。不需要将你的其他代码进行打包。

#丰富的功能

丰富的功能体现于 Vite 自己的配置文件 API ,你可以做很多事情,例如文件别名、接口代理、插件机制、端口、服务器协议、打包配置等等等,都可以根据你生成的配置进行改写。

#通用的插件

Vite 在实现之前就已经考虑到,一定不会如 Webpack 的插件以及社区成熟,那样将好几年都不会被应用到生产中,尤大很聪明,通过目前仅次于 Webpack 的打包器 Rollup 的插件来实现自己的插件机制。

也就是说,你可以使用 Rollup 插件来进行配置 Vite,当然你写的 Vite 插件,按照规范也可以在 Rollup 上使用。实现了通用的插件逻辑,这样在生态上,也不会输于 Webpack

#通过官方的对比图看一下 Vite 和 Webpack 的区别

类似 Webpack 工具的打包方式

Vite 的打包方式

好的,通过上面的对比我们可以看到。

Webpack 的方式是将你的所有的代码统一进行编译,包括所有的 router 以及下面的模块,模块下还有各个组件。打包成浏览器可以识别的代码,都打包完成之后,启动服务器,统一给浏览器使用。

Vite 的方式是直接先启动服务器,其实图上少了一个步骤,在启动服务器之前会先读取你的 package.json 文件,识别出需要进行预编译的包,先进行预编译之后,再去启动服务器。

启动服务器之后会通过发送 HTTP 请求的模式访问入口文件,入口文件访问当前页面路由所需要的模块,以及模块下的组件,当你通过路由导航到另一个路由下,如果这个路由下的模块与上个模块有重合部分,这时 Vite 将会采用缓存的内容,不会发送请求,如果没有则继续发送对应的文件请求。高效的利用了 tree shaking 特性。这也是为什么不管你的项目多大, HMR 都会保持在非常高速的进行的原因。

好了,见到介绍到这之后,我们来看一下 Vite 原理的浅析。

#Vite 原理浅析

本次的原理分析主要是带大家看一下 Vite 在实际工作当中的用途,会写一个简单的 Demo 来实现,只是实现思路,可以保证运行你刚 Create Vite App 的 React 的 Demo

在学习 Vite 原理之前首先得先看一下两个知识点,也是支撑 Vite 这么强大的原因。

#ESM (ECMAScript Modules)

如果你还不了解 ESM,可以简单看一下。

简单来说 ESM 就是原生浏览器支持 importexport 等 ES6 特性。

看一下我们的 Demo HTML 代码

不知道大家之前在开发的时候用到过 type="module" 这个类型没,我是没用到过,接触 Vite 之后才知道这个东西。

我们可以直接指定 type 的类型就可以在现代浏览器上使用 ESM 模块了。我们来看一下它的兼容性

大家可以看到哈,目前现代浏览器都已经支持了,除了 IE。都 2120 年了,谁还在兼容 IE 浏览器?让我康一康

好的,ESM 已经了解了,我们聊下一个比较厉害的工具。

#esbuild

esbuild,是一个极速的 JavaScript 的代码打包器,用 Go 语言进行编写,是其他编译器的 10-100 倍运行速度

Vite 就是采用了 esbuild 作为代码打包的工具,所以他的快也体现在 esbuild

#手写 Vite 感受原理

这里带大家简单写个 Vite 的 Demo,了解一下 Vite 的工作原理,大致的思路,并不会很完善的实现,因为这不现实,如果大家非常感兴趣的话,可以去看一下 Vite 的源码,非常值得学习。

我们主要实现你在创建一个基于 React 的 Vite template Demo 实现,主要实现如下:

  • 启动 Node 服务器
  • 处理 HTML 文件
  • 处理 JSX 文件
  • 处理 CSS 文件
  • 处理 SVG 图片文件
  • 处理裸模块
  • 使用 esbuild 打包 React 源码
  1. 首先在项目根目录创建一个前端模版,代码如下:
代码语言:javascript复制
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <script>
      window.process = {
        env: {
          NODE_ENV: "production",
        },
      };
    </script>
    <div id="root"></div>
    <script type="module" src="./vitesrc/main.jsx"></script>
  </body>
</html>

可以重点看一下 <script type='module' ...></script>

这个类型就是 ESM 的模块的类型。

  1. 创建 vitesrc 文件夹作为我们业务代码的核心代码文件夹

代码省略了,由于是展示 Node 层面代码,所以不展示了,其实就是在你创建 Vite React 模版的代码

代码目录结构如下:

代码语言:javascript复制
vitesrc
├── App.css
├── App.jsx
├── favicon.svg
├── index.css
├── logo.svg
└── main.jsx
  1. 在根目录下创建我们的核心文件,也是 Vite 的源码文件,index.js

直接通过 Koa 来作为我们的服务器框架。代码如下:

代码语言:javascript复制
import Koa from "koa";

const app = new Koa();

app.use(async (ctx) => {
  ctx.type = "text/plan";
  ctx.body = "Hello Vite!";
});

app.listen(24678, () => {
  console.log("App is running");
});

推荐使用 PM2 作为 Node 的进程守护

启动这个文件之后,我们在浏览器输入 localhost:24678 即可看到一个 Node 服务已经启动了。

注意:之后的代码都是新增的代码,就不进行全量展示了,不然太长~

  1. 处理 HTML 文件

这里我们处理一下 HTML 文件,当前端请求的时候,直接通过 Node 读取前端所写的模版,直接进行返回即可。

代码语言:javascript复制
import fs from "fs";
const __dirname = path.resolve(path.dirname(""));

app.use(async (ctx) => {
  const requestUrl = ctx.request.url;

  if (requestUrl === "/") {
    // 根路径返回模版 HTML 文件
    const html = fs.readFileSync(`${__dirname}/index.html`, "utf-8");
    ctx.type = "text/html";
    ctx.body = html;
  }
});

此时我们刷新页面查看一定会报错,因为我们的 index.html 模版引用了 main.jsx 文件,但是我们的 Node 服务器并没有做任何的处理,所以一定会报错 404

  1. 处理 JSX 文件

我们加入处理 JSX 的代码

代码语言:javascript复制
app.use(async (ctx) => {
  // ...

  if (requestUrl === "/") {
    // ...
  } else if (requestUrl.endsWith(".jsx")) {
    // jsx 文件返回 JavaScript 文件类型以及获取文件路径返回前端
    const filePath = path.join(__dirname, `/${requestUrl}`);
    const JSXFile = rewriteImport(fs.readFileSync(filePath).toString());

    ctx.type = "application/javascript";
    ctx.body = JSXFile;
    return;
  }
});

好了,此时我们的 JSX 文件就已经可以正常返回给前端了,但是大家会发现屏幕还是白的。

为什么这样呢?因为我们只是将 JSX 文件的源码给前端了,浏览器并不认识 JSX 文件里面的一些 JSX 语法的代码,所以这里我们需要通过 esbuild 来将 JSX 的代码进行转译(在 Vite 当中也是这样,只不过 Vite 会先将 React 依赖包进行预编译,但是这里需要我们一会进行处理),处理代码如下:

代码语言:javascript复制
import { transformSync } from "esbuild";
// ...
// 通过 esbuild 的 transformSync API 进行代码转译
const out = transformSync(JSXFile, {
  jsxFragment: "Fragment",
  loader: "jsx",
});
ctx.type = "application/javascript";
ctx.body = out.code;
// ...

好的,我们现在可以在刷新浏览器,看一下 JSX 的代码是不是被转译成了浏览器可以认识的代码。

我们所有自己写的 JSX 语法已经被 esbuild 转换成了普通 JS 方法

此时我们去看一下浏览器的 Console 的 Tab ,会有一个 ESM 模块的报错

它说我们的 react 模块它不认识,必须要使用 "/" "./" "../" 这些相对路径才可以,这也是 ESM 不支持裸模块的原因,所以我们需要统一处理裸模块

  1. 处理裸模块
代码语言:javascript复制
if (requestUrl.endsWith(".jsx")) {
  // jsx 文件返回 JavaScript 文件类型以及获取文件路径返回前端
  const filePath = path.join(__dirname, `/${requestUrl}`);
  // 重写路径
  const JSXFile = rewriteImport(fs.readFileSync(filePath).toString());

  const out = transformSync(JSXFile, {
    jsxFragment: "Fragment",
    loader: "jsx",
  });
  ctx.type = "application/javascript";
  ctx.body = out.code;
  return;
}

function rewriteImport(content) {
  return content.replace(/ from ['"](.*)['"]/g, function(s0, s1) {
    if (s1.startsWith("./") || s1.startsWith("/") || s1.startsWith("../")) {
      return s0;
    } else {
      // 裸模块
      return ` from '/@modules/${s1}'`;
    }
  });
}

现在所有的裸模块都已经被我们把路径给重写成 /@modules/ 了,所以浏览器不会报 ESM 的错了,而是报 404 的问题。接下来我们来处理 /@modules/ 模块

  1. 处理自定义的裸模块 '/@modules/'

此时我们来匹配 /@modules/ 模块,因为重写 URL 之后,浏览器会重新发送 /@modules/ 下的请求,从上图的 404 就可以看出来,这时我们在对这个模块进行处理。

代码语言:javascript复制
import { buildSync } from "esbuild";

// ...
if (requestUrl.startsWith("/@modules/")) {
  // 将自定义的模块名称替换掉,拿到原始“裸模块”名称
  const modulesName = requestUrl.replace("/@modules/", "");
  // 获取裸模块的 package.json 文件下的 main 字段,这个字段是代码打包后的入口文件
  const entryFile = JSON.parse(
    fs.readFileSync(
      `${__dirname}/node_modules/${modulesName}/package.json`,
      "utf8"
    )
  ).main;
  // 直接拼接出包的路径
  const pkgPath = `${__dirname}/node_modules/${modulesName}/${entryFile}`;
  let body = {};

  // 尝试去获取 /node_modules/.vite 下的编译过后的文件
  try {
    body = fs.readFileSync(`${__dirname}/node_modules/.vite/${modulesName}.js`);
  } catch (err) {
    // 如果获取不到,使用 ESBuild 打包裸模块里的内容,转换为 ESM 供浏览器使用
    // 并且存入 /node_modules/.vite 缓存目录中
    // 这步操作其实就是 vite 在预编译执行的
    buildSync({
      entryPoints: [pkgPath],
      bundle: true,
      outfile: `${__dirname}/node_modules/.vite/${modulesName}.js`,
      format: "esm",
    });
    // 最后返回当前编译后的 JS 文件
    body = fs.readFileSync(`${__dirname}/node_modules/.vite/${modulesName}.js`);
  }

  ctx.type = "application/javascript";
  ctx.body = body;
}
// ...

此时可以看到我们的裸模块内容已经可以被浏览器正确的识别了。

可以看到还有 css 文件需要处理,接下里处理 css 样式文件

  1. 简单处理 CSS 样式文件
代码语言:javascript复制
// ...
// 通过 esbuild 的 transformSync API 进行代码转译
if (requestUrl.endsWith(".css")) {
  const filePath = path.join(__dirname, `/${requestUrl}`);
  const CSSFile = JSON.stringify(fs.readFileSync(filePath).toString());

  const file = `
      const style = document.createElement('style')
      style.textContent = ${CSSFile}
      document.head.appendChild(style)
      export default {}`;
  ctx.type = "text/javascript";
  ctx.body = file;
}
// ...

由于 ESM 只能接受 Content-Typetext/javascript 类型,所以只能通过 JS 的方式去插入到 index.html

  1. 简单处理 svg 图片

由于 css 和图片并不是要展示的内容,所以就越简单越好,目前的思路是直接将图片转为 base64 格式,在 Vite 当中,只有图片足够小才会使用 base64 的格式,代码如下:

代码语言:javascript复制
// ...
if (requestUrl.endsWith(".svg")) {
  const filePath = path.join(__dirname, `/${requestUrl}`);
  const imageFile = fs.readFileSync(filePath);
  ctx.type = "text/javascript";
  ctx.body = `export default 'data:image/svg xml;base64,${Buffer.from(
    imageFile,
    "binary"
  ).toString("base64")}'`;
}
// ...

然后我们在做一下容错处理,将其他不认识的文件类型都简单容错一下

代码语言:javascript复制
	// ...
	 else {
    ctx.type = 'text/javascript'
    ctx.body = fs.readFileSync(path.join(__dirname, `/${requestUrl}`))
  }
	// ...

好了!!此时我们的 Vite-Demo 已经彻底跑起来了。

  1. 处理 HMR

重点来了,如何简易的实现 HMR 功能

整体的原理为 Websocket 实现热模块替换功能。Vite 是这么做的,通过给每一个文件注入 import.meta.hot 等方式,在保存时,通过服务端通知客户端进行文件的重新请求来实现 HMR,接下来我们简单实现一版

首先改造一下在处理 HTML 文件的时候,加入 Websocket 客户端代码

代码语言:javascript复制
// ...
// 根路径返回模版 HTML 文件
const html = fs.readFileSync(`${__dirname}/index.html`, "utf-8");
const footer = `
    <script>
      const ws = new WebSocket('ws://localhost:1123')

      ws.addEventListener('message', async function incoming(value) {
        function hotUpdate() {
          const script = document.querySelectorAll('script')
          document.body.removeChild(script[script.length - 1])
          const newScript = document.createElement('script')
          newScript.type = 'module'
          newScript.src = './vitesrc/main.jsx?import=${ new Date()}'
          document.body.appendChild(newScript)
        }
        hotUpdate()
      });
    </script>
    `;
ctx.type = "text/html";
ctx.body = `${html}${footer}`;
// ...

通过读取文件,重写文件返回前端,默认添加上 Websocket 相应代码逻辑即可。我们再来添加一下服务端的 Websocket ,通过 ws 模块引入

代码语言:javascript复制
import { WebSocketServer } from "ws";
const Websocket = new WebSocketServer({ port: 1123 });
// HMR
Websocket.on("connection", function connection(ws) {
  ws.on("message", function incoming(message) {
    console.log("received: %s", message);
  });
  // 监听当前路径下的所有文件
  chokidar.watch("./vitesrc").on("change", (changePath) => {
    // const filePath = path.resolve(__dirname, changePath);
    // const data = fs.readFileSync(filePath, 'utf-8');
    ws.send(changePath);
  });
});

监听之后在更改 './vitesrc' 文件下的内容后,就会看到客户端收到服务端发送的数据了。

之后客户端会根据我们之前注入到 index.html 文件内的代码进行重新引入入口文件,实现浏览器的重新请求文件。

然后通过处理 .jsx 文件来热更新

代码语言:javascript复制
let realCode = out.code;
// 自定义 import 为需要热更新
if (ctx.request.url.split("?")[1]?.includes("import")) {
  realCode = out.code.replace(
    / from ['"](.*.jsx.*)['"]/g,
    function rewriteCode(s0, s1) {
      return ` from '${s1}?${ new Date()}'`;
    }
  );
}

ctx.type = "application/javascript";
ctx.body = realCode;

自定义的 import 参数如果有,认为是需要热更新的文件,将 jsx 文件引入路径添加时间戳,让浏览器的缓存失效,请求过后,文件即更新。

好了,此时我们修改 './vitesrc' 下面的任意一个文件,都会看到浏览器会发送对应的请求信息

手写 Vite 的实现思路就到这里。感谢大家的认真观看,接下来将在我们的项目上接入 Vite 并且在踩坑的过程中遇到的问题进行一一解决。

代码语言:javascript复制
import Koa from "koa";
import fs from "fs";
import path from "path";
import { buildSync, transformSync } from "esbuild";
const __dirname = path.resolve(path.dirname(""));

const app = new Koa();

app.use(async (ctx) => {
  const requestUrl = ctx.request.url;

  if (requestUrl === "/") {
    // 根路径返回模版 HTML 文件
    const html = fs.readFileSync(`${__dirname}/index.html`, "utf-8");
    ctx.type = "text/html";
    ctx.body = html;
  } else if (requestUrl.endsWith(".jsx")) {
    // jsx 文件返回 JavaScript 文件类型以及获取文件路径返回前端
    const filePath = path.join(__dirname, `/${requestUrl}`);
    const JSXFile = rewriteImport(fs.readFileSync(filePath).toString());

    const out = transformSync(JSXFile, {
      jsxFragment: "Fragment",
      loader: "jsx",
    });
    ctx.type = "application/javascript";
    ctx.body = out.code;
    return;
  } else if (requestUrl.startsWith("/@modules/")) {
    const modulesName = requestUrl.replace("/@modules/", "");
    const entryFile = JSON.parse(
      fs.readFileSync(
        `${__dirname}/node_modules/${modulesName}/package.json`,
        "utf8"
      )
    ).main;
    const pkgPath = `${__dirname}/node_modules/${modulesName}/${entryFile}`;
    let body = {};

    // 尝试去获取 /node_modules/.vite 下的编译过后的文件
    try {
      body = fs.readFileSync(
        `${__dirname}/node_modules/.vite/${modulesName}.js`
      );
    } catch (err) {
      // 如果获取不到,使用 ESBuild 打包裸模块里的内容,转换为 ESM 供浏览器使用
      // 并且存入 /node_modules/.vite 缓存目录中
      // 这步操作其实就是 vite 在预编译执行的
      buildSync({
        entryPoints: [pkgPath],
        bundle: true,
        outfile: `${__dirname}/node_modules/.vite/${modulesName}.js`,
        format: "esm",
      });
      // 最后返回当前编译后的 JS 文件
      body = fs.readFileSync(
        `${__dirname}/node_modules/.vite/${modulesName}.js`
      );
    }

    ctx.type = "application/javascript";
    ctx.body = body;
  } else if (requestUrl.endsWith(".css")) {
    const filePath = path.join(__dirname, `/${requestUrl}`);
    const CSSFile = JSON.stringify(fs.readFileSync(filePath).toString());

    const file = `
    const style = document.createElement('style')
    style.textContent = ${CSSFile}
    document.head.appendChild(style)
    export default {}`;
    ctx.type = "text/javascript";
    ctx.body = file;
  } else if (requestUrl.endsWith(".svg")) {
    const filePath = path.join(__dirname, `/${requestUrl}`);
    const imageFile = fs.readFileSync(filePath);
    ctx.type = "text/javascript";
    ctx.body = `export default 'data:image/svg xml;base64,${Buffer.from(
      imageFile,
      "binary"
    ).toString("base64")}'`;
  } else {
    ctx.type = "text/javascript";
    ctx.body = fs.readFileSync(path.join(__dirname, `/${requestUrl}`));
  }
});

function rewriteImport(content) {
  return content.replace(/ from ['"](.*)['"]/g, function(s0, s1) {
    if (s1.startsWith("./") || s1.startsWith("/") || s1.startsWith("../")) {
      return s0;
    } else {
      // 裸模块
      return ` from '/@modules/${s1}'`;
    }
  });
}

app.listen(24678, () => {
  console.log("App is running");
});

#Vite 应用及实践

在上面了解到了 Vite 的简单思路和原理,接下来就是应用以及实践的部分了。

这个部分的实践是通过我目前在做的广告投放后台管理系统之上做的。也就是最开头说的,要解决业务的痛点。

#Webpack 升级 Vite

在使用 Webpack 时升级 Vite 有几个步骤

  1. 安装 Vite
代码语言:javascript复制
yarn add vite
  1. 配置 vite.config.js
代码语言:javascript复制
import { defineConfig } from "vite";

// https://vitejs.dev/config/
export default defineConfig({
  // ...
});
  1. 启动 Vite
代码语言:javascript复制
# 当然需要你先去配置 scripts 命令
yarn vite

好了,如果你已经按照上面三个步骤做完了之后,你的项目就是 Vite 的构建了。是不是很简单?

开个玩笑~如果一切正常的话,你会收获一堆报错以及各式各样你没遇到过的问题。。

最后介绍一个工具 wp2vite ,一个让 webpack 项目支持 vite 的前端项目的转换工具。

我在使用的时候也不清楚是项目太老的问题还是配置问题,还是这个工具的 Bug ,总说找不到我的 Webpack 配置文件,就算指定了配置文件入口也不行。所以干脆我就没用,直接自己手动升级。

#广告投放平台升级 Vite

接下来我将把我在历时 2 个多月的时间升级到 Vite 的遇到的问题以及经验分享出来,我会抛出 9 个问题,并且每个问题之后都会给出解决方案,如果小伙伴们有更好的方法或者建议,欢迎评论哦~

这里每个问题都是在升级的时候的绊脚石,一个个的去解决之后,我们的广告投放平台才跑起来,一直到现在直接切入业务开发,为业务开发提效!

#Q1:Vite 不支持 .js 写 JSX 文件

由于我们项目较老,而且开发的时候应该是很着急,所以所有的文件都是已 .js 结尾,然后写的 React 代码,所以我碰到了这个问题,看一下报错信息。

这个是 Vite 默认就不支持的问题,所以这里会有切合业务的两个问题:

  1. 需要将所有的文件类型改写成 JSX or TSX
  2. 项目模块过多,组件更多,一个个改不现实

解决方案

在 Vite 仓库下看到的一个 Issues ,感兴趣的同学可以看一下,还是比较有意思的,贴个截图出来。

通过批量修改文件下所有的 js 文件后缀为 jsx

通过 Node 读取所有的文件内容,如果包含了 React 则一定是 JSX 文件,那么就重新命名为 [name].jsx 即可,代码如下:

代码语言:javascript复制
const path = require("path");
const fs = require("fs");

const dirPath = path.resolve("./src");
const readFile = (filePath) => {
  const files = fs.readdirSync(filePath);
  files.forEach((filename) => {
    const fileDir = path.join(filePath, filename);
    fs.stat(fileDir, (error, stats) => {
      if (error) {
        console.warn("获取文件stats失败", error);
      } else {
        const isFile = stats.isFile(); //是文件
        const isDir = stats.isDirectory(); //是文件夹
        if (isFile) {
          const content = fs.readFileSync(fileDir, "utf-8").toString();
          const fileObj = path.parse(fileDir);
          if (fileObj.ext === ".ts" || fileObj.ext === ".js") {
            if (content.includes("React")) {
              const newFileName = fileObj.name   ".jsx";
              try {
                fs.renameSync(fileDir, path.join(fileObj.dir, newFileName));
              } catch (error) {
                console.log("error", error);
              }
            } else {
              // 不处理
              console.log(`不处理的文件 ===>>> ${fileDir}`);
              // console.log(content)
            }
          }
        }
        if (isDir) {
          readFile(fileDir); //递归,如果是文件夹,就继续遍历该文件夹下面的文件
        }
      }
    });
  });
};

readFile(dirPath);

也有可能误伤哈,这个就需要大家自己看一下了,我这里是有几个小问题,但是影响不大,也就一会就解决了。

#Q2:添加别名

由于我们之前项目中配置的 Webpack 别名 @ === ./src 所以在 Vite. 中也需要配置一下。十分简单,配置文件代码如下:

代码语言:javascript复制
import { defineConfig } from 'vite'

export default defineConfig({
// ...
  resolve: {
    alias: [
      { find: /^@/, replacement: resolve(__dirname, './src') }
    ]
  }
})
// ...
})

但是配置 @ 之后,发现 Vite 报错了。。

应该是将这种包名的也给全局替换了,所以出现这个问题,试了几个办法,并没有有效的解决这个问题。。如果大佬有解决的思路欢迎告知呀!!不胜感激!

解决方案

之后就通过更改别名为 @src 来解决这个问题了。。

注意:如果你没有完全抛弃 Webpack 记得在 Webpack 上的别名配置做同步修改,否则在开发时没问题,打包上线出现问题

#Q3:不支持依赖包内通过相对路径引用

这个问题找了好久,在 Github 上这个仓库的源码的写法也是裸模块的引用,但是在 yarn install 安装依赖的时候,就是相对路径。。

解决方案

通过别名配置 Hack 这个依赖包

代码语言:javascript复制
export default defineConfig({
// ...
  resolve: {
    alias: [
      {
        find: 'venn.js',
        replacement: resolve(__dirname, './node_modules/venn.js/build/venn.js')
      }
    ]
  }
})
// ...
})

参考文献

#Q4:ESM 不支持 ~ 符号

这个是因为我们在使用 less 的时候,less 会使用 ~ 作为自己的特殊标识,所以在 Vite 解析的时候,ESM 并不认识 ~ 符号的问题。

解决方案

还是通过别名来将所有的 ~ 符号进行删除即可,参考文献

代码语言:javascript复制
export default defineConfig({
// ...
  resolve: {
    alias: [
      { find: /^~/, replacement: '' },
    ]
  }
})
// ...
})

#Q5: 无法识别 global 变量

在模版页面报的问题,具体是哪个包的问题我忘记了。。这个解决需要手动简单 Hack 一下。

解决方案

index.html 的 ESM 模块文件之前加入以下代码

代码语言:javascript复制
<script>
  const global = window;
</script>

即可解决这个问题。参考文献

#Q6:antd 的样式包无法应用

通过上面的解决之后,现在可以将系统跑起来了,但是页面的样式都已经丢失掉了,已经面目全非了。。应该是样式丢失的问题。接下来处理一下样式

解决方案

  1. 安装 vite-plugin-imp 插件
  1. 配置一下插件

这样的话,我们的项目应该就可以了,然后继续运行一下。

发现项目报错了。看一下问题

#Q7:无法找到部分 antd 包的样式文件

解决方案

  1. 查看 node_modules 文件
  2. 对比发现 Row 组件和 Col 组件并没有相关的样式文件
  3. 需要对这两个包进行处理

直接排斥着两个包,我们给返回空串来解决未找到包的问题。

#Q8:配置接口代理

当我们在真正开发项目的时候,在联调阶段,我们需要将本地环境代理到后端 API 上才可以获取到真正的数据,所以需要配置本地的接口代理来访问。

解决方案

  1. 配置接口代理,本地开发无法代理到测试环境
  2. 类似 Vue-CLI 配置 Proxy 字段

#Q9:配置接口代理引发的问题

因为我们项目接口字段不统一,所以需要对所有的接口添加 /api 前缀,来实现所有的接口统一处理。但是通过后端修改的话,成本太高,而且后端不会为了前端一些更改去将整个系统的路径都改一遍,可能会引发未知的问题等。而且其目的是前端提效,并不是在项目上或者收益上有所关联。

引发的问题如下:

  • 使用 Webpack 打包时会出现 404 的问题
  • 不使用 /api 前缀,代理配置将无法配置或极其麻烦

解决方案

  • 前端根据环境变量对全局 API 接口路径进行调整
  • 对目前打包上线的 API 不做任何的处理

前端通过注入代码处理

截图中只列出了一个接口的地址,其实每一个模块都是不同的接口地址,例如 /image/... /video/... 等等等等,每一个模块都有自己的专属名称作为开头。所以我们需要通过递归遍历的模式来进行添加接口前缀。

代码语言:javascript复制
const newPaths = {};
for (let key in paths) {
  if (typeof paths[key] !== "string") {
    newPaths[key] = addRequestUrlPrefix(paths[key]);
  } else {
    newPaths[key] = "/api"   paths[key];
  }
}

而且还需要区分开发环境以及线上环境

代码语言:javascript复制
if (process.env.NODE_ENV === "development") {
  return convertUrl(paths);
} else {
  return paths;
}

此时我们本地开发的接口都将自动添加 /api 前缀了,而且在通过接口代理的时候,都统一被 Vite 处理掉了

代码语言:javascript复制
  proxy: {
      '/api': {
        target: 'http://admediatest.58v5.cn',
        changeOrigin: true,
        rewrite: path => path.replace(/^/api/, '')
      }
    }

有的同学可能不知道为什么这样做哈,简单解释一下。如果我们在 rewrite 这里不进行匹配的话,其实我们的前端路由也会被 proxy 给解析出来,所以我们要区分接口和前端路由的路径。所以我们需要这个统一的接口路径来进行区分。

好了,到这里之后,我们的项目就可以用 Vite 跑起来了,而且可以通过和之前一样的开发流程进行开发即可。就是在 HMR 上的速度有点不适应,有点快~在来看一下我们开头的数据对比

Vite

Webpack

提升倍数

启动项目

0.5s

33s

66 倍

首屏

19s

36s

1.9 倍

HMR

0.2s

6s

30 倍

#个人对 Vite 的一些想法

  1. 在开发环境,Vite 的优势已经很明显了。完全值得我们去升级,维护
  2. 线上环境的话可以根据自己的需求,其实一起维护两套配置文件也是比较麻烦比较坑的事情~
  3. 目前在 PC 端使用还是比较不错的,不知道在移动端上会有什么差距因为各个浏览器的兼容性在移动端上本来就要比 PC 麻烦的多
  4. 在线上环境也已经算是成熟了,通过对应的插件都可以实现打包成兼容旧浏览器等。

#总结:

目前项目已经在 Vite 模式下平稳运行,在开发阶段已经有了相当大的提升。目前线上还是通过 Webpack 来进行 bundle ,通过上面的实战已经尽最大努力与 Webpack 持平生产和开发环境。在之后还会持续发掘 unbundled 模式。

Unbundled 开发模式是一个趋势,未来可期~

0 人点赞