前端笔记
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