把一个webpack + React项目改造成Vite,再加点ESLint+Prettier+Husky
把一个webpack + React项目改造成Vite,再加点ESLint+Prettier+Husky
前言
目前的老项目是一个 Menorepo 项目, 使用 CRA 搭建和 Craco 配置的 React + TS 项目,因业务改造,需要抽离其中的一个 repo 独立成一个项目进行维护,如果只是抽离那自然是比较简单的,直接删除多余代码,把耦合部分抽出来合入独立项目即可。
老项目缺乏维护,热更新无效每次都要刷新页面,且启动 dev 和 build 的时间都很长。正好借着这次抽离,利用 Vite 重新搭建,再配合上 ESLint + Prettier + Husky + lint-staged 建设一下代码规范。
话不多说,开始~
Vite 安装配置与使用
vite 安装
# 我使用的包管理器是pnpm,yarn和npm切换成对应的命令就可以了
# 首先, 安装vite, 我这边使用的是5.3.3版本vite,node是 18.16.0
pnpm install -D vite
# 正常情况下,安装vite后使用vite构建项目会自动为你添加
"@vitejs/plugin-react": "^4.3.1",
"@vitejs/plugin-react-swc": "^3.7.0",
如果没有的话,自己-D安装一下
vite 创建项目
如下所示,进入你想创建项目的文件夹,运行 pnpm create vite
, 会提示让你输入项目名,然后选择 React,然后选择 TS + SWC 即可,
SWC 是“Speedy Web Compiler”的缩写,它利用 Rust 语言的高性能特性,实现快速的代码转换和打包
项目就已经创建好了,你想看看演示 demo 的话,依次执行下方的三条命令就行了。
代码规范引入
你需要安装 ESLint 和 Prettier 的 VScode 插件,并且在 setting 里搜索 formatter 配置成Prettier。
如果你更关注怎么从 webpack 迁移到 vite,你可以直接看下一步的踩坑指南。
ESlint 配置
输入下面的命令,进行 Eslint 初始化,然后按下图进行配置选择
pnpm create @eslint/config
一般来说,安装好了是这样,当然 react 版本你可以自己切换哈。
我们应该关注项目根目录的三个文件,如果没有你可以自己创建:
.eslintrc.cjs
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:prettier/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh', 'prettier', '@typescript-eslint', 'react-hooks'],
rules: { // 示例配置,你可以自己根据项目配置规则
"prettier/prettier": "error",
"arrow-body-style": "off",
"prefer-arrow-callback": "off"
},
}
eslint.config.js
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js';
export default [
{ files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'] },
{ languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReactConfig,
];
.eslintignore
# Ignore the packaged file
# 根据你的实际情况配置不需要ESlint校验的内容
public/*
node_modules/*
src/pages/app_portrait/*
# history
.history
Prettier 配置
输入下面的命令,安装Prettier
pnpm install prettier -D
然后在根目录创建 .prettierrc.cjs 和 .prettierignore 配置文件
.prettierrc.cjs
module.exports = {
printWidth: 120,
tabWidth: 4,
useTabs: false,
semi: true,
singleQuote: true,
quoteProps: 'as-needed',
jsxSingleQuote: false,
trailingComma: 'all',
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'always',
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'css',
endOfLine: 'lf',
embeddedLanguageFormatting: 'off',
singleAttributePerLine: false,
overrides: [
{
files: ['*.json'],
options: {
tabWidth: 2,
},
},
],
};
.prettierignore
package.json
dist/
build/
.editorconfig
.prettierignore
.prettierrc.cjs
.eslint.config.js
ESLint + Prettier 联合配置
先安装依赖
pnpm install eslint-config-prettier eslint-plugin-prettier -D
然后修改 .eslintrc.cjs 配置文件,其实上面我已经加入了这部分配置,现在我指出来是哪些配置:
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
+ 'plugin:prettier/recommended',
],
+ plugins: ['react-refresh', 'prettier', '@typescript-eslint', 'react-hooks'],
+ "rules": {
+ "prettier/prettier": "error",
+ "arrow-body-style": "off",
+ "prefer-arrow-callback": "off"
}
}
如果你想要测试一下,你可以手动执行下面的命令,也可以添加在 package.json 的执行脚本中,执行 pnpm lint 即可
" script":{"lint":"eslint --ext .js,.jsx,.ts,.tsx --fix --quiet ./"}
Husky 和 lint-staged 引入
引入 husky 和 lint-staged 可以在你 commit 的时候进行代码规范并且规范你的提交记录,便于规范和管理
还是首先安装依赖,然后执行命令
pnpm install husky -D
pnpm install lint-staged -D
执行如下命令会在根目录生成 .husky文件夹
husky install
然后给husky添加一个钩子
npx husky add .husky/pre-commit "npm run lint-staged"
然后你还需要在package.json 增加一个脚本
"scripts": {
"dev": "vite",
"build": "node --max_old_space_size=8192 node_modules/vite/bin/vite.js build --mode production",
"lint": "eslint . --ext ts,tsx,js,jsx --report-unused-disable-directives --quiet",
"lint-fix": "eslint . --fix --ext ts,tsx,js,jsx --report-unused-disable-directives --quiet",
"preview": "vite preview",
"prepare": "husky install",
+ "lint-staged": "lint-staged"
},
然后在根目录添加一个 .lintstagedrc 文件
目录根据你的实际情况配置哈
{
"src/**/*.{js,jsx,ts,tsx}": ["prettier --w", "eslint --fix --quiet", "eslint --quiet"],
"common/**/*.{js,jsx,ts,tsx}": ["prettier --w", "eslint --fix --quiet", "eslint --quiet"]
}
这样每次提交代码前都会进行 Lint 检查。使用lint-staged是为了只对对改动的文件进行lint,而不是全量lint
迁移踩坑指南
在这里涉及到的文件内容等,我都尽量放上全量的配置,后续的坑如果有涉及的,我会指出是修改了什么配置。
vite.config.ts 配置和迁移
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
import * as fs from 'fs';
import * as dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import vitePluginImp from 'vite-plugin-imp';
// import legacy from '@vitejs/plugin-legacy';
const __dirname = dirname(fileURLToPath(import.meta.url));
export default ({ mode }: any) => {
const env = dotenv.parse(fs.readFileSync(`.env.${mode}`));
return defineConfig({
base: '/appname',
plugins: [
react(),
vitePluginImp({
libList: [
{
libName: 'antd',
style: (name: any) => `antd/es/${name}/style`,
},
],
}),
// legacy({
// targets:
// '>0.3%, edge>=79, firefox>=67, chrome>=52, safari>=12, iOS>=12, samsung>=20, opera>=90, op_mini all',
// polyfills: true,
// additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
// }),
],
resolve: {
alias: [
{ find: '@', replacement: resolve(__dirname, './src') },
{ find: '@common', replacement: resolve(__dirname, './common') },
{ find: 'utils', replacement: resolve(__dirname, './src/utils') },
{ find: 'pages', replacement: resolve(__dirname, './src/pages') },
{ find: /^~/, replacement: '' },
],
},
css: {
preprocessorOptions: {
less: {
// 支持内联 JavaScript
javascriptEnabled: true,
},
},
},
build: {
chunkSizeWarningLimit: 1500,
target: ['es2015'],
cssTarget: ['es2015', 'chrome58', 'edge16', 'firefox57'],
minify: 'terser',
terserOptions: {
compress: {
// 生产环境时移除console
drop_console: true,
drop_debugger: true,
},
},
},
server: {
host: 'localhost',
port: 3000,
open: false,
cors: true,
proxy: {
'/appname/api': {
secure: false,
changeOrigin: true,
target: env.VITE_GATEWAY_URL,
},
},
},
});
};
可以看到一共有 base plugins resolve css build server
等几个配置,如果目前有一些配置你不明白,你可以继续往下看,因为我相信你会遇到和我一样的问题,到时候回来看他们解决了什么就行了。
base: app 的根路径,你没有特殊需求可以不配置,就是
plugins: 插件配置,引入插件,react 和 antd 按需加载
resolve: alias 配置别名映射路径
css: 样式配置
build:vite 打包的配置
server: dev 环境下代理配置等
项目入口修改
vite 的入口 index.html 是在根目录中的,我们把在 CRA 项目中位于 public/index.html 的文件移出来,当然你需要进行修改。
主要关注两点:
静态文件引入部分,你放在 public 文件夹下,vite 会自己寻找,需要去掉 %PUBLIC_URL%
,
另外一个需要注意的就是 <script type="module" src="/src/index.tsx"></script>
,vite 采用 esModule 引入,我们需要指定文件入口。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="你的软件名称"
/>
<title>网页title</title>
</head>
<body>
<div id="root"></div>
<script>
if (typeof window.global === 'undefined') {
window.global = window;
}
</script>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
Antd 引入报错,(Internal server error: '~antd/lib/style/themes/default.less' wasn't found.)
当我们在 vite 中引入 antd,并在 vite.config.ts
配置按需引入,你可能会遇到Internal server error: '~antd/lib/style/themes/default.less' wasn't found 或者 XX.less 无法找到的问题,关键在于 vite 没有成功解析 ~antd/
,当然这是针对 antdV4 版本会有这个问题,V5 已经是 CSSinJS 了。你需要在 vite.config.ts
进行如下的配置:
首先 安装依赖 pnpm install vite-plugin-imp -D
,然后分别在 plungins、css、resolve 中进行如下配置,
可以查看 github 这一个 issue GITHUB ISSUE
plugins: [
react(),
vitePluginImp({
libList: [
{
libName: 'antd',
style: (name: any) => `antd/es/${name}/style`,
},
],
}),
css: {
preprocessorOptions: {
less: {
// 支持内联 JavaScript
javascriptEnabled: true,
},
},
},
resolve: {
alias: [
{ find: /^~/, replacement: '' },
],
},
项目中别名配置错误,(Failed to resolve import "@/common" from "src/index.tsx". Does the file exist?)
报错 :Failed to resolve import "@/common" from "src/index.tsx". Does the file exist
这个问题通常是因为你在代码中使用了类似 import xxx from '@common/utils' 或者 import xxx from '@/pages'
这样的引入方式,可能是因为以前是 menorepo 项目,也可能是以前 webpack 就已经为你配置好了,所以你需要在 vite 中根据你的文件夹结构配置一下别名:
类似的我进行了如下配置:
resolve: {
alias: [
{ find: '@', replacement: resolve(__dirname, './src') },
{ find: '@common', replacement: resolve(__dirname, './common') },
{ find: 'utils', replacement: resolve(__dirname, './src/utils') },
{ find: 'pages', replacement: resolve(__dirname, './src/pages') },
{ find: /^~/, replacement: '' },
],
},
报错 global 无法识别(ReferenceError: global is not defined.)
如果不出意外,你可能会遇到报错 ReferenceError: global is not defined。告诉你 global 没有定义,通常 global 是 node 环境下的,window 下找不到,我们可以添加一段 polyfill 来解决,在你的 index.html 中插入
<script>
if (typeof window.global === 'undefined') {
window.global = window;
}
</script>
Vite 老版本浏览器兼容问题 解决白屏问题
如果你用的老版本的浏览器,你应该会遇到兼容性问题:
默认情况下,Vite 的目标是能够 支持原生 ESM script 标签、
支持原生 ESM 动态导入 和 import.meta 的浏览器: Chrome >=87 Firefox >=78 Safari >=14 Edge >=88 你也可以通过 build.target 配置项 指定构建目标,最低支持 es2015。
这是 vite 的技术基础,我们可以通过配置来解决,但需要注意的是 这部分的配置只是在 build 的时候生效,dev 开发环境你仍然需要使用 vite 支持的浏览器。
兼容性问题分为两种,你使用了更高级的语法,浏览器不支持,这个时候你需要把他翻译成浏览器支持的语法就行,那么你只需要在 build 中进行如下配置,打开你的 vite.config.ts,将 target 设置为 es2015,这厮 vite 最低支持的版本了,当然你也可能需要设置 cssTarget 的值,不然你可能会遇到某些 antd 组件很诡异,例如 switch 看不到开关,modal 点击后弹不出来,其实都是 css 样式不支持,例如 modal 里设置了 inset: 0,其实就等价于 top:0;right:0;bottom:0,left:0,但是 老浏览器是不支持的。
你还可以配置 terser 来压缩你的代码,记得要先装依赖哈。
build: {
chunkSizeWarningLimit: 1500,
target: ['es2015'],
cssTarget: ['es2015', 'chrome58', 'edge16', 'firefox57'],
minify: 'terser',
terserOptions: {
compress: {
// 生产环境时移除console
drop_console: true,
drop_debugger: true,
},
},
},
如果到这里,你还没有解决问题,那可能是你的项目中使用了老浏览器不支持的 API,比如 Promise.allSettled
这个时候就只能使用 polyfill 或者 babe 了,vite 官方为我们提供了一个插件,可以通过配置这个插件来支持老旧的浏览器。
首先安装依赖 pnpm install @vitejs/plugin-legacy -D
,然后配置 legacy 即可,一般情况下,你使用默认配置的 legacy()就可以解决问题,他会加载默认配置进行打包,如果还没解决,你就需要根据你需要适配的浏览器版本进行一个设置,这里如果你配置了 .browserslistrc, 那 vite 会去读这个配置,未设置则是默认值,当然那你也可以显示在这里指定。
import legacy from '@vitejs/plugin-legacy';
plugins: [
react(),
vitePluginImp({
libList: [
{
libName: 'antd',
style: (name: any) => `antd/es/${name}/style`,
},
],
}),
legacy({
targets:
'>0.3%, edge>=79, firefox>=67, chrome>=52, safari>=12, iOS>=12, samsung>=20, opera>=90, op_mini all',
polyfills: true,
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
}),
],
转载自:https://juejin.cn/post/7390416395148722185