0. 起因

最近工作中要对系统做整体重构,在重构过程中发现有些依赖功能不足,我们需要对其做适当修改,目前实践了 2 种方式作为依赖微调如下:

  1. 通过脚本在 postinstall 阶段去替换现有的文件,或者替换字符串;
  2. 通过 patch-package 直接在 node_modules 修改文件,然后 patch-package 生成 patch 文件,在 postinstall 阶段,再 patch 回去;
  3. 通过 webpack 打包工具中的 alias 把原本依赖替换成新的依赖包;

1. 脚本替换处理

脚本替换处理是比较粗糙的替换手段

1.1 替换脚本编写 replace.sh

1
2
3
4
5
6
7
8
9
10

#!/bin/bash
folder="node_modules/some-package"
for file in $(find $folder -name "*.js")
do
# Replace string
sed -i 's/要被替换的文本/被替换的文本/g' $file

echo "Replaced in $file"
done

1.2 配置 package.json

package.jsonscripts 中增加一个 postinstall, 这样在项目安装依赖完成后会执行这个脚本

1
2
3
4
5
6
{
"scripts": {
"start": "echo start",
"postinstall": "sh replace.sh"
}
}

2. 使用 patch-package 更改

2.1 安装 patch-package 依赖

npm i patch-package --save-dev

2.2 修改依赖文件,并生成 patch 文件

  1. 修改我们要更改的依赖包如 react-native-camera 中的文件并报存;
  2. 执行 npx patch-package react-native-camera 生成 patch 文件

27-14-20-HfgZyq-M3VfIh

2.3 配置 package.json

package.jsonscripts 中增加一个 postinstall, 这样在项目安装依赖完成后会执行这个脚本

1
2
3
4
5
6
{
"scripts": {
"start": "echo start",
"postinstall": "patch-package"
}
}

3. 使用 alias 替换依赖

使用 alias 替换依赖比较适合被更改的依赖比较底层,还有更改内容较多,还会编译出文件,这里介绍 antd 中依赖的基础组件 rc-dialog 的替换

3.1 首先在 githubfork 下来 dialog 的代码

27-14-30-b2xRIT-7YlbmR

3.2 拉取 fork 的仓库代码,并把自己需要的功能加上,更改 package.json 中的包名

27-14-34-kVvJHR-P8Tybe

3.3 更改完成后,把这个包构建并发布到 npm

文件目录下执行 npm publish 发布版本到 npm

3.4 在要替换的项目中安装修改的这个依赖

在项目中安装依赖 npm install rc-cfhy-draggable-dialog

3.5 在 webpack 中配置 alias

通过下面的配置,这样项目中使用到 rc-dialog 的都会改为新的依赖 rc-cfhy-draggable-dialog,这样就满足了需求

1
2
3
4
5
6

alias: {
'@': '/src',
'rc-dialog': 'rc-cfhy-draggable-dialog'
},

4. 参考引用

patch-package
Webpack 技巧 - 联合 alias 和 mainFields 提高多库联调效率

0. 痛点-为什么要使用命令式打开弹层

Modal 模态对话框在中台业务中非常常见。声明式 Modal 是一种常见的定义方式,可以通过组件库内置的组件来实现。在声明式 Modal 中,需要手动控制 Modal 的打开状态,并将状态和 UI 绑定在一起。随着业务的增加,页面上可能会出现多个 Modal 和 Button,导致代码变得冗长且难以维护。为了解决这个问题,可以将 Modal 抽离到外部组件中,但这样会导致父组件的状态堆积。另一种解决方案是将 Modal 和 Button 直接耦合在一起,但这样无法单独复用 Modal。为了解耦 Modal,需要编写大量代码并且状态变得混乱。后续可能还会遇到无法在父组件中直接控制 Modal 的问题,可以通过将状态下沉到外部 Store 或者 Context 中来解决。

2. 代码实现

2.1 创建 layer-context.tsx

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

import { Drawer, Modal, ModalProps, DrawerProps } from 'antd';
import { ModalForm, DrawerForm, ModalFormProps, DrawerFormProps } from '@ant-design/pro-components';
import {
Dispatch,
ReactNode,
SetStateAction,
createContext,
useContext,
useId,
useMemo,
useRef,
} from 'react';
import { LayerInstance } from './layer';

type LayerContentProps = {
close: () => void;
};

export type LayerProps = {
id?: string;
content: ReactNode | ((props: LayerContentProps) => ReactNode);
Component: ReactNode | ((props: LayerContentProps) => ReactNode);
} & Omit<ModalProps, 'open' | 'content'> &
DrawerProps &
ModalFormProps &
DrawerFormProps;

interface LayerStackContextValue extends LayerProps {
id: string;
ins?: LayerInstance;
}

export const LayerStackContext = createContext<LayerStackContextValue[]>([]);
export const LayerActionContext = createContext<Dispatch<SetStateAction<LayerStackContextValue[]>>>(
() => void 0,
);

