分享
1、优雅的修改第三方库
小朋友,你是否有很多问号?



在第三方库有 bug,或者不能满足我们的使用需求的情况下。
PR 合并太慢,想立刻用上,但又要等发布?
等等等各种原因的情况下
如何优雅的修改第三方库?
1、 patch-package
2、 yarn v2+
yarn patch <package>
D:\code\GuGo\GuGoMainAppNew>yarn patch react
➤ YN0000: Package react@npm:19.0.0 got extracted with success!
➤ YN0000: You can now edit the following folder: C:\Users\utopia\AppData\Local\Temp\xfs-434126c1\user
➤ YN0000: Once you are done run yarn patch-commit -s "C:\Users\utopia\AppData\Local\Temp\xfs-434126c1\user" and Yarn will store a patchfile based on your changes.
➤ YN0000: Done in 0s 58ms
运行之后,会创建一个工作空间,然后完成修改之后
yarn patch-commit -s "xxxx"
之后会生成一个 patch 文件(需要通过版本管理提交到代码仓库)
以后运行 yarn install 就会自动打上 patch


以上两个方案都是基于 diff patch 的,方案 1 更通用一点,方案 2 依赖 yarn v2 的功能。
当然也完全可以基于 git 的 diff patch 来实现,但是还是需要手动执行 patch,以上两个方案都是会自动在 install 的阶段打上 patch ,完全无感,但是还是建议补全文档。
2、如何优雅的实现一个健壮的列表页面
前提条件:
- 设计规范统一
- 接口规范统一、幂等
- 业务逻辑大致相似
需求:
- 带
loading,error - 带分页可控分页参数
- 返回页面之后自动重加载
步骤
1、 先实现列表页的大致 UI 框架,通过 Props 修改可修改的部分。
2、 思考数据的处理方式
观察如下代码,是否有问题?
function ProductList() {
const [products, setProducts] = useState([]);
const [currentPage, setCurrentPage] = useState(1); // 当前页
const [pageSize, setPageSize] = useState(10); // 每页数量
const [totalCount, setTotalCount] = useState(0); // 总数量
const [isLoading, setIsLoading] = useState(false); // 页面loading状态
const [error, setError] = useState(null); // 页面error状态
useEffect(() => {
let isMounted = true; // 防止卸载后setState警告
const fetchData = async () => {
try {
setIsLoading(true);
const res = await fetch("/api/products");
if (!res.ok) throw new Error("请求失败");
const data = await res.json();
if (isMounted) setProducts(data);
} catch (err) {
if (isMounted) setError(err);
} finally {
if (isMounted) setIsLoading(false);
}
};
fetchData();
return () => {
isMounted = false;
};
}, []);
if (isLoading) return <div>加载中...</div>;
if (error) return <div>出错了:{error.message}</div>;
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
易于阅读、关联性强的写法:
const [listData, setListData] = useState({
users: [],
totalCount: 0,
});
const [pagination, setPagination] = useState({
currentPage: 1,
pageSize: 10,
});
const [uiState, setUiState] = useState({
isLoading: false,
error: null,
});
function fetchUsers() {
setUiState({ isLoading: true, error: null });
api
.getUsers(pagination.currentPage)
.then((res) => {
setListData({
users: res.data,
totalCount: res.total,
});
setUiState({ isLoading: false, error: null });
})
.catch((err) => {
setUiState({ isLoading: false, error: err.message });
});
}
是否所有的数据都需要用 useState 包裹?
totalCount 作为衍生属性,可以直接书写计算,不需要用 state 包裹或使用副作用更新。
借助 useEffect 的写法,此处只是拿 totalCount 来类比,实际可能是其他衍生属性:
const [listData, setListData] = useState([]);
const [totalCount, setTotalCount] = useState(0);
function addItem(item) {
setListData([...listData, item]);
}
function removeItem(itemId) {
setItems(listData.filter((i) => i.id !== itemId));
}
useEffect(() => {
setTotalCount(listData.length);
}, [listData]);
直接计算出来,无需 state、useEffect
const [listData, setListData] = useState([]);
function addItem(item) {
setListData([...listData, item]);
}
function removeItem(itemId) {
setItems(listData.filter((i) => i.id !== itemId));
}
// 这些都是计算出来的,不需要 state
const totalCount = listData.length;
// 如果是购物车价格的话
const totalPrice = listData.reduce((sum, item) => sum + item.price, 0);
对于不需要频繁更新的数据,可以使用 useMemo 来包裹:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
-
react中,可以考虑使用useMemo来实现带缓存的计算属性。 -
vue中的compute是一个自带缓存的方法,可以用来做一些性能优化,不必要将所有属性都挂载在data上(会被转换为响应式从而增加性能损失)。 -
vue中,对于一些外部引入的属性,可以在created阶段直接挂载在this上,避免性能损失(比如外部引入的字典类型)。 -
优雅的设计
store,对于衍生属性,使用getter方法来获取。
3、 如何封装成组件?
组件应该完成的本职工作:
- 渲染
loading,error - 根据
listData渲染列表 - 分页
pagination显示 - 卸载时清理
外部向组件传递的参数:
- 获取数据的
api pagination- 如果必要的话,自定义的
loading,error
拆分的 hook
function Page() {
const [otherQueryParams, setOtherQueryParams] = useState("xx");
const { data, loading, error, pagination } = usePagination(
({ pageNum, pageSize }) => {
return getList({
pageNum,
pageSize,
otherQueryParams,
});
},
[deps]
);
const { pageNum, pageSize, total, onChange } = pagination;
return (
<>
<List
loading={loading}
error={error}
run={run}
pageSize={pageSize}
pageNum={pageNum}
data={data}
/>
<Pagination
pageNum={pageNum}
pageSize={pageSize}
total={total}
onChange={onChange}
/>
</>
);
}
// 纯组件,只负责展示
function List({ data, loading, error, run, pageSize, pageNum }) {
if (loading) {
return <Loading />;
}
if (error) {
return <Error />;
}
return (
<ul>
{data.map((i) => (
<li>{i}</li>
))}
</ul>
);
}
关于 ahook 和 usePagination
感兴趣的可以自行封装一下,或者参考 ahook 源码。
3、 AbortController,优雅的取消一次请求并且防止内存泄漏
AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。
只有一个只读实例属性 signal 和一个实例方法 abort()。
如果页面组件已经卸载,而网络请求此时才完成,那么尝试 setState 的时候,有可能会导致内存泄漏。
// 创建控制器
const controller = new AbortController();
const signal = controller.signal;
// 发起请求时关联signal
fetch("https://api.example.com/data", { signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("请求已取消");
}
});
// 需要取消时调用
controller.abort();
当 abort() 被调用时,这个 fetch() promise 将 reject 一个名为 AbortError 的 DOMException。
import { useState, useEffect } from "react";
import { Card, CardBody, CardTitle } from "reactstrap";
const SafeDataFetcher = () => {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const fetchData = async () => {
try {
const response = await fetch("https://api.example.com/data", {
signal,
});
const result = await response.json();
setData(result);
} catch (error) {
if (error.name !== "AbortError") {
console.error("请求失败:", error);
}
}
};
fetchData();
// 组件卸载时取消请求
return () => {
controller.abort();
};
}, []);
return (
<Card>
<CardBody>
<CardTitle>安全数据加载</CardTitle>
{data ? JSON.stringify(data) : "加载中..."}
</CardBody>
</Card>
);
};
另外的思考,也可以实现一个可以中断的 Promise。

