前端笔记

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

 

posted @ 2022-03-14 14:32  vs1435  阅读(1718)  评论(0编辑  收藏  举报