背景
之前公司项目采用的是umi
脚手架一体化构建工具,得益于对webpack
与各框架的集成和封装,使得快速上手的能力大大加强,但是随着项目的不断迭代与功能增加,依赖的库也是越来越多,目前最明显的感受就是每次启动与打包构建的时长,往往是好几分钟~,热更新有时也要耗费数秒,对于开发效率与体验影响很大。。。
之前尤大发布vite1.0
时也了解了一点,最明显感受就是一个字“快”,不过一直没仔细研究过,只知道是基于`esbuild`和`rollup`,目前`vite2.0`已经发布,完全作为一个独立的构建工具,对`react`等其他非`vue`框架有着很好的支持。最近也算忙里偷闲,算是稍微研究了一下基本知识。本篇文章记录我以`vite`构建`react`的过程及细节,后续会继续深入研究输出`vite`相关系列文章,敬请期待
目标
我对构建项目的要求如下:
- 支持
Typescript
- 支持
React
、JSX
语法 - 支持
ES6
语法 - 支持
Less module
- 支持
Eslint
、Prettier
、Pre-commit hook
- 支持
HMR
快速热更新 - 支持
Antd
按需引入与主题样式覆盖 - 支持
Proxy
代理、alias
别名 - 兼容传统浏览器
- 开发启动速度要够快,以秒计算
- 支持懒加载和
chunk
分割
介绍
前置条件之一
浏览器原生支持 ES 模块。
特点
- 基于原生
ES
模块,即<script type="module" >
,做到快速加载 - 使用
Esbuild
预构建依赖 (本地开发环境) - 使用
Rollup
打包代码(线上生产环境) -
HMR
是在原生ESM
上执行的 - 利用对
HTTP
头信息的控制,优化缓存与重加载,高效率利用浏览器能力。 - 开箱即用,内置多种支持,如:
Typescript
支持、JSX
支持、CommonJS
和UMD
兼容、css
预处理器与css modules
等
vite对模块的分类
-
依赖:
- 在开发时不会变动的纯
JavaScript
。(如第三方依赖antd
、lodash
等) - 在该场景采用具有优势的
Esbuild
处理。(大量模块的组件库、CommonJS
格式的文件等)
- 在开发时不会变动的纯
-
源码:
- 通常包含一些并非直接是
JavaScript
的文件,需要转换,时常会被编辑。(JSX
、CSS
、Vue
/React
组件等) - 会根据路由拆分代码按需加载模块
-
Vite
以 原生ESM
方式提供源码
- 通常包含一些并非直接是
概念
预构建:将有许多内部模块的 ESM
依赖关系转换为单个模块,以提高后续页面加载性能。简单来说就是尽量合并与减少请求。
例如:当我们引入的一个第三方模块依赖了大量其他模块时,在不合并请求的情况下,会请求上百次不等,造成网络拥塞影响性能,而通过预构建合并后只需要一个请求即可。(lodash-es
有超过个内置模块!当我们执行 import { debounce } from 'lodash-es'
时,浏览器同时发出 600 多个 HTTP
请求!通过预构建 lodash-es
成为一个模块,我们就只需要一个 HTTP
请求了!)
相较于传统的 webpack
构建工具,先打包构建所有的依赖和项目代码,然后再启动开发服务器。Vite
则利用浏览器对 ESM
的支持,先启动开发服务器,然后再根据代码执行按需加载剩下所需的对应模块。
因为缓存,在我们第二次启动时几乎可以做到秒开!非常的可怕~
官网图
官网图很清晰的描绘了区别:
步骤
项目初始化
官方支持React
模板预设有:react
、react-ts
,因为我需要Typescript
,所以直接用这个模板,省事了~
# npm 6.x
npm init @vitejs/app my-react-app --template react-ts
# npm 7+, 需要额外的双横线:
npm init @vitejs/app my-react-app -- --template react-ts
# yarn
yarn create @vitejs/app my-react-app --template react-ts
引入react三件套
这里有兴趣的可以尝试下 pnpm
包管理工具,安装速度很快,不了解的可以查看pnpm官方文档,相较于传统的npm
、yarn
工具都有很好的性能提升与使用体验,这里不做过多介绍,放张图大家体会下~
注:就目前我的使用情况来看大部分场景几乎都没问题的,不过还是存在一小部分问题。如:安装precommit后Git hooks不生效等。
安装依赖
# pnpm
pnpm add react react-dom react-router-dom
# or npm
npm i react react-dom react-router-dom
创建页面
在src
目录下创建pages
目录放置页面组件模块,然后我们简单写两个页面测试下:
// pages/Home/index.tsx
import React from 'react';
const Home: React.FC = () => <div> Home </div>;
export default Home;
// pages/About/index.tsx
import React from 'react';
const About: React.FC = () => <div> About </div>;
export default About;
修改文件App.tsx
// App.tsx
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Home from './pages/Home'
import About from './pages/About'
const App = () => {
return (
<Suspense fallback={<span>loading</span>}>
<Router>
<Switch>
<Route key="/home" path="/home" component={Home}></Route>
<Route key="/about" path="/about" component={About}></Route>
</Switch>
</Router>
</Suspense>
);
};
export default App;
配置路由/界面
新建layouts
组件,主要用于区别渲染登录注册页面与布局界面:
layouts/BasicLayout.tsx
、layouts/UserLayout.tsx
这里就不一一做展示了,详细代码见仓库,地址贴在下面了。
新建路由配置文件router/index.ts
:
import React from 'react';
const Page404 = React.lazy(() => import('../pages/404'));
const Home = React.lazy(() => import('../pages/Home'));
const Login = React.lazy(() => import('../pages/User/Login'));
const Register = React.lazy(() => import('../pages/User/Register'));
const routes: IRoute[] = [
{
path: '/user',
component: React.lazy(() => import('../layouts/UserLayout')),
meta: {
title: '用户路由',
},
redirect: '/user/login',
children: [],
},
{
path: '/',
component: React.lazy(() => import('../layouts/BasicLayout')),
meta: {
title: '系统路由',
},
redirect: '/home',
children: [
{
path: '/home',
meta: {
title: '首页',
icon: 'home',
},
component: <Home />,
},
{
path: '/about',
meta: {
title: '关于',
icon: 'about',
},
component: <About />,
},
],
},
]
export default routes;
创建store状态管理文件
自react hooks
诞生后,大部分场景使用hooks
与props
进行状态管理基本可以满足多数需求,少部分全局应用信息与用户信息等需要全局状态管理的,这里我觉得也不需要完整引入一个redux
、mobx
等这种库。当然还要结合具体场景和公司技术栈确定,较大型和复杂的项目等视情况而定~
我这里使用了zustand
做了简单配置,使用起来也是比较的简单,详情参见官方文档
// 创建store
import create from 'zustand'
const useStore = create(set => ({
bears: 0,
increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 })
}))
// 组件绑定
function BearCounter() {
const bears = useStore(state => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
const increasePopulation = useStore(state => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
引入Antd组件库并配置按需加载
这里就不废话了,直接展示如何在vite
中配置antd
的按需加载,首先我们安装一个插件:
pnpm add vite-plugin-imp -D
然后在vite.config.ts
文件的plugins
中添加配置vitePluginImp
这里我们顺势再引入一个less-vars-to-js
包,less-vars-to-js
可以将less
文件转化为json
键值对的形式,当然你也可以直接在modifyVars
属性后写json
键值对。这样做的好处是可以把全局配置统一放到config
文件进行管理,方便维护。
自定义覆盖主题色
config/variables.less // @primary-color: '#ff7875';
import reactRefresh from '@vitejs/plugin-react-refresh';
import lessToJS from 'less-vars-to-js';
import path from 'path';
import { defineConfig } from 'vite';
// vite-plugin-imp 该插件按需加载存在部分样式丢失的情况
// import vitePluginImp from 'vite-plugin-imp';
// 由于 vite 本身已按需导入了组件库,因此仅样式不是按需导入的,因此只需按需导入样式即可。
import styleImport from 'vite-plugin-style-import';
const themeVariables = lessToJS(
fs.readFileSync(path.resolve(__dirname, './config/variables.less'), 'utf8'),
);
export default defineConfig({
base,
plugins: [
reactRefresh(),
// 配置按需引入antd
// vitePluginImp({
// libList: [
// {
// libName: 'antd',
// style: (name) => `antd/es/${name}/style/index.less`,
// },
// ],
// }),
styleImport({
libs: [
{
libraryName: 'antd',
esModule: true,
resolveStyle: (name) => {
return `antd/es/${name}/style/index`;
},
},
],
}),
],
css: {
preprocessorOptions: {
less: {
// 支持内联 JavaScript,支持 less 内联 JS
javascriptEnabled: true,
// 重写 less 变量,定制样式
modifyVars: themeVariables,
},
},
}
})
环境变量
方案一
通过 --mode
注入配置参数以匹配测试/开发环境等。
我们修改下package.json
文件:
scripts: {
"build:beta": "vite build --mode beta",
"build:release": "vite build --mode release",
"build:legacy ": "vite build --mode legacy ",
}
node
环境下直接process.argv
即可获取到,我们可以在vite.config.ts
中打印信息查看
// vite.config.ts
import {defineConfig} from 'vite'
const env = process.argv[process.argv.length - 1];
console.log('env:', env);
export default defineConfig({})
方案二
使用函数式写法配置动态获取环境变量等参数
首先在根目录下创建.env
文件
# port
VITE_PORT = 3100
# HTTP API
VITE_HTTP_API = http://127.0.0.1:8000
# title
VITE_APP_TITLE = Vite React App
然后我们调整下 vite.config.ts
文件
// 函数式配置
import { loadEnv } from 'vite';
import type { ConfigEnv, UserConfig } from 'vite';
export default ({ command, mode }: ConfigEnv): UserConfig => {
const root = process.cwd();
const env = loadEnv(mode, root);
console.log('env', env);
console.log('command', command);
console.log('mode', mode);
}
组件内可通过import.meta.env
获取,我们可以在Home/index.tsx
中打印信息查看
// Home/index.tsx
import React from 'react'
import { Button } from 'antd'
const Home: React.FC = () => {
console.log('import.meta.env', import.meta.env)
return <div>
<Button type='primary'>Home</Button>
</div>
}
export default Home
alias 别名设置
export default defineConfig({
...
resolve: {
alias: [
{ find: /^~/, replacement: path.resolve(__dirname, './') },
{ find: '@', replacement: path.resolve(__dirname, 'src') },
],
// alias: {
// '~': path.resolve(__dirname, './'), // 根路径
// '@': path.resolve(__dirname, 'src') // src 路径
// }
}
...
})
proxy代理配置
export default defineConfig({
...
server: {
port: 8080, // 开发环境启动的端口
proxy: {
'/api': {
// 当遇到 /api 路径时,将其转换成 target 的值
target: 'http://127.0.0.1:8080/api/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''), // 将 /api 重写为空
},
}
}
})
兼容传统浏览器
vite
默认只支持现代浏览器,即ES module
,对于IE
等老版本浏览器,可使用@vitejs/plugin-legacy
插件在build
时进行polyfill
。
yarn add @vitejs/plugin-legacy -D
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
export default {
plugins: [
legacy({
targets: ['ie >= 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
})
]
}
Eslint、Prettier、Stylelint 代码检查与格式化
使用eslint
这种代码检查工具是为了更好的规范代码和写法,列如:禁用var
,建议使用const
、let
,以及配合TS使用时对各种类型规范等,对于代码优化与后期维护都很方便。
而格式化对于多人协同开发时,统一代码风格很有用。
VSCode 集成
插件规则遵循就近原则,会优先启用本项目下的配置文件,当前配置会覆盖全局配置,如果没有就采用全局配置。
source.fixAll.eslint
开启后可使编辑器按照eslint
规则 auto fix
在 vscode
中安装 Eslint
、Prettier
插件,并开启功能,推荐在全局配置(或当前工作目录下)中添加:
// file: vscode setting.json
// onSave
"editor.formatOnSave": true, //每次保存的时候自动格式化
// eslint
"eslint.alwaysShowStatus": true, // 总是在 VSCode 显示 ESLint 的状态
"eslint.quiet": true, // 忽略 warning 的错误
"editor.codeActionsOnSave": { // 保存时使用 ESLint 修复可修复错误
"source.fixAll": true,
"source.fixAll.eslint": true
},
// prettier
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
为了方便配置,可以使用 VsCode
插件 setting sync(该插件通过GitHub Gist ID
绑定,实现云同步上传下载),通过GitHub Gist ID
云同步配置和插件。
git commit 集成
git commit
原理:
git
在执行的过程会提供相关钩子函数,如commit
、push
时,在项目目录下的.git
目录下的hooks
文件夹下存在相关git
生命周期的钩子函数配置,我们可以手动自己配置,只需删除后缀.sample
即可,不过因为这是本地文件,对于团队协同开发同步配置不太友好,所以还是建议采用第三方库统一管理。
hooks
文件夹./.git/hooks/
我们先在项目根目录下创建.husky文件夹
-
然后添加文件pre-commit
#!/bin/sh . "$(dirname "$0")/_/husky.sh" npx --no-install lint-staged
-
package.json
中添加如下配置:script 配置 postinstall是为了确保在安装依赖完成后,npm可以执行postinstall钩子,使husky安装配置文件到.husky文件下,npm在安装执行的过程提供了一些生命周期钩子,postinstall、prepare等。
{ "script": { "postinstall": "husky install", "lint:fix": "eslint --cache --ext .js,.jsx,.ts,.tsx --no-error-on-unmatched-pattern --quiet --fix ./src", "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less", }, "lint-staged": { "**/*.{js,jsx,tsx,ts,json}": [ "npm run lint:fix", "git add --force" ], "**/*.{less}": [ "npm run lint:style", "git add --force" ] }, }
再安装
husky
、lint-staged
依赖库,
在提交代码时,lint git
暂存区的代码,若 lint
不通过则中断提交,保证问题代码不进入代码仓库。
目录结构
├── dist // 默认的 build 输出目录
├── config // 全局配置文件
└── src // 源码目录
├── assets // 公共的文件(如image、css、font等)
├── components // 项目组件
├── constants // 常量/接口地址等
├── layout // 全局布局
├── routes // 路由
├── store // 状态管理器
├── utils // 工具库
├── pages // 页面模块
├── Home // Home模块,建议组件统一大写开头
├── ...
├── App.tsx // react顶层文件
├── main.ts // 项目入口文件
├── typing.d.ts // ts类型文件
├── .editorconfig // IDE格式规范
├── .env // 环境变量
├── .eslintignore // eslint忽略
├── .eslintrc // eslint配置文件
├── .gitignore // git忽略
├── .npmrc // npm配置文件
├── .prettierignore // prettierc忽略
├── .prettierrc // prettierc配置文件
├── .stylelintignore // stylelint忽略
├── .stylelintrc // stylelint配置文件
├── index.html // 项目入口文件
├── LICENSE.md // LICENSE
├── package.json // package
├── pnpm-lock.yaml // pnpm-lock
├── postcss.config.js // postcss
├── README.md // README
├── tsconfig.json // typescript配置文件
└── vite.config.ts // vite
项目地址
【vite】构建标准化react应用 | 小帅の技术博客 (ssscode.com)
参考
Vite 2.0 + React + Ant Design 4.0 搭建开发环境
从零配置 Eslint + Prettier + husky + lint-staged 构建前端代码工作流