4、 如何在意料之外的 error 中保持页面不崩溃?
React 中大家都使用过 Suspense + lazy 来实现一个异步加载组件。
import React, { Suspense } from "react";
const LazyAaa = React.lazy(() => import("./Aaa"));
export default function App() {
return (
<div>
<Suspense fallback={"loading..."}>
<LazyAaa></LazyAaa>
</Suspense>
</div>
);
}
这样在组件加载完成之前,就会显示 loading...。
ErrorBoundary 的特性:
当子组件报错的时候,会把错误传递给它的 getDerivedStateFromError 和 componentDidCatch 方法。
getDerivedStateFromError 接收 error,返回一个新的 state,会触发重新渲染来显示错误对应的 UI。
componentDidCatch 接收 error 和堆栈 info,可以用来打印错误日志。
实现一个 ErrorBoundary
import { Component } from "react";
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
};
}
static getDerivedStateFromError(error) {
return { hasError: true, message: error.message };
}
componentDidCatch(error, errorInfo) {
console.log(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>出错了: {this.state.message}</div>;
}
return this.props.children;
}
}
这样如果页面出错,就会使得 hasError 为 true,组件有了兜底渲染内容,不至于使得页面崩溃。
上述是一个 class 组件,因为几个特性都是 class 组件中的内容,函数式组件没办法实现。
因此,在函数式编程中,可以使用 react-error-boundary 包来使用 ErrorBoundary
import { ErrorBoundary } from "react-error-boundary";
function Bbb() {
const b = window.a.b;
return <div>{b}</div>;
}
function fallbackRender({ error }) {
return (
<div>
<p>出错了:</p>
<div>{error.message}</div>
</div>
);
}
export default function App() {
return (
<ErrorBoundary fallback={fallbackRender}>
<Bbb></Bbb>
</ErrorBoundary>
);
}
任意层级的子组件报错都会找到最近的 ErrorBoundary
关于 ErrorBoundary 和 Suspense 的区别:
ErrorBoundary是捕获组件throw的错误Suspense是捕获组件throw的 promiseErrorBoundary仅能捕获渲染过程中的运行时错误,不能捕获Suspense的“等待”状态。
有兴趣的可以深入了解一下, Suspense 可以配合 use hook 来实现自动 loading。