export const useLayerStack: any = () => {
const id = useId();
const currentCount = useRef(0);
const setLayerStack = useContext(LayerActionContext);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return useMemo(() => {
return {
openModal(props: LayerProps & { id?: string }) {
const layerId = `${id}-Modal-${currentCount.current++}`;
setLayerStack((p = []) => {
const layerProps = {
...props,
id: props?.id ?? layerId,
Component: Modal,
_layerName:'modal'
};
const pp = p?.concat(layerProps);
return pp;
});
const hide = () => {
setLayerStack((p = []) => {
return p?.filter((item) => item.id !== layerId);
});
};
return [layerId, hide];
},
openDrawer(props: LayerProps & { id?: string }) {
const layerId = `${id}-Drawer-${currentCount.current++}`;
setLayerStack((p = []) => {
const layerProps = {
...props,
id: props?.id ?? layerId,
Component: Drawer,
_layerName:'drawer'
};
const pp = p?.concat(layerProps);
return pp;
});
const hide = () => {
setLayerStack((p) => {
return p?.filter((item) => item.id !== layerId);
});
};
return [layerId, hide];
},
openFormModal(props: LayerProps & { id?: string }) {
const layerId = `${id}-ModalForm-${currentCount.current++}`;
setLayerStack((p = []) => {
const layerProps = {
...props,
id: props?.id ?? layerId,
Component: ModalForm,
_layerName:'formModal'
};
const pp = p?.concat(layerProps);
return pp;
});
const hide = () => {
setLayerStack((p = []) => {
return p?.filter((item) => item.id !== layerId);
});
};
return [layerId, hide];
},
openFormDrawer(props: LayerProps & { id?: string }) {
const layerId = `${id}-DrawerForm-${currentCount.current++}`;
setLayerStack((p = []) => {
const layerProps = {
...props,
id: props?.id ?? layerId,
Component: DrawerForm,
_layerNameName:'drawerForm'
};
const pp = p?.concat(layerProps);
return pp;
});
const hide = () => {
setLayerStack((p) => {
return p?.filter((item) => item.id !== layerId);
});
};
return [layerId, hide];
},

closeByLayerId(id: string) {
setLayerStack((p) => {
const m = p.find((item) => item.id === id);
if (m?.ins) {
m?.ins?.close?.();
return p;
}
return p.filter((item) => item.id !== id);
});
},
destroyAll() {
setLayerStack((p) => {
p.forEach((m) => {
if (m?.ins) {
m?.ins?.close?.();
return p;
}
});
return p;
});
},
};
}, [id, setLayerStack]);
};

2.2 创建 layer-stack-provider.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { FC, PropsWithChildren, useState } from 'react';
import { LayerActionContext, LayerStackContext } from './layer-context';
import LayerStack from './layer-stack';

export const LayerStackProvider: FC<PropsWithChildren> = ({ children }) => {
const [layerStack, setLayerStack] = useState([] as React.ContextType<typeof LayerStackContext>);
return (
<LayerActionContext.Provider value={setLayerStack}>
{children}
<LayerStackContext.Provider value={layerStack}>
<LayerStack />
</LayerStackContext.Provider>
</LayerActionContext.Provider>
);
};

2.3 创建 layer-stack.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { FC, PropsWithChildren, useContext } from 'react';
import { LayerStackContext } from './layer-context';
import Layer from './layer';

const LayerStack: FC<PropsWithChildren> = ({}) => {
const layerStack = useContext(LayerStackContext);

return (
<>
{layerStack?.map?.((item, index) => {
return <Layer key={item?.id} item={item} index={index} />;
})}
</>
);
};

export default LayerStack;

2.4 创建 layer.tsx

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import {
memo,
useContext,
useCallback,
useRef,
useState,
useEffect,
createElement,
MouseEvent,
} from 'react';
import { LayerActionContext, LayerProps } from './layer-context';
import { ProFormInstance } from '@ant-design/pro-components';

export type LayerInstance = {
close: () => void;
};

const Layer = memo<{
item: LayerProps & { id: string; onOk: (e: any, close: any) => void };
index: number;
}>(({ item, index }) => {
const setLayerStack = useContext(LayerActionContext);

const close = useCallback(() => {
setTimeout(() => {
setLayerStack((p) => {
return p?.filter((layer) => layer?.id !== item.id);
});
}, 500);
}, [item.id]);

const { content, title, centered = true, Component, onOk, modalProps, footer, _layerName, ...rest } = item;


const [open, setOpen] = useState(false);
const formRef = useRef<ProFormInstance>();
const dataRef = useRef<any>();
const instanceRef = useRef<LayerInstance>({
close: () => {
setOpen(false);
},
});
useEffect(() => {
setOpen(true);

setLayerStack((p) => {
const newStack = [...p];
newStack[index].ins = instanceRef.current;
return newStack;
});
}, []);
const handleClose = useCallback(() => {
setOpen(false);
}, []);
const afterClose = useCallback(() => {
item?.afterClose?.();
close();
}, []);
const onCancel = useCallback((e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>) => {
item?.onCancel?.(e);
handleClose();
}, []);
const afterOpenChange = useCallback((open: boolean) => {
if (!open) {
item?.afterClose?.();
handleClose();
close();
}
}, []);
const handleOnOk = useCallback(() => {
const data = dataRef.current;
onOk?.(data, handleClose);
}, []);

let handleFooter = footer;
if (typeof handleFooter === 'function') {
// @ts-ignore
handleFooter = handleFooter.bind(null, handleClose);
}

let layerSelfProps = {}
if(_layerName === 'modal' || _layerName === 'drawer') {
layerSelfProps ={
centered,
afterClose,
afterOpenChange,
onOk: handleOnOk,
onCancel,
}
}

return (
<Component
open={open}
{...layerSelfProps}
formRef={formRef}
onClose={onCancel}
onOpenChange={afterOpenChange}
title={title}
modalProps={{ centered, ...modalProps }}
footer={handleFooter}
{...rest}
>
{createElement(content, {
close: handleClose,
formRef: formRef,
dataRef: dataRef,
})}
</Component>
);
});

export default Layer;

2.5 创建导出 index.tsx

1
2
3
4
5
import Layer from './layer';
import LayerStack from './layer-stack';
import { LayerStackProvider } from './layer-stack-provider';
export * from './layer-context';
export { Layer, LayerStack, LayerStackProvider };

3. 如何使用

  1. 在根节点引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

