分享

1、优雅的修改第三方库

小朋友,你是否有很多问号?

alt text

alt text

alt text

在第三方库有 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

alt text

alt text

以上两个方案都是基于 diff patch 的,方案 1 更通用一点,方案 2 依赖 yarn v2 的功能。

当然也完全可以基于 gitdiff 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、 如何封装成组件?

组件应该完成的本职工作:

  • 渲染 loadingerror
  • 根据 listData 渲染列表
  • 分页 pagination 显示
  • 卸载时清理

外部向组件传递的参数:

  • 获取数据的 api
  • pagination
  • 如果必要的话,自定义的 loadingerror

拆分的 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>
  );
}

关于 ahookusePagination

ahook

usePagination

感兴趣的可以自行封装一下,或者参考 ahook 源码。

3、 AbortController,优雅的取消一次请求并且防止内存泄漏

MDN

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() promisereject 一个名为 AbortErrorDOMException

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。

alt text

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 的特性:

当子组件报错的时候,会把错误传递给它的 getDerivedStateFromErrorcomponentDidCatch 方法。

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

这样如果页面出错,就会使得 hasErrortrue,组件有了兜底渲染内容,不至于使得页面崩溃。

上述是一个 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

关于 ErrorBoundarySuspense 的区别:

  • ErrorBoundary 是捕获组件 throw 的错误
  • Suspense 是捕获组件 throw 的 promise
  • ErrorBoundary 仅能捕获渲染过程中的运行时错误,不能捕获 Suspense 的“等待”状态。

有兴趣的可以深入了解一下, Suspense 可以配合 use hook 来实现自动 loading

use