注:React涉及到的东西不少,该文档按照个人习惯和认知所写。 所写内容是经过一定筛选的,所用到的东西都是被认为是好的解决方案的。 如果不满足这个条件,是不会写进去的
react的基本原理是,由于原生DOM操作耗费较大,然后通过React的render建立vdom来对应真实dom,在vdom改变的时候,通过对比改变的情况对真实DOM做操作,减少对真实DOM的操作的运行成本。
但是很多时候react的render也是要通过优化方式去阻止的
字典
- typescript类型定义解释:https://juejin.cn/post/6912314481716494349
ctrl+左键点击组件渲染的 dom 属性名可以直接看它的 typedef,如图所示:
- https://juejin.cn/post/7090094935663181831
- react 18 新特性解读:https://juejin.cn/post/7094037148088664078#heading-24
生命周期
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属性进行操作。一般用两种策略:
- 完全受控——将state运作交给React:
属性填入value,input变化时,立刻通过onChange,使得父容器重新渲染,改变填入的value属性 - 加入key完全不受控——将state运作交给Dom
属性填入defaultValue,且key为属性值。
input变化使得父容器重新渲染时,直接更换
但是这两种情况不符合所有的场景,例如加入表单验证,允许input进行修改但是回车时才进行提交,需要自己包装HOC,办法是加入状态缓存
ref
ref就是父组件保留对子组件的引用,然后直接对子组件进行操作时会用到。
创建
- 在
constructor中声明一个属性this.ref = createRef() - 渲染的组件树中,被挂载的组件加入
ref={this.ref}
这样,读取this.ref.current获得的就是最近一次渲染的对应组件节点/vdom了。
注意被挂载的组件不能是函数组件,如果想要这么做,需要:
- 函数组件通过
forwardRef包装 函数组件内使用
[useImperativeHandle](https://react.docschina.org/docs/hooks-reference.html#useimperativehandle),这时上层this.ref读到的就是其中暴露的属性了useRef
此外,函数组件的
useRef不但可以挂子组件,还可以挂载任何作用域内可变值,在通过useEffect可使得任何时候调用此值时,都是组件即时的结果(而不是产生该值时那一次渲染的结果)
该函数需要注意的:useRef返回的ref是引用不变的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,你需要将Editor加forwardRef包装,然后将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,如下图:
注意:
- effect内的函数在执行时,引用的函数内的值是该次渲染时的值。如果这些函数在渲染后且状态再次改变再执行,引用的函数内的值就不会是现在的值。需要将值加入依赖数组才可以消除这个bug(?
- 执行的时机:1. 首次渲染
建立,2. 之后渲染删除``建立,3. 卸载时删除
加依赖数组条件渲染,只有在数组元素中的值变化了(数组为空就是永不变化),才会执行2 useEffect在浏览器完成布局与绘制之后执行,如果effect对DOM做了变更,请使用useLayoutEffectuseContext
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挂载到被包装的组件,记得使用forwardRefFragment
https://zh-hans.reactjs.org/docs/react-api.html#reactfragment
可以使得组件返回多个无父节点的元素,减少一些DOM。
只要渲染返回的元素写成<>{elements}</>就可以了。Memo
https://zh-hans.reactjs.org/docs/react-api.html#reactmemoconst MemoCom = React.memo(com: React.FC, areEqual: (prevProps, nextProps) => boolean)
Memo组件通过记忆的方式,在属性不变时直接返回相同渲染结果而不进行render过程。
如果不通过areEqual决定是否更新就只是对props每一个字段作Object.is比较(props浅比较)来决定。性能优化(阻止渲染)
防止属性等变化
- 组件内的回调函数,如果不依赖组件属性,请使用提前定义的统一引用
- 传给子组件的对象/数组,一定使用
useMemo``useCallback等渲染时记忆函数优化。仅可视区渲染
在antd中有提到通过虚拟滚动的方法优化长列表,
分页,懒加载。减少DOM/组件嵌套
代补充immutable 约定
要求 react 的状态数据必须满足 immutable 性质,以满足在大型树组件中的效能。
为满足这一点,我们需要做的工作就是让引用类型对象的修改满足 immutable 性质。
树满足 immutable 性质定义为:对一个树形对象,修改其中的一个节点时,判等方法会认为仅有该节点以及该节点的父节点链发生变化。 注:这个性质定义并不是很严谨,没有关注树的子节点是否有序可重复的问题,以及改变树的多种可能操作等
用一张图概括:
假设我们不更换判等方法,要实现一个对象树的修改,需要将修改节点的上层所有父节点做浅拷贝。
这样是比较麻烦的。如果你想简化该过程,可以使用 immer.js
相关资源:[官方文档](https://immerjs.github.io/immer/)``[掘金](https://juejin.cn/post/6844904024693555213)
节点变化引起树变化的范围
一个节点变化,实际上引起树变化的范围,根据具体变化是不一样的。
下面列举了可能出现的情况。但是要知道,这些情况都是相对性的变化:
其它问题
- 为什么不使用
immutable.js
这个东西是完全定义了一个新的数据结构,已经不同于原生对象了。在应用时很容易需要来回toJS和fromJS,这样反而性能更慢了。 - wait to add
合成事件问题
在 react 中,使用节点事件只有在渲染的 jsx 元素上,以挂载属性的方式来声明事件。
react 自己实现了一套合成事件机制,重写了 dom 的事件模型,来处理 react 框架下的事件。
react 自己实现的事件机制有如下特点:
- 事件实际上挂载到
ReactDOM.render时所挂载的节点上(16版本是document上) react定义的各种参数类型等等和dom实际的并不完全相同,主要体现在ts类型定义不兼容
在dom事件的类型定义和react事件类型定义混用时,你需要对react中导入的类使用**as**加别名导入react隐去了addEventListener的用法
这对开发者是无感的,完全可以这么用,大多数情况不会出现问题。
但是在混用的情况下(尤其是对document这样的节点加监听时),就会出现合成事件的问题,包括:
同一个js中出现了全局dom的事件操作,以及reactdom的事件操作
Redux
redux 是一个
s, a => s',且state和view一一对应的框架。状态状态存在
store中,通过store.getState()取得。
- 一个
store对应一个reducer,创建时在createStore(reducer, initState)传入reducer
- 动作通过
store.dispatch(a)做出动作来改变状态
- 通过Action Creators函数创建
a
- 状态转换
reducer规定s, a => s'过程。注意s'不要通过直接改变s而是新建立来取得。 - 映射同步监听函数通过
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])的参数:
(state, [ownProps]) => stateProps
state是store的状态,ownProps是组件本来没有redux干涉传的props{[propName: string]: function}
是一个值为动作函数的对象,会将这些函数包装并同属性名分配到链接组件的props中,修改时调用
注意调用的是props.funcnameuseSelector
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 |
个人给中间件定义:
enhancer增强器,其满足的性质:wrapper包装器,功能是将原有的进行包装,变成一个更大的
-
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这两步都会做。
这意味着以下的场景要做到: 如果你的动作是执行异步请求并根据请求结果决定动作,透传的动作应该是无效动作。
- 如果你真的应用一个纯动作,就像没有saga,你不在saga里面做和这个动作相关的
take监听就行了
saga生成器函数就是一个纯对象action执行之前的全局控制处理器
生成器函数每一步yield都返回一个由saga api创建的Effect,saga将effect解释执行。
不过生成effect可以完全代表动作是如何处理的。
take:saga生成器暂停,直到监听到符合pattern条件的动作
注意put的新动作也可以触发相应的take使得saga effect产出yield
saga控制器是一个树形的结构,每一个节点都是一个生成器函数,其中子节点是fork出来的,就像fork进程一样。
这个生成器函数只在开始执行一次,
它使用生成器纯粹就只是为了易于测试。
异步动作情况分为两种:
- 动作本身需要时间获取,且获取途中无法继续做动作
- 执行动作,store改变后产生需要时间的作用。在作用执行完毕之前无法继续执行动作
React-Router
注:这里写的都是
v6的内容,和之前的版本差别很大的
[官方教程](https://reactrouter.com/docs/en/v6/getting-started/tutorial)
通过react-router和react-router-dom两个包。
基本使用
Link/NavLink
Link组件可以代替超链接,跳转到 url,但是:
- 在路由域内跳转不刷新!
- 浏览器左上角后退前进按照预期工作,也不用刷新!
NavLink 还可以帮你处理“当前选中”的情况,在此情形下应用新的 style 或 className,只需要:
<NavLink// 用一个函数返回 style 或 className// 当前 url 的路径值和 to 的值(注意这里是转到绝对路径)匹配时, isActive = truestyle={({ isActive }) => {return {display: "block",margin: "1rem 0",color: isActive ? "red" : "",};}}to={`/invoices/${invoice.number}`}key={invoice.number}>{invoice.name}</NavLink>
Routes
通过 BrowserRouter 嵌进去 Routes,然后里面嵌入 Route 工作。Route的子节点是子路由,可以嵌套接上。
在父路由的element(这里是<App/>)中加入<Outlet/>。
跳到子路由时,App可以正常渲染,且<Outlet/>这里就会变成子路由的节点,实现 UI 嵌套
<BrowserRouter><Routes><Route path="/" element={<App />}><Route path="expenses" element={<Expenses />} /><Route path="invoices" element={<Invoices />}>// 索引路由,和父节点共用路径,但是填补了没这一层路径时,<Invoices /> 中 outlet 的空白<Route indexelement={<main style={{ padding: "1rem" }}><p>Select an invoice</p></main>}/>/** 下面的写法,将 uri 输入的值匹配到 invoiceId 参数,* 然后在 Invoice 中通过 let { invoiceId } = useParams() 拿到* 类似于后端路由的写法*/<Route path=":invoiceId" element={<Invoice />} /></Route><Route path="*" // * 有特殊含义,仅适用于上面都匹配不着的时候element={<main style={{ padding: "1rem" }}><p>There's nothing here!</p></main>}/></Route></Routes></BrowserRouter>
得到url信息
路径
let location = useLocation()``let navigate = useNavigate()
{pathname: "/invoices",search: "?filter=sa",hash: "",state: null,key: "ae4cz2j"}
调用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,然后就没有然后了。
按照下面的方式运行命令。你发现了什么?:
PS E:\Front-end\dumi-trial> npm ls react react-dom @types/react @types/react-domdumi-trial@1.0.0 E:\Front-end\dumi-trial+-- @testing-library/react@12.1.4| `-- @types/react-dom@18.0.0| `-- @types/react@18.0.1+-- dumi@1.1.40| +-- @umijs/preset-dumi@1.1.40| | +-- @umijs/runtime@3.5.22| | | +-- @types/react-router@5.1.12| | | | `-- @types/react@18.0.1 deduped| | | `-- @types/react-router-dom@5.1.7| | | `-- @types/react@18.0.1 deduped| | `-- @umijs/types@3.5.22| | `-- @umijs/renderer-react@3.5.22| | +-- @types/react@16.14.24| | +-- @types/react-dom@16.9.14| | | `-- @types/react@16.14.24 deduped| | `-- @types/react-router-config@5.0.6| | `-- @types/react@18.0.1 deduped| `-- umi@3.5.22| +-- @umijs/preset-built-in@3.5.22| | +-- @types/react-router-config@5.0.2| | | `-- @types/react@18.0.1 deduped| | `-- @umijs/renderer-mpa@3.5.22| | +-- @types/react@16.14.24| | `-- @types/react-dom@16.9.14| | `-- @types/react@16.14.24 deduped| +-- UNMET PEER DEPENDENCY react@16.14.0| `-- react-dom@16.14.0`-- react@17.0.2npm ERR! peer dep missing: react@^16.14.0, required by react-dom@16.14.0
问:以上表格涉及到了几个版本的 react ?看到上面的情况,你觉得你的项目不会出问题?
这要是新人已经劝退了好吧。
解决方案:安装之前把package.json写死:react@^16.14.0dependenciesreact-dom@^16.14.0dependenciestypes\react@16.14.24devDependenciestypes\react-dom@16.9.14devDependencies
注意:
package.json中的依赖包名一定要按照字母顺序排列。不要问我是通过怎样的方法知道这一点的。
我对 npm 不熟,不知道安装之后在改这个然后npm i能不能解决问题;
但删package.lock.json和node_modules对我来说已经很熟练了。干啥啥不行,删库跑路第一名
做完以上操作之后,还是运行这个命令检查一下:npm ls react react-dom @types/react @types/react-dom
PS E:\Front-end\大创网页\json-schemaeditor-antd> npm ls react react-dom @types/react @types/react-domjson-schemaeditor-antd@1.0.0 E:\Front-end\大创网页\json-schemaeditor-antd+-- @testing-library/react@12.1.4| `-- @types/react-dom@16.9.14 deduped+-- @types/react@16.14.24+-- @types/react-dom@16.9.14| `-- @types/react@16.14.24 deduped+-- dumi@1.1.40| +-- @umijs/preset-dumi@1.1.40| | +-- @umijs/runtime@3.5.22| | | +-- @types/react-router@5.1.12| | | | `-- @types/react@16.14.24 deduped| | | `-- @types/react-router-dom@5.1.7| | | `-- @types/react@16.14.24 deduped| | `-- @umijs/types@3.5.22| | `-- @umijs/renderer-react@3.5.22| | +-- @types/react@16.14.24| | +-- @types/react-dom@16.9.14| | | `-- @types/react@16.14.24 deduped| | `-- @types/react-router-config@5.0.6| | `-- @types/react@16.14.24 deduped| `-- umi@3.5.22| +-- @umijs/preset-built-in@3.5.22| | +-- @types/react-router-config@5.0.2| | | `-- @types/react@16.14.24 deduped| | `-- @umijs/renderer-mpa@3.5.22| | +-- @types/react@16.14.24| | `-- @types/react-dom@16.9.14| | `-- @types/react@16.14.24 deduped| +-- react@16.14.0| `-- react-dom@16.14.0 deduped+-- react@16.14.0`-- react-dom@16.14.0
PS E:\Front-end\大创网页\json-schemaeditor-antd> npm ls react react-dom @types/react @types/react-domjson-schemaeditor-antd@1.0.0 E:\Front-end\大创网页\json-schemaeditor-antd+-- @testing-library/react@12.1.4| `-- @types/react-dom@16.9.14 deduped+-- @types/react@16.14.24+-- @types/react-dom@16.9.14| `-- @types/react@16.14.24 deduped+-- dumi@1.1.40| +-- @umijs/preset-dumi@1.1.40| | +-- @umijs/runtime@3.5.22| | | +-- @types/react-router@5.1.12| | | | `-- @types/react@16.14.24 deduped| | | `-- @types/react-router-dom@5.1.7| | | `-- @types/react@16.14.24 deduped| | `-- @umijs/types@3.5.22| | `-- @umijs/renderer-react@3.5.22| | +-- @types/react@16.14.24 deduped| | +-- @types/react-dom@16.9.14 deduped| | `-- @types/react-router-config@5.0.6| | `-- @types/react@16.14.24 deduped| `-- umi@3.5.22| +-- @umijs/preset-built-in@3.5.22| | +-- @types/react-router-config@5.0.2| | | `-- @types/react@16.14.24 deduped| | `-- @umijs/renderer-mpa@3.5.22| | +-- @types/react@16.14.24 deduped| | `-- @types/react-dom@16.9.14 deduped| +-- react@16.14.0 deduped| `-- react-dom@16.14.0 deduped+-- react@16.14.0`-- react-dom@16.14.0
问:你做完之后应该是哪一个输出?哪一个输出是正确的?有什么区别?
安装 dev 删包问题
问题描述: 在安装dev包的情况下,umi会卸载掉非常多其它的包,必须要在
npm i一下才行。这个问题还没解决。。。
修改测试配置
umi 对测试的默认配置是通过@umijs/test包给的配置来测试的。通过 jest 框架。
这个包的目录结构长这样:
很容易看出,createDefaultConfig.ts是创建 jest 配置的文件。
发现它传入两个参数,分别是cwd: string, args: IUmiTestArgs;生成 jest 配置。cwd很明显是当前工作路径,下面看args是什么类型:
export interface IUmiTestArgs extends Partial<ArgsType<typeof runCLI>['0']> {version?: boolean;cwd?: string;debug?: boolean;e2e?: boolean; // 是否支持 e2e 中缀(默认支持 spec, test)。下面可看出只有它有默认值为 truepackage?: string; // 对 lerna 多包项目测试时,测试的包名}
可以看出,它是继承自测试命令的参数。这里先忽略继承的 runCLI 函数的参数有什么用(若有必要作用,在这里忽略了,请大佬补充),只看这5个,然后扒一下bin目录里面umi-test.js看默认值:
const args = yParser(process.argv.slice(2), {alias: {watch: ['w'],version: ['v'],},boolean: ['coverage', 'watch', 'version', 'debug', 'e2e'],default: {e2e: true,},});
看得出来,这五个字段是参数,默认只有 e2e 为 true。简单看了下源代码,生成配置用的参数注释到了上面。
export default function (cwd: string, args: IUmiTestArgs) {const testMatchTypes = ['spec', 'test'];if (args.e2e) {testMatchTypes.push('e2e');}const isLerna = isLernaPackage(cwd);const hasPackage = isLerna && args.package;const testMatchPrefix = hasPackage ? `**/packages/${args.package}/` : '';const hasSrc = existsSync(join(cwd, 'src'));if (hasPackage) {assert(existsSync(join(cwd, 'packages', args.package!)),`You specified --package, but packages/${args.package} does not exists.`,);}return {collectCoverageFrom: ['index.{js,jsx,ts,tsx}',hasSrc && 'src/**/*.{js,jsx,ts,tsx}',isLerna && !args.package && 'packages/*/src/**/*.{js,jsx,ts,tsx}',isLerna &&args.package &&`packages/${args.package}/src/**/*.{js,jsx,ts,tsx}`,'!**/typings/**','!**/types/**','!**/fixtures/**','!**/examples/**','!**/*.d.ts',].filter(Boolean),moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],moduleNameMapper: {'\\.(css|less|sass|scss|stylus)$': require.resolve('identity-obj-proxy'),},setupFiles: [require.resolve('../../helpers/setupFiles/shim')],setupFilesAfterEnv: [require.resolve('../../helpers/setupFiles/jasmine')],testEnvironment: require.resolve('jest-environment-jsdom-fourteen'),testMatch: [`${testMatchPrefix}**/?*.(${testMatchTypes.join('|')}).(j|t)s?(x)`,],testPathIgnorePatterns: ['/node_modules/', '/fixtures/'],transform: {'^.+\\.(js|jsx|ts|tsx)$': require.resolve('../../helpers/transformers/javascript',),'^.+\\.(css|less|sass|scss|stylus)$': require.resolve('../../helpers/transformers/css',),'^(?!.*\\.(js|jsx|ts|tsx|css|less|sass|scss|stylus|json)$)':require.resolve('../../helpers/transformers/file'),},verbose: true,transformIgnorePatterns: [// 加 [^/]*? 是为了兼容 tnpm 的目录结构// 比如:_umi-test@1.5.5@umi-test// `node_modules/(?!([^/]*?umi|[^/]*?umi-test)/)`,],// 用于设置 jest worker 启动的个数...(process.env.MAX_WORKERS? { maxWorkers: Number(process.env.MAX_WORKERS) }: {}),};}
然后在自己的 node_modules 里面黑一下对应的 js 文件,把生成的参数拿出来看看:
config = {collectCoverageFrom: ['index.{js,jsx,ts,tsx}','src/**/*.{js,jsx,ts,tsx}','!**/typings/**','!**/types/**','!**/fixtures/**','!**/examples/**','!**/*.d.ts'],moduleFileExtensions: [ 'js', 'jsx', 'ts', 'tsx', 'json' ],moduleNameMapper: {'\\.(css|less|sass|scss|stylus)$': 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\identity-obj-proxy\\src\\index.js'},setupFiles: ['E:\\Front-end\\json-schemaeditor-antd\\node_modules\\@umijs\\test\\helpers\\setupFiles\\shim.js'],setupFilesAfterEnv: ['E:\\Front-end\\json-schemaeditor-antd\\node_modules\\@umijs\\test\\helpers\\setupFiles\\jasmine.js'],testEnvironment: 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\jest-environment-jsdom-fourteen\\lib\\index.js',testMatch: [ '**/?*.(spec|test|e2e).(j|t)s?(x)' ],testPathIgnorePatterns: [ '/node_modules/', '/fixtures/' ],transform: {'^.+\\.(js|jsx|ts|tsx)$': 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\@umijs\\test\\helpers\\transformers\\javascript.js','^.+\\.(css|less|sass|scss|stylus)$': 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\@umijs\\test\\helpers\\transformers\\css.js','^(?!.*\\.(js|jsx|ts|tsx|css|less|sass|scss|stylus|json)$)': 'E:\\Front-end\\json-schemaeditor-antd\\node_modules\\@umijs\\test\\helpers\\transformers\\file.js'},verbose: true,transformIgnorePatterns: []}
接下来,因为配置在 node_modules 里面,所以不能直接写。想要设置这个配置,得自己配置 .jest.config.ts 用来当 jest 的配置,架空 umi 默认的 umi-test 命令。
正好,给它抽离一下复制到自己的 .jest.config.ts 里面,如下:
注意:这个文件不应用其默认名称而是
.jest.config.ts,是因为打包时会误把这个文件的.d.ts打包里面
// @ts-ignoreimport { isLernaPackage } from '@umijs/utils';import { existsSync } from 'fs';import { join } from 'path';import type {Config} from '@jest/types';const testMatchTypes = ['spec', 'test', 'e2e'];const isLerna = isLernaPackage(process.cwd());const hasPackage = false;const testMatchPrefix = hasPackage ? `**/packages/1/` : '';const hasSrc = existsSync(join(process.cwd(), 'src'));const config: Config.InitialOptions = {collectCoverageFrom: ['index.{js,jsx,ts,tsx}',hasSrc && 'src/**/*.{js,jsx,ts,tsx}',isLerna && 'packages/*/src/**/*.{js,jsx,ts,tsx}','!**/typings/**','!**/types/**','!**/fixtures/**','!**/examples/**','!**/*.d.ts',].filter(dict => typeof dict === 'string') as string[],coveragePathIgnorePatterns: ["/node_modules/",".umi",".umi-production",'demos'],moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],moduleNameMapper: {'\\.(css|less|sass|scss|stylus)$': require.resolve('identity-obj-proxy'),},setupFiles: [require.resolve('@umijs/test/helpers/setupFiles/shim'),require.resolve('./config/test/setupTest')],setupFilesAfterEnv: [require.resolve('@umijs/test/helpers/setupFiles/jasmine')],testEnvironment: require.resolve('jest-environment-jsdom-fourteen'),testMatch: [`${testMatchPrefix}**/?*.(${testMatchTypes.join('|')}).(j|t)s?(x)`,],testPathIgnorePatterns: ['/node_modules/', '/fixtures/'],transform: {'^.+\\.(js|jsx|ts|tsx)$': require.resolve('@umijs/test/helpers/transformers/javascript',),'^.+\\.(css|less|sass|scss|stylus)$': require.resolve('@umijs/test/helpers/transformers/css',),'^(?!.*\\.(js|jsx|ts|tsx|css|less|sass|scss|stylus|json)$)':require.resolve('@umijs/test/helpers/transformers/file'),},verbose: true,transformIgnorePatterns: [// 加 [^/]*? 是为了兼容 tnpm 的目录结构// 比如:_umi-test@1.5.5@umi-test// `node_modules/(?!([^/]*?umi|[^/]*?umi-test)/)`,],// 用于设置 jest worker 启动的个数...(process.env.MAX_WORKERS? { maxWorkers: Number(process.env.MAX_WORKERS) }: {}),};console.log('cpu-pro: 已使用 jest.config.ts 作为 jest 配置文件。');export default config
主要的说明(jest配置文档)
- 通过看源码以及黑到的 config,可知源码里面
require.resolve给出以require形式引包的字符串对应的模块路径。现在抽出来放根目录了,也得改一下 args.package用来表明多包应用测试哪个包,是通过 umi 命令传进来的。这里去掉了args层,而且我这个也不是多包项目,所以先废了这个功能。如果有大佬知道怎么整上,还望指出- 我在这里加入了一个
setupTest.tsx文件,直接给添上了。 - 最后那句 console.log 是我拿来做特殊标记,来证明这个配置生效了。
然后把package.json的npm 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 标签引入即可一些认识
- 组件需要可以支持一部分dom api给根节点,尤其是style
组件被get ref的时候,能够返回一个有用的current对象做事情
dva
dva的作用是整合并统一了应用的数据流方案,首先,dva将数据流的管理分成了五个部分:
注册dva app
- 注册应用模型。模型是一个完整的有
redux系列特性的对象。 - 进行组件编写。
dva的connect会将模型值和dispatch函数传入。
在发送动作时,dispatch的动作类型是对模型的namespace下的reducers的动作的斜线链接形式 - 路由
-
自定义Hook
自定义hook的规律:
一旦涉及到非即时执行(意图使输入函数在实际调用后一段时间执行,只要有可能,就满足该条件),必然会涉及到
useEffect- 如果hook执行时改变了状态,请不要将其放入
useEffect的效果初始化中,这会导致二次渲染问题。
你可以选择用相同依赖的useMemo做这个初始化ahooks 库
[ahooks](https://ahooks.js.org/zh-CN/hooks)封装了很多的常用的hook逻辑。
这种情况下,我们应该根据应用场景去选择hook:异步加载Hook
从以下方面考虑异步操作的情况:
- 依赖项:异步数据作为函数输出的自变量是什么?如果自变量是异步返回结果该怎么办?
最复杂的可能性:大量异步数据呈树状依赖 - 异步操作是否返回值并影响组件渲染
- 异步操作进行时,是否遮蔽当前目标数据的渲染(读取时将原数据置为undefined或者promise,在给出相应响应即可)
- 加载触发时机,是渲染时还是外部触发
- 当前的异步请求是否可以被新的异步请求顶替(包括了如果不能顶替的条件验证,和ui上的响应)
考虑异步操作发生的情况:
- 如果rejected,如何处理
- 如果加载时,提交了相同数据的新请求,如何处理
- 如果加载时,组件没了,如何处理(尤其是外部操作加载的)
所以列下基本规则:
- 如果异步操作返回数据,将
null或undefined作为加载中的标志。
如果异步操作被中断,值还是加载中的值。
如果异步操作出错,将出错值返回。
如果需要或者异步操作不返回标记,加载中标记可以另设状态。 - 如果异步操作串行依赖,最好在全部加载完时统一赋值
写下了三个用于异步加载的hook,可以支持中断代替加载。
唯一的问题是,使用effect来派发加载,会导致初次加载时的二次渲染问题
考虑useEffect的挂载全部通过提前useMemo来解决
import React, { useState, useReducer, useEffect, useMemo } from "react"import { enAbort } from "./AbortablePromise"export interface AsyncState {error: any | undefinedloading: ((reason: any) => void) | undefinedresult: any | undefined}/*** 渲染时随依赖改变异步加载数据,支持未加载完成中断* @param loader 加载函数,参数自动拓展赋值为依赖数组* @param deps 依赖数组* @param clearNowResult 重新加载是否清空现有结果,默认覆盖* @returns 异步状态*/export function useAsync(loader: (...args: any[]) => Promise<any>,deps: React.DependencyList,clearNowResult = true) {const [asyncObj, setAsyncObj] = useState({error: undefined,loading: undefined,result: undefined,} as AsyncState)useEffect(() => {const [promise, abort] = enAbort(loader(...deps))setAsyncObj({loading: abort,error: undefined,result: clearNowResult ? undefined : asyncObj.result,})promise.then((result) => {setAsyncObj({ result, error: undefined, loading: undefined })},(reason) => {if (reason !== "覆盖中断")setAsyncObj(Object.assign({}, asyncObj, {result: asyncObj.result,error: reason,loading: undefined,}))})return () => {abort("覆盖中断")}}, deps)return asyncObj}/*** 触发时随依赖改变异步加载数据,支持未加载完成中断* @param loader 加载函数,参数自动拓展赋值为依赖数组* @param deps 依赖数组* @param clearNowResult 重新加载是否清空现有结果,默认覆盖* @returns [异步状态, 加载触发函数]*/export function useAsyncTrigger(loader: (...args: any[]) => Promise<any>,deps: React.DependencyList,clearNowResult = true) {const [asyncObj, setAsyncObj] = useState({error: undefined,loading: undefined,result: undefined,} as AsyncState)useEffect(() => {// 这个effect不建立只清除,防止卸载时还在加载return () => {if (asyncObj.loading) asyncObj.loading("覆盖中断")}}, deps)const cb = useMemo(() => {return (...args: any[]) => {const [promise, abort] = enAbort(loader(...args))// 中断旧加载,处理新加载if (asyncObj.loading) asyncObj.loading("覆盖中断")setAsyncObj({loading: abort,error: undefined,result: clearNowResult ? undefined : asyncObj.result,})promise.then((result) => {setAsyncObj({ result, error: undefined, loading: undefined })},(reason) => {if (reason !== "覆盖中断")setAsyncObj(Object.assign({}, asyncObj, {result: asyncObj.result,error: reason,loading: undefined,}))})}}, deps)return [asyncObj, cb]}/*** 通过监听器触发的,随依赖改变异步加载数据,支持未加载完成中断* @param onFunc 监听器挂载函数* @param offFunc 监听器卸载函数* @param loader 监听器需要挂载的函数,也就是加载函数* @param deps 依赖数组* @param clearNowResult 重新加载是否清空现有结果,默认覆盖* @returns 异步状态*/export function useAsyncListener(onFunc: (listener: (...args: any[]) => void) => void,offFunc: (listener: (...args: any[]) => void) => void,listener: (...args: any[]) => Promise<any>,deps: React.DependencyList,clearNowResult = true) {const [asyncObj, setAsyncObj] = useState({error: undefined,loading: undefined,result: undefined,} as AsyncState)// 这是一个 memo 返回实际触发的监听器函数const realListener = useMemo(() => {return (...args: any[]) => {const [promise, abort] = enAbort(listener(...args))// 中断旧加载,处理新加载if (asyncObj.loading) asyncObj.loading("覆盖中断")setAsyncObj({loading: abort,error: undefined,result: clearNowResult ? undefined : asyncObj.result,})promise.then((result) => {setAsyncObj({ result, error: undefined, loading: undefined })},(reason) => {if (reason !== "覆盖中断")setAsyncObj(Object.assign({}, asyncObj, {result: asyncObj.result,error: reason,loading: undefined,}))})}}, deps)useEffect(() => {onFunc(realListener)// 这个effect不建立只清除,防止卸载时还在加载return () => {if (asyncObj.loading) asyncObj.loading("覆盖中断")offFunc(realListener)}}, deps)return asyncObj}
Create React App
https://segmentfault.com/a/1190000039850941
Create React App 在未通过 eject 展开之前,webpack配置不会暴露。
个人使用文章中的方法改
webpack5 polyfill问题
按照所给的提示办法,修改webpack.config.js,加入resolve.fallback对应字段即可。
这里通过链接中给出的方式修改webpack配置