import { Button } from 'antd';
import React from 'react';
import { LayerStackProvider } from '@/components/Layer';

const AppFC: React.FC = () => {

return (
<div>
<LayerStackProvider>
<div>{children}<div/>
</LayerStackProvider>
</div>
);
};

export default TestFC;

  1. 在其他地方使用消费
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { Button } from 'antd';
import React from 'react';
import { useLayerStack } from '@/components/Layer';

const TestFC: React.FC = () => {

const { openDrawer, openModal, destroyAll, openFormDrawer, openFormModal } = useLayerStack();
const openM = () => {
openModal({
title: 'openModal',
content: ({ close }) => {
return (
<div className="">
<div
className=""
onClick={() => {
close();
}}
>
关闭
</div>
<div className="" onClick={() => {
openD();
}} >再开一个</div>
<div
className=""
onClick={() => {
destroyAll();
}}
>关闭所有</div>
</div>
);
},
});
};
const openFM = () => {
openFormModal({
title: 'openFormModal',
content: ({ close, formRef }) => {
return (
<div className="">
<ProFormText
width="md"
name="name"
label="签约客户名称"
tooltip="最长为 24 位"
placeholder="请输入名称"
rules={[{ required: true }]}
/>

<ProFormText width="md" name="company" label="我方公司名称" placeholder="请输入名称" />
<Button
onClick={() => {
console.log('清楚签约客户名称', formRef);

formRef?.current?.setFieldsValue?.({ name: '' });
}}
>
清楚签约客户名称
</Button>
</div>
);
},
onFinish: async (values) => {
console.log(values);
return true;
},
});
};
const openFD = async () => {
const [layerId, hide] = await openFormDrawer({
title: 'openFormDrawer',
content: ({ close, formRef }) => {
return (
<div className="">
<ProFormText
width="md"
name="name"
label="签约客户名称"
tooltip="最长为 24 位"
placeholder="请输入名称"
rules={[{ required: true }]}
/>

<ProFormText width="md" name="company" label="我方公司名称" placeholder="请输入名称" />
<Button
onClick={() => {
console.log('清楚签约客户名称', formRef);
formRef?.current?.setFieldsValue?.({ name: '' });
}}
>
清楚签约客户名称
</Button>
</div>
);
},
onFinish: async (values) => {
console.log(values);
return true;
},
});
};
const openD = () => {
openDrawer({
title: 'openDrawer',
content: ({ close }) => {
return (
<div className="">
<div
className=""
onClick={() => {
close();
}}
>
关闭
</div>
<div className="" onClick={() => {
openD();
}} >再开一个</div>
<div
className=""
onClick={() => {
destroyAll();
}}
>关闭所有</div>
</div>
);
},
});
};

return (
<div className={styles.container}>
<Button type="primary" key="new" size="small" onClick={openM}>打开 Modal</Button>
<Button type="primary" key="new" size="small" onClick={openD}>打开 Drawer</Button>
<Button type="primary" key="new" size="small" onClick={openFM}>打开 FormModal</Button>
<Button type="primary" key="new" size="small" onClick={openFD}>打开 FormDrawer</Button>
</div>
);
};

export default TestFC;

4. 参考引用

为什么我更推荐命令式 Modal

0. 起因

最近工作中要对系统做整体重构,需要封装一些基础的组件,封装基础组件参照如下准则:

  1. 组件基本样式统一,留有可修改空间;
  2. 把默认要的一些基本功能配置,直接写到基础组件配置中,避免重复配置;
  3. 具备和原本组件一致的接口定义;

下面就通过封装 ProTable 表格基础组件的例子.

1. 分析业务的默认需求配置

  1. 在业务中有很多默认的设置,比如单行文本过长折叠,页脚设置统一等;
  2. 通过封装使功能简单就可以调用,比如 设置表头的排序展示本地存储;
  3. 搜索展示多行的功能;

27-10-18-0oNfH4-xpDHz0

2. 代码实现

2.1 单行文本过长折叠默认配置

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

// 1. 默认全部折叠,新增一个 excludeEllipsis 排除折叠的数组配置
/** ProTable 的类型定义 继承自 antd 的 Table */
export declare type CSProTableProps<DataSource, U, ValueType = 'text'> = {
excludeEllipsis?: string[]; // 不需要折叠的数组,默认所有的都折叠 option 不折叠
} & ProTableProps<DataSource, U, ValueType>;


/****** 2. 设置默认配置 *******/
const newColumns = useMemo(() => {
const nc: any = columns?.map?.(it => {
const { dataIndex = '', valueType = '' }: any = it
const tempIt = { ...it, ellipsis: true }
if (excludeEllipsis?.includes?.(dataIndex)) {
tempIt.ellipsis = false
}
if (it?.hasOwnProperty('ellipsis')) {
tempIt.ellipsis = it?.ellipsis
}
return tempIt
})
return nc
}, [columns, excludeEllipsis])

/****** 3. 传入到原有里面 *******/

const getTableProps = () => ({
...rest,
columns: newColumns,
})
return (
<ProTable<DataType, Params, ValueType>
{...getTableProps()}
>{props?.children}</ProTable>
);

2.2 简单配置设置表头的排序展示本地存储

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
// 1. 添加一个新传参字段 persistenceKey
/** ProTable 的类型定义 继承自 antd 的 Table */
export declare type CSProTableProps<DataSource, U, ValueType = 'text'> = {
persistenceKey?: string; // 表头的排序持久化 key,提出来传参,实现简单调用
} & ProTableProps<DataSource, U, ValueType>;

