FerryVip's Blog

技术分享

MacMini M4 本地部署运行 DeepSeek R1 大模型

一、模型介绍

DeepSeek R1 是由 DeepSeek 公司开发的一款基于 LLaMA 模型的开源大模型,它在多个基准测试中表现出色,包括 GSM8K、MATH、ARC、BBH 等。

下面介绍我们在自己的机器上部署该大模型。

二、部署工具

推荐使用 Ollama 框架,它支持 macOS 的本地模型管理,并简化了依赖安装流程:

  1. 安装 Ollama

    1. 官网下载安装
      官网下载安装
    2. 在终端执行命令一键安装:
    1
    curl -fsSL https://ollama.com/install.sh | sh

    验证安装:ollama -v

  2. 下载模型
    运行命令下载 7B 模型:

    1
    ollama run deepseek-r1:7b

23-02-46-下载执行
模型将自动下载至本地(约 4.7GB)。


三、交互方式

  1. 终端直接对话
    运行 ollama run deepseek-r1:7b 后输入问题即可,退出输入 /bye
    23-03-12-命令行运行

  2. Web 界面(推荐)
    使用 Open WebUI 提供类似 ChatGPT 的图形界面:

    1
    2
    3
    4
    5
    6
    docker run -d -p 3000:8080 \
    --add-host=host.docker.internal:host-gateway \
    -v open-webui:/app/backend/data \
    --name open-webui \
    --restart unless-stopped \
    ghcr.io/open-webui/open-webui:main

docker 启动 OpenWebUI
访问 http://localhost:3000,选择 deepseek-r1:7b 即可使用。

OpenWebUI初始化界面
OpenWebUI初始化界面

创建管理员账号
创建管理员账号

选择模型开始对话
选择模型开始对话

模型对话
模型对话

本地模型速度
本地模型速度


总结

通过 Ollama 框架,我们可以方便地在本地部署和运行 DeepSeek R1 大模型,实现高效的交互体验。在 MacMini M4 16GB 内存最低配的机器上,发现模型运行速度还是很不错的,基本可以满足日常使用。

Cursor Translate 翻译应用项目实战教程

0. 前言

这是一篇简单的从前端开发人员视角介绍 Cursor 编辑器的教程,主要介绍 Cursor 编辑器的安装和使用,以及如何使用 Cursor 编辑器生成代码,并按照产品需求,完善代码,并优化 UI 界面;

教程配套的 git 代码仓库: Cursor Translate 翻译应用项目实战

AI 交互的记录文档: Cursor Composer 记录

1. Cursor 介绍和安装

Cursor 是一个目前比较流程很火的 AI Code Editor,它可以帮助你快速生成代码,提高开发效率。

Cursor 官网介绍图

Cursor 官网 去下载对应的安装包,安装完成后,打开软件,会提示你注册,注册完成后,就可以开始使用了。

cursor 编辑器是基于 VSCode 开发的,所以基本和 VSCode 界面类似,也可以直接使用 VSCode 的插件。

09-35-07-reo9nx

2. Cursor Translate 翻译应用项目实战

2.1 创建项目文件夹,并使用 Cursor 打开

1
2
3
mkdir Translate && cd Translate
cursor .
# 通过命令行打开,或者使用 cursor 直接打开该文件目录

2.2 创建 README.md 文件,并写一些产品需求内容,技术选型等

1
2
3
4
touch README.md
# 创建好 README.md 文件后,建议给项目做一个 git init 初始化,方便后续的版本控制,当然这个不是必须的
cd Translate
git init

2.3 AI 生成代码使用

2.3.1 通过 Chat 交互模式,生成代码

  1. 打开 Chat 交互模式
  2. 输入需求内容,点击发送
  3. 等待 AI 生成代码
  4. 点击复制代码,复制到 README.md 文件中
  5. 点击保存,保存到本地
    Chat 交互模式

保存应用或者回退应用代码

2.3.2 通过 Composer 交互模式,生成代码

  1. 打开 Composer 交互模式
  2. 输入需求内容,点击发送
  3. 等待 AI 生成代码
  4. 直接生成文件和代码,不需要复制代码

Composer 交互模式

2.3.3 生成 Next.js 项目代码

  1. 刚才只是完善了 README.md 文件,现在我们来生成 Next.js 项目代码
  2. 打开 Chat 交互模式,问它 如何创建 Nextjs 项目,并跑起来
  3. 等待 AI 生成回复,这个地方比较好的方式是生成基本 Next.js 项目代码,然后从根目录复制代码进当前文件夹下面,这样可以避免 AI 生成的代码有问题,导致我们无法运行;
  4. 如果确定了技术栈,可以先生成初始代码后,在通过 cursor 打开项目文件,然后再继续下面的交互;
