如果你的React项目现在已经比较庞大,每次启动的时候都需要几十秒,热更新也是非常的慢,你想要缓解这种现象,提高开发效率的时候,可能首先就会想到vite,今天聊一聊React接入vite的方案。
一、方案介绍
新项目如何使用vite?
1.最直接也是简单的,使用vite中文官网提供的脚手架,初始化项目,不过,官网的脚手架,只提供了最基本的react项目搭建,关于路由、api、mock、redux等常用功能,需要自己封装。
2.使用阿里旗下的飞冰框架,最新版本支持一键配置vite,这个封装的功能更多一点,但是封装多意味着我们被框架限制也更多,很多东西需要按照框架要求,不过也能满足我们日常大部分开发需求。
3.umijs想想大部分人也很熟悉,umi4beta版本,也将要支持vite开发环境+webpack生产环境,但是这个版本什么时候会正式上线,目前未知。
老项目如何接入vite?
其实我们更多的时候是想要缓解,已有的项目,启动慢,热更新缓慢的问题,我们进入正题。
如题,本次文章的主题react接入vite的开发环境,之所以只接入开发环境,是因为一下几个方面的考量。
- 目前vite的生态相比webpack还不是那么完善,有一定风险
- vite兼容性较差
- 如果完全把项目的webpack替换成vite,可能需要做全量的测试,有出线上问题的风险
开发环境接入方案介绍
我们可以先看一下,使用vite官网脚手架搭建的项目目录
目录结构也很简单,其中红圈的三个文件是构成一个vite项目的核心文件。
index.html: vite服务的入口文件,用script标签引入了main.tsx
main.tsx: react的入口文件
vite.config.ts: vite配置文件。
我们在在老项目中使用vite作为开发环境的思路,就是在在项目中,生成一个这样的vite环境,然后在main中,我们把原有项目中的页面、路由配置、redux配置、mock配置等引入进来,这样就算完成了,听起来好像也不是很复杂。
我们现在开始试验一下
找一个项目在原有项目,可以直接在原有项目用官网的脚手架生成一个vite环境的文件夹
1.首先我们开始改造main.tsx 代码如下,我们在main当中增加动态懒加载路由部分,路由部分大家可以根据原有项目情况,使用v5或者v6版本,v5到v6版本变化还有有点大的。 由于我们原有项目使用umi框架,开启了dva配置,我们也需要加入dva的部分
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import 'antd/dist/antd.css';
import { umiConfig, viteConfig } from '../viteConfig';
import dva from 'dva';
import { createBrowserHistory } from 'history';
import { HashRouter, Route, BrowserRouter, Switch } from 'react-router-dom';
import loadable from '@loadable/component'
// 引入所有model
const allModel = import.meta.globEager('../src/models/*.ts');
// 不使用不会引入,导致动态导入失败
const loada = loadable(() => { })
let routerConfig = [];
// 渲染路由方法,递归渲染子路由
const renderRoute = (router: any) => {
if (!router) return
return router.map((item: any) => {
const child = renderRoute(item?.routes)
let Element = item.component
return <Route
key={item.path}
path={`${item.path.replace('./', '/*').replace('/', '/*')}`}
component={() => (
<Element>{child}</Element>
)}
></Route>
});
};
// 默认使用dva,可以根据自己项目需求自行改造
// 创建 dva 实例
const app = dva({
history: createBrowserHistory(),
});
// 注册 model
Object.keys(allModel).forEach((key) => {
allModel[key]?.default && app.model(allModel[key].default);
});
const router = (
<BrowserRouter>
<Switch>
{renderRoute(routerConfig)}
</Switch>
</BrowserRouter>
);
app.router(() => router);
// 渲染
ReactDOM.render(
<div>
{
<Provider store={app.start()().props.store}>
{/* {<Main></Main>} */}
{router}
</Provider>
}
</div>,
document.getElementById('root'),
);
2.第二步,开始配置vite配置文件
代码如下,我们在配置中,尤其是插件中,做了很多的东西。
-
首先利用插件,配置了动态路由,因为直接在main里面写,vite不支持动态路由里面写动态字符串。
-
因为我们的vite项目中直接引用的redux上下文,和原有项目引用的umi的redux上下文,不是一个环境,需要利用插件更换为原生的引用。
-
原生Vite 构建模式在开发阶段基于浏览器加载 ESM 模块,不支持 require 语法的导入,需要引入一个vite-plugin-require的插件。
-
然后还利用vite-plugin-mock 插件实现了mock数据的处理
-
因为原有umi项目中有一些全局变量,我们在vite中define.process 增加了一些全局变量,解决报错
-
利用css.preprocessorOptions配置全局less变量
-
利用resolve.alias配置实现全局引用路径配置,比如【@】这种
-
还需要注意的一个是,原生vite的cssmodule只支持.module.less文件,我们又不能去修改原有项目的所有less文件,这里配合修改vite源码中compileCSS方法,识别a.less?module=true,这种结尾的less文件,然后再利用插件,修改所有文件中/import [a-z]+ from ".+.less" 引入的less文件,使得支持cssmodule。
-
更多配置,大家可以参考官方文档。
-
vite修改源码部分如下:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import { viteMockServe } from 'vite-plugin-mock';
import vitePluginRequire from 'vite-plugin-require';
import { umiConfig, viteConfig } from '../viteConfig';
let config = { ...umiConfig, ...viteConfig };
// 生成router 字符串 因为动态路由vite直接写会报错!!
let routerConfig = config.routes;
const initRoute = (routers) => {
routers.forEach(item => {
if (item.component) {
let atr = item.component.replace('@', '../src')
item.component = `loadable(() => import('${atr}'))`
}
item?.routes?.length && initRoute(item?.routes)
});
}
initRoute(routerConfig)
let routerStr = JSON.stringify(routerConfig)
routerStr = routerStr.replace(/"loadable/g, "loadable")
routerStr = routerStr.replace(/\)\)"/g, "))")
routerStr = `
let routerConfig = ${routerStr}
`
// 生成router 字符串结束
const localEnabled = config.hasOwnProperty('mock') ? false : true;
const prodEnabled = !!process.env.USE_CHUNK_MOCK || false;
export default defineConfig({
plugins: [
{
name: 'originjs:commonjs',
apply: 'serve',
transform(code, id) {
let result = code;
// 增加cssmodule
if (code.match(/import [a-z]+ from ".+\.less"/)) {
result = code.replace(/(import [a-z]+ from ".+\.less)(")/, ($1, $2, $3) => {
return `${$2}?module=true${$3}`;
});
}
// 修改umi的useDispatch 没有上下文环境,这段代码可以根据自己项目情况,判断是否需要
if (code.indexOf('useDispatch()') > -1) {
result = `import { useDispatch as viteUseDispatch, useSelector as viteUseSelector } from 'react-redux' \n` + result;
// 全局替换
result = result.replace(/useSelector\(/g, "viteUseSelector(")
result = result.replace(/useDispatch\(\)/g, "viteUseDispatch()")
}
// 修改umi的useDispatch 没有上下文环境,这段代码可以根据自己项目情况,判断是否需要
// if (code.indexOf(`connect(`) > -1 && id.indexOf('.tsx') > -1) {
// result = `import { connect as viteconnect } from 'react-redux' \n` + result;
// // 全局替换
// result = result.replace(/connect\(/g, "viteconnect(")
// }
// 在main.tsx 中增加路由相关
if (id.indexOf(`/vite-project/main.tsx`) > -1) {
result = result.replace('let routerConfig = [];', routerStr);
}
return {
code: result,
map: null,
warnings: null,
};
},
},
react(),
// require插件
vitePluginRequire({
// @fileRegex RegExp
// optional:default file processing rules are as follows
// fileRegex:/(.jsx?|.tsx?|.vue)$/
}),
// mock数据插件
viteMockServe({
mockPath: './mock',
localEnabled: localEnabled, // 开发打包开关
prodEnabled: prodEnabled, // 生产打包开关
// 这样可以控制关闭mock的时候不让mock打包到最终代码内
injectCode: `
import { setupProdMockServer } from './mockProdServer';
setupProdMockServer();
`,
logger: false, //是否在控制台显示请求日志
supportTs: true, //打开后,可以读取 ts 文件模块。 请注意,打开后将无法监视.js 文件
}),
],
define: {
process: {
env: {
__IS_SERVER: null,
isVite: true,
},
},
},
css: {
// 解决cssmodule
// @ts-ignore
isModule(id) {
if (id.indexOf('module=true') !== -1) {
return true;
}
},
preprocessorOptions: {
less: {
modifyVars: {
'primary-color': '#ff8420',
'link-color': '#2DB7F5',
'border-radius-base': '4px',
'text-color': '#666',
'font-size-base': '14px',
},
javascriptEnabled: true,
},
},
},
resolve: {
preserveSymlinks: true,
alias: {
'@': path.resolve(__dirname, '../src'),
'@@': path.resolve(__dirname, '../src/.umi'),
},
},
// server:{
// hmr:{
// overlay: false
// }
// }
});
进行到这里,对整个项目的改造大部分已经完成了,剩下就是处理一些报错,因为毕竟原来的项目有各种各样的逻辑,和配置,下面我罗列了我遇到的一些错误和解决办法,大家可以参考。
问题列表
1.首先需要在主项目安装less、和react-router,因为大家的项目可能有一些已经安装过了,就没在脚手架自动安装,react-router可以自行选择是v5版本还是v6版本
2.
这个问题是antd3.x导致的问题,4.x版本没有这个问题,一直没有找到好的解决办法,只能去antd的源码里加一个安全判断。如果大家有什么更好的解决办法,可以评论区告诉我。
3.
这个问题可能是由于require插件导致的,这个插件目前可能不太完善,有的项目报错,有的不报错。把require插件注释掉。
但是。。。如果不使用这个插件,就得手动把项目里的require都替换成import。
大致的问题就这些,如果在接入过程中,遇到其他问题,可以仿照自定义插件,对项目做一些修改,这里只会修改vite生成的文件,对原有项目没有任何影响。
二、接入脚手架介绍
这里开发了一个脚手架方便大家使用,
- 全局安装脚手架:npm i add-vite-cli -g 或者 yarn安装。需要内网环境,git目前是在内网(文末有git链接)。
- 脚手架目前只支持react接入,后续考虑加入Vue,其实Vue和react接入的的思路都是一样的,后面会讲到。
- 进入项目根目录 addVite
- cd ./vite-project
现在做一些最基本的修改
-
mock数据的修改,这里需要引入之前项目的mock配置文件,下面注释内容是vite需要的mock格式,可能需要一个转化的过程,我下面自带了umi mock数据转化的方法。
-
路由文件配置,目前配置文件也只支持路由的配置。routers写在哪个对象都行,因为我们的vite环境是开放的,很多东西都可以直接在vite目录修改,这里目前没有多做配置,之所以把路由提出来,也是为了和旧项目的兼容,防止出现两套路由配置的情况。
因为毕竟项目会有各种各样的架构,架手架也没办法考虑到所有的情况,甚至这里也只是考虑到一小部分,需要根据不同的项目,做出不同的配置和修改
git地址
脚手架地址:igit.58corp.com/weijie03/ad…
项目模板地址:igit.58corp.com/weijie03/re…