/******** 2. 设置默认传参 *********/
const newColumnsState = useMemo(() => {
let ncs: any = {
persistenceType: 'localStorage',
}
if (persistenceKey && !columnsState) {
ncs.persistenceKey = persistenceKey
}
if (columnsState) {
ncs = { ...ncs, columnsState }
if (!ncs?.persistenceKey && persistenceKey) {
ncs.persistenceKey = persistenceKey
}
}
return ncs
}, [persistenceKey, columnsState])

/****** 3. 传入到原有里面 *******/
const getTableProps = () => ({
...rest,
columnsState: newColumnsState,
})
return (
<ProTable<DataType, Params, ValueType>
{...getTableProps()}
>{props?.children}</ProTable>
);

2.3 封装前后调用对比

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

const columns = [
{
title: '序号',
valueType: 'index',
width: 65,
},
{
dataIndex: 'name',
title: '用户名称',
},
{
dataIndex: 'mobile',
title: '联系电话',
},
{
dataIndex: 'type',
title: '好友类型',
hideInSearch: true,
valueEnum: [],
},
{
title: '操作',
valueType: 'option',
width: 180,
render: (_, record) => [<a key="manage" onClick={() => {}}>管理</a>]
},
]

<ProTable
columns={columns}
columnsState={{
persistenceType: 'localStorage',
persistenceKey: 'persistenceKey'
}}
rowKey="id"
scroll={{ x: 800 }}
actionRef={actionRef}
request={getContactListApi}
/>
<CSProTable
persistenceKey="persistenceKey"
columns={columns}
rowKey="id"
scroll={{ x: 800 }}
actionRef={actionRef}
request={getContactListApi}
/>

两者调用基本差不多,设置标题持久化 CSProTable 的配置会简单些,还有他会默认对单行文本过长折叠

3. 完整封装代码

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/*
* @Description: CSProTable,对原有的做一些定制化的东西,方便调用
*/
import type { ParamsType, ProTableProps } from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components';
import { Icon } from 'antd';
import { isBoolean } from 'lodash';
import React, { ReactNode, useMemo } from 'react';

/** ProTable 的类型定义 继承自 antd 的 Table */
export declare type CSProTableProps<DataSource, U, ValueType = 'text'> = {
searchTitle?: string | ReactNode; // 搜索标题
excludeEllipsis?: string[]; // 不需要折叠的数组,默认所有的都折叠 option 不折叠
persistenceKey?: string; // 表格列表持久化 key,提前出来传参
} & ProTableProps<DataSource, U, ValueType>;

const CSProTable = <
DataType extends Record<string, any>,
Params extends ParamsType = ParamsType,
ValueType = 'text',
>(
props: CSProTableProps<DataType, Params, ValueType>
) => {
const {
searchTitle,
excludeEllipsis = [],
columns = [],
pagination = {},
search,
scroll = {},
searchFormRender,
persistenceKey,
columnsState,
options,
cardBordered = true,
bordered = true,
...rest } = props
const newColumns = useMemo(() => {
const nc: any = columns?.map?.(it => {
const { dataIndex = '', valueType = '' }: any = it
const tempIt = { ...it, ellipsis: true }
if (excludeEllipsis?.includes?.(dataIndex)) {
tempIt.ellipsis = false
}
if (it?.hasOwnProperty('ellipsis')) {
tempIt.ellipsis = it?.ellipsis
}
if (it?.hasOwnProperty('valueEnum') || valueType === 'select') {
tempIt.fieldProps = { showSearch: true, ...tempIt?.fieldProps }
}
if (dataIndex === 'option' || valueType === 'option') {
tempIt.fixed = 'right'
tempIt.hideInSetting = true
}
if (dataIndex === 'index' || valueType === 'index') {
tempIt.fixed = 'left'
}
if (dataIndex === 'indexBorder' || valueType === 'indexBorder') {
tempIt.fixed = 'left'
}
return tempIt
})
return nc
}, [columns, excludeEllipsis])
const newOptions = useMemo(() => {
let no = {}
if (options) {
no = options
if (!options?.hasOwnProperty?.('reloadIcon')) {
no = { ...no, reloadIcon: <Icon type="icon-shuaxin" /> }
}
if (!options?.hasOwnProperty?.('density')) {
no = { ...no, density: false }
}
if (!options?.hasOwnProperty?.('setting')) {
let setting = options?.setting
if (isBoolean(setting)) {
no = { ...no, setting }
} else {
if (!setting?.hasOwnProperty?.('settingIcon')) {
setting = { ...setting, settingIcon: <CFIcon type="icon-shezhi" /> }
}
no = { ...no, setting }
}
}
} else if (isBoolean(options)) {
no = options
} else {
no = {
setting: {
draggable: true,
settingIcon: <Icon type="icon-shezhi" />
},
reloadIcon: <Icon type="icon-shuaxin" />,
density: false
}
}
return no
}, [])
const newPagination = useMemo(() => {
let np = {}
if (pagination) {
np = pagination
if (!pagination?.hasOwnProperty?.('showSizeChanger')) {
np = { ...np, showSizeChanger: true }
}
if (!pagination?.hasOwnProperty?.('showQuickJumper')) {
np = { ...np, showQuickJumper: true }
}
} else if (isBoolean(pagination)) {
np = options
} else {
np = {
showSizeChanger: true,
showQuickJumper: true
}
}
return np
}, [])
const newColumnsState = useMemo(() => {
let ncs: any = {
persistenceType: 'localStorage',
}
if (persistenceKey && !columnsState) {
ncs.persistenceKey = persistenceKey
}
if (columnsState) {
ncs = { ...ncs, columnsState }
if (!ncs?.persistenceKey && persistenceKey) {
ncs.persistenceKey = persistenceKey
}
}
return ncs
}, [persistenceKey, columnsState])
const newSearch = useMemo(() => {
let ns: any = {
span: {
xs: 24,
sm: 24,
md: 12,
lg: 8,
xl: 6,
xxl: 6,
}
}
if (isBoolean(search)) {
ns = search
} else {
ns = { ...ns, ...search }
}
return ns
}, [search])
const newScroll = useMemo(() => {
return { x: 1300, ...scroll }
}, [scroll])
const newSearchFormRender = (props, defaultDom) => {
return <>
{searchTitle && <div className='searchTitle absolute text-base z-50 font-medium text-[#333]' style={{ top: '40px', left: '44px' }}>{searchTitle}</div>}
{defaultDom}
</>
}
const getTableProps = () => ({
...rest,
cardBordered,
bordered,
columnsState: newColumnsState,
search: newSearch,
scroll: newScroll,
pagination: newPagination,
columns: newColumns,
options: newOptions,
searchFormRender: searchFormRender ? searchFormRender : newSearchFormRender
})
return (
<ProTable<DataType, Params, ValueType>
{...getTableProps()}
>{props?.children}</ProTable>
);
};

