前端筆記


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

  1. 修改 src/App.tsx,引入 antd 的按鈕組件。

import { Button } from 'antd';

<Button type="primary">Button</Button>

  1. 修改 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

 

別名配置

  1. 安裝 react-app-rewired:

npm install -S react-app-rewired

  1. package.json文件中的腳本替換成如下:
    1. 創建config-overrides.js
    2. 配置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. 如果1設置title: false,后那么3路由title設置也會無效
  2. 如果使用了plugin-layout插件, 那么只能用插件來設置title, 1、3設置都會失效,如果2沒設置,那么會使用默認值 ant-design-pro

 

  1. 使用了plugin-layout插件,同時設置了1或者3,那title會閃爍,先變1/3,在變2;
  2. 如果左側有菜單,ttitle的表現形式是 “菜單名稱”+ “layout設置的title”

 

解決

https://beta-pro.ant.design/docs/title-landing-cn

ProLayout 會根據菜單和路徑來自動匹配瀏覽器的標題。可以設置 pageTitleRender=false 來關掉它。

  1. 如果項目由此至終都只需要一個title,那么可以這樣設置:

 

  1. 如果需要根據路由來顯示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自動生成,參考:

https://pro.ant.design/zh-CN/docs/new-page#%E5%9C%A8%E8%8F%9C%E5%8D%95%E4%B8%AD%E4%BD%BF%E7%94%A8-iconfont

下面是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); // 不知道為什么類型沒有提示

樣式/圖片

樣式

  1. 約定 src/global.css 為全局樣式,如果存在此文件,會被自動引入到入口文件最前面,可以用於覆蓋ui組件樣式。
  2. Umi 會自動識別 CSS Modules 的使用,你把他當做 CSS Modules 用時才是 CSS Modules。
  3. 內置支持 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 獲取到 historylocationmatch 對象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);
});
 
原理就是使用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>
原理其實就是重寫在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)} />

原理使用es6 Proxy對象,劫持對象上屬性;

在get的時候, 遞歸創建Proxy對象,這樣就能讓所有對象屬性都劫持;

在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>
原就是使用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>

 

原理也很簡單,就是利用useRef, 創建一個標識,初始化false

當觸發函數,設置true,等異步執行完畢,或者異常,就重新設置false

標識為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,
});
 
        

 

可能你不需要默認的 JSON.stringify/JSON.parse 來序列化,;

useLocalStorageState 在往 localStorage 寫入數據前,會先調用一次 serializer在讀取數據之后,會先調用一次 deserializer

 

useUpdateEffect 

useUpdateEffect 用法等同於 useEffect,會忽略首次執行,只在依賴更新時執行

原理就是創建一個ref,首次渲染設置false, 運行的第一次設置為true;

往后就是執行正常的邏輯

 

useEventEmitter

多個組件之間進行事件通知;

通過 props 或者 Context ,可以將 event$ 共享給其他組件。

調用 EventEmitter 的 emit 方法,推送一個事件

調用 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

 

文檔:

https://blog.csdn.net/m0_37527015/article/details/84338831?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2.pc_relevant_default&utm_relevant_index=3

 

 

 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 具有更強大的功能。

  1. 在項目中直接運行指令,直接運行node_modules中的某個指令,不需要輸入文件路徑

 

  1. 避免全局安裝模塊:npx 臨時安裝一個模塊,使用過后刪除這個模塊(下面的兩個模塊不需要全局安裝)

 

  1. 使用不同版本的命令,使用本地或者下載的命令

 

一些優秀的博客

如何面試

前端如何面試: 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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM