一篇非常 Nice 的 UmiJS 教程
前言
網(wǎng)上的umi教程是真的少,很多人都只寫了一點(diǎn)點(diǎn),很多水文,所以打算自己寫一篇,自己搭建umi,并封裝了一下常用的功能,并用到公司實(shí)際項(xiàng)目中.
umi介紹
Umi 是什么?
Umi,中文可發(fā)音為烏米,是可擴(kuò)展的企業(yè)級前端應(yīng)用框架。Umi 以路由為基礎(chǔ)的,同時(shí)支持配置式路由和約定式路由,保證路由的功能完備,并以此進(jìn)行功能擴(kuò)展。然后配以生命周期完善的插件體系,覆蓋從源碼到構(gòu)建產(chǎn)物的每個(gè)生命周期,支持各種功能擴(kuò)展和業(yè)務(wù)需求。
Umi 是螞蟻金服的底層前端框架,已直接或間接地服務(wù)了 3000+ 應(yīng)用,包括 java、node、H5 無線、離線(Hybrid)應(yīng)用、純前端 assets 應(yīng)用、CMS 應(yīng)用等。他已經(jīng)很好地服務(wù)了我們的內(nèi)部用戶,同時(shí)希望他也能服務(wù)好外部用戶。
它主要具備以下功能:
- 🎉 可擴(kuò)展,Umi 實(shí)現(xiàn)了完整的生命周期,并使其插件化,Umi 內(nèi)部功能也全由插件完成。此外還支持插件和插件集,以滿足功能和垂直域的分層需求。
- 📦 開箱即用,Umi 內(nèi)置了路由、構(gòu)建、部署、測試等,僅需一個(gè)依賴即可上手開發(fā)。并且還提供針對 React 的集成插件集,內(nèi)涵豐富的功能,可滿足日常 80% 的開發(fā)需求。
- 🐠 企業(yè)級,經(jīng)螞蟻內(nèi)部 3000+ 項(xiàng)目以及阿里、優(yōu)酷、網(wǎng)易、飛豬、口碑等公司項(xiàng)目的驗(yàn)證,值得信賴。
- 🚀 大量自研,包含微前端、組件打包、文檔工具、請求庫、hooks 庫、數(shù)據(jù)流等,滿足日常項(xiàng)目的周邊需求。
- 🌴 完備路由,同時(shí)支持配置式路由和約定式路由,同時(shí)保持功能的完備性,比如動(dòng)態(tài)路由、嵌套路由、權(quán)限路由等等。
- 🚄 面向未來,在滿足需求的同時(shí),我們也不會(huì)停止對新技術(shù)的探索。比如 dll 提速、modern mode、webpack@5、自動(dòng)化 external、bundler less 等等。
什么時(shí)候不用 umi?
如果你,
- 需要支持 IE 8 或更低版本的瀏覽器
- 需要支持 React 16.8.0 以下的 React
- 需要跑在 Node 10 以下的環(huán)境中
- 有很強(qiáng)的 webpack 自定義需求和主觀意愿
- 需要選擇不同的路由方案
Umi 可能不適合你。
為什么不是?
create-react-app
create-react-app 是基于 webpack 的打包層方案,包含 build、dev、lint 等,他在打包層把體驗(yàn)做到了極致,但是不包含路由,不是框架,也不支持配置。所以,如果大家想基于他修改部分配置,或者希望在打包層之外也做技術(shù)收斂時(shí),就會(huì)遇到困難。
next.js
next.js 是個(gè)很好的選擇,Umi 很多功能是參考 next.js 做的。要說有哪些地方不如 Umi,我覺得可能是不夠貼近業(yè)務(wù),不夠接地氣。比如 antd、dva 的深度整合,比如國際化、權(quán)限、數(shù)據(jù)流、配置式路由、補(bǔ)丁方案、自動(dòng)化 external 方面等等一線開發(fā)者才會(huì)遇到的問題。
umi3項(xiàng)目初始化
環(huán)境準(zhǔn)備
首先得有 node,并確保 node 版本是 10.13 或以上。
推薦使用 yarn 管理 npm 依賴
本項(xiàng)目使用的版本為 node v14.17.5 yarn 1.22.15
腳手架
桌面新建umi3文件夾, 用vscode打開, 打開vscode終端,
執(zhí)行 yarn create @umijs/umi-app 創(chuàng)建項(xiàng)目
安裝依賴 yarn
啟動(dòng)項(xiàng)目 yarn start
配置 prettier,eslint, stylelint
umi 維護(hù)了一個(gè) prettier,eslint,stylelint 的配置文件合集 umi-fabric
- yarn add @umijs/fabric -D
根目錄新建下面三個(gè)文件,刪除.prettierrc文件
.eslintrc.js、.prettierrc.js、.stylelintrc.js
配置如下
- //.eslintrc.js 配置
- module.exports = {
- extends: [require.resolve('@umijs/fabric/dist/eslint')],
- // in antd-design-pro
- globals: {
- ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
- page: true,
- },
- rules: {
- // your rules
- 'prefer-const': 0,
- },
- };
- //.prettierrc.js 配置
- const fabric = require('@umijs/fabric')
- module.exports = {
- ...fabric.prettier,
- semi: false,
- }
- //.stylelintrc.js 配置
- const fabric = require('@umijs/fabric')
- module.exports = {
- ...fabric.stylelint,
- }
根目錄新建eslint忽略文件 .eslintignore
- .eslintrc.js
- node_modules
在package.json 里面的lint-staged 新增 "eslint --fix"
最后你的 vscode 要安裝這三個(gè)同名擴(kuò)展插件,這時(shí)候分別去更改 js、less 文件,會(huì)發(fā)現(xiàn)已經(jīng)有風(fēng)格校驗(yàn)了。
驗(yàn)證
修改src/paegs文件夾下的index.tsx文件,新增一個(gè)a變量,有eslint錯(cuò)誤提示,說明eslint生效了
然后再單獨(dú)提交index.tsx這一個(gè)文件
會(huì)提示有錯(cuò)誤,無法提交,說明pre-commmit 鉤子生效
保存時(shí)自動(dòng)格式化代碼
在vscode設(shè)置 文本編輯器的格式化里面 勾選Format on Save
我的eslint,或者prettier 不生效?
去到終端里的輸出,找到eslint或者prettier 看他們的輸出日志,是否正常。如果有報(bào)錯(cuò),根據(jù)報(bào)錯(cuò)信息處理問題
檢查步驟:
- 確保安裝umi-fabric
- 檢查配置文件是否存在
- vscode 得eslint 和prettier 插件是否下載
- 確認(rèn)輸出日志,是否有報(bào)錯(cuò)
pre-commit時(shí)的lint-staged 不生效
在package.json中 我們配置了如下得代碼
意思是 在代碼commit之前 執(zhí)行prettier格式化代碼和eslint fix 如果你在提交代碼的時(shí)候沒有生效,請執(zhí)行
- yarn install --force
執(zhí)行這個(gè)命令重新拉取依賴
不生效的原因?
自己剛開始也是各種google,查看文檔,也沒有找出原因,最后在umi2的一個(gè)issue里面,自己找到了答案。
原因在于我們初始化git倉庫的順序,如果我們先初始化git倉庫 然后再創(chuàng)建項(xiàng)目,再拉取依賴。是沒有任何問題的。
如果我們先創(chuàng)建了umi項(xiàng)目,拉去依賴,最后初始化git,提交代碼到git倉庫,當(dāng)我們拉去依賴時(shí), 這是就還沒有.git 就沒有生成相關(guān)的pre-commit,所以就沒有生效。所以這時(shí)我們就只需要在重新拉取下依賴就可以了。
配置css初始化代碼
為什么要初始化css
建站老手都知道,這是為了考慮到瀏覽器的兼容問題,其實(shí)不同瀏覽器對有些標(biāo)簽的默認(rèn)值是不同的,如果沒對CSS初始化往往會(huì)出現(xiàn)瀏覽器之間的頁面差異。當(dāng)然,初始化樣式會(huì)對SEO有一定的影響,但魚和熊掌不可兼得,但力求影響最小的情況下初始化。
最簡單的初始化方法就是:* {padding: 0; margin: 0;} 。有很多人也是這樣寫的。這確實(shí)很簡單,但有人就會(huì)感到疑問:*號這樣一個(gè)通用符在編寫代碼的時(shí)候是快,但如果網(wǎng)站很大,CSS樣式表文件很大,這樣寫的話,他會(huì)把所有的標(biāo)簽都初始化一遍,這樣就大大的加強(qiáng)了網(wǎng)站運(yùn)行的負(fù)載,會(huì)使網(wǎng)站加載的時(shí)候需要很長一段時(shí)間。
CSS初始化是指重設(shè)瀏覽器的樣式。不同的瀏覽器默認(rèn)的樣式可能不盡相同,所以開發(fā)時(shí)的第一件事可能就是如何把它們統(tǒng)一。如果沒對CSS初始化往往會(huì)出現(xiàn)瀏覽器之間的頁面差異。每次新開發(fā)網(wǎng)站或新網(wǎng)頁時(shí)候通過初始化CSS樣式的屬性,為我們將用到的CSS或html標(biāo)簽更加方便準(zhǔn)確,使得我們開發(fā)網(wǎng)頁內(nèi)容時(shí)更加方便簡潔,同時(shí)減少CSS代碼量,節(jié)約網(wǎng)頁下載時(shí)間。
Umi 中約定 src/global.css 為全局樣式,如果存在此文件,會(huì)被自動(dòng)引入到入口文件最前面。
src下面新建global.css,代碼如下
- body,
- ol,
- ul,
- h1,
- h2,
- h3,
- h4,
- h5,
- h6,
- p,
- th,
- td,
- dl,
- dd,
- form,
- fieldset,
- legend,
- input,
- textarea,
- select,
- figure,
- figcaption {
- margin: 0;
- padding: 0;
- }
- li {
- list-style-type: none;
- }
- a {
- text-decoration: none;
- }
- a:hover {
- text-decoration: underline;
- }
- img {
- border: none;
- }
- input {
- outline: none;
- }
配置文件
Umi 在 .umirc.ts 或 config/config.ts 中配置項(xiàng)目和插件,支持 es6。
如果項(xiàng)目的配置不復(fù)雜,推薦在 .umirc.ts 中寫配置;如果項(xiàng)目的配置比較復(fù)雜,可以將配置寫在 config/config.ts 中,并把配置的一部分拆分出去,比如路由配置可以拆分成單獨(dú)的 routes.ts
推薦兩種配置方式二選一,.umirc.ts 優(yōu)先級更高。
我們采用config的方式,刪除.umirc.ts,根目錄新建config文件夾, 在里面新建config.ts
默認(rèn)內(nèi)容如下
- import { defineConfig } from 'umi';
- export default defineConfig({
- nodeModulesTransform: {
- type: 'none',
- },
- routes: [
- { path: '/', component: '@/pages/index' },
- ],
- fastRefresh: {},
- });
多環(huán)境多配置文件
可以通過環(huán)境變量 UMI_ENV 區(qū)分不同環(huán)境來指定配置。
為了兼容性,可借助三方工具 cross-env來設(shè)置環(huán)境變量
- yarn add cross-env --dev
在package.json中的script中
- "start": "cross-env UMI_ENV=dev umi dev",
- "start:test": "cross-env UMI_ENV=test umi dev",
- "start:prd": "cross-env UMI_ENV=prd umi dev",
- "build": "cross-env UMI_ENV=dev umi build",
- "build:test": "cross-env UMI_ENV=test umi build",
- "build:prd": "cross-env UMI_ENV=prd umi build",
然后再config文件夾下 新建
config.dev.ts,config.test.ts,config.prd.ts
代表開發(fā)環(huán)境,測試環(huán)境,生產(chǎn)環(huán)境的配置文件.
config.dev.ts
- import { defineConfig } from 'umi';
- export default defineConfig({
- define: {
- CurrentEnvironment: 'dev',
- },
- });
config.test.ts
- import { defineConfig } from 'umi';
- export default defineConfig({
- define: {
- CurrentEnvironment: 'test',
- },
- });
config.prd.ts
- import { defineConfig } from 'umi';
- export default defineConfig({
- define: {
- CurrentEnvironment: 'prd',
- },
- });
CurrentEnvironment 變量代表當(dāng)前的環(huán)境,后面根據(jù)不同的環(huán)境配置不同的請求地址會(huì)用到
用于提供給代碼中可用的變量,定義的變量可以全局拿到
這時(shí) 執(zhí)行 yarn start:prd,然后去到pages的index.tsx打印CurrentEnvironment.
這時(shí)需要去到根目錄的 typings.d.ts 添加
- // 聲明當(dāng)前的環(huán)境
- declare const CurrentEnvironment: 'dev' | 'test' | 'prd';
然后報(bào)錯(cuò)消失 控制臺(tái)打印如下
這時(shí) 重新執(zhí)行yanr start:test 控制臺(tái)打印如下
環(huán)境變量和多環(huán)境多配置 成功
**注意點(diǎn)**:
config.ts作為配置文件時(shí),記得刪除.umirc.ts 不然config.ts不會(huì)生效。
自定義環(huán)境變量
如果我們想自定義一個(gè)環(huán)境變量,REACT_APP_ENV. 同樣我們可以在package.json里面設(shè)置
然后我們要這樣拿到這個(gè)變量呢?
首先 我們要在config.ts 的 define 配置
- define: {
- REACT_APP_ENV: process.env.REACT_APP_ENV,
- },
然后再在根目錄的 typings.d.ts 定義
- declare const REACT_APP_ENV: string;
這樣就可以以在全局中拿到和使用 REACT_APP_ENV這個(gè)環(huán)境變量了.
可以在任意組件 直接打印
- console.log('自定義環(huán)境變量', REACT_APP_ENV);
系統(tǒng)自帶的環(huán)境變量
官方提供的環(huán)境變量
怎么使用?
在根目錄新建.env 環(huán)境變量配置文件
然后寫入
- PORT=3000 // 表示啟動(dòng)的端口號為3000
- COMPRESS = none // 不壓縮 CSS 和 JS
還有一些環(huán)境變量 不能配在 .env 中,只能在命令行里添加
比如 FORK_TS_CHECKER 默認(rèn)不開啟 TypeScript 類型檢查,值為 1 時(shí)啟用。
- "start": "cross-env FORK_TS_CHECKER=1 UMI_ENV=dev umi dev",
請求的封裝
src文件夾下新建 request文件夾 新建request.ts
request.ts
- /**
- * 網(wǎng)絡(luò)請求工具 封裝umi-request
- * 更詳細(xì)的 api 文檔: https://github.com/umijs/umi-request
- */
- import { extend } from 'umi-request';
- import type { RequestOptionsInit } from 'umi-request';
- import { notification } from 'antd';
- // codeMessage僅供參考 具體根據(jù)和后端協(xié)商,在詳細(xì)定義.
- const codeMessage = {
- 200: '服務(wù)器成功返回請求的數(shù)據(jù)。',
- 400: '發(fā)出的請求有錯(cuò)誤,服務(wù)器沒有進(jìn)行新建或修改數(shù)據(jù)的操作。',
- 500: '服務(wù)器發(fā)生錯(cuò)誤,請檢查服務(wù)器。',
- };
- type mapCode = 200 | 400 | 500;
- /**
- * 錯(cuò)誤異常處理程序
- */
- const errorHandler = (error: { response: Response }): Response => {
- const { response } = error;
- if (response && response.status) {
- let errorText = codeMessage[response.status as mapCode] || response.statusText;
- const { status, url } = response;
- response
- ?.clone()
- ?.json()
- ?.then((res) => {
- // 后端返回錯(cuò)誤信息,就用后端傳回的
- errorText = res.msg ? res.msg : errorText;
- notification.error({
- message: `請求錯(cuò)誤 ${status}: ${url}`,
- description: errorText,
- });
- });
- } else if (!response) {
- notification.error({
- description: '您的網(wǎng)絡(luò)發(fā)生異常,無法連接服務(wù)器',
- message: '網(wǎng)絡(luò)異常',
- });
- }
- return response;
- };
- /**
- * 配置request請求時(shí)的默認(rèn)參數(shù)
- */
- const request = extend({
- errorHandler, // 默認(rèn)錯(cuò)誤處理
- credentials: 'include', // 默認(rèn)請求是否帶上cookie
- });
- // 根據(jù)不同的開發(fā)環(huán)境,配置請求前綴
- interface ApiPrefix {
- dev: string;
- test: string;
- prd: string;
- }
- const apiPreFix: ApiPrefix = {
- dev: 'http://120.55.193.14:3030/',
- test: 'http://120.55.193.14:3030/',
- prd: 'http://120.55.193.14:3030/',
- };
- // request攔截器, 攜帶token,以及根據(jù)環(huán)境,配置不同的請求前綴
- request.interceptors.request.use((url: string, options: RequestOptionsInit) => {
- // 不攜帶token的請求數(shù)組
- let notCarryTokenArr: string[] = [];
- if (notCarryTokenArr.includes(url)) {
- return {
- url: `${apiPreFix[CurrentEnvironment]}${url}`,
- options,
- };
- }
- // 給每個(gè)請求帶上token
- let token = localStorage.getItem('tokens') || '';
- let headers = {
- Authorization: `Bearer ${token}`,
- };
- return {
- url: `${apiPreFix[CurrentEnvironment]}${url}`,
- options: { ...options, interceptors: true, headers },
- };
- });
- /**
- * @url 請求的url
- * @parameter 上傳的參數(shù)
- */
- // 封裝的get,post.put,delete請求
- const get = async (url: string, parameter?: Record<string, unknown>): Promise<any> => {
- try {
- const res = await request(url, { method: 'get', params: parameter });
- return res;
- } catch (error) {
- console.error(error);
- }
- };
- const deletes = async (url: string, parameter?: Record<string, unknown>): Promise<any> => {
- try {
- const res = await request(url, { method: 'delete', params: parameter });
- return res;
- } catch (error) {
- console.error(error);
- }
- };
- const post = async (url: string, parameter?: Record<string, unknown>): Promise<any> => {
- try {
- const res = await request(url, { method: 'post', data: parameter });
- return res;
- } catch (error) {
- console.error(error);
- }
- };
- const put = async (url: string, parameter?: Record<string, unknown>): Promise<any> => {
- try {
- const res = await request(url, { method: 'put', data: parameter });
- return res;
- } catch (error) {
- console.error(error);
- }
- };
- export default {
- get,
- post,
- put,
- deletes,
- };
這里封裝了umi-request,統(tǒng)一處理了接口錯(cuò)誤,請求攔截器攜帶token等.最后在配合useRequest 非常的好用.
umi中使用dva
介紹
包含以下功能,
- 內(nèi)置 dva,默認(rèn)版本是 ^2.6.0-beta.20,如果項(xiàng)目中有依賴,會(huì)優(yōu)先使用項(xiàng)目中依賴的版本。
- 約定式的 model 組織方式,不用手動(dòng)注冊 model
- 文件名即 namespace,model 內(nèi)如果沒有聲明 namespace,會(huì)以文件名作為 namespace
- 內(nèi)置 dva-loading,直接 connect loading 字段使用即可
- 支持 immer,通過配置 immer 開啟
約定式的 model 組織方式
符合以下規(guī)則的文件會(huì)被認(rèn)為是 model 文件,
- src/models 下的文件
- src/pages 下,子目錄中 models 目錄下的文件
- src/pages 下,所有 model.ts 文件(不區(qū)分任何字母大小寫)
實(shí)際使用
比如在src下新建 models文件夾,里面新建test.ts
test.ts
- import type { Effect, Reducer, Subscription } from 'umi'; // 映入umi 定義好的ts類型
- import axios from '../request/request'; // 引入封裝好的網(wǎng)絡(luò)請求
- // state 接口
- export interface TextModelState {
- name?: string;
- testData?: string;
- }
- // test model接口
- export interface TextModelType {
- namespace: 'testModel';
- state: TextModelState;
- effects: {
- query: Effect;
- };
- reducers: {
- save: Reducer<TextModelState>;
- msg: Reducer<TextModelState>;
- };
- subscriptions?: { setup: Subscription };
- }
- const IndexModel: TextModelType = {
- namespace: 'testModel',
- state: {
- name: '初始名字',
- testData: '初始testData',
- },
- effects: {
- *query(action, { call, put }) {
- const getDataTest = async () => {
- const data = await axios.get('test');
- return data;
- };
- let testData = yield call(getDataTest);
- yield put({
- type: 'msg',
- data: { testData: testData?.msg },
- });
- },
- },
- reducers: {
- save(state) {
- return {
- ...state,
- name: 'jimmy',
- };
- },
- msg(state, action) {
- return {
- ...state,
- testData: action?.data?.testData,
- testData2: action?.data?.testData2,
- };
- },
- },
- };
- export default IndexModel;
在src/pages下的index.tsx中使用
index.tsx
- import type { Effect, Reducer, Subscription } from 'umi'; // 引入umi 定義好的ts類型
- import axios from '../request/request'; // 引入封裝好的網(wǎng)絡(luò)請求
- // state 接口
- export interface TextModelState {
- name?: string;
- testData?: string;
- }
- // test model接口
- export interface TextModelType {
- namespace: 'testModel';
- state: TextModelState;
- effects: {
- query: Effect;
- };
- reducers: {
- save: Reducer<TextModelState>;
- msg: Reducer<TextModelState>;
- };
- subscriptions?: { setup: Subscription };
- }
- const IndexModel: TextModelType = {
- namespace: 'testModel',
- state: {
- name: '初始名字',
- testData: '初始testData',
- },
- effects: {
- *query(action, { call, put }) {
- const getDataTest = async () => {
- const data = await axios.get('test');
- return data;
- };
- let testData = yield call(getDataTest);
- yield put({
- type: 'msg',
- data: { testData: testData?.msg },
- });
- },
- },
- reducers: {
- save(state) {
- return {
- ...state,
- name: 'jimmy',
- };
- },
- msg(state, action) {
- return {
- ...state,
- testData: action?.data?.testData,
- testData2: action?.data?.testData2,
- };
- },
- },
- };
- export default IndexModel;
mfsu
啟用 mfsu 后,熱啟動(dòng)得到 **10 倍** 提升;熱更新提升 **50%** 以上!
如何啟用
在 config/config.ts 中添加 mfsu:{}
項(xiàng)目源代碼
請點(diǎn)擊我
和兩個(gè)小伙伴一起,會(huì)根據(jù)實(shí)際運(yùn)用中出現(xiàn)的問題或者沒有考慮完善的地方,持續(xù)的更新迭代.如有問題,歡迎提Issue或者在評論區(qū)留言
FAQ
umi 不是內(nèi)部或外部命令
解決辦法
執(zhí)行 yarn global bin 拿到 bin 路徑。然后把這個(gè)路徑添加到環(huán)境變量里面的系統(tǒng)變量的path里面
如果還是不行,執(zhí)行
- yarn global add umi
如遇到更多問題,請查考
官方FAQ
官方倉庫的issue