export { CSProTable };

4. 参考引用

Pro-Components 组件 ProTable 文档

1. 遇到的问题

在我们的微前端开发中,子服务的 cicd 基本是一样的,如果需要改动其中的某一步,就会涉及把所有子服务中的 .gitlab.yml 文件都改一遍,这样就会发现很繁琐,所以有没有办法统一做配置的吗?

2. 示例

原本子服务中 .gitlab.yml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
variables:
GIT_SUBMODULE_STRATEGY: recursive

before_script:
- git config --global user.name "${GITLAB_USER_NAME}"
- git config --global user.email "${GITLAB_USER_EMAIL}"

stages:
- build
build:
stage: build
image: hub.docker.com/ci/ci-nodejs:14.5.0
script:
- npm install
- npm run build
- npm run cdn
only:
- /^v?\d+(\.\d+)+[\.\-_\w]*/
tags:
- i7

目前build阶段有 3 步骤,如果我们需要加一个发现通知 npm run notification

解决方法

  1. 在每个子服务中都添加该步骤-缺点比较麻烦,还有如果下次再改,还有重复修改;
  2. 通过仓库配置,统一管理 .gitlab.yml 的配置-优点只需要配置一次,后面都只要改仓库中模板的配置就行

3. 配置

1. 创建一个配置的仓库和文件

配置仓库比如: ci_template,在仓库中新建模板文件如: web-childServise-ci-base.yml 文件内容基本和子服务的一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# web-childServise-ci-base.yml
variables:
GIT_SUBMODULE_STRATEGY: recursive

before_script:
- git config --global user.name "${GITLAB_USER_NAME}"
- git config --global user.email "${GITLAB_USER_EMAIL}"

stages:
- build
build:
stage: build
image: hub.docker.com/ci/ci-nodejs:14.5.0
script:
- npm install
- npm run build
- npm run cdn
- npm run notification # 新加的通知
only:
- /^v?\d+(\.\d+)+[\.\-_\w]*/
tags:
- i7

2. 修改子服务中 .gitlab.yml 文件

1
2
3
4
5
6
7
8
9
10
11
# .gitlab.yml
variables:
GIT_SUBMODULE_STRATEGY: recursive

include:
# base 仓库的路径
- project: 'devops/ci_template'
file: '/web/web-childServise-ci-base.yml'
# ref 可以不配置,默认是 master 分支
ref: 'feature/addNotification'
# 指定仓库的具体分支,一般测试 ci 脚本时候用

4. 总结

通过仓库统一配置,这样如果下次需要更改 runner 机器或者添加新脚本,通过直接修改统一的配置,在子服务触发新的构建的时候就会使用最新的配置去跑服务

1. 前端防御性问题

在实现某种css效果时,往往我们能很快的完成css的编写,如文档溢出、垂直居中、各种布局等效果。但是当一个或一组元素具有动态,或者面对无法预测内容形式的时候,可能会出现样式不兼容,不适配导致塌陷,甚至异常。

2. 前端防御性实际情况

实际情况远比想象的复杂。以下列举了一些用户在使用链路上可能存在的“危机点”,这也是防御性开发中着重要考虑的“防御点”。归纳为两大类问题:

1.UI的防御性

  • 防白屏 – 白屏时间小于1秒(小于200ms最佳)
  • 防布局错乱 – 布局被动态内容撑垮
  • 防极端内容 – 缺失 / 超长 / 连续字符 / 未转义
  • 防慢 – 网络慢 / 响应慢 / 渲染慢 / 执行慢
  • 防卡 – 卡顿 / 假死
  • 防一致性问题 – 不一致的交互方式、图标、 标准组件等
  • 防UI状态不全 – 五层UI状态栈(加载状态/空状态/部分缺失状态/出错状态/理想状态)
  • 防样式污染 – 样式冲突,局部模块的样式影响全局
  • 防Chartjunk – 可读性差的图表用法
  • 防误操作 / 危险操作 – 对不可逆的操作二次确认+强提示

2.代码的防御性

  • 防报错 – 语法错误 / 逻辑错误
  • 防兼容性问题
  • 防安全性问题
  • 防意外输入和交互
  • 防数据 – 防极端数据 / 无效数据 / 接囗变更
  • 防代码坏味道 / 防工程腐化 – 代码复杂度 / 重复率 / 反模式 / 死代码等
  • 防语法风格不一致
  • 防代码冲突
  • 防代码冗余

3. 如果做防御性开发

人是代码的创作者,提高代码防御性,写出高质量的代码,最终靠人。团队中的CR是从外部反馈来优化开发,而作为开发者,则更需要自己对自身的约束。
那应该从哪些方面去做防御性考虑呢?

1. 需求

  • 功能实现问题
  • UI和交互问题

