背景
之前公司項目采用的是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 2.0 + React + Ant Design 4.0 搭建開發環境
從零配置 Eslint + Prettier + husky + lint-staged 構建前端代碼工作流