React 基础
-
通过
import React, { useState } from 'react';
来使用 useState如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19let [opt, setOpt] = useState({
page: 1,
limit: 10,
kwd: ''
})
// useState 返回一个数组,第一个为传入的值,第二个为修改第一个值的方法(dispatch),一般通过结构赋值来获得该状态和修改状态的方法。
//
setOpt({
...opt,
page: 2
})
// 也可以接受一个函数。
setOpt((oldOpt) => {
console.log(oldOpt)
return {
...opt,
page: 2
}
}) 路由
通过
import { history } from 'umi';
使用 history如
1
2
3
4
5
6
7
8history.push({
pathname: '/ratings/template-list',
query: { id: row.key },
});
// 接受
this.props.location.query.name
// 参考 https://www.cnblogs.com/houjl/p/10056718.html监听属性改变时触发副作用
通过
import { useEffect } from 'react';
来使用 useEffect1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 第二个参数传空数组 相当于 Vue 中的 Created
useEffect(() => {
console.log('第一次render之后执行!')
}, [])
// 不传第二个参数 每次 render 都会执行
useEffect(() => {
console.log('每次 render 之后执行!')
})
// 第二个参数为不为空数组时,当 n 发生变化就执行,相当于 Vue Watch 了
useEffect(() => {
console.log('每次render之后执行!')
}, [n])
//
useEffect(() => {
return function () {
console.log('组件被 componentWillUnmount 之前执行!')
}
}, [])React 中的 ref
通过
import { useRef } from 'react';
来使用 useRef,它也可以被使用为函数式组件中的 this, 因为函数式组件没有 this 来存放东西1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// 有些情况下,我们需要保证函数组件每次 render 之后,某些变量不会被重复申明,比如说 Dom 节点,定时器的 id 等等,在类组件中,我们完全可以通过给类添加一个自定义属性来保留,比如说 this.xxx, 但是函数组件没有 this,自然无法通过这种方法使用,有的朋友说,我可以用 useState 来保留变量的值,但是 useState 会触发组件 render,在这里完全是不需要的,我们就需要使用 useRef 来实现了,具体看下面例子
function App () {
const [ count, setCount ] = useState(0)
const timer = useRef(null)
let timer2
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + 1)
}, 500)
timer.current = id
timer2 = id
return () => {
clearInterval(timer.current)
}
}, [])
const onClickRef = useCallback(() => {
clearInterval(timer.current)
}, [])
const onClick = useCallback(() => {
clearInterval(timer2)
}, [])
return (
<div>
点击次数: { count }
<button onClick={onClick}>普通</button>
<button onClick={onClickRef}>useRef</button>
</div>
)
}
// 当我们们使用普通的按钮去暂停定时器时发现定时器无法清除,因为 App 组件每次 render,都会重新申明一次 timer2, 定时器的 id 在第二次 render 时,就丢失了,所以无法清除定时器,针对这种情况,就需要使用到 useRef,来为我们保留定时器 id,类似于 this.xxx,这就是 useRef 的另外一种用法。
// Ref.current 相当于 Vue 里初始化时 data() return 的对象了!! 非常关键umi.request 中,GET 请求带数组时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 参考 https://github.com/umijs/umi-request/blob/master/README_zh-CN.md
import request from '@/utils/request';
export async function getListData(params) {
return request('/api/listdata', {
params,
});
}
// 如果传入的 params 为 { array: [0, 1] } ,期望请求格式为 array[0]=0&array[1]=1, umi.request 会转换成 array=0&array=1 导致后端无法接收数据,因为它默认的序列化方式是 Qs.stringify(params, { arrayFormat: 'repeat', strictNullHandling: true })
// 可以使用 paramsSerializer 对 params 做自定义的序列化。 一般引入 qs 使用
import Qs from "qs";
import request from '@/utils/request';
export async function getListData(params) {
return request('/api/listdata', {
paramsSerializer: (params) => {
return Qs.stringify(params)
},
params,
});
}ProFormCaptcha 组件中,低版本 pro-form 会出现 bug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58// 原版
<ProFormCaptcha
fieldProps={{
size: 'large',
prefix: <MailTwoTone className={styles.prefixIcon} />,
}}
captchaProps={{
size: 'large',
}}
placeholder={intl.formatMessage({
id: 'pages.login.captcha.placeholder',
defaultMessage: '请输入验证码',
})}
captchaTextRender={(timing, count) => {
if (timing) {
return `${count} ${intl.formatMessage({
id: 'pages.getCaptchaSecondText',
defaultMessage: '获取验证码',
})}`;
}
return intl.formatMessage({
id: 'pages.login.phoneLogin.getVerificationCode',
defaultMessage: '获取验证码',
});
}}
name="captcha"
// phoneName="mobile" 加入这一行即可验证手机号。
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.captcha.required"
defaultMessage="请输入验证码!"
/>
),
},
]}
onGetCaptcha={async (mobile) => {
form.validateFields(['mobile'])
.then(res => {
console.log(res)
})
// if (!form.getFieldValue('mobile')) {
// message.error('请先输入手机号');
// return;
// }
const result = await getFakeCaptcha(mobile);
if (result === false) {
return;
}
message.success('获取验证码成功!验证码为:1234');
}}
/>
// 点击获取验证码按钮时,会出现要求验证码非空而不是手机号非空 解决方法: 升级 pro-form 至 1.11版本以上
// 升级后点击按钮不再触发 验证码的验证。而是直接通过,看了源码之后了解到,需要配置 phoneName="mobile",将会在获取验证码之前验证 'mobile' 表单字段。函数组件传值
函数组件接受一个对象,挂载着传入进来的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// Cmp
export default function Cmp(props) {
const { value } = props
return (
<div>
{value} /* 将显示 9527 */
</div>
)
}
// Father
export default function Father() {
return (
<Cmp value={9527} />
)
}Authorized 鉴权
直接去看 utils 下的 Authorized.js, 发现它引入了相同目录下的 authority.js 文件来获取权限,同时引入了
@/components/Authorized
组件,authority.js 作用是获取当前 localStorage 里的角色名称。之后把这个角色名称传入@/components/Authorized
这个组件中。来到
@/components/Authorized/index.jsx
之后发现它引入了同级目录下的 renderAuthorize.js 和 Authorized.jsx,Authorized 是一个组件,接收 children, authority, noMatch 三个参数,里面通过引入同级目录下的 checkPermissions 来得到需要渲染的 dom,调用方式为 check(authority, childrenRender, noMatch),参数一为通过渲染所需的权限(即在 router 中定义的组件的 authority 角色权限数组),参数二为权限通过时渲染的组件,参数三为权限不通过时渲染的组件。调用 check 时,会调用 checkPermissions(authority, CURRENT, target, Exception),中间多了一个 CURRENT 参数,这个参数就是从当前 localStorage 中读取的角色权限,之后会判断是否在 authority 中,如果存在,返回 target,即正确路由,否则返回未通过组件,一般为 403.renderAuthorize 是一个方法,接受一个组件,返回一个方法,返回的方法接受角色名称,最后把之前的组件返回,同时对外暴露当前的角色名称 CURRENT。
页面路由守卫
antd pro 中使用 umi.js,查看 https://umijs.org/zh-CN/docs/routing#redirect,通过在 route 中增加 wrappers 属性来完成前置路由守卫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40// route
{
name: 'task-list',
path: '/ratings/task-list',
wrappers: [
'./wrappers/auth',
],
component: './ratings/task-list',
},
// wrappers
import { Redirect } from 'umi'
import { Result } from 'antd'
import { getAuthority } from '@/utils/authority'
export default (props) => {
const authorityList = props.route.authority // 获取页面权限,如果该路由设置了 authority 就能获取到。
const CURRENT = getAuthority() && getAuthority()[0]
if (CURRENT && Array.isArray(authorityList) && authorityList.some(authority => CURRENT === authority)) {
return (
<div>
{ props.children }
</div>
)
}
return <Result
status="403"
title="403"
subTitle="Sorry, you are not authorized to access this page."
/>
// if (isLogin) {
// return <div>{ props.children }</div>;
// }
// else {
// return <Redirect to="/user/login" />;
// }
}登录逻辑
点击登录后,在 User/login/index.jsx 中会触发 handleSubmit 方法,它会触发 model 层的 dispatch ,调用 login/login 接口,而 dispatch 需要到 @/models/login.js 中查看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79// User/login/index.jsx
...
const handleSubmit = (values) => {
const { dispatch } = props;
dispatch({
type: 'login/login',
payload: { ...values, type },
});
};
...
// login.js
import { stringify } from 'querystring';
import { history } from 'umi';
import { fakeAccountLogin } from '@/services/login';
import { setAuthority } from '@/utils/authority';
import { getPageQuery } from '@/utils/utils';
import { message } from 'antd';
const Model = {
namespace: 'login',
state: {
status: undefined,
},
effects: {
*login({ payload }, { call, put }) {
const response = yield call(fakeAccountLogin, payload);
console.log(response)
yield put({
type: 'changeLoginStatus',
payload: response,
}); // Login successfully
if (response.status === 'ok') {
const urlParams = new URL(window.location.href);
const params = getPageQuery(); // 获取地址栏 ? 后的参数 返回一个对象
message.success('🎉 🎉 🎉 登录成功!');
let { redirect } = params;
if (redirect) {
const redirectUrlParams = new URL(redirect);
if (redirectUrlParams.origin === urlParams.origin) {
redirect = redirect.substr(urlParams.origin.length);
if (redirect.match(/^\/.*#/)) {
redirect = redirect.substr(redirect.indexOf('#') + 1);
}
} else {
window.location.href = '/';
return;
}
}
history.replace(redirect || '/');
}
},
logout() {
const { redirect } = getPageQuery(); // Note: There may be security issues, please note
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: stringify({
redirect: window.location.href,
}),
});
}
},
},
reducers: {
changeLoginStatus(state, { payload }) {
setAuthority(payload.currentAuthority);
return { ...state, status: payload.status, type: payload.type };
},
},
};
export default Model;proTable 组件里的 editable 属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// 列出的可配置选项为 https://www.npmjs.com/package/@ant-design/pro-table#editable
// 默认会出现保存、删除、取消 三个按钮,现在需要隐藏删除。通过 actionRender 自定义操作栏。但无法获取修改后表单,经过一番实验,发现返回的 config 参数中,里面的 form 对象上存在获取表单值的方法。文档上并未提及 actionRender 属性的具体用法。以下为获取行内修改后表单元素的暂行办法,找不到哪个方法能够触发 文档中的 onSave 方法。
<ProTable
editable={{
type: 'multiple',
actionRender: (row, config) => {
return (
[
<a
key="save"
onClick={async () => {
const opt = config.form.getFieldsValue([config.recordKey])[config.recordKey]
opt.id = config.recordKey
await updateUser(opt)
actionRef.current.reload()
config.cancelEditable(config.recordKey)
}}
>保存</a>,
<a
key="cancel"
onClick={() => config.cancelEditable(config.recordKey)}
>取消</a>
]
)
}
}}
// ...
/>React 修改组件的初始值,如添加和编辑使用同一组件的情况
使用一个变量去控制这个组件的渲染,就像 Vue 中 v-if 一样
1
2如
{ createModalVisible && <ActionForm/> }antd 中,当表单元素与 Form 组件同时使用时, Form 组件将会接管表单元素的一些属性,如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54const normFile = (e) => {
console.log('Upload event:', e);
if (Array.isArray(e)) {
return e;
}
return e && e.fileList.filter(file => !!file.status);
};
<Form>
{/* 接管了 name 属性 */}
<Form.Item
label="模板名称"
name="name"
rules={[
{
required: true,
message: '请输入模板名称',
},
]}
>
<Input />
</Form.Item>
{/* 接管了 name fileList 属性 */}
{/* fileList 由 getValueFromEvent 函数的返回值定义 */}
{/* valuePropName 指定触发事件后返回的 e 对象中的属性作为该 FormItem 属性验证的标准;比如下面的上传组件中写了 fileList,如果改成 fff,那么在上传时虽然文件上传成功,但 FormItem 找不到返回的事件对象 e 中 fff 属性,仍然定义为未上传文件,即使 e.fileList 有值,同时也不会触发 Upload 组件中的 beforeUpload。 默认值为 value */}
<Form.Item
name="upload"
label="上传模板"
valuePropName="fileList"
getValueFromEvent={normFile}
extra="请上传 excel 格式文件"
rules={[
{
required: true,
message: '请上传模板',
},
]}
>
<Upload
maxCount={1}
listType="picture"
beforeUpload={file => {
let suffix = name.substr(name.lastIndexOf("."))
if (".xls" != suffix && ".xlsx" != suffix) {
return false
} else {
return true
}
}}
>
<Button icon={<UploadOutlined />}>上传模板</Button>
</Upload>
</Form.Item>
</Form>React 中样式问题
修改组件样式, Vue 中可以通过不添加 scoped 或者样式穿透来修改组件样式, React 中通过将类名包裹在 :global() 来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 如下 .ant-statistic-title 是 antd Statistic 组件默认的属性名
// 这样直接修改会破坏其他的组件的样式,可以在想要修改样式的标签外面定义一个父级元素,在用一个父级选择器就可以了。
:global(.ant-statistic-title) {
font-size: 16px;
}
// 对比在 Vue 中,写法大概是这样,这应该和上面 React 的修改方法等同,都会修改其他组件的样式。
<style lang="less">
.ant-statistic-title {
font-size: 16px;
}
</style>
// 或
<style lang="stylus" scoped>
>>> .ant-statistic-title {
font-size: 16px;
}
</style>React 中,所有数组组件必须绑定 key 值,因此在 antd 中,Table 组件的 dataSource 和 columns 都需要指定 key值,columns key 值在它的本身的数组中的每个对象上分别定义,dataSource 需要使用 rowKey 指定需要绑定的 主键,默认为 ‘key’,如果数据中没有 key,将会报错,错误为,看了半天才发现是 Table 组件导致的问题。
1
2
3
4devScripts.js:5836 Warning: Each child in a list should have a unique "key" prop.
Check the render method of `Body`. See https://reactjs.org/link/warning-keys for more information.
at BodyRow (http://localhost:8000/vendors~p__TableList~p__Welcome~p__ratings__project-list~p__ratings__review-list~p__ratings__task-li~a67d39dd.js:2555:25)
antd pro 中,ProTable 的 columns 中设置的属性,如果为 dateRange,将无法设置格式化时间,需要自己手动修改
1
2
3
4
5
6
7
8
9
10
11const columns = [
{
title: '完成时间',
dataIndex: 'end_at',
hideInTable: true,
valueType: 'dateRange',
search: {
transform: (value) => ({ end_at: [value[0].split(' ')[0], value[1].split(' ')[0]] }) // 格式化
}
}
]react 中的 {…obj} 和普通 JS 文件中的 {…obj} 不同,由于正常情况下,Object 对象没有内置迭代器,无法使用展开运算符,但是由于 babel 转义,使得 react 中也可以对对象使用展开运算符。
1
2
3
4
5
6
7
8
9
10
11
12
13// xxx.jsx
const obj = {
a: 1,
b: 2
}
console.log(...obj) // 1 2
// xxx.js
const obj = {
a: 1,
b: 2
}
console.log(...obj) // error obj is not iterable
题外:
ES6 的解构赋值
多层解构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32var res = {
data: {
data: {
msg: 'last'
},
meta: {
pagination: {
count: 2,
current_page: 1,
links: {},
per_page: 20,
total: 2,
total_pages: 1
}
}
},
status: 'success'
}
const { data: { data, meta: { pagination: { total, total_pages: pageSize, current_page: current } } }, status } = res
// 此时能够解构出 data, total, pageSize, current, status 五种变量, meta 和 pagination 并不是变量
console.log(data) // { msg: 'last' }
console.log(total) // 2
console.log(pageSize) // 1
console.log(current) // 1
console.log(status) // 'success'
console.log(meta) // ERROR meta is not defined
console.log(pagination) // ERROR pagination is not defined
// 如果需要同时解构出 meta 和 pagination, 在外层多定义一次即可,解构赋值已定义的变量永远在 : 右边。
const { data: { data, meta, meta: { pagination, pagination: { total, total_pages: pageSize, current_page: current } } }, status } = res剥离属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46// 正常写的时候,后端返回的 resData 如下,但此时我们不需要 id 和 category_id 时,可能会写成下面这样
const resData = [
{
id: 1,
category_id: 1,
content: 'content1',
detail: 'detail1',
score: 'score1',
type: 'type1',
},
{
id: 2,
category_id: 2,
content: 'content2',
detail: 'detail2',
score: 'score2',
type: 'type2',
},
]
const tableDataList = resData.map(item => {
return {
columns,
data: {
content: item.content,
detail: item.detail,
score: item.score,
type: item.type,
}
}
})
// 但我们可以通过 ... 来快速剥离属性,省略了很多不必要的写法,并且非常清楚
const tableDataList = resData.map(({ id, category_id, ...item}) => {
return {
columns,
data: item
}
})
// 数组
const deleteNumber = (arr, index) => {
return [...arr.slice(0, index), ...arr.slice(index + 1)];
};
let testArr = [1, 2, 3, 4, 5];
console.log(deleteNumber(testArr, 4)); // [1, 2, 3, 4]
linux 与 windows 的文件名
服务器一般是 linux ,而 linux 是大小写敏感的, windows 是大小写不敏感的
1 | // actionForm.jsx |