2. 开发

  • JS防御性原则
防御点 原则
逻辑问题 1. 判断条件有误 / 忽略了必要条件
2. 循环 / 递归的退出条件
3. 显隐逻辑和跳转逻辑控制
4. 缺少校验或错判参数类型 / 空值 / 边界条件
5. 缺少对默认值 / 缺省状态的校验 / 判断 / 处理
6. 接囗调用逻辑和组合关系
7. 忽略一些组件之间的联动关系
全局副作用 1. 变更公共代码,对其他部分产生影响
2. 变更配置文件 / 全局变量
3. 代码的冲突和污染
4. 基础库版本升级
容错问题 1. 错误输入 / 特殊字符 / 数据类型的容错
2. 接囗返回值的不确定性
3. 接囗请求失败的容错
4. 缺少error boundary,避免导致白屏
5. 错误要上报
表单校验问题 前端校验条件不全
编译 & 依赖问题 1. JS编译漏掉对一些语法的处理
2. 本地和发布构建有差异
3. 本地和线上依赖版本有差异
兼容性问题 1. Polyfill不全
2. CSS兼容性问题
文案问题 1. 文案错误 / 不准确 / 折行
2. 国际化不完整
  • CSS防御性原则
防御点 原则
避免用JavaScript控制布局 JavaScript实现的布局组件不要用。它会多出很多层没用的嵌套,同时把布局定义的很死,难以再用CSS控制。
永远没有原生的流畅,同时增加代码的复杂,容易用问题。除非解决一些必要的兼容性问题。l
避免用float / position: absolute / display: table等过时的布局技术 优先用Flexbox/Grids布局。你会说绝对定位还是有用的。你要强迫自己不用,经过反复尝试过发现绝对定位是最优的选择那就用。重要的是有这个“强迫自己”的过程。
避免定高/定宽 固定宽/高最容易出现的问题是内容溢出。没必要通过定宽高对齐,可以利用Flexbox的位伸/收缩特性。一般情况下用最小宽/高、calc()、相对单位替代。同上要“强迫自己”不用。
避免侵入性的写法 避免影响全局样式,如:* { … }、:root {…} 、div { ….}等。避免影响通用组件样式,如:.next-card {…},如果要定制单加一个class名。不要直接修改全局CSS变量,把自己的CSS变量定义在模块的范围内。不要写z-index:999。一般1~9,防止被遮挡10~99,绝对够用了。不要在标签上定义style属性。不要在JS代码中做样式微调,这样今后无法统一升级CSS样式。只有完全不可修改的样式才能用!important,利用选择器优先级调整样式。
避免CSS代码的误改 / 漏改 将选择器拆开写,如.card-a, .card-b { … },写时方便,改时难,修改时容易影响其它元素,不如分开写(除非像css reset这种特别确定的情况)。将样式集中在一起,容易改错。保持CSS代码和文件在相应的层级上,同一模块的放一起。避免混入通用样式中,为了避免改错,允许适当冗余。用@media时,会集中覆写一批元素的样式,更新样式时非常容易遗漏。所以必须拆开写,和对应模块的样式放在一起。不要集中放在文件底部,或是集中放在某一个文件里。及时清除“死代码”。定义样式要写全,微调样式要写具体,如:.mod {
margin: 0;
}
/* 其它地方需要微调时 */
.biz-card .mod {
margin-bottom: 16px;
}
避免CSS样式冲突 限定作用范围。如,.my-module .xxx { … }。业务代码中的样式要加前缀,或借鉴BEM命名方式。如:.overview-card-title { … }。用CSS Module也可以。注意选择器的精确性。级层过长过于复杂的CSS选择器会影响性能,但要注意:有时需要精确选择某些元素,如仅选择一级子元素,.overview-card-content > .item { … }。
防止内容不对齐 应该说Flexbox侧重“对齐”,Grids是专为布局设计的。受字体、行高等因素影响(如图),用Flexbox实现对齐最可靠:
1、height / line-height 不可靠。
2、display:inline-block / vertical-align:middle不可靠。
防止内容溢出 包括文字 / 图表等内容在宽度变化时或是英文版下容易出现溢出。
1、图表要支持自动 resize。
2、图片要限制大小范围,如:max-width、max-height min(100px, 100%)、max(100px, 100%)(注意:min() / max() 兼容性:chrome 79+ / safari 11 / firefox 75)
3、不要固定宽/高。(见规则3)
4、不要在容器元素定义overflow:hidden防止内容不可见。宁可难看关键信息也要显示全。
5、用min-width:0防止Flexbox项被内容撑开。例如:html如下,.canvas的style是JS写死的,不可避免的溢出了(如图)。
<div class="wrapper"><div class="item"> <div class="canvas" style="width:300px;height:200px;">canvas</div></div></div>
这种情况下.item可以定义为:
display: flex;
min-width: 0;
防止内容被遮挡 定义负值时(负margin / top / left),小心内容被遮挡,避免这么定义。定义margin统一朝一个方向,向下和向右定义,再重置一下:last-child。position: relative 平时很常用,发生遮挡时会造成链接无法点击。
防止可点击区域过小 小于32x32像素的可点击元素,通过下面的方式扩大可点击区域:
.btn-text {
position: relative;
}
// 比 padding 副作用小
.btn-text::before {
content: ‘’;
position: absolute;
top: -6px;
left: -8px;
right: -8px;
bottom: -6px;
}
防止内容显示不全 / 被截断 在定义overflow:hidden时,就要考虑内容是否有被截断的可能。一般不要加在容器元素上。防止长文字被生生截断,加省略号。UI实现过程中要对内容做出判断,哪些是不应该折行的,哪些是不应该省略的,如:
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
防止图片变形 图片被置于特定比例的容器中时,固定宽/高和约束最大宽/高,都可能会导致图片变形。
.head img {
width: 100%;
height: 100%;
object-fit: cover;
}
在Flexbox容器内,图片高度会被自动拉伸。因为不要定义align-items,默认是stretch。
防止图片加载失败 需要考虑图片加载慢或加载失败的情景。在图片的容器上加边或加底色。
Flexbox常见防御性写法 Flexbox的默认表现比较多,不能简单的定义display:flex,或是flex:1。
1. Flexbox容器元素通常要做如下定义:要支持多行(默认是单行),交叉轴上垂直居中(默认是stretch),主轴上采用space-between,将自由空间分配到相邻元素之间。一般都要写上:
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
2. Flexbox的盒子元素要定义间距。