1
2
3
4
5
6
7
8
9
10
npx create-next-app@latest translate
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for `next dev`? … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
✔ What import alias would you like configured? … @/*

生成 Next.js 项目代码

2.3.4 让 AI 按照产品需求,完善代码

  1. 通过 Composer 交互模式,让 AI 按照产品需求,完善代码
  2. 待代码生成结束后,我们先点击 save all 保存所有代码,然后查看程序运行情况
  3. 确定代码可以使用后,选择 accept all 接受所有代码
  4. 一次生成的代码量不会太多,他会先生成基础代码,然后会有未完成的功能列表,让你去继续选择执行下面的功能,所以我们可以根据产品需求,选择执行下面的功能;
  5. 遇到需要安装依赖的,我们根据提示,安装依赖,然后继续执行下面的功能;
  6. 继续添加功能,直到所有功能都执行完毕;

按照产品需求,完善代码
第一次生成,初始样式

4.选取功能点让继续生成

继续执行功能生成

安装依赖

基本功能实现完毕

2.3.5 UI 界面优化精细化调整

方法 1:让其使用某个组件库如: shadcn ,并按照 Apple 的设计风格重新设计一下页面,并添加一些交互功能
使用组件
方法 2: 自己手绘草图或者 UI 设计图,让其调整页面,并添加一些交互功能
使用手绘草图,让其修改

2.3.6 使用的一些技巧

  1. 报错问题修复,直接在命令行中框选后,直接添加到 Chat 或者 Composer 中,让 AI 帮我们修复报错问题;
    报错问题修复

  2. 可以使用 Chat 模式处理一些简单的问题,如: 如何安装依赖,如何运行程序,如何打包程序等,使用 Composer 模式处理一些复杂的问题还有需要更改文件的地方; 模型能力上目前还是 claude 模型比较好,生成后需要我们自己去优化;

  3. .cursorrules 文件可以配置一些规则,让 AI 生成代码的时候,按照我们的规则生成,如: 生成的代码需要使用 typescript,生成的代码需要使用 tailwind css 等;

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
# .cursorrules 文件
你是一名资深前端开发专家,精通 ReactJS、NextJS、JavaScript、TypeScript、HTML、CSS 以及现代 UI/UX 框架(如 TailwindCSS、Shadcn、Radix)。
你思维缜密,能够提供细致入微的答案,并擅长逻辑推理。你总是谨慎地提供准确、事实性、深思熟虑的答案,并且在推理方面堪称天才。

严格遵循用户的需求,一丝不苟。
首先逐步思考——用伪代码详细描述你的构建计划。
确认后,再编写代码!
始终编写正确、符合最佳实践、遵循 DRY 原则(不要重复自己)、无错误、功能齐全且可运行的代码,同时确保代码符合以下列出的代码实现指南。
注重代码的简洁性和可读性,而非性能。
完全实现所有请求的功能。
不要留下任何待办事项、占位符或缺失的部分。
确保代码完整!彻底验证最终结果。
包含所有必需的导入,并确保关键组件的命名正确。
保持简洁,尽量减少其他描述。
如果你认为可能没有正确答案,请如实说明。
如果你不知道答案,请如实说明,而不是猜测。

### 编码环境
用户询问以下编程语言相关的问题:
- ReactJS
- NextJS
- JavaScript
- TypeScript
- TailwindCSS
- HTML
- CSS

### 代码实现指南
编写代码时请遵循以下规则:
- 尽可能使用提前返回来提高代码的可读性。
- 始终使用 Tailwind 类来为 HTML 元素设置样式;避免使用 CSS 或标签。
- 在类标签中尽可能使用 `class:` 而不是三元运算符。
- 使用描述性的变量和函数/常量名称。事件函数应以 `handle` 为前缀命名,例如 `onClick` 的事件函数命名为 `handleClick``onKeyDown` 的事件函数命名为 `handleKeyDown`
- 在元素上实现无障碍功能。例如,标签应具有 `tabindex="0"``aria-label``on:click``on:keydown` 等属性。
- 使用 `const` 而不是函数,例如 `const toggle = () =>`。如果可能,定义类型。
- 使用 `clsx` 来处理多个条件类。

  1. .cursorignore 该文件是和 .gitignore 一样的语法,可以配置忽略的文件和文件夹,减少 AI 索引文件的时间,提高生成代码的速度;

  2. 可以多个 Chat 和 Composer 分功能模块,进行 AI 交互代码生成,推荐一个插件 SpecStory (Cursor Extension),该插件可以导出 Chat 和 Composer 的交互记录;

Chat 和 Composer 的交互记录

3. 总结

  1. 通过使用 Cursor 编辑器,发现现在 AI 的能力越来越强,可以为我们提效很多;
  2. 目前需要学的是和 AI 沟通技巧,让 AI 按照我们的需求生成代码,如何让 AI 按照我们的需求优化代码;
  3. 做新产品和小工具通过 Cursor 是真的非常快,不过还是需要一定的产品和代码基础,不然容易出现 bug 等边界问题;

小红书分享

起因是小红书大量涌入美国的 tiktok 难民,但是小红书的内容大部分都是中文,想着能不能用 AI 工具去翻译一下,顺便熟悉下当前最流行的 cursor 编辑器,然后就有了这个项目.

项目主页

这是一个使用 cursor 编辑器通过自然语言写出的 AI 工具集合,目前主要包含以下功能:

  1. 小红书中英文翻译;
  2. AI 起中文名;
  3. 表情包内容解析;
  4. 小红书使用教程;
  5. 后续可能会再增加其他功能;

1. 项目展示

小红书中英文翻译 AI 起中文名 表情包内容解析 小红书使用教程
14-20-59-shotEasy-screencapture-xiaohongshushare-online 14-21-18-shotEasy-screencapture-xiaohongshushare-online(1) 14-21-32-shotEasy-screencapture-xiaohongshushare-online(2) 14-21-43-shotEasy-screencapture-xiaohongshushare-online(3)

在线访问: 小红书分享

代码仓库: xiaohongshushare

2. 项目运行

  1. 安装依赖
1
npm install --force
  1. 配置环境变量
1
2
3
4
cp .env.example .env
// 重命名为 .env
// 填入你的 DEEPSEEK_API_KEY 和 GEMINI_API_KEY
// DEEPSEEK_API_KEY 翻译和起名字用到 GEMINI_API_KEY 识别图片用到
  1. 运行项目
1
npm run dev

3. cursor 的项目开发过程

访问仓库下的 .specstory/history/小红书中英文翻译应用开发.md 查看详细的开发过程.


项目一开始的产品说明文档

小红书中英文翻译

主要功能点

  1. 用户文本框中输入文本,通过 AI 对内容进行翻译;
  2. 翻译的输出格式分为两种显示,一种是直接整体翻译输出显示,另一种是根据内容段落,分为原内容一段,翻译内容换行一段,交叉显示,有点像双语字幕的形式;
  3. 翻译内容可以做点击语音播报出来;
  4. 翻译历史展示,翻译数据存储起来;

技术选型

  1. 使用 nextjs 的全栈模式去开发功能;
  2. 使用 DeepSeek 大语言模型对内容做 AI 翻译; api 开发文档
  3. 部署到 vercel 上面,数据库使用 vercel-postgres 存储

ComfyUI安装

  1. ComfyUI Desktop 官方下载

选择对应的电脑系统版本下载,我这边是 Mac 系统,所以本文是基于 Mac 系统的安装教程

选择对应的系统下载

  1. 下载完成后,双击安装包,按照提示安装即可

    2.1 选择安装目录,直接选择默认目录即可

选择安装目录

2.2 选择合并之前的模型等文件目录,未安装直接选择默认目录即可

模型等存放目录

2.3 客户端设置,设置完成之后就开始安装软件的依赖
  1. 安装过程中可能会出现依赖安装失败的情况

3.1 出现依赖安装失败的情况,可以尝试重新安装依赖

1
2
3
4
5
# 出现下面的依赖安装错误
/Applications/ComfyUI.app/Contents/Resources/uv/macos/uv "pip" "install" "-r" "/Applications/ComfyUI.app/Contents/Resources/ComfyUI/requirements.txt"
# 出现这个问题有可能原因是网络问题,可以尝试使用国内的源去按照
/Applications/ComfyUI.app/Contents/Resources/uv/macos/uv pip install --dry-run -r /Applications/ComfyUI.app/Contents/Resources/ComfyUI/requirements.txt in /Users/m4/Documents/ComfyUI [200~-i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com

3.2 安装失败后重新打开软件,出现下面的界面
这个地方只能通过 continue 来继续安装,或者自己点击 install 来安装依赖
安装失败

实在没有解决只能卸载后,重新安装客户端,保证网络通畅

  1. 安装完成后,打开软件,会出现下面的界面,点击 Launch ComfyUI 来启动软件

启动页面

  1. 启动后成功进入 ComfyUI 界面,有个默认的工作流

默认工作流

  1. 页面设置成中文

系统设置中文

AIGODLIKE-COMFYUI-TRANSLATION插件设置成中文

通过搜索下载插件,点击下载安装,安装后需要重启软件才能生效

插件安装

插件翻译的比较全

  1. 运行默认工作流

软件刚刚安装完成,电脑内还没有模型,所以运行默认工作流会报错,需要先下载模型
模型缺失下载

默认工作流执行

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;
}