React
Create react app
官網:https://create-react-app.dev/docs/getting-started
中文:https://www.html.cn/create-react-app/docs/getting-started/
創建項目
創建ts項目:
npx create-react-app my-app --template typescript
ant design + ts:
參考:https://ant.design/docs/react/use-with-create-react-app-cn
yarn create react-app antd-demo-ts –-template typescript
npx create-react-app antd-demo-ts –typescript
yarn add antd
npm run start
- 修改
src/App.tsx
,引入 antd 的按鈕組件。
import { Button } from 'antd';
<Button type="primary">Button</Button>
- 修改
src/App.css
,在文件頂部引入antd/dist/antd.css
。
@import '~antd/dist/antd.css';
使用scss
安裝node-sass就可以在項目直接使用:
yarn add node-sass
npm install node-sass –save
使用less(雖然可用,但存在問題,暫時找不方案)
初始化的項目不支持less,,不像scss,需要修改配置文件;先安裝less插件:
yarn add less less-loader
暴露配置文件:
npm run eject
如果報錯:Remove untracked files, stash or commit any changes, and try again.
那么先提交:
修改配置文件:
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;
{
test: lessRegex,
exclude: lessModuleRegex,
use: getStyleLoaders({ importLoaders: 2 }, 'less-loader'),
},
{
test: lessModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
},
'less-loader'
),
},
添加.prettierrc.json
別名配置
- 安裝 react-app-rewired:
npm install -S react-app-rewired
- package.json文件中的腳本替換成如下:
- 創建config-overrides.js
- 配置tsconfig.json
3.
"scripts": {
4.
"start": "react-app-rewired start",
5.
"build": "react-app-rewired build",
6.
"test": "react-app-rewired test",
7.
"eject": "react-app-rewired eject"
8. }
const path = require('path');
module.exports = function override(config) {
config.resolve = {
...config.resolve,
alias: {
...config.alias,
'@': path.resolve(__dirname, 'src'),
},
};
return config;
};
"paths": {
"@/*": ["./src/*"],
}
添加路由
yarn add react-router-dom
安裝的是react-router-dom6 版本,與之前的舊版本用法很大區別參考:https://www.jianshu.com/p/7c777d5cd476
使用:
import { HashRouter, Routes, Route } from "react-router-dom";
const App: React.FC = () => {
return (
<HashRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="/" element={<Flow />} />
<Route path="/flow" element={<Flow />} />
<Route path="/matrix" element={<Matrix />} />
</Route>
</Routes>
</HashRouter>
);
};
export default App;
子路由與跳頁面:
import { Outlet, useNavigate } from "react-router-dom";
return (
<div>
<Button onClick={() => { navigate("/flow") }}>跳頁面</Button>
<Outlet /> // 子路由
</div>
);
懶加載:
import {lazy, Suspense} from 'react';
const Matrix = lazy(() => import('@/pages/Matrix'));
const Flow = lazy(() => import('@/pages/Flow'));
<Route
path="/flow"
element={<Suspense fallback={<Loading />}><Flow /></Suspense>}
/>
<Route
path="/matrix"
element={<Suspense fallback={<Loading />}><Matrix /></Suspense>}
/>
暴露配置文件的配置方式
npm run eject 需要全部提交暫存文件,才可以執行
按需加載ant design 樣式
yarn add babel-plugin-import –D
package.json:
"plugins": [
[
"import",
{ "libraryName": "antd", "style": "css" }
]
]
定制主題
圖1圈住代碼:
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;
圖2圈住代碼:
if (preProcessor === "less-loader") {
loaders.push(
{
loader: require.resolve("resolve-url-loader"),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
{
loader: require.resolve(preProcessor),
options: {
lessOptions: {
sourceMap: true,
modifyVars: {
"@primary-color": "red",
},
javascriptEnabled: true,
},
},
}
);
} else if (preProcessor) {
// .....
}
圖3圈住代碼:
{
test: lessRegex,
exclude: lessModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
"less-loader"
),
sideEffects: true,
},
{
test: lessModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: { getLocalIdent: getCSSModuleLocalIdent },
},
"less-loader"
),
},
問題:樣式變量不能使用rem
報錯:
別名配置
"@": path.resolve(__dirname, "../src"),
"paths": { "@/*": ["./src/*"] }
Ant Design Pro
初始化項目
yarn create umi umi-app
npx create-umi myapp
cd umi-app && yarn
啟動:npm run start
問題1:如果使用npm run dev 啟動,會登錄不上
解決:使用npm run start
問題2:初始化項目后,不知道為什么import react from ‘react’ 報錯:找不到模塊“react”或其相應的類型聲明
解決:重新打開vscode編輯器就沒有
使用mock數據
官網:https://umijs.org/zh-CN/config#mock
配置完成,保存后,會自動生成數據:
禁用:
mock: false
也可以通過環境變量臨時關閉:
MOCK=none umi dev
刪除國際化
1: npm run i18n-remove
2: 刪除locales文件夾
刪除用例測試
刪除:根目錄下的tests文件夾
刪除:\src\e2e文件夾
刪除:配置文件:jest.config.js
刪除:下面配置
設置瀏覽器title
問題
- 如果1設置title: false,后那么3路由title設置也會無效
- 如果使用了plugin-layout插件, 那么只能用插件來設置title, 1、3設置都會失效,如果2沒設置,那么會使用默認值 ant-design-pro
- 使用了plugin-layout插件,同時設置了1或者3,那title會閃爍,先變1/3,在變2;
- 如果左側有菜單,ttitle的表現形式是 “菜單名稱”+ “layout設置的title”
解決
https://beta-pro.ant.design/docs/title-landing-cn
ProLayout 會根據菜單和路徑來自動匹配瀏覽器的標題。可以設置 pageTitleRender=false 來關掉它。
- 如果項目由此至終都只需要一個title,那么可以這樣設置:
- 如果需要根據路由來顯示title,那么可以這樣設置:
保留1的配置,然后各自在路由上設置title:
todo: 有個bug,就是在登錄界面登進去,會顯示config.js 上的title,刷新后才會顯示路由設置的title, 可以讓它們保持一致。
3. 果不設置pageTitleRender: false,ttitle的表現形式是 “菜單名稱”+ “layout設置的title”; pageTitleRender 可以是一個方法,返回字符串,就是瀏覽器的title,只是在瀏覽器刷新時候生效,切換頁面,會被路由的title或者 config.ts 設置的title 覆蓋。
4.
當您想從 React 組件更改標題時,可以使用第三方庫 React Helmet。react-helmet
https://www.npmjs.com/package/react-helmet
修改加載頁
首次進入的加載
js 還沒加載成功,但是 html 已經加載成功的 landing 頁面:src\pages\document.ejs
使用了 home_bg.png
,pro_icon.svg
和 KDpgvguMpGfqaHPjicRK.svg
三個帶有品牌信息的圖片,你可以按需修改他們。
切換頁面加載
項目中打開了代碼分割的話,在每次路由切換的時候都會進入一個加載頁面。
dynamicImport: {
loading: '@ant-design/pro-layout/es/PageLoading',
}
業務中的加載
等待用戶信息或者鑒權系統的請求完成后才能展示頁面。 getInitialState
支持了異步請求,同時在請求時會停止頁面的渲染。這種情況下加載頁的。我們可以在 src\app.tsx
中配置:
/** 獲取用戶信息比較慢的時候會展示一個 loading */
export const initialStateConfig = {
loading: <PageLoading />,
};
插件
文檔:https://umijs.org/zh-CN/docs/plugin
全局數據
插件:https://umijs.org/zh-CN/plugins/plugin-initial-state
有 src/app.ts
並且導出 getInitialState
方法時啟用
本插件不可直接使用,必須搭配 @umijs/plugin-model
一起使用。
getInitialState
使用插件plugin-initial-state, 項目啟動會先在app.tsx 執行getInitialState方法,是async,可以執行異步請求;返回數據后才會加載路由頁面,數據可以全局使用。
代碼模板:
export async function getInitialState(): Promise<{
loading?: boolean;
currentUser?: API.CurrentUser;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
await Promise.resolve('')
return {
loading: false,
currentUser: {}
};
}
獲取數據:
import { useModel } from 'umi';
const { initialState } = useModel('@@initialState');
console.log(initialState?.currentUser);
initialStateConfig
initialStateConfig 是 getInitialState 的補充配置,getInitialState 支持異步的設置,在初始化沒有完成之前我們展示了一個 loading,initialStateConfig 可以配置這個 loading。
import { PageLoading } from '@ant-design/pro-layout';
/** 獲取用戶信息比較慢的時候會展示一個 loading */
export const initialStateConfig = {
loading: <PageLoading />,
};
布局
使用插件:plugin-layout
插件文檔:https://umijs.org/zh-CN/plugins/plugin-layout
配置文檔:https://procomponents.ant.design/components/layout/
運行時配置布局:
childrenRender
這是文檔找不到的配置,可以在每一個路由頁面添加點東西:
權限
有 src/access.ts
時啟用。約定了 src/access.ts
為我們的權限定義文件,需要默認導出一個方法,導出的方法會在項目初始化時被執行。該方法需要返回一個對象,對象的每一個值就對應定義了一條權限。如下所示:
initialState
是通過初始化狀態插件 @umijs/plugin-initial-state
提供的數據,你可以使用該數據來初始化你的用戶權限。
useAccess
我們提供了一個 Hooks 用於在組件中獲取權限相關信息,如下所示:
import { useAccess } from 'umi';
const access = useAccess();
if (access.canReadFoo) { }
Access
組件 <Access />
對應用進行權限控制, 支持的屬性如下:
accessible: Type: boolean 是否有權限,通常通過 useAccess 獲取后傳入進來。
fallback: Type: React.ReactNode無權限時的顯示,默認無權限不顯示任何內容。
children: Type: React.ReactNode有權限時的顯示。
import { useAccess, Access } from 'umi';
const access = useAccess();
<Access
accessible={access.canReadFoo}
fallback={<div>無權限顯示</div>}>
有權限顯示
</Access>
菜單/路由
type RouteType = {
path?: string;
component?: string | (() => any);
wrappers?: string[];
redirect?: string;
exact?: boolean;
routes?: any[];
[k: string]: any;
};
interface MenuType {
path?: string;
component?: string;
name?: string;
icon?: string;
target?: string;
headerRender?: boolean;
footerRender?: boolean;
menuRender?: boolean;
menuHeaderRender?: boolean;
access?: string;
hideChildrenInMenu?: boolean;
hideInMenu?: boolean;
hideInBreadcrumb?: boolean;
flatMenu?: boolean;
}
type RoutersType = (RouteType & MenuType)[];
菜單
菜單可以根據routes.ts
自動生成,參考:
下面是routes配置中,關於菜單的配置說明:
name
- name:string 配置菜單的 name,不配置,不會顯示菜單,配置了國際化,name 為國際化的 key。
- icon:string 配置菜單的圖標,默認使用 antd 的 icon 名,默認不適用二級菜單的 icon。
- access:string 權限配置,需要預先配置權限
- hideChildrenInMenu:true 用於隱藏不需要在菜單中展示的子路由。
- layout:false 隱藏布局
- hideInMenu:true 可以在菜單中不展示這個路由,包括子路由。
- hideInBreadcrumb:true 可以在面包屑中不展示這個路由,包括子路由。
- headerRender:false 當前路由不展示頂欄
- footerRender:false 當前路由不展示頁腳
- menuRender: false 當前路由不展示菜單
- menuHeaderRender: false 當前路由不展示菜單頂欄
- flatMenu 子項往上提,只是不展示父菜單
Icon
access
hideChildrenInMenu
layout
hideInMenu
hideInBreadcrumb
headerRender
footerRender
menuRender
menuHeaderRender
flatMenu
路由
文檔:https://umijs.org/zh-CN/docs/routing
配置文件中通過 routes
進行配置,格式為路由信息的數組。
import type { IConfigFromPlugins } from '@@/core/pluginConfig';
type RoutersType = IConfigFromPlugins['routes'];
const routers: RoutersType = [
{ exact: true, path: '/', component: 'index' },
{ exact: true, path: '/user', component: 'user' },
];
export default routers;
path
配置可以被 path-to-regexp@^1.7.0 理解的路徑通配符。
component
React 組件路徑。可以是絕對路徑,也可以是相對路徑,如果是相對路徑,會從 src/pages
開始找起。可以用 @
,也可以用 ../
。比如
component: '@/layouts/basic'
,
component: '../layouts/basic'
exact
Default: true 表示是否嚴格匹配,即 location 是否和 path 完全對應上
// url 為 /one/two 時匹配失敗
{ path: '/one', exact: true },
// url 為 /one/two 時匹配成功
{ path: '/one' },
{ path: '/one', exact: false },
routes
配置子路由,通常在需要為多個路徑增加 layout 組件時使用
{
path: '/',
component: '@/layouts/index',
routes: [
{ path: '/list', component: 'list' },
{ path: '/admin', component: 'admin' },
]
}
在 src/layouts/index
中通過 props.children
渲染子路由
export default (props) => {
return <div style={{ padding: 20 }}>{ props.children }</div>;
}
這樣,訪問 /list
和 /admin
就會帶上 src/layouts/index
這個 layout 組件
redirect
重定向,例子:
{ exact: true, path: '/', redirect: '/list' }
訪問 /
會跳轉到 /list
,並由 src/pages/list
文件進行渲染
wrappers
配置路由的高階組件封裝,比如,可以用於路由級別的權限校驗:
export default {
routes: [
{ path: '/user', component: 'user', wrappers: ['@/wrappers/auth'] },
],
};
然后在 src/wrappers/auth
中:
import { Redirect } from 'umi';
export default (props: any) => {
const isLogin = false;
if (isLogin) {
return <div>{props.children}</div>;
} else {
return <Redirect to="/login" />;
}
};
target
{
// path 支持為一個 url,必須要以 http 開頭
path: 'https://pro.ant.design/docs/getting-started-cn',
target: '_blank', // 點擊新窗口打開
name: '文檔',
}
頁面跳轉
import { history } from 'umi';
history.push('/list');
history.push('/list?a=b');
history.push({ pathname: '/list', query: { a: 'b' } });
history.goBack();
link: 只用於單頁應用的內部跳轉,如果是外部地址跳轉請使用 a 標簽
import { Link } from 'umi';
<Link to="/users">Users Page</Link>
獲取參數
import { useLocation, history } from 'umi';
const query = history.location.query;
const location = useLocation();
console.log(location.query); // 不知道為什么類型沒有提示
樣式/圖片
樣式
- 約定
src/global.css
為全局樣式,如果存在此文件,會被自動引入到入口文件最前面,可以用於覆蓋ui組件樣式。 - Umi 會自動識別 CSS Modules 的使用,你把他當做 CSS Modules 用時才是 CSS Modules。
- 內置支持 less,不支持 sass 和 stylus,但如果有需求,可以通過 chainWebpack 配置或者 umi 插件的形式支持。
圖片/svg
export default () => <img src={require('./foo.png')} />
export default () => <img src={require('@/foo.png')} />
import { ReactComponent as Logo } from './logo.svg'
<Logo width={90} height={120} />
import logoSrc from './logo.svg'
<img src={logoSrc} alt="logo" />
相對路徑引用: background: url(./foo.png);
支持別名: background: url(~@/foo.png);
Umijs api
官網:https://umijs.org/zh-CN/api
dynamic
動態加載組件。使用場景:組件體積太大,不適合直接計入 bundle 中,以免影響首屏加載速度
// AsyncHugeA.tsx
import { dynamic } from 'umi';
export default dynamic({
loader: async function () {
// 注釋 webpackChunkName:webpack 將組件HugeA以這個名字單獨拆出去
const { default: HugeA } = await import(
/* webpackChunkName: "external_A" */ './HugeA'
);
return HugeA;
}
});
// 使用:
import AsyncHugeA from './AsyncHugeA';
<AsyncHugeA />
history
獲取信息
// location 對象,包含 pathname、search 和 hash、query
console.log(history.location.pathname);
console.log(history.location.search);
console.log(history.location.hash);
console.log(history.location.query);
跳轉路由
history.push('/list');
history.push('/list?a=b');
history.push({ pathname: '/list', query: { a: 'b' } });
history.goBack();
監聽路由變化
const unlisten = history.listen((location, action) => {
console.log(location.pathname);
});
unlisten(); // 取消監聽
Link
import { Link } from 'umi';
<Link to="/courses?sort=name">Courses</Link>
<Link to={{
pathname: '/list',
search: '?sort=name',
hash: '#the-hash',
state: { fromDashboard: true },
}}>List</Link>
// 跳轉到指定 /profile 路由,附帶所有當前 location 上的參數
<Link to={ loca => {return { ... loca, pathname: '/profile' }}}/>
// 轉到指定 /courses 路由,替換當前 history stack 中的記錄
<Link to="/courses" replace />
NavLink
特殊版本的 <Link />
。當指定路由(to=
指定路由
)命中時,可以附着特定樣式。
https://umijs.org/zh-CN/api#link
Prompt
<Prompt message="你確定要離開么?" />
{/* 用戶要跳轉到首頁時,提示一個選擇 */}
<Prompt message={loc => loc.pathname !== '/' ? true : `您確定要跳轉到首頁么?`}/>
{/* 根據一個狀態來確定用戶離開頁面時是否給一個提示選擇 */}
<Prompt when={formIsHalfFilledOut} message="您確定半途而廢么?" />
withRouter
高階組件,可以通過withRouter
獲取到history
、location
、match
對象withRouter(({ history, location, match }) => {})
useHistory
hooks,獲取 history
對象
useLocation
hooks,獲取 location
對象
useParams
hooks,獲取params
對象。params
對象為動態路由(例如:/users/:id
)里的參數鍵值對。
Umijs 配置
官網:https://umijs.org/zh-CN/config#alias
proxy代理
proxy: {
'/api': {
'target': 'http://jsonplaceholder.typicode.com/',
'changeOrigin': true,
'pathRewrite': { '^/api' : '' },
}
}
訪問 /api/users
就能訪問到 http://jsonplaceholder.typicode.com/users 的數據
alias別名
export default { alias: { foo: '/tmp/a/b/foo'} };
然后import('foo')
,實際上是import('/tmp/a/b/foo')
。
Umi 內置了以下別名:
@,項目 src 目錄
@@,臨時目錄,通常是 src/.umi 目錄
umi,當前所運行的 umi 倉庫目錄
base路由前綴
Default: /
設置路由前綴,通常用於部署到非根目錄。
比如,你有路由 /
和 /users
,然后設置base為 /foo/
,那么就可以通過 /foo/
和 /foo/users
訪問到之前的路由。
publicPath
Default: /
配置 webpack 的 publicPath。當打包的時候,webpack 會在靜態文件路徑前面添加 publicPath 的值,當你需要修改靜態文件地址時,比如使用 CDN 部署,把 publicPath 的值設為 CDN 的值就可以。
如果你的應用部署在域名的子路徑上,例如 https://www.your-app.com/foo/
,你需要設置 publicPath
為 /foo/
,如果同時要兼顧開發環境正常調試,你可以這樣配置:
publicPath: process.env.NODE_ENV === 'production' ? '/foo/' : '/',
chainWebpack webpack配置
通過 webpack-chain 的 API 修改 webpack 配置。
dynamicImport
是否啟用按需加載,即是否把構建產物進行拆分,在需要的時候下載額外的 JS 再執行。關閉時,只生成一個 js 和一個 css,即 umi.js
和 umi.css
。優點是省心,部署方便;缺點是對用戶來說初次打開網站會比較慢。
包含以下子配置項: loading, 類型為字符串,指向 loading 組件文件
externals
favicon
Type: string: 配置 favicon 地址(href 屬性)。
配置:
favicon: '/ass/favicon.ico',
生成:
<link rel="shortcut icon" type="image/x-icon" href="/ass/favicon.ico" />
fastRefresh
- Type: object
快速刷新(Fast Refresh),開發時可以保持組件狀態,同時編輯提供即時反饋。
hash
links/metas/styles
配置額外的 link 標簽。
配置額外的 meta 標簽。數組中可以配置key:value形式的對象。
Default: [] 配置額外 CSS。
headScripts/scripts
headScripts: 配置 <head>
里的額外腳本,數組項為字符串或對象。
Scripts: 同 headScripts,配置 <body>
里的額外腳本。
ignoreMomentLocale
- Type: boolean
- Default: false
忽略 moment 的 locale 文件,用於減少尺寸。
mock
theme
配置主題,實際上是配 less 變量。
export default {
theme: {
'@primary-color': '#1DA57A',
},
};
Theme for antd: https://ant.design/docs/react/customize-theme-cn
title
配置標題。(設置false可以關閉)
title: '標題',
React優化
React.memo
React.memo()
是一個高階函數,它與 React.PureComponent
類似,但是一個函數組件而非一個類。
React.memo()
可接受2個參數,第一個參數為純函數的組件,第二個參數用於對比props控制是否刷新,與shouldComponentUpdate()
功能類似。
一般可以配個useCallback使用,防止使用onClick={() => { //…. }}導致子組件每次渲染
useCallback
問1:
回到剛剛例子,這次傳遞一個函數callback, 你會發現,React.memo無效:
解決:那就是使用useCallback包裹函數:
const callback = useCallback((e: any) => setnum(Math.random()), []);
修改后,你會發現和第一個例子那樣,memo包裹的,如果callback不變,只會在第一次觸發;
問2:
useCallback 第二個參數,是依賴項, 如果依賴項變化, 那么函數還是會頻繁創建, 導致React.meno包裹的組件重新渲染. 有什么方法可以保證函數地址一值不變?
官方臨時提議,使用ref, 變量重新緩存useCallback需要訪問的值:
最后抽個自定義hooks:
再優化, 每次都傳遞依賴項,太麻煩,可以優化下,不需要傳遞deps,傳遞deps目的就是為了依賴變化,重新復制當前函數,如果次次都賦值,就不需要傳遞.
阿里開源的 react hooks 工具庫 ahooks中的usePersistFn(3.x 是useMemoizedFn )就是這種思路實現不需要傳遞依賴項的。源碼:
源碼地址:
https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useMemoizedFn/index.ts
type PickFunction<T extends noop> = (
this: ThisParameterType<T>,
...args: Parameters<T>
) => ReturnType<T>;
type noop = (this: any, ...args: any[]) => any;
function useMemoizedFn<T extends noop>(fn: T) {
const fnRef = useRef<T>(fn)
fnRef.current = useMemo(() => fn, [fn])
const memoizedFn = useRef<PickFunction<T>>()
if (!memoizedFn.current) {
memoizedFn.current = function(this, ...args) {
return fnRef.current.apply(this, args)
}
}
return memoizedFn.current
}
問3:
有一個問題,如果需要傳遞額外的參數,怎么辦?例如列表循環,需要傳遞事件本身參數,還有當前的index?
為了接受子組件的參數,我們通常下面的寫法,但是你會發現,每次父組件更新,子組件都會更新,因為{ () => {//xxx} } 每次都會生新函數.
那么有什么辦法,可以做到父組件更新,只要props不變,就不影響子組件,然后還可以接受子組件傳遞的參數呢? 結果是暫時想不到,曾經以為下面寫法行,結果還是不行,這樣寫,只是徒增理解難度:
常用庫
ahooks
git: https://github.com/alibaba/hooks
文檔: https://ahooks.js.org/zh-CN/guide
useMemoizedFn
理論上,可以使用 useMemoizedFn 完全代替 useCallback。
useCallback 來記住一個函數,但是在第二個參數 deps 變化時,會重新生成函數,導致函數地址變化。
useMemoizedFn,可以省略第二個參數 deps,同時保證函數地址永遠不會變化。
const [state, setState] = useState('');
// func 地址永遠不會變化
const func = useMemoizedFn(() => {
console.log(state);
});
l 原理就是使用useRef,每次父組件更新,current都指向新的回調函數;然后再創建另一個ref,值是一個函數,函數里執行第一個ref緩存的函數. 源碼:
https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useMemoizedFn/index.ts
useSetState
用法與 class 組件的 this.setState
基本一致。意思是setStates是合並對象,而不是替換對象;
import { useSetState } from 'ahooks';
const [state, setState] = useSetState<State>({ hello: '',count: 0 });
<button onClick={() => setState({ hello: 'world' })}>set hello</button>
l 原理其實就是重寫在setState方法基礎上,重新封裝,通過setState能夠接受函數作為參數,獲得上一個props,然后合並返回,這樣就可以達到效果.
useReactive
數據狀態不需要寫useState
,直接修改屬性即可刷新視圖。
const state = useReactive({
count: 0,
inputVal: '',
obj: { value: '' }
});
<button onClick={() => state.count--}>state.count--</button>
<input onChange={(e) => (state.obj.value = e.target.value)} />
l 原理使用es6 Proxy對象,劫持對象上屬性;
l 在get的時候, 遞歸創建Proxy對象,這樣就能讓所有對象屬性都劫持;
l 在set和delete的時候, 先執行原生的邏輯,然后再強制觸發頁面的更新(useUpdate)
源碼:
https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useReactive/index.ts#L48
useUpdate
useUpdate 會返回一個函數,調用該函數會強制組件重新渲染。
import { useUpdate } from 'ahooks';
const update = useUpdate();
<button onClick={update}>update</button>
l 原就是使用useState新建一個變量,然后返回一個函數,函數的邏輯就是修改變量,強制觸發頁面更新;
源碼:
https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useUpdate/index.ts
useLockFn
用於給一個異步函數增加競態鎖,防止並發執行。
說點人話,就是點擊保存的時候,如果需要保存成功后,才能繼續保存,那么就使用它;
import { useLockFn } from 'ahooks';
const submit = useLockFn(async () => {
message.info('Start to submit');
await mockApiRequest();
setCount((val) => val + 1); // await沒有完成多次點擊無效
message.success('Submit finished');
});
<button onClick={submit}>Submit</button>
l 原理也很簡單,就是利用useRef, 創建一個標識,初始化false
l 當觸發函數,設置true,等異步執行完畢,或者異常,就重新設置false
l 標識為true,那函數就不往下執行
useThrottleFn / useDebounceFn
頻繁調用 run,但只會每隔 500ms 執行一次相關函數。
import { useThrottleFn } from 'ahooks';
const { run } = useThrottleFn(
() => setValue(value + 1),
{ wait: 500 },
);
<button onClick={run}>Click fast!</button>
useLocalStorageState/useSessionStorageState
將狀態存儲在 localStorage 中的 Hook 。
import { useLocalStorageState } from 'ahooks';
const [message, setMessage] = useLocalStorageState('storage-key1', {
defaultValue: 'Hello~'
});
const [value, setValue] = useLocalStorageState(' storage-key2', {
defaultValue: defaultArray,
});
l 可能你不需要默認的 JSON.stringify/JSON.parse
來序列化,;
l useLocalStorageState 在往 localStorage 寫入數據前,會先調用一次 serializer
在讀取數據之后,會先調用一次 deserializer
。
useUpdateEffect
useUpdateEffect
用法等同於 useEffect
,會忽略首次執行,只在依賴更新時執行
原理就是創建一個ref,首次渲染設置false, 運行的第一次設置為true;
往后就是執行正常的邏輯
useEventEmitter
多個組件之間進行事件通知;
l 通過 props
或者 Context
,可以將 event$
共享給其他組件。
l 調用 EventEmitter
的 emit
方法,推送一個事件
l 調用 useSubscription
方法,訂閱事件。
const event$ = useEventEmitter()
event$.emit('hello')
event$.useSubscription(val => {
console.log(val)
})
在組件多次渲染時,每次渲染調用 useEventEmitter
得到的返回值會保持不變,不會重復創建 EventEmitter
的實例。useSubscription
會在組件創建時自動注冊訂閱,並在組件銷毀時自動取消訂閱。
例子:
import { useEventEmitter } from 'ahooks';
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
// 父組件有2個子組件:
const focus$ = useEventEmitter();
<MessageBox focus$={focus$} />
<InputBox focus$={focus$} />
子組件1:
const InputBox: FC<{ focus$: EventEmitter<void> }> = (props) => {
props.focus$.useSubscription((‘參數’) => {});
};
子組件2:
const InputBox: FC<{ focus$: EventEmitter<void> }> = (props) => {
props.focus$.emit(‘參數’);
};
Immutable
用於保存原始對象,修改對象后,不會更新原始對象的值
GitHub: https://github.com/immutable-js/immutable-js
文檔:
Vue
Vue3常用api
defineEmits (setup
定義
emits)
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
defineProps (setup
定義
props)
withDefaults(defineProps<{
foo: string
bar?: number
msg: string
}>(), {
msg: '',
bar: 1,
foo: '000'
})
defineExpose
(setup
定義
暴露出去的屬性)
const a = 1
const b = ref(2)
defineExpose({ a, b})
useSlots , useAttrs
對應:
$slots
和$attrs
,因為在模板中可以直接訪問,所以很少使用。
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
inheritAttrs
<style module>
<template>
<p :class="$style.red">
This should be red
</p>
</template>
<style module>
.red { color: red;}
</style>
動態樣式
<script setup lang="ts">
const theme = {
color: 'red'
}
</script>
<template>
<p>hello</p>
</template>
<style scoped>
p {
color: v-bind("theme.color");
}
</style>
開發前配置
Yarn: npm I yarn -g
淘寶鏡像: npm i -g cnpm --registry=https://registry.npm.taobao.org
vscode不能使用cnpm:
右擊VSCode圖標,選擇以管理員身份運行;
在終端中執行get-ExecutionPolicy,顯示Restricted,表示狀態是禁止的;
插件
Volar
Vue3 代碼格式工具
報錯:
解決:
// tsconfig.json
{
"compilerOptions": {
"types": [
"vite/client", // if using vite
]
}
}
ESLint
官網:http://eslint.cn/docs/user-guide/configuring
安裝:yarn add -D eslint
初始化:npx eslint –init
初始化之后,自動安裝eslint-plugin-vue@latest, @typescript-eslint/eslint-plugin@latest, @typescript-eslint/parser@latest;同時,項目根目錄回出現.eslintrc.js 文件;
setup語法糖報錯:
解決:添加配置 parser: 'vue-eslint-parser',
報錯:The template root requires exactly one element.eslintvue/no-multiple-template-root意思是說模板根只需要一個元素
解決:'plugin:vue/essential' -> 'plugin:vue/vue3-essential'
extends: [
'eslint:recommended',
'plugin:vue/vue3-essential',
'plugin:@typescript-eslint/recommended',
],
配置說明:rules
l "off" 或 0 - 關閉規則
l "warn" 或 1 - 開啟規則,使用警告級別的錯誤:warn (不會導致程序退出)
l "error" 或 2 - 開啟規則,使用錯誤級別的錯誤:error (當被觸發的時候,程序會退出)
配置定義在插件中的一個規則的時候,你必須使用 插件名/規則ID 的形式:
臨時禁止規則出現警告:
/* eslint-disable */
/* eslint-disable no-alert, no-console */
.eslintrc.json配置:
{
"root": true,
"env": {
"es2021": true,
"node": true,
"browser": true
},
"globals": {
"node": true
},
"extends": [
"plugin:prettier/recommended"
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"ignorePatterns": ["types/env.d.ts", "node_modules/**", "**/dist/**"],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline": "off",
"vue/no-v-html": "off",
"space-before-blocks": "warn",
"space-before-function-paren": "error",
"space-in-parens": "warn",
"no-whitespace-before-property": "off",
"semi": ["error", "never"],
"quotes": ["warn", "single"]
}
}
EditorConfig for vs code
配置的代碼規范規則優先級高於編輯器默認的代碼格式化規則。如果我沒有配置editorconfig,執行的就是編輯器默認的代碼格式化規則;如果我已經配置了editorConfig,則按照我設置的規則來,從而忽略瀏覽器的設置。
對應配置.editorconfig:
root = true
[*]
charset = utf-8
# end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
ij_html_quote_style = double
max_line_length = 120
tab_width = 2
# 刪除行尾空格
trim_trailing_whitespace = true
Prettier - Code formatter
前端代碼格式化工具,對應.prettierrc.json配置:
官網:https://prettier.io/docs/en/options.html
以下是配置說明:
printWidth // 默認80,一行超過多少字符換行
tabWidth // 默認2,tab鍵代表2個空格
useTabs // 默認false, 用制表符而不是空格縮進行
semi // 默認true, 使用分號
singleQuote // 默認false, 使用單引號
quoteProps // 默認 as-needed
jsxSingleQuote // 默認false, 在JSX中使用單引號而不是雙引號。
trailingComma
// 默認es5: 在es5尾隨逗號(對象、數組等); ts中的類型參數中沒有尾隨逗號
// node: 不尾隨
// all: 所有都尾隨
bracketSpacing // 默認true;對象文字中括號之間的空格
bracketSameLine // 默認 false
arrowParens // 默認always;函數參數周圍包含括號,可選avoid
vueIndentScriptAndStyle
// 默認false;是否縮進Vue文件中<script>和<style>標記內的代碼
{
"printWidth": 100, // 一行超過多少字符換行
"tabWidth": 2, // tab鍵代碼2個空格
"useTabs": false, // 用制表符而不是空格縮進行
"semi": false,
"singleQuote": true,
"vueIndentScriptAndStyle": true,
"quoteProps": "as-needed",
"bracketSpacing": true,
"trailingComma": "es5",
"jsxBracketSameLine": true,
"jsxSingleQuote": false,
"arrowParens": "always",
"insertPragma": false,
"requirePragma": false,
"proseWrap": "never",
"htmlWhitespaceSensitivity": "ignore",
"endOfLine": "auto",
"rangeStart": 0
}
Chinese (Simplified) (簡體中文)
中文插件
其他包
有些插件需要一些包配合使用:
cnpm install @typescript-eslint/eslint-plugin @typescript-eslint/parser @vitejs/plugin-vue eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue prettier vue-eslint-parser -D
設置快捷鍵
自定義代碼片段
{
"v3-setup": {
"scope": "vue",
"prefix": "v3-setup",
"body": [
"<script setup lang='ts'>\nimport { ref } from 'vue'\n${1}\nwithDefaults(defineProps<{}>(), {})\n\ndefineEmits<{\n\t(e: 'change', id: number): void\n}>()\n\n</script>\n\n<template>\n</template>\n\n<style scoped>\n</style>"
]
},
"v3-getCurrentInstance": {
"scope": "javascript,typescript",
"prefix": "v3-getCurrentInstance",
"body": [
"import { getCurrentInstance } from 'vue'\n\nconst internalInstance = getCurrentInstance()\n"
]
},
"v3-computed": {
"scope": "javascript,typescript",
"prefix": "v3-computed",
"body": [
"const $1 = computed(() => {\n\treturn $2\n})"
]
},
"v3-defineEmits": {
"scope": "javascript,typescript",
"prefix": "v3-emits",
"body": [
"const ${1:emit} = defineEmits<{\n\t(e: '${2:change}'): ${3:void}\n}>()"
]
},
"v3-defineProps": {
"scope": "javascript,typescript",
"prefix": "v3-props",
"body": [
"defineProps<$0>()\n"
]
},
"l1-setTimeout": {
"scope": "javascript,typescript",
"prefix": "l1-sett",
"body": [
"const ${1:timer} = setTimeout(() => {\n\t$3\n}, ${2:60})"
]
},
"l1-map": {
"scope": "javascript,typescript",
"prefix": "l1-map",
"body": [
"${1:arr}.${2:map}((item, index) => {\n\t${3}\n})"
]
},
"l1-reduce": {
"scope": "javascript,typescript",
"prefix": "l1-reduce",
"body": [
"${1:arr}.reduce((data, cur) => {\n\t${2}\n\treturn data\n}, {})",
]
},
"l1-promise": {
"scope": "javascript,typescript",
"prefix": "l1-promise",
"body": [
"return new Promise((resolve, reject) => {\n\t${1}\n})",
]
}
}
保存自動格式化
"editor.formatOnSave": true,
創建項目
yarn create vite
cd 項目名稱
yarn
yarn dev
配置別名
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import * as path from 'path'
const resovle = (p:string) => {
return path.resolve(__dirname, p)
}
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resovle('./src')
}
}
})
如果發現引入path報錯 “找不到模塊“path”或其相應的類型聲明”
解決:cnpm install @types/node -D
還需要配置tsconfig.json:(配置完成后,會自動引入本地模塊)
"lib": ["esnext", "dom"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
引入vue-router4
安裝:cnpm install vue-router@4 -S
路由文件目錄結構:
main.ts上引用:
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
Index.ts 全局引入module下的路由,具體代碼:
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = []
const modules = import.meta.globEager('./module/*.ts')
for (const path in modules) {
routes.push(...modules[path].default)
}
const router = createRouter({
routes,
history: createWebHashHistory()
})
export default router
Vue3常用庫
vueuse
git: https://github.com/vueuse/vueuse
文檔: https://vueuse.org/functions.html#category=Watch
相關: https://juejin.cn/post/7030395303433863205
less文檔
https://less.bootcss.com/#%E6%A6%82%E8%A7%88
變量(Variables)
命名規范,參考:
https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
@width: 10px;
@height: @width + 10px;
#header {
width: @width;
height: @height;
}
混合(Mixins)
.bordered {
border-top: dotted 1px black;
border-bottom: solid 2px black;
}
#menu a { color: #111; .bordered(); }
.post a { color: red; .bordered(); }
嵌套(Nesting)
.clearfix {
display: block;
zoom: 1;
&:after {
content: " ";
display: block; font-size: 0;
height: 0; clear: both;
visibility: hidden;
}
}
(&
表示當前選擇器的父級)
css module修改UI庫樣式
使用:global
ts文檔
Required / Readonly
Required<T>
的作用就是將某個類型里的屬性全部變為必選項。
Readonly<T>
的作用是將某個類型所有屬性變為只讀屬性,也就意味着這些屬性不能被重新賦值。
Record
Partial
extends
in
typeof
keyof
Pick
Exclude
Extract
Omit
ReturnType
設置全局ts類型
Src文件夾添加typings.d.ts文件:
上面為例子,src下面所有的tsx都可以這樣使用CompTableAPI.ColumnsProps
其他
vscode設置代碼片段
window刪除文件夾以及文件
rd /s/q 文件夾
npx和npm的區別
npx 是 npm 的高級版本,npx 具有更強大的功能。
- 在項目中直接運行指令,直接運行node_modules中的某個指令,不需要輸入文件路徑
- 避免全局安裝模塊:npx 臨時安裝一個模塊,使用過后刪除這個模塊(下面的兩個模塊不需要全局安裝)
- 使用不同版本的命令,使用本地或者下載的命令
一些優秀的博客
如何面試
前端如何面試: https://juejin.cn/post/6844903509502984206
問1:
有一塊區域要展示一組數據,但數據需要請求 3 個接口才能計算得到,請問前端是怎么做的,如何優化,前端什么情況下可以放棄合並接口的要求。這個地方至少會考察到異步,本地緩存,延展下會問下並發,競態,協程等。答得好不好完全在於你的知識面的深度和廣度.
問2:
需要簡歷有故事性,比如項目背景,項目的內容,成果,你做了些什么。有沒有相關的 paper 或是開源工程。簡歷中一定要體現出你的價值。如果沒有,我一般會先問一個問題,在過去一年中你遇到的最大挑戰是什么。其實這個問題很難回答,尤其是你自己在過去的工作中沒有總結和思考的話。
1. 是否有抽象。有很多問題本身都非常小,但是否能以點及面,考慮更大的層面。比如做不同項目,有沒考慮體系建設,怎么考慮歷史庫的升級及維護;
2. 是否有向前看。對新內容的判斷,怎么使用也是考察的重點之一。尤其是為什么要用某個技術這個問題是我常問的。為了技術而技術,考慮問題的全面性就會差很多。
繼續探索的領域
前端工程化
前端微服務
前端分布式架構
低代碼平台
小程序
Vue文檔生成vuepress
Github: https://github.com/vuejs/vuepress
文檔: https://vuepress.vuejs.org/zh/guide/
React文檔生成dumi
文檔: https://d.umijs.org/zh-CN/guide
GitHub: https://github.com/umijs/dumi