3. 数据

  • 请求失败问题
  • 字段问题问题
  • 状态不全问题

4. 性能

  • 显示性能
  • 运行性能
  • 交互性能

5. 安全

Requestly 是一个 Chrome 插件,用于修改网络请求,例如重定向 URL、修改请求/响应头等。

1. 实际应用场景

1. 使用这个插件,我们可以在UI样式走查中,把返回的数据改成和 UI 设计图上一样的数据,这样就可以快速做走查

2. 对请求的返回数据做修改,以快速验证前端展示等功能

3. 接口文件的 mock

4. 等更多使用场景自己去发现

2. 插件安装

通过 Chrome 插件地址下载安装,并固定到菜单栏上,点击打开 Requestly 设置页面;

i0GNos-14-04-d0hI8a

3. 简单使用介绍

我们主要说下请求返回数据的修改,比较常用的就是这个;

1. 打开 app,新建规则,选择 Modify API Response

wps_doc_7-14-08-mvTUFE
wps_doc_1-14-08-hOgEPq

2. 设置名称,填入重写的 URL,填入修改的返回,最后保存

wps_doc_2-14-08-sHwIE6

3. 打开使用开关,在需要修改的页面打开调试面板,请求网络,这时就能按规则重写返回

wps_doc_3-14-09-gGMCfO
wps_doc_4-14-09-RxBcEw

4. 有时我们需要动态修改数据,设置 Dynamic 通过 JavaScript 脚本去修改返回值

wps_doc_5-14-09-yPgr4D
wps_doc_6-14-09-tCmhsH

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
function modifyResponse(args) {
const {
method,
url,
response,
responseType,
requestHeaders,
requestData,
responseJSON,
} = args;
// Change response below depending upon request attributes received in args
const res = JSON.parse(response);
let { mobileLayout } = res?.value;
if (mobileLayout !== "GUIDEPAGE") {
const ress = { value: { ...res.value, mobileLayout: "GUIDEPAGE" } };
console.log(
`%cRequestly Modify Response %c`,
"color: #3c89e8; padding: 1px 5px; border-radius: 4px; border: 1px solid #91caff;",
null,
ress, 'origin', res
);
return JSON.stringify(ress);
}
return response;
}

1. Copixel 工具基本介绍和安装

Copixel 是一个 UI 还原走查的工具,目前只支持 Chrome 浏览器使用,需要去 Chrome 商店下载使用 下载地址;

2. Copixel 工具的使用

1. 点击插件使用

UPvroX-14-22-OwH3tV

2. 从 Figma 导出设计图,并添加到图片中;

wps_doc_1-14-24-AUlp6E

3. 打开页面,调整窗口宽度,使用校稿工具比对

wps_doc_2-14-24-UAjIpC

4. 通过截图软件记录问题的地方

wps_doc_3-14-24-zJDraz

3. 浏览器屏幕宽度快捷设置

wps_doc_4-14-24-dWN0vY

4. 其他相关

官方的指南: Copixel走查验收插件使用指南-对外版

1. 常用的文件类型规范

  • 客户端基本上都是ES的开发规范;
  • nodejs服务端项目基本上都是CJS的开发规范;
  • 近两年的nestjs框架才让服务端出现了ES的文件类型;

2. 介绍

1. CJS

CJS 是 CommonJS的缩写,通过 module|require 来导入导出:

1
2
3
4
5
6
7
// importing 
const doSomething = require('./doSomething.js');

// exporting
module.exports = function doSomething(n) {
// do something
}
  • CJS通常是在Node中、Node服务端框架中。
  • CJS是同步导入模块
  • 你可以从node-modules中引入一个库或者从本地目录引入一个文件
  • CJS不能在浏览器中工作,它必须经过转换和打包
  • CJS的输出是运行时加载,输出的内容是值的拷贝而不是值的引用。

2. AMD

AMD代表异步模块定义

1
2
3
4
define(['dep1', 'dep2'], function (dep1, dep2) { 
//Define the module value by returning a value.
return function () {};
});
  • AMD 是异步(asynchronously)导入模块的(因此得名)
  • 一开始被提议的时候,AMD 是为前端而做的(而 CJS 是后端)
  • AMD 的语法不如 CJS 直观。

3. UMD

UMD 代表通用模块定义(Universal Module Definition)

1
2
3
4
5
6
7
8
9
10
11
(function (root, factory) { 
if (typeof define === "function" && define.amd) {
define(["jquery", "underscore"], factory);
} else if (typeof exports === "object") {
module.exports = factory(require("jquery"), require("underscore"));
} else {
root.Requester = factory(root.$, root._);
}
}(this, function ($, _) {
// this is where I defined my module implementation var Requester = { // ... }; return Requester;
}));
  • 在前端和后端都适用(“通用”因此得名)
  • 与 CJS 或 AMD 不同,UMD 更像是一种配置多个模块系统的模式。这里可以找到更多的模式
  • 当使用 Rollup/Webpack 之类的打包器时,UMD 通常用作备用模块
  • UMD先判断支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。都不行就挂载到 window 全局对象上面去

