注:React涉及到的东西不少,该文档按照个人习惯和认知所写。 所写内容是经过一定筛选的,所用到的东西都是被认为是好的解决方案的。 如果不满足这个条件,是不会写进去的

react的基本原理是,由于原生DOM操作耗费较大,然后通过React的render建立vdom来对应真实dom,在vdom改变的时候,通过对比改变的情况对真实DOM做操作,减少对真实DOM的操作的运行成本。
但是很多时候react的render也是要通过优化方式去阻止的

字典

  1. typescript类型定义解释:https://juejin.cn/post/6912314481716494349
    ctrl+左键点击组件渲染的 dom 属性名可以直接看它的 typedef,如图所示:
    image.png
  2. https://juejin.cn/post/7090094935663181831
  3. react 18 新特性解读:https://juejin.cn/post/7094037148088664078#heading-24

生命周期

image.png

props/state

props通过在<Com propField = {value}/>的方式传入。

  • <Com>{children}</Com>当中的子组件children会以props.children传递到组件Com
    需注意:有些字段除外,如key
  • 只写字段名无=赋值时,字段值为true
  • 可以使用拓展运算符{...p}批量赋值

state在构造函数处赋初值,之后只能通过setState(s')通过assign的方法改变。

  • 函数组件使用const [state, changeStateFunc] = useState(initialState)做状态周期

props/state改变后,会通过shouldComponentUpdate进行检查,如果确实与之前不同,会触发渲染。

render

注意渲染只是react通过对比vdom的改变来选择性修改真实dom,从而控制真实dom的方式。

  • render生成的组件树和浏览器render得到的DOM树,层级不是一一对应的
  • 注意有些真实dom是有自身状态的(input),这一部分不受react控制。
    应当合适的处理他们,办法就是手动写代码使其受控h

    受控问题

    对于input组件,我们一般会从上层对value属性进行操作。一般用两种策略:
  1. 完全受控——将state运作交给React:
    属性填入value,input变化时,立刻通过onChange,使得父容器重新渲染,改变填入的value属性
  2. 加入key完全不受控——将state运作交给Dom
    属性填入defaultValue,且key为属性值。
    input变化使得父容器重新渲染时,直接更换

但是这两种情况不符合所有的场景,例如加入表单验证,允许input进行修改但是回车时才进行提交,需要自己包装HOC,办法是加入状态缓存

ref

ref就是父组件保留对子组件的引用,然后直接对子组件进行操作时会用到。

创建

  1. constructor中声明一个属性this.ref = createRef()
  2. 渲染的组件树中,被挂载的组件加入ref={this.ref}
    这样,读取this.ref.current获得的就是最近一次渲染的对应组件节点/vdom了。

注意被挂载的组件不能是函数组件,如果想要这么做,需要:

  1. 函数组件通过forwardRef包装
  2. 函数组件内使用 [useImperativeHandle](https://react.docschina.org/docs/hooks-reference.html#useimperativehandle),这时上层this.ref读到的就是其中暴露的属性了

    useRef

    此外,函数组件的useRef不但可以挂子组件,还可以挂载任何作用域内可变值,在通过useEffect可使得任何时候调用此值时,都是组件即时的结果(而不是产生该值时那一次渲染的结果)
    该函数需要注意的:

  3. useRef返回的ref引用不变的

  4. useRef传入的参数只是ref.current初始化值,只在初始化时生效(可以说只能用于第一次渲染)
    每次渲染ref.current会依据返回组件树中挂载的ref值确定,如果没有挂载,为null

react 很多时候都需要让数据immutable。但是也有一些时候还想让数据mutable
这就是要用useRef来实现了

转发(forwardRef)

以上直接获取的方式只能让一个组件获取render树中的组件/DOM,如果想要获取render树中组件渲染获取的组件/DOM,你必须要ref转发。
具体来说,<App>渲染了<Editor ref={ref}/>Editor渲染了<input/>
如果想让App中的ref拿到的是<input/>而不是Editor,你需要将EditorforwardRef包装,然后将input挂上Editor的ref,就可以了

向下传递被挂载的组件

如果你要将Ref得到的被挂载的组件传给它的子节点等等,
不要直接传递ref,而是传递一个返回ref的函数,
这样可以保证下面得到的返回值不是null等

组件类型

函数组件(Hook)

最佳实践:组件的render函数所有操作最好是由use包装,尤其是费时的操作(useMemo或更有效的统一缓存)
这样使得整个render函数的理想开销很小。
然后使用memo减少渲染,这样react的性能一般都是最理想的情况

useState

const [state, changeStateFunc] = useState(initialState)
状态钩子,给出了状态值和状态值改变函数。
可以有多个独立地运作

useEffect

useEffect(() => function | void, [])

useEffect用来替代生命周期函数的作用,但发挥作用的方式又和生命周期函数略有不同。

副作用钩子,第一个函数体是didmount/didupdate时执行的函数,返回的函数体是效果清除时使用的函数。
第二个数组是依赖列表,如果有的话,只有列表中元素变了之后的渲染时机,才会执行这个effect,如下图:
image.png
注意:

  • effect内的函数在执行时,引用的函数内的值是该次渲染时的值。如果这些函数在渲染后且状态再次改变再执行,引用的函数内的值就不会是现在的值。需要将值加入依赖数组才可以消除这个bug(?
  • 执行的时机:1. 首次渲染建立,2. 之后渲染删除``建立,3. 卸载时删除
    加依赖数组条件渲染,只有在数组元素中的值变化了(数组为空就是永不变化),才会执行2
  • useEffect在浏览器完成布局与绘制之后执行,如果effect对DOM做了变更,请使用useLayoutEffect

    useContext

    const value = useContext(myContext)
    context改变必定触发 整个Context子树的重新渲染,即使使用了memo或者shouldComponentUpdate。这一点不使用Hook也不会改变

    useMemo

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
    仅保证多次渲染的时候仅依赖属性没变时不重新计算。也就是说,切换在切回来还得重新计算。
    而且相同组件多个实例,对于相同依赖项,每个组件也都会各自重新计算。
    如果要更可靠的,建议自己建立公共缓存,然后访问

    useCallback

    useCallback(fn, deps)相当于useMemo(() => fn, deps)
    主要用于防止渲染子组件的callback随渲染而改变引用

    高阶组件(HoC)

    https://juejin.cn/post/6844903549013327880
    高阶组件不仅仅是属性传入组件然后渲染时拿出来安排上那么简单,还需要修改属性。
    注意高阶组件如果使用ref挂载到被包装的组件,记得使用forwardRef

    Fragment

    https://zh-hans.reactjs.org/docs/react-api.html#reactfragment
    可以使得组件返回多个无父节点的元素,减少一些DOM。
    只要渲染返回的元素写成<>{elements}</>就可以了。

    Memo

    https://zh-hans.reactjs.org/docs/react-api.html#reactmemo
    const MemoCom = React.memo(com: React.FC, areEqual: (prevProps, nextProps) => boolean)
    Memo组件通过记忆的方式,在属性不变时直接返回相同渲染结果而不进行render过程。
    如果不通过areEqual决定是否更新就只是对props每一个字段作Object.is比较(props浅比较)来决定。

    性能优化(阻止渲染)

    防止属性等变化

  1. 组件内的回调函数,如果不依赖组件属性,请使用提前定义的统一引用
  2. 传给子组件的对象/数组,一定使用useMemo``useCallback等渲染时记忆函数优化。

    仅可视区渲染

    在antd中有提到通过虚拟滚动的方法优化长列表,
    分页,懒加载。

    减少DOM/组件嵌套

    代补充

    immutable 约定

    要求 react 的状态数据必须满足 immutable 性质,以满足在大型树组件中的效能。
    为满足这一点,我们需要做的工作就是让引用类型对象的修改满足 immutable 性质。
    树满足 immutable 性质定义为:

    对一个树形对象,修改其中的一个节点时,判等方法会认为仅有该节点以及该节点的父节点链发生变化。 注:这个性质定义并不是很严谨,没有关注树的子节点是否有序可重复的问题,以及改变树的多种可能操作等

用一张图概括:
React - 图4
假设我们不更换判等方法,要实现一个对象树的修改,需要将修改节点的上层所有父节点做浅拷贝。
这样是比较麻烦的。如果你想简化该过程,可以使用 immer.js
相关资源:[官方文档](https://immerjs.github.io/immer/)``[掘金](https://juejin.cn/post/6844904024693555213)

节点变化引起树变化的范围

一个节点变化,实际上引起树变化的范围,根据具体变化是不一样的。
下面列举了可能出现的情况。但是要知道,这些情况都是相对性的变化:

其它问题

  1. 为什么不使用 immutable.js
    这个东西是完全定义了一个新的数据结构,已经不同于原生对象了。在应用时很容易需要来回toJSfromJS,这样反而性能更慢了。
  2. wait to add

    合成事件问题

在 react 中,使用节点事件只有在渲染的 jsx 元素上,以挂载属性的方式来声明事件。
react 自己实现了一套合成事件机制,重写了 dom 的事件模型,来处理 react 框架下的事件。
react 自己实现的事件机制有如下特点:

  1. 事件实际上挂载到ReactDOM.render时所挂载的节点上(16版本是document上)
  2. react定义的各种参数类型等等和dom实际的并不完全相同,主要体现在ts类型定义不兼容
    在dom事件的类型定义和react事件类型定义混用时,你需要对react中导入的类使用**as**加别名导入
  3. react隐去了addEventListener的用法

这对开发者是无感的,完全可以这么用,大多数情况不会出现问题。
但是在混用的情况下(尤其是对document这样的节点加监听时),就会出现合成事件的问题,包括:

  1. 同一个js中出现了全局dom的事件操作,以及reactdom的事件操作

    Redux

    redux 是一个s, a => s',且stateview一一对应的框架。

  2. 状态状态存在store中,通过store.getState()取得。

  • 一个store对应一个reducer,创建时在createStore(reducer, initState)传入reducer
  1. 动作通过store.dispatch(a)做出动作来改变状态
  • 通过Action Creators函数创建a
  1. 状态转换reducer规定s, a => s'过程。注意s'不要通过直接改变s而是新建立来取得。
  2. 映射同步监听函数通过store.subscribe(listener)绑定,状态变化时调用
  • 监听函数没有输入参数,且无执行者。请注意使用bind绑定或是传入箭头函数避免this错误问题

    react-redux

    https://www.yuque.com/marckon/react-redux.cn/api
    通过Provider store包裹子组件。
    组件被connect之后,redux在store状态变化时会将state映射成prop对象,然后合并到组件的props中。
    对于connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])的参数:
  1. (state, [ownProps]) => stateProps
    state是store的状态,ownProps是组件本来没有redux干涉传的props
  2. {[propName: string]: function}
    是一个值为动作函数的对象,会将这些函数包装并同属性名分配到链接组件的props中,修改时调用
    注意调用的是props.funcname

    useSelector

    const deepNodeState : any = useSelector(selector : Function, equalityFn? : Function)
    由于hooks在执行时必然意味着重新渲染,所以该方法大多数情况不会用
    用memo+connect,通过属性的方式接收很深层的值,可以做到不重新渲染。

    相关中间件

    redux 有许多的中间件来使默认的 redux 功能加强。下面列出一些会用到的中间件:
名称 中间件类型 功能 相关资源
undo wrapper 加入撤销重做的功能
thunk enhancer 可以使用function作为动作,在这个function中去dispatch纯动作
saga enhancer 给对应的action对应了effect https://zhuanlan.zhihu.com/p/114409848

个人给中间件定义:

  1. enhancer增强器,其满足的性质:
  2. wrapper包装器,功能是将原有的React - 图5进行包装,变成一个更大的
  3. other其它功能

    redux 中间件开发

    https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware

    redux-saga

    redux-saga在初始化时,会开始运行用户绑定的saga生成器函数。
    dispatch动作时,所有正在执行的saga生成器函数(包括root和fork的子生成器)内,等待这个动作的take收到,继续yield执行后面的如call``put之类的effect(等价于动作触发了生成器的步骤)。
    然后会把动作透传给reducer
    saga effect和传给reducer这两步都会做。
    这意味着以下的场景要做到:

  4. 如果你的动作是执行异步请求并根据请求结果决定动作,透传的动作应该是无效动作。

  5. 如果你真的应用一个纯动作,就像没有saga,你不在saga里面做和这个动作相关的take监听就行了

saga生成器函数就是一个纯对象action执行之前的全局控制处理器
生成器函数每一步yield都返回一个由saga api创建的Effect,saga将effect解释执行。
不过生成effect可以完全代表动作是如何处理的。
take:saga生成器暂停,直到监听到符合pattern条件的动作
注意put的新动作也可以触发相应的take使得saga effect产出yield
saga控制器是一个树形的结构,每一个节点都是一个生成器函数,其中子节点是fork出来的,就像fork进程一样。
这个生成器函数只在开始执行一次,
它使用生成器纯粹就只是为了易于测试。
异步动作情况分为两种:

  1. 动作本身需要时间获取,且获取途中无法继续做动作
  2. 执行动作,store改变后产生需要时间的作用。在作用执行完毕之前无法继续执行动作

    React-Router

    注:这里写的都是v6的内容,和之前的版本差别很大的

[官方教程](https://reactrouter.com/docs/en/v6/getting-started/tutorial)
通过react-routerreact-router-dom两个包。

基本使用

Link/NavLink

Link组件可以代替超链接,跳转到 url,但是:

  • 在路由域内跳转不刷新!
  • 浏览器左上角后退前进按照预期工作,也不用刷新!

NavLink 还可以帮你处理“当前选中”的情况,在此情形下应用新的 style 或 className,只需要:

  1. <NavLink
  2. // 用一个函数返回 styleclassName
  3. // 当前 url 的路径值和 to 的值(注意这里是转到绝对路径)匹配时, isActive = true
  4. style={({ isActive }) => {
  5. return {
  6. display: "block",
  7. margin: "1rem 0",
  8. color: isActive ? "red" : "",
  9. };
  10. }}
  11. to={`/invoices/${invoice.number}`}
  12. key={invoice.number}
  13. >
  14. {invoice.name}
  15. </NavLink>

Routes

通过 BrowserRouter 嵌进去 Routes,然后里面嵌入 Route 工作。
Route的子节点是子路由,可以嵌套接上。
在父路由的element(这里是<App/>)中加入<Outlet/>
跳到子路由时,App可以正常渲染,且<Outlet/>这里就会变成子路由的节点,实现 UI 嵌套

  1. <BrowserRouter>
  2. <Routes>
  3. <Route path="/" element={<App />}>
  4. <Route path="expenses" element={<Expenses />} />
  5. <Route path="invoices" element={<Invoices />}>
  6. // 索引路由,和父节点共用路径,但是填补了没这一层路径时,<Invoices /> 中 outlet 的空白
  7. <Route index
  8. element={
  9. <main style={{ padding: "1rem" }}>
  10. <p>Select an invoice</p>
  11. </main>
  12. }
  13. />
  14. /** 下面的写法,将 uri 输入的值匹配到 invoiceId 参数,
  15. * 然后在 Invoice 中通过 let { invoiceId } = useParams() 拿到
  16. * 类似于后端路由的写法
  17. */
  18. <Route path=":invoiceId" element={<Invoice />} />
  19. </Route>
  20. <Route path="*" // * 有特殊含义,仅适用于上面都匹配不着的时候
  21. element={
  22. <main style={{ padding: "1rem" }}>
  23. <p>There's nothing here!</p>
  24. </main>
  25. }
  26. />
  27. </Route>
  28. </Routes>
  29. </BrowserRouter>

得到url信息

路径

let location = useLocation()``let navigate = useNavigate()

  1. {
  2. pathname: "/invoices",
  3. search: "?filter=sa",
  4. hash: "",
  5. state: null,
  6. key: "ae4cz2j"
  7. }

调用navigate(url)可以按照 react-router 的跳转模式跳转到对应 url。
如果跳转时保留查询参数等,可以先提前拿到,跳转时加到 url 字符串后边儿。

查询参数

const [searchParams, setSearchParams] = useSearchParams();
searchParams.get('name')``setSearchParams({ name: 'abcd' })

umi+dva

开始先踩坑

react 版本问题

umi 本身依赖 16 的 react,package.json 中 react 可以是 17,而最近 react 18 又出来了!
为什么 react 18 的 .d.ts 的 FC 不带 children 了?我的 jsx 全都是不能加 children 的红。别说我是 ts 没写好,变红的组件都是组件库的!
你直接初始化一个 umi 项目,然后npm install,然后就没有然后了。
按照下面的方式运行命令。你发现了什么?:

  1. PS E:\Front-end\dumi-trial> npm ls react react-dom @types/react @types/react-dom
  2. dumi-trial@1.0.0 E:\Front-end\dumi-trial
  3. +-- @testing-library/react@12.1.4
  4. | `-- @types/react-dom@18.0.0
  5. | `-- @types/react@18.0.1
  6. +-- dumi@1.1.40
  7. | +-- @umijs/preset-dumi@1.1.40
  8. | | +-- @umijs/runtime@3.5.22
  9. | | | +-- @types/react-router@5.1.12
  10. | | | | `-- @types/react@18.0.1 deduped
  11. | | | `-- @types/react-router-dom@5.1.7
  12. | | | `-- @types/react@18.0.1 deduped
  13. | | `-- @umijs/types@3.5.22
  14. | | `-- @umijs/renderer-react@3.5.22
  15. | | +-- @types/react@16.14.24
  16. | | +-- @types/react-dom@16.9.14
  17. | | | `-- @types/react@16.14.24 deduped
  18. | | `-- @types/react-router-config@5.0.6
  19. | | `-- @types/react@18.0.1 deduped
  20. | `-- umi@3.5.22
  21. | +-- @umijs/preset-built-in@3.5.22
  22. | | +-- @types/react-router-config@5.0.2
  23. | | | `-- @types/react@18.0.1 deduped
  24. | | `-- @umijs/renderer-mpa@3.5.22
  25. | | +-- @types/react@16.14.24
  26. | | `-- @types/react-dom@16.9.14
  27. | | `-- @types/react@16.14.24 deduped
  28. | +-- UNMET PEER DEPENDENCY react@16.14.0
  29. | `-- react-dom@16.14.0
  30. `-- react@17.0.2
  31. npm ERR! peer dep missing: react@^16.14.0, required by react-dom@16.14.0

问:以上表格涉及到了几个版本的 react ?看到上面的情况,你觉得你的项目不会出问题?
这要是新人已经劝退了好吧。
解决方案:安装之前把package.json写死:
react@^16.14.0dependencies
react-dom@^16.14.0dependencies
types\react@16.14.24devDependencies
types\react-dom@16.9.14devDependencies

注意:package.json中的依赖包名一定要按照字母顺序排列。不要问我是通过怎样的方法知道这一点的。

我对 npm 不熟,不知道安装之后在改这个然后npm i能不能解决问题;
但删package.lock.jsonnode_modules对我来说已经很熟练了。干啥啥不行,删库跑路第一名
做完以上操作之后,还是运行这个命令检查一下:
npm ls react react-dom @types/react @types/react-dom

  1. PS E:\Front-end\大创网页\json-schemaeditor-antd> npm ls react react-dom @types/react @types/react-dom
  2. json-schemaeditor-antd@1.0.0 E:\Front-end\大创网页\json-schemaeditor-antd
  3. +-- @testing-library/react@12.1.4
  4. | `-- @types/react-dom@16.9.14 deduped
  5. +-- @types/react@16.14.24
  6. +-- @types/react-dom@16.9.14
  7. | `-- @types/react@16.14.24 deduped
  8. +-- dumi@1.1.40
  9. | +-- @umijs/preset-dumi@1.1.40
  10. | | +-- @umijs/runtime@3.5.22
  11. | | | +-- @types/react-router@5.1.12
  12. | | | | `-- @types/react@16.14.24 deduped
  13. | | | `-- @types/react-router-dom@5.1.7
  14. | | | `-- @types/react@16.14.24 deduped
  15. | | `-- @umijs/types@3.5.22
  16. | | `-- @umijs/renderer-react@3.5.22
  17. | | +-- @types/react@16.14.24
  18. | | +-- @types/react-dom@16.9.14
  19. | | | `-- @types/react@16.14.24 deduped
  20. | | `-- @types/react-router-config@5.0.6
  21. | | `-- @types/react@16.14.24 deduped
  22. | `-- umi@3.5.22
  23. | +-- @umijs/preset-built-in@3.5.22
  24. | | +-- @types/react-router-config@5.0.2
  25. | | | `-- @types/react@16.14.24 deduped
  26. | | `-- @umijs/renderer-mpa@3.5.22
  27. | | +-- @types/react@16.14.24
  28. | | `-- @types/react-dom@16.9.14
  29. | | `-- @types/react@16.14.24 deduped
  30. | +-- react@16.14.0
  31. | `-- react-dom@16.14.0 deduped
  32. +-- react@16.14.0
  33. `-- react-dom@16.14.0
  1. PS E:\Front-end\大创网页\json-schemaeditor-antd> npm ls react react-dom @types/react @types/react-dom
  2. json-schemaeditor-antd@1.0.0 E:\Front-end\大创网页\json-schemaeditor-antd
  3. +-- @testing-library/react@12.1.4
  4. | `-- @types/react-dom@16.9.14 deduped
  5. +-- @types/react@16.14.24
  6. +-- @types/react-dom@16.9.14
  7. | `-- @types/react@16.14.24 deduped
  8. +-- dumi@1.1.40
  9. | +-- @umijs/preset-dumi@1.1.40
  10. | | +-- @umijs/runtime@3.5.22
  11. | | | +-- @types/react-router@5.1.12
  12. | | | | `-- @types/react@16.14.24 deduped
  13. | | | `-- @types/react-router-dom@5.1.7
  14. | | | `-- @types/react@16.14.24 deduped
  15. | | `-- @umijs/types@3.5.22
  16. | | `-- @umijs/renderer-react@3.5.22
  17. | | +-- @types/react@16.14.24 deduped
  18. | | +-- @types/react-dom@16.9.14 deduped
  19. | | `-- @types/react-router-config@5.0.6
  20. | | `-- @types/react@16.14.24 deduped
  21. | `-- umi@3.5.22
  22. | +-- @umijs/preset-built-in@3.5.22
  23. | | +-- @types/react-router-config@5.0.2
  24. | | | `-- @types/react@16.14.24 deduped
  25. | | `-- @umijs/renderer-mpa@3.5.22
  26. | | +-- @types/react@16.14.24 deduped
  27. | | `-- @types/react-dom@16.9.14 deduped
  28. | +-- react@16.14.0 deduped
  29. | `-- react-dom@16.14.0 deduped
  30. +-- react@16.14.0
  31. `-- react-dom@16.14.0

问:你做完之后应该是哪一个输出?哪一个输出是正确的?有什么区别?

安装 dev 删包问题

问题描述: 在安装dev包的情况下,umi会卸载掉非常多其它的包,必须要在npm i一下才行。

这个问题还没解决。。。

修改测试配置

umi 对测试的默认配置是通过@umijs/test包给的配置来测试的。通过 jest 框架。
这个包的目录结构长这样:
image.png
很容易看出,createDefaultConfig.ts是创建 jest 配置的文件。
发现它传入两个参数,分别是cwd: string, args: IUmiTestArgs;生成 jest 配置。
cwd很明显是当前工作路径,下面看args是什么类型:

  1. export interface IUmiTestArgs extends Partial<ArgsType<typeof runCLI>['0']> {
  2. version?: boolean;
  3. cwd?: string;
  4. debug?: boolean;
  5. e2e?: boolean; // 是否支持 e2e 中缀(默认支持 spec, test)。下面可看出只有它有默认值为 true
  6. package?: string; // 对 lerna 多包项目测试时,测试的包名
  7. }

可以看出,它是继承自测试命令的参数。这里先忽略继承的 runCLI 函数的参数有什么用(若有必要作用,在这里忽略了,请大佬补充),只看这5个,然后扒一下bin目录里面umi-test.js看默认值:

  1. const args = yParser(process.argv.slice(2), {
  2. alias: {
  3. watch: ['w'],
  4. version: ['v'],
  5. },
  6. boolean: ['coverage', 'watch', 'version', 'debug', 'e2e'],
  7. default: {
  8. e2e: true,
  9. },
  10. });

看得出来,这五个字段是参数,默认只有 e2e 为 true。简单看了下源代码,生成配置用的参数注释到了上面。

  1. export default function (cwd: string, args: IUmiTestArgs) {
  2. const testMatchTypes = ['spec', 'test'];
  3. if (args.e2e) {
  4. testMatchTypes.push('e2e');
  5. }
  6. const isLerna = isLernaPackage(cwd);
  7. const hasPackage = isLerna && args.package;
  8. const testMatchPrefix = hasPackage ? `**/packages/${args.package}/` : '';
  9. const hasSrc = existsSync(join(cwd, 'src'));
  10. if (hasPackage) {
  11. assert(
  12. existsSync(join(cwd, 'packages', args.package!)),
  13. `You specified --package, but packages/${args.package} does not exists.`,
  14. );
  15. }
  16. return {
  17. collectCoverageFrom: [
  18. 'index.{js,jsx,ts,tsx}',
  19. hasSrc && 'src/**/*.{js,jsx,ts,tsx}',
  20. isLerna && !args.package && 'packages/*/src/**/*.{js,jsx,ts,tsx}',
  21. isLerna &&
  22. args.package &&
  23. `packages/${args.package}/src/**/*.{js,jsx,ts,tsx}`,
  24. '!**/typings/**',
  25. '!**/types/**',
  26. '!**/fixtures/**',
  27. '!**/examples/**',
  28. '!**/*.d.ts',
  29. ].filter(Boolean),
  30. moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
  31. moduleNameMapper: {
  32. '\\.(css|less|sass|scss|stylus)$': require.resolve('identity-obj-proxy'),
  33. },
  34. setupFiles: [require.resolve('../../helpers/setupFiles/shim')],
  35. setupFilesAfterEnv: [require.resolve('../../helpers/setupFiles/jasmine')],
  36. testEnvironment: require.resolve('jest-environment-jsdom-fourteen'),
  37. testMatch: [
  38. `${testMatchPrefix}**/?*.(${testMatchTypes.join('|')}).(j|t)s?(x)`,
  39. ],
  40. testPathIgnorePatterns: ['/node_modules/', '/fixtures/'],
  41. transform: {
  42. '^.+\\.(js|jsx|ts|tsx)$': require.resolve(
  43. '../../helpers/transformers/javascript',
  44. ),
  45. '^.+\\.(css|less|sass|scss|stylus)$': require.resolve(
  46. '../../helpers/transformers/css',
  47. ),
  48. '^(?!.*\\.(js|jsx|ts|tsx|css|less|sass|scss|stylus|json)$)':
  49. require.resolve('../../helpers/transformers/file'),
  50. },
  51. verbose: true,
  52. transformIgnorePatterns: [
  53. // 加 [^/]*? 是为了兼容 tnpm 的目录结构
  54. // 比如:_umi-test@1.5.5@umi-test
  55. // `node_modules/(?!([^/]*?umi|[^/]*?umi-test)/)`,
  56. ],
  57. // 用于设置 jest worker 启动的个数
  58. ...(process.env.MAX_WORKERS
  59. ? { maxWorkers: Number(process.env.MAX_WORKERS) }
  60. : {}),
  61. };
  62. }

然后在自己的 node_modules 里面黑一下对应的 js 文件,把生成的参数拿出来看看:

  1. config = {
  2. collectCoverageFrom: [
  3. 'index.{js,jsx,ts,tsx}',
  4. 'src/**/*.{js,jsx,ts,tsx}',
  5. '!**/typings/**',
  6. '!**/types/**',
  7. '!**/fixtures/**',
  8. '!**/examples/**',
  9. '!**/*.d.ts'
  10. ],
  11. moduleFileExtensions: [ 'js', 'jsx', 'ts', 'tsx', 'json' ],
  12. moduleNameMapper: {
  13. '\\.(css|less|sass|scss|stylus)$': 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\identity-obj-proxy\\src\\index.js'
  14. },
  15. setupFiles: [
  16. 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\@umijs\\test\\helpers\\setupFiles\\shim.js'
  17. ],
  18. setupFilesAfterEnv: [
  19. 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\@umijs\\test\\helpers\\setupFiles\\jasmine.js'
  20. ],
  21. testEnvironment: 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\jest-environment-jsdom-fourteen\\lib\\index.js',
  22. testMatch: [ '**/?*.(spec|test|e2e).(j|t)s?(x)' ],
  23. testPathIgnorePatterns: [ '/node_modules/', '/fixtures/' ],
  24. transform: {
  25. '^.+\\.(js|jsx|ts|tsx)$': 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\@umijs\\test\\helpers\\transformers\\javascript.js',
  26. '^.+\\.(css|less|sass|scss|stylus)$': 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\@umijs\\test\\helpers\\transformers\\css.js',
  27. '^(?!.*\\.(js|jsx|ts|tsx|css|less|sass|scss|stylus|json)$)': 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\@umijs\\test\\helpers\\transformers\\file.js'
  28. },
  29. verbose: true,
  30. transformIgnorePatterns: []
  31. }

接下来,因为配置在 node_modules 里面,所以不能直接写。想要设置这个配置,得自己配置 .jest.config.ts 用来当 jest 的配置,架空 umi 默认的 umi-test 命令。
正好,给它抽离一下复制到自己的 .jest.config.ts 里面,如下:

注意:这个文件不应用其默认名称而是.jest.config.ts,是因为打包时会误把这个文件的.d.ts打包里面

  1. // @ts-ignore
  2. import { isLernaPackage } from '@umijs/utils';
  3. import { existsSync } from 'fs';
  4. import { join } from 'path';
  5. import type {Config} from '@jest/types';
  6. const testMatchTypes = ['spec', 'test', 'e2e'];
  7. const isLerna = isLernaPackage(process.cwd());
  8. const hasPackage = false;
  9. const testMatchPrefix = hasPackage ? `**/packages/1/` : '';
  10. const hasSrc = existsSync(join(process.cwd(), 'src'));
  11. const config: Config.InitialOptions = {
  12. collectCoverageFrom: [
  13. 'index.{js,jsx,ts,tsx}',
  14. hasSrc && 'src/**/*.{js,jsx,ts,tsx}',
  15. isLerna && 'packages/*/src/**/*.{js,jsx,ts,tsx}',
  16. '!**/typings/**',
  17. '!**/types/**',
  18. '!**/fixtures/**',
  19. '!**/examples/**',
  20. '!**/*.d.ts',
  21. ].filter(dict => typeof dict === 'string') as string[],
  22. coveragePathIgnorePatterns: [
  23. "/node_modules/",
  24. ".umi",
  25. ".umi-production",
  26. 'demos'
  27. ],
  28. moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
  29. moduleNameMapper: {
  30. '\\.(css|less|sass|scss|stylus)$': require.resolve('identity-obj-proxy'),
  31. },
  32. setupFiles: [require.resolve('@umijs/test/helpers/setupFiles/shim'),
  33. require.resolve('./config/test/setupTest')],
  34. setupFilesAfterEnv: [require.resolve('@umijs/test/helpers/setupFiles/jasmine')],
  35. testEnvironment: require.resolve('jest-environment-jsdom-fourteen'),
  36. testMatch: [
  37. `${testMatchPrefix}**/?*.(${testMatchTypes.join('|')}).(j|t)s?(x)`,
  38. ],
  39. testPathIgnorePatterns: ['/node_modules/', '/fixtures/'],
  40. transform: {
  41. '^.+\\.(js|jsx|ts|tsx)$': require.resolve(
  42. '@umijs/test/helpers/transformers/javascript',
  43. ),
  44. '^.+\\.(css|less|sass|scss|stylus)$': require.resolve(
  45. '@umijs/test/helpers/transformers/css',
  46. ),
  47. '^(?!.*\\.(js|jsx|ts|tsx|css|less|sass|scss|stylus|json)$)':
  48. require.resolve('@umijs/test/helpers/transformers/file'),
  49. },
  50. verbose: true,
  51. transformIgnorePatterns: [
  52. // 加 [^/]*? 是为了兼容 tnpm 的目录结构
  53. // 比如:_umi-test@1.5.5@umi-test
  54. // `node_modules/(?!([^/]*?umi|[^/]*?umi-test)/)`,
  55. ],
  56. // 用于设置 jest worker 启动的个数
  57. ...(process.env.MAX_WORKERS
  58. ? { maxWorkers: Number(process.env.MAX_WORKERS) }
  59. : {}),
  60. };
  61. console.log('cpu-pro: 已使用 jest.config.ts 作为 jest 配置文件。');
  62. export default config

主要的说明(jest配置文档)

  1. 通过看源码以及黑到的 config,可知源码里面require.resolve给出以require形式引包的字符串对应的模块路径。现在抽出来放根目录了,也得改一下
  2. args.package用来表明多包应用测试哪个包,是通过 umi 命令传进来的。这里去掉了args层,而且我这个也不是多包项目,所以先废了这个功能。如果有大佬知道怎么整上,还望指出
  3. 我在这里加入了一个setupTest.tsx文件,直接给添上了。
  4. 最后那句 console.log 是我拿来做特殊标记,来证明这个配置生效了。

然后把package.jsonnpm test命令改成jest --config .jest.config.ts,就完成了。
相应的,test: coverage可以改成jest --config .jest.config.ts --coverage
实测有效。

father(打包)

打包时会忽略`

dumi

组件开发

大多情况看它的文档来就行了。只说一些难以察觉到的问题:

  • index.md中的 jsx,不应用一些特性,如@umijs/plugin-antd的 babel 按需读取做不到。
  • index.md中的 jsx 只适合写小 demo,大的 demo 写进./demos文件夹内(一定注意文件夹名),仅这个文件夹里面的文件才不会打包时打包进去。引入按照文档的说法通过 code 标签引入即可

    一些认识

  1. 组件需要可以支持一部分dom api给根节点,尤其是style
  2. 组件被get ref的时候,能够返回一个有用的current对象做事情

    dva

    dva的作用是整合并统一了应用的数据流方案,首先,dva将数据流的管理分成了五个部分:

  3. 注册dva app

  4. 注册应用模型。模型是一个完整的有redux系列特性的对象。
  5. 进行组件编写。
    dva的connect会将模型值dispatch函数传入。
    在发送动作时,dispatch的动作类型是对模型的namespace下的reducers的动作的斜线链接形式
  6. 路由
  7. 正式运行

    自定义Hook

    自定义hook的规律:

  8. 一旦涉及到非即时执行(意图使输入函数在实际调用后一段时间执行,只要有可能,就满足该条件),必然会涉及到useEffect

  9. 如果hook执行时改变了状态,请不要将其放入useEffect的效果初始化中,这会导致二次渲染问题。
    你可以选择用相同依赖的useMemo做这个初始化

    ahooks 库

    [ahooks](https://ahooks.js.org/zh-CN/hooks)封装了很多的常用的hook逻辑。
    这种情况下,我们应该根据应用场景去选择hook

    异步加载Hook

    从以下方面考虑异步操作的情况:
  • 依赖项:异步数据作为函数输出的自变量是什么?如果自变量是异步返回结果该怎么办?
    最复杂的可能性:大量异步数据呈树状依赖
  • 异步操作是否返回值并影响组件渲染
  • 异步操作进行时,是否遮蔽当前目标数据的渲染(读取时将原数据置为undefined或者promise,在给出相应响应即可)
  • 加载触发时机,是渲染时还是外部触发
  • 当前的异步请求是否可以被新的异步请求顶替(包括了如果不能顶替的条件验证,和ui上的响应)

考虑异步操作发生的情况:

  • 如果rejected,如何处理
  • 如果加载时,提交了相同数据的新请求,如何处理
  • 如果加载时,组件没了,如何处理(尤其是外部操作加载的)

所以列下基本规则:

  1. 如果异步操作返回数据,将nullundefined作为加载中的标志。
    如果异步操作被中断,值还是加载中的值。
    如果异步操作出错,将出错值返回。
    如果需要或者异步操作不返回标记,加载中标记可以另设状态。
  2. 如果异步操作串行依赖,最好在全部加载完时统一赋值

写下了三个用于异步加载的hook,可以支持中断代替加载。
唯一的问题是,使用effect来派发加载,会导致初次加载时的二次渲染问题
考虑useEffect的挂载全部通过提前useMemo来解决

  1. import React, { useState, useReducer, useEffect, useMemo } from "react"
  2. import { enAbort } from "./AbortablePromise"
  3. export interface AsyncState {
  4. error: any | undefined
  5. loading: ((reason: any) => void) | undefined
  6. result: any | undefined
  7. }
  8. /**
  9. * 渲染时随依赖改变异步加载数据,支持未加载完成中断
  10. * @param loader 加载函数,参数自动拓展赋值为依赖数组
  11. * @param deps 依赖数组
  12. * @param clearNowResult 重新加载是否清空现有结果,默认覆盖
  13. * @returns 异步状态
  14. */
  15. export function useAsync(
  16. loader: (...args: any[]) => Promise<any>,
  17. deps: React.DependencyList,
  18. clearNowResult = true
  19. ) {
  20. const [asyncObj, setAsyncObj] = useState({
  21. error: undefined,
  22. loading: undefined,
  23. result: undefined,
  24. } as AsyncState)
  25. useEffect(() => {
  26. const [promise, abort] = enAbort(loader(...deps))
  27. setAsyncObj({
  28. loading: abort,
  29. error: undefined,
  30. result: clearNowResult ? undefined : asyncObj.result,
  31. })
  32. promise.then(
  33. (result) => {
  34. setAsyncObj({ result, error: undefined, loading: undefined })
  35. },
  36. (reason) => {
  37. if (reason !== "覆盖中断")
  38. setAsyncObj(
  39. Object.assign({}, asyncObj, {
  40. result: asyncObj.result,
  41. error: reason,
  42. loading: undefined,
  43. })
  44. )
  45. }
  46. )
  47. return () => {
  48. abort("覆盖中断")
  49. }
  50. }, deps)
  51. return asyncObj
  52. }
  53. /**
  54. * 触发时随依赖改变异步加载数据,支持未加载完成中断
  55. * @param loader 加载函数,参数自动拓展赋值为依赖数组
  56. * @param deps 依赖数组
  57. * @param clearNowResult 重新加载是否清空现有结果,默认覆盖
  58. * @returns [异步状态, 加载触发函数]
  59. */
  60. export function useAsyncTrigger(
  61. loader: (...args: any[]) => Promise<any>,
  62. deps: React.DependencyList,
  63. clearNowResult = true
  64. ) {
  65. const [asyncObj, setAsyncObj] = useState({
  66. error: undefined,
  67. loading: undefined,
  68. result: undefined,
  69. } as AsyncState)
  70. useEffect(() => {
  71. // 这个effect不建立只清除,防止卸载时还在加载
  72. return () => {
  73. if (asyncObj.loading) asyncObj.loading("覆盖中断")
  74. }
  75. }, deps)
  76. const cb = useMemo(() => {
  77. return (...args: any[]) => {
  78. const [promise, abort] = enAbort(loader(...args))
  79. // 中断旧加载,处理新加载
  80. if (asyncObj.loading) asyncObj.loading("覆盖中断")
  81. setAsyncObj({
  82. loading: abort,
  83. error: undefined,
  84. result: clearNowResult ? undefined : asyncObj.result,
  85. })
  86. promise.then(
  87. (result) => {
  88. setAsyncObj({ result, error: undefined, loading: undefined })
  89. },
  90. (reason) => {
  91. if (reason !== "覆盖中断")
  92. setAsyncObj(
  93. Object.assign({}, asyncObj, {
  94. result: asyncObj.result,
  95. error: reason,
  96. loading: undefined,
  97. })
  98. )
  99. }
  100. )
  101. }
  102. }, deps)
  103. return [asyncObj, cb]
  104. }
  105. /**
  106. * 通过监听器触发的,随依赖改变异步加载数据,支持未加载完成中断
  107. * @param onFunc 监听器挂载函数
  108. * @param offFunc 监听器卸载函数
  109. * @param loader 监听器需要挂载的函数,也就是加载函数
  110. * @param deps 依赖数组
  111. * @param clearNowResult 重新加载是否清空现有结果,默认覆盖
  112. * @returns 异步状态
  113. */
  114. export function useAsyncListener(
  115. onFunc: (listener: (...args: any[]) => void) => void,
  116. offFunc: (listener: (...args: any[]) => void) => void,
  117. listener: (...args: any[]) => Promise<any>,
  118. deps: React.DependencyList,
  119. clearNowResult = true
  120. ) {
  121. const [asyncObj, setAsyncObj] = useState({
  122. error: undefined,
  123. loading: undefined,
  124. result: undefined,
  125. } as AsyncState)
  126. // 这是一个 memo 返回实际触发的监听器函数
  127. const realListener = useMemo(() => {
  128. return (...args: any[]) => {
  129. const [promise, abort] = enAbort(listener(...args))
  130. // 中断旧加载,处理新加载
  131. if (asyncObj.loading) asyncObj.loading("覆盖中断")
  132. setAsyncObj({
  133. loading: abort,
  134. error: undefined,
  135. result: clearNowResult ? undefined : asyncObj.result,
  136. })
  137. promise.then(
  138. (result) => {
  139. setAsyncObj({ result, error: undefined, loading: undefined })
  140. },
  141. (reason) => {
  142. if (reason !== "覆盖中断")
  143. setAsyncObj(
  144. Object.assign({}, asyncObj, {
  145. result: asyncObj.result,
  146. error: reason,
  147. loading: undefined,
  148. })
  149. )
  150. }
  151. )
  152. }
  153. }, deps)
  154. useEffect(() => {
  155. onFunc(realListener)
  156. // 这个effect不建立只清除,防止卸载时还在加载
  157. return () => {
  158. if (asyncObj.loading) asyncObj.loading("覆盖中断")
  159. offFunc(realListener)
  160. }
  161. }, deps)
  162. return asyncObj
  163. }

Create React App

https://segmentfault.com/a/1190000039850941
Create React App 在未通过 eject 展开之前,webpack配置不会暴露。
个人使用文章中的方法改

webpack5 polyfill问题

按照所给的提示办法,修改webpack.config.js,加入resolve.fallback对应字段即可。
这里通过链接中给出的方式修改webpack配置

配置 gh-pages

https://zhuanlan.zhihu.com/p/88481760