4. ESM

ESM 代表 ES 模块。这是 Javascript 提出的实现一个标准模块系统的方案。
我相信你们很多人都看到过这个:

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react'

//or

import {useState,useEffect} from 'react';

//or

const demo=()=> 1;
export default demo;
//or
export {demo};
  • 在很多现代浏览器可以使用
  • 它兼具两方面的优点:具有 CJS 的简单语法和 AMD 的异步
  • ESM是编译时输出接口,输出的是值得引用。
  • ES6 模块不是对象,而是通过 export命令显式指定输出的代码,import时采用静态命令的形式。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。模块内部引用的变化,会反应在外部。
  • 在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。
  • 得益于 ES6 的静态模块结构,可以进行 Tree Shaking
  • ESM 允许像 Rollup 这样的打包器,删除不必要的代码,减少代码包可以获得更快的加载
  • 可以在 HTML 中调用,只要如下
1
2
3
4
<script type="module"> 
import {func1} from 'my-lib';
func1();
</script>

但是不是所有的浏览器都支持

3.结论

  • UMD 随处可见,通常在 ESM 不起作用的情况下用作备用
  • ESM 是异步的,适合前端,ESM输出是静态的,在编译时就能完成输出,输出的是值的引用,ESM(ESModule)是ECMAScript自己的模块化体系,具有简单的语法,异步特性和可摇树性,因此它是最好的模块化方案
  • CJS 是同步的,适合后端,CJS输出是一个对象,所以输出是在脚本运行完成后才执行,输出的是值的拷贝
  • AMD 是异步的,适合前端

1. 断点

在 Chrome 的 dev tools 里面断点是有多种方式的,但是最简单的断点方式对于调试一些 race condition 的代码是有负面影响的,特别是多重断点对于调试流程阻碍性很强。
那么就需要一些新的方式来组合使用保证调试到目标逻辑代码。

2. 代码断点

右键对应的行数,有相关菜单功能出来

wps_doc_0-16-18-3jQF7Z

1.Add breakpoint

最简单的基础断点功能

2. Add conditional breakpoint

具备条件执行断点功能的断点方式,允许通过调用执行代码环境的变量判断是否需要断点。
wps_doc_1-16-18-b8OiKv

表达式的执行允许修改变量的值

1
a = 5 && false

3. Add logpoint

日志断点,类似条件断点的 console.log({expression}) && false, 始终不会断点,但是可以不断输出环境里面的变量和计算的值。

3. 断点工具栏

断点控制栏,除了常见的暂停、return 等功能,我们可以看到还有最右边两个功能
wps_doc_2-16-18-iEx6tF

1. Deactivated breakpoint

禁止所有的断点

2. Pause on exceptions

当发生 uncaught exceptions 时,将会直接断点。
如果勾选了 Pause on caught exceptions 时,只要碰到 throw 关键字,会自动断点。

4. 网络断点与事件断点

1. 网络断点

网络断点将在发起网络请求的时候直接断点住对应代码,目前最大的作用是断点后修改其参数来实现一些目的。

2. 事件断点

目前来说事件断点在 react fiber 的实现下功能并没有特别大的用处,但是对于一些原生调试还是有用的。
wps_doc_3-16-18-ruYm6N

5. Dom 断点

Deactivated breakpoints 对 dom 断点无效
Dom 断点是一个特殊的功能,对于一些实现奇葩的情况下是可以直接找到修改 dom 代码的源头进行追踪代码的。
使用方式
wps_doc_4-16-18-ItHN05
1.Subtree modifications
当 children 发生变化的时候,将会断点到对应的代码
2.Attribute modifications
当 dom 属性发生变化的时候,将会断点到对应的代码
3.Node removal
当被选择的 dom 被删除的时候(如果是父级 dom 删除不算),将会断点到对应代码调用栈
wps_doc_5-16-18-VcZpIa

之前写文章用的图床是在微博,后面发现微博把这个给封了,这就让人很尴尬,最主要问题就是图片没了,之前的文章图片也没有备份,只能找寻新的图床,后面找到了 uPic 软件,开源的用着也还不错,主要是可以上传图片到 git 仓库,还有外链,图片还能备份,满足了我基本使用,今天的文章就是 uPicgitee 配合的基本配置;

1. uPic 软件下载

https://github.com/gee1k/uPic/releases GitHub 去下载最新版本的文件,解压缩托进应用程序文件夹,双击打开就可使用

2. gitee 配置

  1. 新建仓库

16-19-zNF3dd-gitee仓库创建

  1. Token 创建
      1. 进入码云 Token 创建页面
      1. 勾选 repo 访问权限。然后滚动页面到底部,点击Generate token按钮来生成 token

        16-19-J68X4y-Token生成1

      1. 复制生成好的 Token 值到 uPic token 输入框
        16-19-hkrtd9-Token生成2
        注意:此 Token 只会显示一次!请务必保存好,否则之后丢失了,就得重新创建

3. uPic 软件配置

打开 uPic 的偏好设置,选择图床,添加 Gitee 图床,输入我们刚才创建的仓库资料和 Token,配置完成可以点击验证,看是否能够成功;

保存路径我是设置为 uPic/{year}-{month}-{day}/{filename}-{hour}-{minute}-{random}{.suffix}

16-19-z3yzaa-uPic设置


16-19-4fmCAf-uPic上传设置


4. uPic 软件使用

一般使用的话我是选择文件上传

16-19-q8z5tJ-uPic选取文件


还有一个是拖拽文件直接上传

16-19-8WcCLu-uPic拖拽文件上传