brickspert / blog Goto Github PK
View Code? Open in Web Editor NEW个人技术博客,博文写在 Issues 里。
个人技术博客,博文写在 Issues 里。
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
这是蚂蚁金服内部技术分享的文字稿,本次分享主要介绍了为什么要用 Hooks?以及如何使用 Umi Hooks 提效?
Umi Hooks http://github.com/umijs/hooks
大家好,我叫尽龙,来自体验技术部。社区名称叫 brickspert,砖家的意思。
自从 React 推出 React Hooks 后,就开始尝试使用 Hooks,并逐渐喜欢上了它。目前,几乎 100% 的组件都是使用 Hooks 开发。经过大半年的实践,在 Hooks 使用方面沉淀了一些经验,很高兴今天有机会能分享给大家。
在分享开始之前,我想了解下:“有多少同学目前已经在项目中大量使用 Hooks 了?”
嗯嗯,谢谢。看举手的同学,大概一半一半吧。没关系,听完今天的分享,我相信你一定有兴趣尝试下 Hooks 的。
React Hooks 是 react v16.8 的一个新特性,很佩服这么重磅的功能,在一个小版本中发布,说明 React 团队有足够的信心向上兼容。
为什么要放弃 Class,转用 Hooks 呢?在内部外部有很多争论,包括知乎也有类似提问。我们也不免俗套的要对比下 Class 和 Hooks 了。当然为了保证今天的分享效果,我肯定会偏向 Hooks 的(哈哈哈哈)。
Class 学习成本很高。首当其中的就是生命周期,多,太多了。不仅多,还会变!React v15 和 v16 就不一样。下面是我在网上随便找的一张图。
这个是 React v15 的生命周期,你都掌握了吗?你知道 v16 有什么变化吗?
之前无论你去哪里面试,基本都会有几个必问问题:
生命周期最重要,但是有很高的学习成本,需要大量实践才能积累足够的经验。当然,这几个问题回答不好,百分之八十以上的几率会挂掉。
当然不止是生命周期,this 也是一个很大的问题。你有没有在组件写很多 bind
?或者所有的函数都用箭头函数定义?
this.someFunction = this.someFunction.bind(this);
// 或
someFunction = ()=>{}
为什么要这样写呢?如果不写会有什么问题?哎呦,又多了一个面试题,你会吗?
对比 Class,Hooks 的学习成本可就太低了!掌握了 useState 和 useEffect,80% 的事情就搞定了。
Class 业务逻辑分散,实现一个功能,我要写在不同的生命周期里面,不聚合~
比如,如果你有个定时器,你一定要在 componentWillUnMount
去卸载。
再比如,我们要写一个请求用户信息的组件,当userId
变化时,要重新发起请求。我们就要在两个生命中期中写请求的逻辑。
相信上面的逻辑,大家也是经常会写的吧。
奥奥,sorry,上面的 componentWillReceiveProps
已经被废弃了,我们应该用 componentDidUpdate
来代替。
“咦,这是为啥呢?好好的为什么要废弃,不让这么用了?”
又来一个面试题!你知道答案吗?
而 Hooks 的业务逻辑就非常聚合了。上面的两个例子,改成 Hooks 你会写吗?
简直不要太简单!香啊!我可以提前下班了。
说到逻辑复用,很多同学会说 Class 的 Render Props 和 HOC(高阶组件)可以做逻辑复用!那我们看看 Class 的逻辑复用有多么的惨不忍睹。
首先我们看看 Render Props。
首先我们想复用监听 window size 变化的逻辑,开开心心的写了下面的代码。
然后,我又想复用监听鼠标位置的逻辑,我只能这么写了。
到这里你应该看到了问题所在。这简直就是地狱!我不忍心复用其它逻辑了。
我们放过 Render Props,来看看 HOC 吧。
如果你要问什么是 HOC,那我不得不推荐我的另外一篇文章《助你完全理解React高阶组件(Higher-Order Components)》。
哪怕你不知道 HOC 是啥,你也一定用过。比如 redux 的 connect。
上面的代码,我用了三个 HOC,分别是 redux 的 connect,react-intl 的 injectIntl,以及 AntD 的 Form.create()。
这是一个非常常见的用法。如果你光看代码,大概已经懵圈了。“我是谁?我在哪?我要干什么?”
这会我仿佛听见 HOC 在说:“我不仅让你看不懂我,我还很容易出各种问题。”
是的,HOC 很容易出问题。大家都往组件的 props 上面挂属性,万一有个重名的,那就只能说一句“不好意思,GG思密达”!
Hooks 来了,它表示,我要一个打五个!Render Props 和 HOC 联合起来也被我秒杀!
Hooks 表示,来十个,来一百个我也能打。
Hooks 最强的能力就是逻辑复用了,这是我最最最爱的能力了。
是的,我也不偏袒 Hooks,由于 React Hooks 的机制,如果用法不正确,会导致各种奇怪的闭包问题。
如果你要问 React Hooks 的机制是什么的话,我又要给你推荐一篇我之前写的文章了:《React Hooks 原理》。
那面对这个问题,怎么解呢?说实话,我也没有很好的解决办法。
但是,这可能也有好处。如果碰到想不明白的问题,那 99% 是由于闭包导致的,我们有很确定的方向去排查问题。
记住这句话,你可以少走很多弯路。
当然,说再多,吹再好,也没多大用。我上面讲的 Class 和 Hooks 的优缺点,网上的也有很多人讲,大家也肯定都看过。
用程序员的交流方式,就是“Talk is cheap,Show me the code.”。
亮剑吧!
接下来,我会用一个例子,让你折服,拜倒在 Hooks 的石榴裙下。如果你不服,咱们单独撕~
接下来,我们来实现一个最最最常见的组件。该组件接收 userId,然后发起网络请求,获得用户信息。
说白了,就是最简单的发起网络请求的组件。我们先用 Class 来实现看看。
这段代码,是最简单的网络请求。
美滋滋。但是少了点东西。网络请求,我们肯定要维护一个 loading 状态,保证用户体验比较好。
那我们加上吧。
这张图,我们增加了 loading 状态,在网络请求发起前,置为 true,在网络请求结束后,置为 false。
美滋滋。但是还是少点东西。userId 变化后,我要重新发起网络请求吧。
我们再加点代码吧。
我们增加了对 userId 变化的监听,如果 userId 变化后,重新发起请求。
这次稳了吧?
不不不,还不够。如果 userId 连续变化了 5 次,发送了 5 个网络请求,我们要保证总是最后一次网络请求有效。也就是经常说的的“竞态处理”或者“时序控制”。
我加!加还不行吗!
其实到这里,有些同学已经懵了。“你说的时序控制,听着很有道理,但我平时都没处理过这个问题,我看下你怎么实现的。”
确实,时序控制不算一个简单的问题,很多新手都不会解决这个问题。
稳了!到这里你觉得稳了吧。
还是年轻啊,小伙子。
如果用上面的代码来玩,你可能会偶尔碰到上面的警告。这个警告是怎么造成的呢?我说一下你就明白了。下面四个步骤执行,必会报警告:
看出问题了吗?组件已经卸载了,还去 setState,造成了内存溢出。
怎么解决呢?
在组件卸载的时候,放弃最后一次请求。
到这里为止,我们就完成了一个完美的网络请求。这次真结束了!
看下写了多少行代码。
除去空格,我们写了 38 行代码。实话说,38 行代码我能忍,但是这些逻辑我忍不了!回想下我们处理了多少逻辑:
关键这些逻辑是没办法复用的!每个项目可能有数十上百个组件会发网络请求,我就要写几十,几百遍这样的逻辑。想想我都难受。
说实话,我在写项目的时候经常会偷懒。要不就不写 loading,要不就不管竞态,要不就不管最后的内存溢出警告。
你有没有和我一样呢?嘿嘿。
言归正传,接下来就邀请 Hooks 登场了。
三下五除二,我们用 Hooks 实现了刚才所有的逻辑。
17 行!代码量减少了 50% 以上。好像还行!
但是,别忘了,Hooks 最重要的能力就是逻辑复用!这些逻辑我们完全可以封装起来!我们把刚才的逻辑全部封装起来!
useAsync 封装了刚才我们说的所有功能,一行代码完成了网络请求。
最后整个组件会长这样。
哇!我自己都佩服自己!简直了!美呆了,帅毙了,感觉自己无敌了!提前完成工作,下班回家!
通过这个例子,我想证明一个论点:“使用 Hooks 封装逻辑的能力,可以极大提高开发效率”。
这时候你肯定要问,useAsync 在哪里?给我瞧瞧?
useAsync 在这里,快来瞧,快来看啦!
useAsync 是 Umi Hooks 库的核心 Hooks 之一,Umi Hooks 提供了大量提炼自业务的 Hooks。一行代码真的可以实现很多功能!
当然,useAsync 不止包含上面说的功能,还支持“手动触发”执行,还支持“轮询”!
只要简单的配置一个 pollingInterval
,就能轮询发送请求了。快去试试啦!
接下来我们会介绍几个更牛的 Hooks 给大家认识!
AntD 的 Table 组件,想必大家在项目中经常用到吧!除了刚才异步请求的所有逻辑外,你还得处理其它的逻辑。
比如维护 page、pageSize、sorter、filter 的状态,还得考虑搜索条件变化后,重置 page 到第一页。这些逻辑光想想就头疼了,别说写了。
现在一行代码就可以实现了!useAntdTable,封装了所有的逻辑,只要一行代码!如图上所示,你只要 ...tableProps
,就可以了。这也许就是幸福的味道吧~
加载更多的场景,比如下面动图的场景,想必大家在工作中都写过。
这样一个加载更多的场景,我们要维护多少状态?写多少行逻辑?本来我打算写个 Class 实现的例子贴出来的,但是我放弃了,因为太难了~~
随便想想要处理的逻辑:
脑壳疼,真的脑壳疼。我会写,但是写起来真的好累。
还没完,一般产品同学还会要求,上拉加载更多......
这时候我们还得监听滚动位置,如果快到底了,触发加载更多。脑壳更疼了!
Umi Hooks 听到了你的求救,派出 useLoadMore 来拯救你了。一行代码就可以实现所有的功能!一个小时变一分钟,又可以早点下班了。
还有更好用的,比如 useDynamicList,下面的动态列表,一行代码搞定。
不仅是上面讲到的各种复杂逻辑可以封装。简单的逻辑封装起来也是极其好用的,比如 Boolean 值的管理。
我们一般控制 Modal,Popover 等显示隐藏的时候,都要维护一个 visible 状态。大概会是这样。
这样的逻辑,你写过多少遍?没有几千也有几百吧!
以后你就可以用 useBoolean 咯!
不仅是上面讲到的这些,我们还有很多很多的 Hooks。
比如 useSearch,就封装了通常异步搜索场景的逻辑,比如 debounce。
比如 useVirtualList,就封装了虚拟列表的逻辑。
比如 useMouse,封装了监听鼠标位置的逻辑。
比如 useKeyPress,封装了监听键盘按键的逻辑。
30+ Hooks 供您选择,并且我们仍然处于婴儿期,快速发展中。我们的愿景就是:封装别人的逻辑,让别人无逻辑可写。
如上面所述,我们现在还处于婴儿期,需要不断汲取能量,更多的 Hooks 正在路上!要实现“让别人无逻辑可写”的目标,还需继续奋斗。
大家应该都听过 useSWR 吧?是 zeit 公司开发的一个专门做网络请求的 Hooks,提供了很多新颖的思路,给了我们非常大的启发,github star 就像坐火箭一样。但在实际项目使用中,还是会有很多地方不符合蚂蚁内部的体系。但是它给我们非常大的启发,基于 swr 的思路,我们可以实现更强大的 useRequest!图上的能力,我们都要!
useRequest 目前已经处于内测期了,下个版本将会与大家见面!我们的目标是:所有的网络请求,只用 useRequest 就够了!
目前社区上 Hooks 相关的基础教程、进阶教程、原理深入、常见问题等文档都比较分散,我们准备向 Hooks 生态发展,提供各式各样的文章。以后学习 Hooks,使用 Hooks,找 Umi Hooks 就对了。
当然,生态方面目前正在规划中,预计年后启动。
Umi Hooks,你值得拥有。
我们目前处于发展阶段,欢迎大家一起共建。
你可以提 idea,我们负责实现。
你可以提 issue,我们负责改 bug。
你可以提 PR,将你封装的 Hooks 分享给大家,让更多人收益。
❤️期待您的参与。
这是 React 官方 2022.03.08 发表的文章《How to Upgrade to the React 18 Release Candidate》的译文,通过本文,可以对 React 18 的新特性有一个全面的认知。
接下来,我还会翻译其它几篇比较重要的 React 18 文章,以便以更好的姿势使用 React 18,关注不迷路。
今天,我们发布了 React 18 RC 版本。正如我们在 React Conf 上分享的那样,React 18 基于 concurrent 模式,带来了更多能力,同时提供了渐进升级的方法。在这篇文章中,我们会一步一步的带您升级到 React 18。
使用 @rc
标签来安装最新版 React
## npm
$ npm install react@rc react-dom@rc
## yarn
$ yarn add react@rc react-dom@rc
当你首次安装 React 18 的时候,你会看到如下警告
ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it’s running React 17. Learn more: https://reactjs.org/link/switch-to-createroot
React 18 提供了更合理的初始化 API,使用该 API,会自动启用 concurrent 模式:
// Before
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);
// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App tab="home" />);
同时我们将卸载方法从 unmountComponentAtNode
修改为 root.unmount
:
// Before
unmountComponentAtNode(container);
// After
root.unmount();
我们移除了 ReactDOM.render
函数的 callback
,因为当使用 Suspense 的时候,它会有问题:
// Before
const container = document.getElementById('app');
ReactDOM.render(<App tab="home" />, container, () => {
console.log('rendered');
});
// After
function AppWithCallbackAfterRender() {
useEffect(() => {
console.log('rendered');
});
return <App tab="home" />
}
const container = document.getElementById('app');
const root = ReactDOM.createRoot(container);
root.render(<AppWithCallbackAfterRender />);
最后,如果你使用 hydration 来实现了 SSR,需要将 hydrate
替换为 hydrateRoot
:
// Before
import { hydrate } from 'react-dom';
const container = document.getElementById('app');
hydrate(<App tab="home" />, container);
// After
import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = hydrateRoot(container, <App tab="home" />);
// Unlike with createRoot, you don't need a separate root.render() call here.
更多信息可见 Replacing render with createRoot
在 React 18 中,为了支持服务端的 Suspense 和流式 SSR,优化了 react-dom/server
的 API。
使用以下 API,将会抛出警告:
renderToNodeStream
:废弃 ⛔️️相反,对于 Node 环境中的流式传输,请使用:
renderToPipeableStream
:新增 ✨我们还引入了一个新的 API,以在现代边缘运行时环境支持流式 SSR 和 Suspense,例如 Deno 和 Cloudflare workers:
renderToReadableStream
:新增 ✨下面的两个 API 可以继续使用,但是不支持 Suspense:
renderToString
:限制 renderToStaticMarkup
:限制 下面的 API 没有变化:
renderToStaticNodeStream
更多信息可见Upgrading to React 18 on the server、New Suspense SSR Architecture in React 18
批处理是指:React 将多个状态更新,聚合到一次 render 中执行,以提升性能。
在 React 18 之前,只能在 React 自己的事件机制中使用批处理,而在 Promise、setTimeout、原生事件等场景下,是不能使用批处理的。
React 18 支持了更多场景下的批处理,以提供更好的性能。
// 在 React 18 之前,只有 React 事件,才会使用批处理
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会 re-render 一次,这就是批处理
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 会 render 两次,每次 state 变化更新一次
}, 1000);
使用 createRoot
初始化 React 18 之后,所有的状态更新,会自动使用批处理,不关心应用场景。
// React 18 之后,Promise、setTimeout、原生事件中,都会自动批处理
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会 re-render 一次,这就是批处理
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会 re-render 一次,这就是批处理
}, 1000);
这是一个 break change,但是我们希望这能提升你的产品性能。当然,你仍然可以使用 flushSync
来手动取消批处理,强制同步执行:
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React 更新一次 DOM
flushSync(() => {
setFlag(f => !f);
});
// React 更新一次 DOM
}
更多信息可见 Automatic batching for fewer renders in React 18
在 React 18 中,我们和三方库作者合作,定义了一些新的 API,以满足三方库在 concurrent 模式下特定场景的诉求。比如 styles 管理、外部状态管理、可访问性(accessibility)等场景。
为了支持 React 18,一些三方库可能需要用到下面的 API:
useId
是一个新的 Hook,支持在客户端和服务端生成唯一的 ID,同时避免 hydration
的不兼容。它可以解决在 React 17 及更低版本一直存在的问题。在 React 18 中,这个问题尤为重要,因为流式 SSR 返回的 HTML 片段是无序的。更多信息可见 Intent to Ship: useId
useSyncExternalStore
是一个新的 Hook,允许外部状态管理器,强制立即同步更新,以支持并发读取。这个新的 API 推荐用于所有 React 外部状态管理库。详情见 useSyncExternalStore overview post、useSyncExternalStore API details
useInsertionEffect
是一个新的 Hook,它可以解决 CSS-in-JS 库在渲染中动态注入样式的性能问题。除非你已经构建了一个 CSS-in-JS 库,否则我们不希望你使用它。这个 Hook 执行时机在 DOM 生成之后,Layout Effect 执行之前。更多信息可见 Library Upgrade Guide for style
React 18还为 concurrent 渲染引入了新的 API,例如 startTransition
和 useDeferredValue
,在即将发布的稳定版本中会分享更多相关内容。
未来,我们希望添加一个功能,允许 React 保存组件的状态,但移除 UI 部分。比如在返回旧的页面时,React 立即恢复之前的内容。为此,React 将使用之前保留的状态重新加载组件。
这个功能会给 React 项目带来非常好的体验,但要求组件支持 state 不变的情况下,组件多次卸载和重载。
为了检查出不合适的组件写法,React 18 在开发模式渲染组件时,会自动执行一次卸载,再重新加载的行为,以便检查组件是否支持 state 不变,组件卸载重载的场景。
在以前,React 加载组件的逻辑为:
* React mounts the component.
* Layout effects are created.
* Effect effects are created.
在 React 18 严格模式的开发环境,React 会模拟卸载并重载组件:
* React mounts the component.
* Layout effects are created.
* Effect effects are created.
* React simulates unmounting the component.
* Layout effects are destroyed.
* Effects are destroyed.
* React simulates mounting the component with the previous state.
* Layout effect setup code runs
* Effect setup code runs
更多信息可见: Adding Strict Effects to Strict Mode 、How to Support Strict Effects
当你第一次在测试用例中使用 createRoot
时候,你会看到以下警告:
The current testing environment is not configured to support act(…)
为了修复这个问题,你需要在执行用例之前设置 globalThis.IS_REACT_ACT_ENVIRONMENT
为 true
。
// In your test setup file
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
这个标记告诉 React,它在一个类似单元测试的环境中运行。如果你忘了使用 act,React 将打印一些有用的警告。
你也可以将标志设置为 false 来告诉 React 不需要 act。 这对于模拟浏览器环境的端到端测试很有用。
当然,我们希望测试库会自动为您加上这个配置。 例如,下一个版本的 React Testing Library 内置了对 React 18 的支持,无需任何额外配置。
更多信息可见:More background on the the act testing API and related changes
在此版本中,React 将放弃对 Internet Explorer 的支持。我们进行此更改是因为 React 18 中引入的新功能是基于现代浏览器开发的,部分能力在 IE 上是不支持的,比如 microtasks。
如果您需要支持 Internet Explorer,我们建议您继续使用 React 17。
之前写了一篇文章《React Hooks 使用误区,驳官方文档》,文中抛出了两个观点:
这两个观点引起了剧烈的讨论,当然大多数人还是持反对意见的,甚至质疑我不会用 Hooks,(⊙o⊙)… 我想说我写的 Hooks 比你吃的盐都多(开玩笑 😋 ~)
然后呢,知乎上来了个提问《如何看待《React Hooks 使用误区,驳官方文档》?》,大家依旧是讨论激烈,甚至 #黄玄 大佬也亲自来回答了。
很多同学极力反对我的观点,刚开始我还想一争高下,后来实在没精力一个一个对线。
这不,React 官方来帮我助阵了?React 官方为啥出 useEvent?就是发现以前要求的依赖写法,实在有太大问题,不加一个新的 API,官方示例都没法写了 🙂。
以前一直觉得 React Hooks 教程,包括 Dan 写的 useEffect 教程,都只是写了基础场景,对于稍微复杂点的场景,都避而不谈。因为这些复杂场景,在之前的规则下,确实是没法玩。
关于 useEvent 是什么,官方 RFC 文档有非常详细的解释,并且目前社区上也有非常多的文章介绍(其实很多介绍都是有问题的)。接下来用一个官方文档上的一个例子,来认识一下 useEvent。
需求很简单,我们希望 url 变化的时候,上报下当前 url
和 username
。
function Page({ route, currentUser }) {
useEffect(() => {
logAnalytics('visit_page', route.url, currentUser.name);
}, [route.url]);
// ...
}
如上代码,会有 warning,告诉我们 currentUser.name
要放到 deps 中。修正后代码是这样
function Page({ route, currentUser }) {
useEffect(() => {
logAnalytics('visit_page', route.url, currentUser.name);
}, [route.url, currentUser.name]);
// ...
}
但这样明显满足不了我们的业务需求,因为 currentUser.name
变化后,也触发了上报请求。
很多杠精就问,为啥你的需求要这样设计?为啥
currentUser.name
变化后不要上报?你的需求不合理吧?
这个你去问 dan 吧~
以前的解决方案可能有两个:
eslint-plugin-react-hooks
卸载掉currentUser.name
function Page({ route, currentUser }) {
const ref = useRef(currentUser.name);
ref.current = currentUser.name;
useEffect(() => {
logAnalytics('visit_page', route.url, ref.current);
}, [route.url]);
// ...
}
两个方案都有缺点:
基于 useEvent 改造起来就很简单了
function Page({ route, currentUser }) {
// ✅ Stable identity
const onVisit = useEvent(visitedUrl => {
logAnalytics('visit_page', visitedUrl, currentUser.name);
});
useEffect(() => {
onVisit(route.url);
}, [route.url]); // ✅ Re-runs only on route change
// ...
}
useEvent 会将一个函数「持久化」,同时可以保证函数内部的变量引用永远是最新的。如果你用过 ahooks 的 useMemoizedFn
,实现的效果是几乎一致的。
再强调下 useEvent 的两个特性:
useEvent 可以用来代替 useCallback,以前这样写,在 text
变化的时候,函数地址会变化。
function Chat() {
const [text, setText] = useState('');
// 🟡 A different function whenever `text` changes
const onClick = useCallback(() => {
sendMessage(text);
}, [text]);
return <SendButton onClick={onClick} />;
}
通过 useEvent 代替 useCallback 后,不用写 deps 函数了,并且函数地址永远是固定的,text
也永远是最新的。
function Chat() {
const [text, setText] = useState('');
// ✅ Always the same function (even if `text` changes)
const onClick = useEvent(() => {
sendMessage(text);
});
return <SendButton onClick={onClick} />;
}
useEvent 的实现原理比较简单,但现在看到的社区上的介绍文章几乎都有问题。
// (!) Approximate behavior
function useEvent(handler) {
const handlerRef = useRef(null);
// In a real implementation, this would run before layout effects
useLayoutEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args) => {
// In a real implementation, this would throw if called during render
const fn = handlerRef.current;
return fn(...args);
}, []);
}
上面的代码是官方提供的一个示例代码,需要重点注意这句注释 In a real implementation, this would run before layout effects
,翻译过来就是 “在真实的实现中,这里用的 Hooks 执行时机在 useLayoutEffect
之前”。
这里一定是不能用 useLayoutEffect
来更新 ref
的,因为子组件的 useLayoutEffect
比父组件的执行更早,如果这样用的话,子组件的 useLayoutEffect
中访问到的 ref
一定是旧的。
所以官方为了实现 useEvent
,一定是要加一个在 useLayoutEffect
之前执行的 Hooks 的,并且这个 Hooks 应该不会开放给普通用户使用的。
另外 React 要求不要在 render 中直接调用 useEvent
返回的函数,原理也是一样的,在 render 中访问的函数一定是旧的,因为 useLayoutEffect
还没执行呢。
在 React 18 之前,社区上有很多类似 useEvent
的实现,比如 ahooks 的 useMemoizedFn,类似下面这样
function useMemoizedFn(fn) {
const fnRef = useRef(fn);
fnRef.current = useMemo(() => fn, [fn]);
return useCallback((...args) => {
return fnRef.current.apply(args);
}, []);
}
之前很多同学问,为啥不用 useLayoutEffect
,是不是有问题?现在应该明白了吧?我们需要一个比useLayoutEffect
执行更早的 Hooks,很遗憾的是之前更没有,所以只能放到 render 中。
为什么之前官方没有提供类似的 Hooks?useMemoizedFn 有问题吗?
之前 React Issue #16956 上对类似的封装做了很多讨论,官方的态度一直是 “在 concurrent 下可能会存在问题” ,也就是官方也吃不准未来会不会出问题。随着 React 18 发布,concurrent 模式稳定之后,官方发现,这种写法不会有问题,索性就自己提供了一个。
在 React 18 之前,因为没有 concurrent,所以 useMemoizedFn 不会有任何问题。在 React 18 之后,我目前也没看到有什么问题。不过为了稳妥起见,后面 ahooks 的 useMemoizedFn 会做一次升级,向官方的 useEvent 看齐。
最后用知乎上一个同学的评论结尾“面多了加水,水多了加面”。
最近 React 18 发布后,部分改动对我们使用 React Hooks 有一些影响。这篇文章对官方的文档《Update to remove the "setState on unmounted component" warning》做了翻译,好让大家清晰的认识到这个改动的背景和影响。
这是 React 18 对 Hooks 的影响系列第一篇,后面我还会整理其它有影响的改动,关注不迷路。
之前在已经卸载的组件中调用 setState
时,会有一个警告,本次我们将这个警告移除了。警告内容如下:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
警告:不能在已经卸载的组件中更改 state。这是一个无用的操作,它表明你的项目中存在内存泄漏。要解决这个问题,请在 useEffect 清理函数中取消所有订阅和异步任务。
不幸的是,这个警告经常被误解,并且会误导大家。
它原本是想保证如下示例能正常工作的:
useEffect(() => {
function handleChange() {
setState(store.getState())
}
store.subscribe(handleChange)
return () => store.unsubscribe(handleChange)
}, [])
在这个例子中,如果你忘记了在 effect 清理函数中调用 unsubscribe
,那肯定是有内存泄漏的。
事实上,上面的场景并不常见。反而如下场景是更常见的:
async function handleSubmit() {
setPending(true)
await post('/someapi') // component might unmount while we're waiting
setPending(false)
}
在上面的代码中,如果发送请求时,组件卸载了,会抛出警告。但是,在这种场景下,警告误导了大家。
这里其实没有内存泄漏:
一般,我们会通过如下代码来消除警告:
let isMountedRef = useRef(false)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
async function handleSubmit() {
setPending(true)
await post('/someapi')
if (!isMountedRef.current) {
setPending(false)
}
}
实际上,这种写法是没用的,并没有解决所谓的“内存泄漏”,它仅仅只是抑制了警告。正如前面说的,这里其实是没有内存泄漏的。内存会随着 Promise 执行完而释放,并没有什么在无限执行。
上述抑制警告的解决方案,现在非常非常普遍。但它其实没任何好处,反而比不处理更糟糕:
未来,React 会提供一个新能力,在组件卸载不可见时,我们会保存组件现在的 state,但仍会卸载组件。下次加载组件的时候,我们会用之前保存的 state 来渲染组件,以便恢复之前的页面。
在组件卸载之后,setPending(false)
不会被执行到,所以 pending
会一直是 true
,那下次恢复组件的时候,看起来是请求没有执行完成,会变的更糟糕。(译者注:关于这一块详细的行为和影响面,下一篇文章介绍)
假设用户点击一个按钮,发起一个网络请求,请求结束后更新 state。为了避免这个警告,有些人会将请求行为放到 useEffect 中,因为在 useEffect 中可以监听到组件卸载,以忽略后续的 state 更新,消除警告。这样代码变的非常不清晰,非常糟糕!就是因为这个错误的警告,会让大家写出更烂的代码。
最终,我们决定移除这个警告。这个警告想解决的订阅问题,在日常代码中并不常见。大部分情况下,它反而会误导大家,为了避免告警写出更烂的代码。
希望这个警告的移除,可以让你移除代码中的 isMounted
。
在 ahooks 中的 useUnmountedRef、useSafeState 等都是为了解决这个警告而生的。同时我们在 ahooks 中必要的地方,为了避免这个告警,也会在组件卸载后,忽略后续的 setState
。
目前来看,这些代码是多余的,后续 ahooks 会陆续优化相关场景,但不会太快。因为在 react 16、17 中这个告警仍然会有,会对新人造成不必要的困扰。我们会等 React 18 覆盖面比较广之后,再进行代码优化。
以后在代码中大家应该不需要再考虑这个告警了,不需要再使用 useUnmountedRef、useSafeState 等 Hooks 了。
这篇文章只为说明一个问题:“Pull Request 与 Merge Request 有什么区别?”在我的想象中,有一双滑板鞋~不好意思,跑偏了。
在我的想象中,它俩肯定是不一样的,并且大部分人的想法应该和我是一样的,我先来说说我的想法。
如果经常用 Github,一定十分了解 Pull Request。
如果经常用 Gitlab,一定十分了解 Merge Request。
基于对 Github 和 Gitlab 的了解,我潜意识里感知到 Pull Request 与 Merge Request 是有区别的。
Github 一般是公开库,当然没有人愿意别人直接在自己的仓库上面修改代码。所以我们如果要给别人的仓库贡献代码,一般是要 fork 一个仓库,在自己的仓库改完后,给原仓库提交 PR 请求,请求原仓库主人把你的代码拉(pull)回去。
下图是一般的 Github 工作流程。
Gitlab 一般是私有库,一个团队维护一个仓库,通常大家会新建自己的分支,开发完成后,请求合并回主干分支。
下图是一般的 Gitlab 工作流程。
基于上面的认知,我起初觉得
说实话,我自己都说服不了自己,上面的理解是正确的。毕竟
我们来看看 gitlab 官方是怎么说的:
Merge or pull requests are created in a git management application and ask an assigned person to merge two branches. Tools such as GitHub and Bitbucket choose the name pull request since the first manual action would be to pull the feature branch. Tools such as GitLab and Gitorious choose the name merge request since that is the final action that is requested of the assignee.
大概意思就是 Merge Request 和 Pull Request 是同一个东西,仅仅只是名字不一样。
一般我们执行分支合并,需要执行下面两个命令:
git pull // 拉回需要合并的分支
git merge // 合并进目标分支
Github 选择了第一个命令来命名,叫 Pull Request。
Gitlab 选择了最后一个命令来命名,叫 Merge Request。
这个理由是 Gitlab 官方给的,我觉得还是可信的。所以我们的结论就是**“Pull Request 和 Merge Request”是一个东西**。
Pull Request 这个词起的真不好!我想起来我刚开始用 Github 的时候,根本看不懂 Pull Request 是干啥的。
如果我来起名的话,我应该会起这几个名字:
无论如何也想不到 Pull Request,一个好名字还是非常非常重要的。网上能搜到很多人问 Pull Request 是什么意思,然后大家的解释都差不多“请求别人拉你的代码”。如果当时不用这个名字,大家一看就明白了,也不用问了。
又让我想起了小白时期被 redux 中的名词支配的恐惧, reducer
等新名字,看的我一愣一愣的。
就像我在 github 的简介中写的话一样,一切都是纸老虎。
有时候人们很喜欢造一些名字很吓人的名词,让人一听这个名词就觉得自己不可能学会,从而让人望而却步。但是其实这些名词背后所代表的东西其实很简单。
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
react-refresh-webpack-plugin 是 React 官方提供的一个 模块热替换(HMR)插件。
A Webpack plugin to enable "Fast Refresh" (also previously known as Hot Reloading) for React components.
在开发环境编辑代码时,react-refresh 可以保持组件当前状态,仅仅变更编辑的部分。在 umi 中可以通过 fastRefresh: {}
快速开启该功能。
这张 gif 动图展示的是使用 react-refresh 特性的开发体验,可以看出,修改组件代码后,已经填写的用户名和密码保持不变,仅仅只有编辑的部分变更了。
对于 Class 类组件,react-refresh 会一律重新刷新(remount),已有的 state 会被重置。而对于函数组件,react-refresh 则会保留已有的 state。所以 react-refresh 对函数类组件体验会更好。
本篇文章主要讲解 React Hooks 在 react-refresh 模式下的怪异行为,现在我来看下 react-refresh 对函数组件的工作机制。
useState
和 useRef
的值不会更新。useEffect
、useCallback
、useMemo
等会重新执行。When we update the code, we need to "clean up" the effects that hold onto past values (e.g. passed functions), and "setup" the new ones with updated values. Otherwise, the values used by your effect would be stale and "disagree" with value used in your rendering, which makes Fast Refresh much less useful and hurts the ability to have it work with chains of custom Hooks.
如上图所示,在文本修改之后,state
保持不变,useEffect
被重新执行了。
在上述工作机制下,会带来很多问题,接下来我会举几个具体的例子。
import React, { useEffect, useState } from 'react';
export default () => {
const [count, setState] = useState(0);
useEffect(() => {
setState(s => s + 1);
}, []);
return (
<div>
{count}
</div>
)
}
上面的代码很简单,在正常模式下,count
值最大为 1
。因为 useEffect
只会在初始化的时候执行一次。
但在 react-refresh 模式下,每次热更新的时候,state
不变,但 useEffect
重新执行,就会导致 count
的值一直在递增。
如上图所示,count
随着每一次热更新在递增。
如果你使用了 ahooks 或者 react-use 的 useUpdateEffect
,在热更新模式下也会有不符合预期的行为。
import React, { useEffect } from 'react';
import useUpdateEffect from './useUpdateEffect';
export default () => {
useEffect(() => {
console.log('执行了 useEffect');
}, []);
useUpdateEffect(() => {
console.log('执行了 useUpdateEffect');
}, []);
return (
<div>
hello world
</div>
)
}
useUpdateEffect
与 useEffect
相比,它会忽略第一次执行,只有在 deps 变化时才会执行。以上代码的在正常模式下,useUpdateEffect
是永远不会执行的,因为 deps 是空数组,永远不会变化。
但在 react-refresh 模式下,热更新时,useUpdateEffect
和 useEffect
同时执行了。
造成这个问题的原因,就是 useUpdateEffect
用 ref
来记录了当前是不是第一次执行,见下面的代码。
import { useEffect, useRef } from 'react';
const useUpdateEffect: typeof useEffect = (effect, deps) => {
const isMounted = useRef(false);
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
export default useUpdateEffect;
上面代码的关键在 isMounted
useEffect
执行,标记 isMounted
为 true
useEffect
重新执行了,此时 isMounted
为 true
,就往下执行了最初发现这个问题,是 ahooks 的 useRequest
在热更新后,loading
会一直为 true。经过分析,原因就是使用 isUnmount
ref 来标记组件是否卸载。
import React, { useEffect, useState } from 'react';
function getUsername() {
console.log('请求了')
return new Promise(resolve => {
setTimeout(() => {
resolve('test');
}, 1000);
});
}
export default function IndexPage() {
const isUnmount = React.useRef(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
getUsername().then(() => {
if (isUnmount.current === false) {
setLoading(false);
}
});
return () => {
isUnmount.current = true;
}
}, []);
return loading ? <div>loading</div> : <div>hello world</div>;
}
如上代码所示,在热更新时,isUnmount
变为了true,导致二次执行时,代码以为组件已经卸载了,不再响应异步操作。
第一个解决方案是从代码层面解决,也就是要求我们在写代码的时候,时时能想起来 react-refresh 模式下的怪异行为。
比如 useUpdateEffect
我们就可以在初始化或者热替换时,将 isMounted
ref 初始化掉。如下:
import { useEffect, useRef } from 'react';
const useUpdateEffect: typeof useEffect = (effect, deps) => {
const isMounted = useRef(false);
+ useEffect(() => {
+ isMounted.current = false;
+ }, []);
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
export default useUpdateEffect;
这个方案对上面的问题二和三都是有效的。
根据官方文档,我们可以通过在文件中添加以下注释来解决这个问题。
/* @refresh reset */
添加这个问题后,每次热更新,都会 remount,也就是组件重新执行。useState
和 useRef
也会重置掉,也就不会出现上面的问题了。
本来 React Hooks 已经有蛮多潜规则了,在使用 react-refresh 时,还有潜规则要注意。但官方回复说这是预期行为,见该 issue。
Effects are not exactly "mount"/"unmount" — they're more like "show"/"hide".
不管你晕没晕,反正我是晕了,╮(╯▽╰)╭。
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
github: https://github.com/umijs/hox
别着急喷,我已经能想到你为什么会进来看这个文章了,当你看到这个题目的时候,你一定会有几连问:
基于 React Hooks 状态管理器的轮子太多了,你们再造一个有什么意思?
我并不是针对某个轮子,我只想说现有所有的轮子都囿于 redux
, unstated-next
的**,无非就是 action
, dispatch
, reducer
, useStore
, Provider
, Context
这些东西,在这些东西上做排列组合。概念一大堆,理解成本不低,用起来还都差不多。
为什么你敢说你们是“下一代”?
hox 够简单,一个 API,几乎无学习成本。够好用,你会用 Hooks,就会用 hox。我想象不到比我们更简单,更好用的轮子怎么造出来?
不想看,不想学,学不动了,咋办?
一个 API,眼睛一瞪就会用,没有任何学习成本。
你们够权威吗?你们会弃坑吗?
hox 的开发者来自蚂蚁金服体验技术部,我们有 umi、dva、antd、antv 等一堆开源软件,团队足够权威。
同时 hox 的**足够简单,放心用好了。
你们能完全替代 redux,dva 吗?
状态管理器解决的问题都一样,用 hox 完全可以实现所有需求。
hox 是完全拥抱 React Hooks 的状态管理器,model 层也是用 custom Hook 来定义的,它有以下几个特性:
下面我们进入正题,hox 怎么用?
任意一个 custom Hook ,用 createModel
包装后,就变成了持久化,且全局共享的数据。
import { createModel } from 'hox';
/* 任意一个 custom Hook */
function useCounter() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return {
count,
decrement,
increment
};
}
export default createModel(useCounter)
createModel
返回值是个 Hook,你可以按 React Hooks 的用法正常使用它。
import { useCounterModel } from "../models/useCounterModel";
function App(props) {
const counter = useCounterModel();
return (
<div>
<p>{counter.count}</p>
<button onClick={counter.increment}>Increment</button>
</div>
);
}
useCounterModel
是一个真正的 Hook,会订阅数据的更新。也就是说,当点击 "Increment" 按钮时,会触发 counter model 的更新,并且最终通知所有使用 useCounterModel
的组件或 Hook。
import { useCounterModel } from "./useCounterModel";
export function useCounterDouble() {
const counter = useCounterModel();
return {
...counter,
count: counter.count * 2
};
}
import { useCounterModel } from "./useCounterModel";
export function useCounterDouble() {
const counter = useCounterModel.data;
return {
...counter,
count: counter.count * 2
};
}
你肯定遇到过这样的场景:
如果你用 hox,故事就完全不一样了,你只需要把逻辑和数据层代码直接复制出去就完事了。
const CountApp = () => {
const [count, setCount] = useState(0)
const decrement = () => setCount(count - 1)
const increment = () => setCount(count + 1)
return (
<div>
count: {count}
<button onClick={increment}>自增</button>
<button onClick={decrement}>自减</button>
</div>
)
}
createModel
包一层就完事了。import { createModel } from 'hox';
/* 逻辑原样复制过来 */
function useCounter() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return {
count,
decrement,
increment
};
}
/* 用 createModel 包一下就行 */
export default createModel(useCounter)
import { useCounterModel } from "./useCounterModel";
export function CountApp() {
const {count, increment, decrement} = useCounterModel();
return (
<div>
count: {count}
<button onClick={increment}>自增</button>
<button onClick={decrement}>自减</button>
</div>
)
}
讲完了,核心内容很短,因为足够简单,更多内容可以见 github。如果你觉得 redux、dva 等太难学习,使用繁琐,如果你觉得 unstated-next
Provider 嵌套太多,太乱的话,不妨试试 hox,保证会给你全新的开发体验。
hox,下一代 React 状态管理器。
文末再打个广告:umi hooks 最好的 react hooks 逻辑库。
src/redux/reducers/counter.js
import {INCREMENT, DECREMENT, RESET} from '../actions/counter';
const initState = {
count: 0
};
export default function reducer(state = initState, action) {
switch (action.type) {
case INCREMENT:
return {
count: state.count + 1
};
case DECREMENT:
return {
count: state.count - 1
};
case RESET:
return {count: 0}; //这里是不是应该是 return 0
default:
return state
}
}
如果reset返回的不是0而是一个对象,console的结果会是
{ counter: { count: 0 } }
{ counter: { count: 1 } }
{ counter: { count: 0 } }
{ counter: { count: { count: 0 } } }
webpack testRedux.js build.js
这里我这么写会报错,最后用的是 webpack testRedux.js -o build.js
是不是因为我用的webpack版本是4.16.5?所以不一样
在 2021 年 6 月 8 号,React 公布了 v18 版本的发布计划,并发布了 alpha 版本。经过将近一年的发布前准备,在 2022 年 3 月 29 日,React 18 正式版终于和大家见面了。
React 18 应该是最近几年的一个重磅版本,React 官方对它寄予了厚望。不然也不会将 React 17 作为一个过渡版本,也不会光发布准备工作就做了一年。
在过去一年,我们已经或多或少了解到一些 React 18 的新功能。这篇文章我会通过丰富的示例,向大家系统的介绍 React 18 带来的改变。当然本文融入了很多个人理解,如有不对,烦请指正。
Concurrent Mode(以下简称 CM)翻译叫并发模式,这个概念我已经听了好多年了,并且一度非常担忧
随着对 CM 的了解,我发现它其实是人畜无害的。
CM 本身并不是一个功能,而是一个底层设计,它使 React 能够同时准备多个版本的 UI。
在以前,React 在状态变更后,会开始准备虚拟 DOM,然后渲染真实 DOM,整个流程是串行的。一旦开始触发更新,只能等流程完全结束,期间是无法中断的。
在 CM 模式下,React 在执行过程中,每执行一个 Fiber,都会看看有没有更高优先级的更新,如果有,则当前低优先级的的更新会被暂停,待高优先级任务执行完之后,再继续执行或重新执行。
CM 模式有点类似计算机的多任务处理,处理器在同时进行的应用程序之间快速切换,也许 React 应该改名叫 ReactOS 了。
这里举个例子:我们正在看电影,这时候门铃响了,我们要去开门拿快递。
在 React 18 以前,一旦我们开始看电影,就不能被终止,必须等电影看完之后,才会去开门。
而在 React 18 CM 模式之后,我们就可以暂停电影,等开门拿完快递之后,再重新继续看电影。
不过对于普通开发者来说,我们一般是不会感知到 CM 的存在的,在升级到 React 18 之后,我们的项目不会有任何变化。
我们需要关注的是基于 CM 实现的上层功能,比如 Suspense、Transitions、streaming server rendering(流式服务端渲染), 等等。
React 18 的大部分功能都是基于 CM 架构实现出来的,并且这这是一个开始,未来会有更多基于 CM 实现的高级能力。
我们如果要主动发挥 CM 的优势,那就离不开 startTransition。
React 的状态更新可以分为两类:
我以前会认为,CM 模式会自动帮我们区分不同优先级的更新,一键无忧享受。很遗憾的是,CM 只是提供了可中断的能力,默认情况下,所有的更新都是紧急更新。
这是因为 React 并不能自动识别哪些更新是优先级更高的。
const [inputValue, setInputValue] = useState();
const onChange = (e)=>{
setInputValue(e.target.value);
// 更新搜索列表
setSearchQuery(e.target.value);
}
return (
<input value={inputValue} onChange={onChange} />
)
比如以上示例,用户的键盘输入操作后,setInputValue
会立即更新用户的输入到界面上,是紧急更新。而setSearchQuery
是根据用户输入,查询相应的内容,是非紧急的。
但是 React 确实没有能力自动识别。所以它提供了 startTransition
让我们手动指定哪些更新是紧急的,哪些是非紧急的。
// 紧急的
setInputValue(e.target.value);
startTransition(() => {
setSearchQuery(input); // 非紧急的
});
如上代码,我们通过 startTransition
来标记一个非紧急更新,让该状态触发的变更变成低优先级的。
光用文字描述大家可能没有体验,接下来我们通过一个示例来认识下可中断渲染对性能的爆炸提升。
示例页面:https://react-fractals-git-react-18-swizec.vercel.app/
如下图,我们需要画一个毕达哥拉斯树,通过一个 Slider 来控制树的倾斜。
那我们的代码会很简单,如下所示,我们只需要一个 treeLean
state 来管理状态。
const [treeLean, setTreeLean] = useState(0)
function changeTreeLean(event) {
const value = Number(event.target.value);
setTreeLean(value);
}
return (
<>
<input type="range" value={treeLean} onChange={changeTreeLean} />
<Pythagoras lean={treeLean} />
</>
)
在每次 Slider 拖动后,React 执行流程大致如下:
每一次用户拖动 Slider,都会同步执行上述三步。但当树的节点足够多的时候,Pythagoras 渲染一次就非常慢,就会导致 Slider 的 value 回填变慢,用户感觉到严重的卡顿。如下图。
当数的节点足够大时,已经卡到爆炸了。在 React 18 以前,我们是没有什么好的办法来解决这个问题的。但基于 React 18 CM 的可中断渲染机制,我们可以将树的更新渲染标记为低优先级的,就不会感觉到卡顿了。
const [treeLeanInput, setTreeLeanInput] = useState(0);
const [treeLean, setTreeLean] = useState(0);
function changeTreeLean(event) {
const value = Number(event.target.value);
setTreeLeanInput(value)
// 将 treeLean 的更新用 startTransition 包裹
React.startTransition(() => {
setTreeLean(value);
});
}
return (
<>
<input type="range" value={treeLeanInput} onChange={changeTreeLean} />
<Pythagoras lean={treeLean} />
</>
)
以上代码,我们通过 startTransition 标记了非紧急更新,让树的更新变成低优先级的,可以被随时中止,保证了高优先级的 Slider 的体验。
此时更新流程变为了
React 会在高优先级更新渲染完成之后,才会启动低优先级更新渲染,并且低优先级渲染随时可被其它高优先级更新中断。
当然,在低优先状态等待更新过程中,如果能有一个 Loading 状态,那就更好了。React 18 提供了 useTransition
来跟踪 transition 状态。
const [treeLeanInput, setTreeLeanInput] = useState(0);
const [treeLean, setTreeLean] = useState(0);
// 实时监听 transition 状态
const [isPending, startTransition] = useTransition();
function changeTreeLean(event) {
const value = Number(event.target.value);
setTreeLeanInput(value)
React.startTransition(() => {
setTreeLean(value);
});
}
return (
<>
<input type="range" value={treeLeanInput} onChange={changeTreeLean} />
<Spin spinning={isPending}>
<Pythagoras lean={treeLean} />
</Spin>
</>
)
批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能。比如
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会 re-render 一次,这就是批处理
}
在 React 18 之前,React 只会在事件回调中使用批处理,而在 Promise、setTimeout、原生事件等场景下,是不能使用批处理的。
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 会 render 两次,每次 state 变化更新一次
}, 1000);
而在 React 18 中,所有的状态更新,都会自动使用批处理,不关心场景。
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会 re-render 一次,这就是批处理
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会 re-render 一次,这就是批处理
}, 1000);
如果你在某种场景下不想使用批处理,你可以通过 flushSync
来强制同步执行(比如:你需要在状态更新后,立刻读取新 DOM 上的数据等。)
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React 更新一次 DOM
flushSync(() => {
setFlag(f => !f);
});
// React 更新一次 DOM
}
React 18 的批处理在绝大部分场景下是没有影响,但在 Class 组件中,如果你在两次 setState 中间读取了 state 值,会出现不兼容的情况,如下示例。
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// 在 React17 及之前,打印出来是 { count: 1, flag: false }
// 在 React18,打印出来是 { count: 0, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
当然你可以通过 flushSync
来修正它。
handleClick = () => {
setTimeout(() => {
ReactDOM.flushSync(() => {
this.setState(({ count }) => ({ count: count + 1 }));
});
// 在 React18,打印出来是 { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
SSR 一次页面渲染的流程大概为:
在传统的 SSR 模式中,上述流程是串行执行的,如果其中有一步比较慢,都会影响整体的渲染速度。
而在 React 18 中,基于全新的 Suspense,支持了流式 SSR,也就是允许服务端一点一点的返回页面。
假设我们有一个页面,包含了 NavBar、Sidebar、Post、Comments 等几个部分,在传统的 SSR 模式下,我们必须请求到 Post 数据,请求到 Comments 数据后,才能返回完整的 HTML。
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section>
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</section>
</main>
在 React 18 中,我们通过 Suspense
包裹,可以告诉 React,我们不需要等这个组件,可以先返回其它内容,等这个组件准备好之后,单独返回。
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
如上,我们通过 Suspense
包裹了 Comments
组件,那服务器首次返回的 HTML 是下面这样的,<Comments />
组件处通过 loading
进行了占位。
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section id="comments-spinner">
<!-- Spinner -->
<img width=400 src="spinner.gif" alt="Loading..." />
</section>
</main>
当 <Comments />
组件准备好之后,React 会通过同一个流(stream)发送给浏览器(res.send 替换成 res.socket),并替换到相应位置。
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
// This implementation is slightly simplified
document.getElementById('sections-spinner').replaceChildren(
document.getElementById('comments')
);
</script>
更多关于流式 SSR 的讲解可见:reactwg/react-18#37
Server Component 叫服务端组件,目前还在开发过程中,没有正式发布,不过应该很快就会和我们见面的。
Server Component 的本质就是由服务端生成 React 组件,返回一个 DSL 给客户端,客户端解析 DSL 并渲染该组件。
Server Component 带来的优势有:
// NoteWithMarkdown.js
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)
function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return (/* render */);
}
假设我们有一个 markdown 渲染组件,以前我们需要将依赖 marked
和 sanitize-html
打包到 JS 中。如果该组件在服务端运行,则最终返回给客户端的是转换完成的文本。
// Note.server.js - Server Component
import fs from 'react-fs';
function Note({id}) {
const note = JSON.parse(fs.readFile(`${id}.json`));
return <NoteWithMarkdown note={note} />;
}
当然说了这么多好处,Server Component 肯定也是有一些局限性的:
OffScreen 目前也在开发中,会在未来某个版本中发布。但我们非常有必要提前认识下它,因为你现在的代码很可能已经有问题了。
OffScreen 支持只保存组件的状态,而删除组件的 UI 部分。可以很方便的实现预渲染,或者 Keep Alive。比如我们在从 tabA 切换到 tabB,再返回 tabA 时,React 会使用之前保存的状态恢复组件。
为了支持这个能力,React 要求我们的组件对多次安装和销毁具有弹性。那什么样的代码不符合弹性要求呢?其实不符合要求的代码很常见。
async function handleSubmit() {
setPending(true)
await post('/someapi') // component might unmount while we're waiting
setPending(false)
}
在上面的代码中,如果发送请求时,组件卸载了,会抛出警告。
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
警告:不能在已经卸载的组件中更改 state。这是一个无用的操作,它表明你的项目中存在内存泄漏。要解决这个问题,请在 useEffect 清理函数中取消所有订阅和异步任务。
所以我们一般都会通过一个 unmountRef
来标记当前组件是否卸载,以避免所谓的「内存泄漏」。
function SomeButton(){
const [pending, setPending] = useState(false)
const unmountRef = useUnmountedRef();
async function handleSubmit() {
setPending(true)
await post('/someapi')
if (!unmountRef.current) {
setPending(false)
}
}
return (
<Button onClick={handleSubmit} loading={pending}>
提交
</Button>
)
}
我们来模拟执行一次组件,看看组件的变化状态:
在 OffScreen 中,React 会保存住最后的状态,下次会用这些状态重新渲染组件。惨了,此时我们发现重新渲染组件一直在 loading。
怎么解决?解决办法很简单,就是回归最初的代码,删掉 unmountRef
的逻辑。至于「内存泄漏」的警告,React 18 删除了,因为这里不存在内存泄漏(参考:https://mp.weixin.qq.com/s/fgT7Kxs_0feRx4TkBe6G5Q)。
async function handleSubmit() {
setPending(true)
await post('/someapi')
setPending(false)
}
为了方便排查这类问题,在 React 18 的 Strict Mode 中,新增了 double effect,在开发模式下,每次组件初始化时,会自动执行一次卸载,重载。
* React mounts the component.
* Layout effects are created.
* Effects are created.
* React simulates unmounting the component.
* Layout effects are destroyed.
* Effects are destroyed.
* React simulates mounting the component with the previous state.
* Layout effects are created.
* Effects are created.
这里还是要再提示下:开发环境,在 React 18 的严格模式下,组件初始化的 useEffect 会执行两次,也就是可能 useEffect 里面的请求被执行了两次等。
const deferredValue = useDeferredValue(value);
useDeferredValue 可以让一个 state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValue 和 startTransition 一样,都是标记了一次非紧急更新。
之前 startTransition 的例子,就可以用 useDeferredValue
来实现。
const [treeLeanInput, setTreeLeanInput] = useState(0);
const deferredValue = useDeferredValue(treeLeanInput);
function changeTreeLean(event) {
const value = Number(event.target.value);
setTreeLeanInput(value)
}
return (
<>
<input type="range" value={treeLeanInput} onChange={changeTreeLean} />
<Pythagoras lean={deferredValue} />
</>
)
const id = useId();
支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration
的不兼容。原理是每个 id 代表该组件在组件树中的层级结构。
const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
useSyncExternalStore 能够让 React 组件在 Concurrent Mode 下安全地有效地读取外接数据源。
在 Concurrent Mode 下,React 一次渲染会分片执行(以 fiber 为单位),中间可能穿插优先级更高的更新。假如在高优先级的更新中改变了公共数据(比如 redux 中的数据),那之前低优先的渲染必须要重新开始执行,否则就会出现前后状态不一致的情况。
useSyncExternalStore 一般是三方状态管理库使用,一般我们不需要关注。
useInsertionEffect(didUpdate);
这个 Hooks 只建议 css-in-js
库来使用。
这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 生效之前,一般用于提前注入 <style>
脚本。
HTML:
3. 使用 data- 属性的好处是什么?
可以去 时间 下data-*的使用啦
++++
“时间”是不是用错啦,应该改为“实践”?
++++
src/router/router.js
少了 return();
import React from 'react';
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';
const getRouter = () => (
return (
<Router>
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/page1">Page1</Link></li>
</ul>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/page1" component={Page1}/>
</Switch>
</div>
</Router>
);
);
export default getRouter;
这样写后 webpack-dev-server 才编译显示出来
确认下是这样的么?
记录看到的最优美的JS代码
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
记得开始接触 react 技术栈的时候,最难理解的地方就是 redux。全是新名词:reducer、store、dispatch、middleware 等等,我就理解 state 一个名词。
网上找的 redux 文章,要不有一本书的厚度,要不很玄乎,晦涩难懂,越看越觉得难,越看越怕,信心都没有了!
花了很长时间熟悉 redux,慢慢的发现它其实真的很简单。本章不会把 redux 的各种概念,名词解释一遍,这样和其他教程没有任何区别,没有太大意义。我会带大家从零实现一个完整的 redux,让大家知其然,知其所以然。
开始前,你必须知道一些事情:
Let's Go!
redux 是一个状态管理器,那什么是状态呢?状态就是数据,比如计数器中的 count。
let state = {
count: 1
}
我们来使用下状态
console.log(state.count);
我们来修改下状态
state.count = 2;
好了,现在我们实现了状态(计数)的修改和使用了。
读者:你当我傻吗?你说的这个谁不知道?捶你👊!
笔者:哎哎哎,别打我!有话好好说!redux 核心就是这个呀!我们一步一步扩展开来嘛!
当然上面的有一个很明显的问题:修改 count 之后,使用 count 的地方不能收到通知。我们可以使用发布-订阅模式来解决这个问题。
/*------count 的发布订阅者实践------*/
let state = {
count: 1
};
let listeners = [];
/*订阅*/
function subscribe(listener) {
listeners.push(listener);
}
function changeCount(count) {
state.count = count;
/*当 count 改变的时候,我们要去通知所有的订阅者*/
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
我们来尝试使用下这个简单的计数状态管理器。
/*来订阅一下,当 count 改变的时候,我要实时输出新的值*/
subscribe(() => {
console.log(state.count);
});
/*我们来修改下 state,当然我们不能直接去改 state 了,我们要通过 changeCount 来修改*/
changeCount(2);
changeCount(3);
changeCount(4);
现在我们可以看到,我们修改 count 的时候,会输出相应的 count 值。
现在有两个新的问题摆在我们面前
我们尝试来解决这个问题,把公共的代码封装起来
const createStore = function (initState) {
let state = initState;
let listeners = [];
/*订阅*/
function subscribe(listener) {
listeners.push(listener);
}
function changeState(newState) {
state = newState;
/*通知*/
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
return {
subscribe,
changeState,
getState
}
}
我们来使用这个状态管理器管理多个状态 counter 和 info 试试
let initState = {
counter: {
count: 0
},
info: {
name: '',
description: ''
}
}
let store = createStore(initState);
store.subscribe(() => {
let state = store.getState();
console.log(`${state.info.name}:${state.info.description}`);
});
store.subscribe(() => {
let state = store.getState();
console.log(state.counter.count);
});
store.changeState({
...store.getState(),
info: {
name: '前端九部',
description: '我们都是前端爱好者!'
}
});
store.changeState({
...store.getState(),
counter: {
count: 1
}
});
到这里我们完成了一个简单的状态管理器。
这里需要理解的是 createStore
,提供了 changeState
,getState
,subscribe
三个能力。
本小节完整源码见 demo-1
我们用上面的状态管理器来实现一个自增,自减的计数器。
let initState = {
count: 0
}
let store = createStore(initState);
store.subscribe(() => {
let state = store.getState();
console.log(state.count);
});
/*自增*/
store.changeState({
count: store.getState().count + 1
});
/*自减*/
store.changeState({
count: store.getState().count - 1
});
/*我想随便改*/
store.changeState({
count: 'abc'
});
你一定发现了问题,count 被改成了字符串 abc
,因为我们对 count 的修改没有任何约束,任何地方,任何人都可以修改。
我们需要约束,不允许计划外的 count 修改,我们只允许 count 自增和自减两种改变方式!
那我们分两步来解决这个问题
我们来设置一个 plan 函数,接收现在的 state,和一个 action,返回经过改变后的新的 state。
/*注意:action = {type:'',other:''}, action 必须有一个 type 属性*/
function plan(state, action) {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1
}
case 'DECREMENT':
return {
...state,
count: state.count - 1
}
default:
return state;
}
}
我们把这个计划告诉 store,store.changeState 以后改变 state 要按照我的计划来改。
/*增加一个参数 plan*/
const createStore = function (plan, initState) {
let state = initState;
let listeners = [];
function subscribe(listener) {
listeners.push(listener);
}
function changeState(action) {
/*请按照我的计划修改 state*/
state = plan(state, action);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
return {
subscribe,
changeState,
getState
}
}
我们来尝试使用下新的 createStore 来实现自增和自减
let initState = {
count: 0
}
/*把plan函数*/
let store = createStore(plan, initState);
store.subscribe(() => {
let state = store.getState();
console.log(state.count);
});
/*自增*/
store.changeState({
type: 'INCREMENT'
});
/*自减*/
store.changeState({
type: 'DECREMENT'
});
/*我想随便改 计划外的修改是无效的!*/
store.changeState({
count: 'abc'
});
到这里为止,我们已经实现了一个有计划的状态管理器!
我们商量一下吧?我们给 plan 和 changeState 改下名字好不好?**plan 改成 reducer,changeState 改成 dispatch!**不管你同不同意,我都要换,因为新名字比较厉害(其实因为 redux 是这么叫的)!
本小节完整源码见 demo-2
这一小节我们来处理下 reducer 的问题。啥问题?
我们知道 reducer 是一个计划函数,接收老的 state,按计划返回新的 state。那我们项目中,有大量的 state,每个 state 都需要计划函数,如果全部写在一起会是啥样子呢?
所有的计划写在一个 reducer 函数里面,会导致 reducer 函数及其庞大复杂。按经验来说,我们肯定会按组件维度来拆分出很多个 reducer 函数,然后通过一个函数来把他们合并起来。
我们来管理两个 state,一个 counter,一个 info。
let state = {
counter: {
count: 0
},
info: {
name: '前端九部',
description: '我们都是前端爱好者!'
}
}
他们各自的 reducer
/*counterReducer, 一个子reducer*/
/*注意:counterReducer 接收的 state 是 state.counter*/
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1
}
case 'DECREMENT':
return {
...state,
count: state.count - 1
}
default:
return state;
}
}
/*InfoReducer,一个子reducer*/
/*注意:InfoReducer 接收的 state 是 state.info*/
function InfoReducer(state, action) {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.name
}
case 'SET_DESCRIPTION':
return {
...state,
description: action.description
}
default:
return state;
}
}
那我们用 combineReducers 函数来把多个 reducer 函数合并成一个 reducer 函数。大概这样用
const reducer = combineReducers({
counter: counterReducer,
info: InfoReducer
});
我们尝试实现下 combineReducers 函数
function combineReducers(reducers) {
/* reducerKeys = ['counter', 'info']*/
const reducerKeys = Object.keys(reducers)
/*返回合并后的新的reducer函数*/
return function combination(state = {}, action) {
/*生成的新的state*/
const nextState = {}
/*遍历执行所有的reducers,整合成为一个新的state*/
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
const reducer = reducers[key]
/*之前的 key 的 state*/
const previousStateForKey = state[key]
/*执行 分 reducer,获得新的state*/
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
}
return nextState;
}
}
我们来尝试下 combineReducers 的威力吧
const reducer = combineReducers({
counter: counterReducer,
info: InfoReducer
});
let initState = {
counter: {
count: 0
},
info: {
name: '前端九部',
description: '我们都是前端爱好者!'
}
}
let store = createStore(reducer, initState);
store.subscribe(() => {
let state = store.getState();
console.log(state.counter.count, state.info.name, state.info.description);
});
/*自增*/
store.dispatch({
type: 'INCREMENT'
});
/*修改 name*/
store.dispatch({
type: 'SET_NAME',
name: '前端九部2号'
});
本小节完整源码见 demo-3
上一小节,我们把 reducer 按组件维度拆分了,通过 combineReducers 合并了起来。但是还有个问题, state 我们还是写在一起的,这样会造成 state 树很庞大,不直观,很难维护。我们需要拆分,一个 state,一个 reducer 写一块。
这一小节比较简单,我就不卖关子了,用法大概是这样(注意注释)
/* counter 自己的 state 和 reducer 写在一起*/
let initState = {
count: 0
}
function counterReducer(state, action) {
/*注意:如果 state 没有初始值,那就给他初始值!!*/
if (!state) {
state = initState;
}
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1
}
default:
return state;
}
}
我们修改下 createStore 函数,增加一行 dispatch({ type: Symbol() })
const createStore = function (reducer, initState) {
let state = initState;
let listeners = [];
function subscribe(listener) {
listeners.push(listener);
}
function dispatch(action) {
state = reducer(state, action);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
/* 注意!!!只修改了这里,用一个不匹配任何计划的 type,来获取初始值 */
dispatch({ type: Symbol() })
return {
subscribe,
dispatch,
getState
}
}
我们思考下这行可以带来什么效果?
state = reducer(state, action)
你可以试试
/*这里没有传 initState 哦 */
const store = createStore(reducer);
/*这里看看初始化的 state 是什么*/
console.dir(store.getState());
本小节完整源码见 demo-4
到这里为止,我们已经实现了一个七七八八的 redux 啦!
中间件 middleware 是 redux 中最难理解的地方。但是我挑战一下用最通俗的语言来讲明白它。如果你看完这一小节,还没明白中间件是什么,不知道如何写一个中间件,那就是我的锅了!
中间件是对 dispatch 的扩展,或者说重写,增强 dispatch 的功能!
我现在有一个需求,在每次修改 state 的时候,记录下来 修改前的 state ,为什么修改了,以及修改后的 state。我们可以通过重写 store.dispatch 来实现,直接看代码
const store = createStore(reducer);
const next = store.dispatch;
/*重写了store.dispatch*/
store.dispatch = (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
我们来使用下
store.dispatch({
type: 'INCREMENT'
});
日志输出为
this state { counter: { count: 0 } }
action { type: 'INCREMENT' }
1
next state { counter: { count: 1 } }
现在我们已经实现了一个完美的记录 state 修改日志的功能!
我又有一个需求,需要记录每次数据出错的原因,我们扩展下 dispatch
const store = createStore(reducer);
const next = store.dispatch;
store.dispatch = (action) => {
try {
next(action);
} catch (err) {
console.error('错误报告: ', err)
}
}
这样每次 dispatch 出异常的时候,我们都会记录下来。
我现在既需要记录日志,又需要记录异常,怎么办?当然很简单了,两个函数合起来呗!
store.dispatch = (action) => {
try {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
} catch (err) {
console.error('错误报告: ', err)
}
}
如果又来一个需求怎么办?接着改 dispatch 函数?那再来10个需求呢?到时候 dispatch 函数肯定庞大混乱到无法维护了!这个方式不可取呀!
我们需要考虑如何实现扩展性很强的多中间件合作模式。
我们把 loggerMiddleware 提取出来
const store = createStore(reducer);
const next = store.dispatch;
const loggerMiddleware = (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
store.dispatch = (action) => {
try {
loggerMiddleware(action);
} catch (err) {
console.error('错误报告: ', err)
}
}
我们把 exceptionMiddleware 提取出来
const exceptionMiddleware = (action) => {
try {
/*next(action)*/
loggerMiddleware(action);
} catch (err) {
console.error('错误报告: ', err)
}
}
store.dispatch = exceptionMiddleware;
现在的代码有一个很严重的问题,就是 exceptionMiddleware 里面写死了 loggerMiddleware,我们需要让 next(action)
变成动态的,随便哪个中间件都可以
const exceptionMiddleware = (next) => (action) => {
try {
/*loggerMiddleware(action);*/
next(action);
} catch (err) {
console.error('错误报告: ', err)
}
}
/*loggerMiddleware 变成参数传进去*/
store.dispatch = exceptionMiddleware(loggerMiddleware);
同样的道理,loggerMiddleware 里面的 next 现在恒等于 store.dispatch,导致 loggerMiddleware 里面无法扩展别的中间件了!我们也把 next 写成动态的
const loggerMiddleware = (next) => (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
到这里为止,我们已经探索出了一个扩展性很高的中间件合作模式!
const store = createStore(reducer);
const next = store.dispatch;
const loggerMiddleware = (next) => (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
const exceptionMiddleware = (next) => (action) => {
try {
next(action);
} catch (err) {
console.error('错误报告: ', err)
}
}
store.dispatch = exceptionMiddleware(loggerMiddleware(next));
这时候我们开开心心的新建了一个 loggerMiddleware.js
,一个exceptionMiddleware.js
文件,想把两个中间件独立到单独的文件中去。会碰到什么问题吗?
loggerMiddleware 中包含了外部变量 store,导致我们无法把中间件独立出去。那我们把 store 也作为一个参数传进去好了~
const store = createStore(reducer);
const next = store.dispatch;
const loggerMiddleware = (store) => (next) => (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
const exceptionMiddleware = (store) => (next) => (action) => {
try {
next(action);
} catch (err) {
console.error('错误报告: ', err)
}
}
const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));
到这里为止,我们真正的实现了两个可以独立的中间件啦!
现在我有一个需求,在打印日志之前输出当前的时间戳。用中间件来实现!
const timeMiddleware = (store) => (next) => (action) => {
console.log('time', new Date().getTime());
next(action);
}
...
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));
本小节完整源码见 demo-6
上一节我们已经完全实现了正确的中间件!但是中间件的使用方式不是很友好
import loggerMiddleware from './middlewares/loggerMiddleware';
import exceptionMiddleware from './middlewares/exceptionMiddleware';
import timeMiddleware from './middlewares/timeMiddleware';
...
const store = createStore(reducer);
const next = store.dispatch;
const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));
其实我们只需要知道三个中间件,剩下的细节都可以封装起来!我们通过扩展 createStore 来实现!
先来看看期望的用法
/*接收旧的 createStore,返回新的 createStore*/
const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);
/*返回了一个 dispatch 被重写过的 store*/
const store = newCreateStore(reducer);
实现 applyMiddleware
const applyMiddleware = function (...middlewares) {
/*返回一个重写createStore的方法*/
return function rewriteCreateStoreFunc(oldCreateStore) {
/*返回重写后新的 createStore*/
return function newCreateStore(reducer, initState) {
/*1. 生成store*/
const store = oldCreateStore(reducer, initState);
/*给每个 middleware 传下store,相当于 const logger = loggerMiddleware(store);*/
/* const chain = [exception, time, logger]*/
const chain = middlewares.map(middleware => middleware(store));
let dispatch = store.dispatch;
/* 实现 exception(time((logger(dispatch))))*/
chain.reverse().map(middleware => {
dispatch = middleware(dispatch);
});
/*2. 重写 dispatch*/
store.dispatch = dispatch;
return store;
}
}
}
现在还有个小问题,我们有两种 createStore 了
/*没有中间件的 createStore*/
import { createStore } from './redux';
const store = createStore(reducer, initState);
/*有中间件的 createStore*/
const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);
const newCreateStore = rewriteCreateStoreFunc(createStore);
const store = newCreateStore(reducer, initState);
为了让用户用起来统一一些,我们可以很简单的使他们的使用方式一致,我们修改下 createStore 方法
const createStore = (reducer, initState, rewriteCreateStoreFunc) => {
/*如果有 rewriteCreateStoreFunc,那就采用新的 createStore */
if(rewriteCreateStoreFunc){
const newCreateStore = rewriteCreateStoreFunc(createStore);
return newCreateStore(reducer, initState);
}
/*否则按照正常的流程走*/
...
}
最终的用法
const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);
const store = createStore(reducer, initState, rewriteCreateStoreFunc);
本小节完整源码见 demo-7
不能退订的订阅都是耍流浪!我们修改下 store.subscribe 方法,增加退订功能
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
使用
const unsubscribe = store.subscribe(() => {
let state = store.getState();
console.log(state.counter.count);
});
/*退订*/
unsubscribe();
现在的中间件拿到了完整的 store,他甚至可以修改我们的 subscribe 方法,按照最小开放策略,我们只用把 getState 给中间件就可以了!因为我们只允许你用 getState 方法!
修改下 applyMiddleware 中给中间件传的 store
/*const chain = middlewares.map(middleware => middleware(store));*/
const simpleStore = { getState: store.getState };
const chain = middlewares.map(middleware => middleware(simpleStore));
我们的 applyMiddleware 中,把 [A, B, C] 转换成 A(B(C(next))),是这样实现的
const chain = [A, B, C];
let dispatch = store.dispatch;
chain.reverse().map(middleware => {
dispatch = middleware(dispatch);
});
redux 提供了一个 compose 方式,可以帮我们做这个事情
const chain = [A, B, C];
dispatch = compose(...chain)(store.dispatch)
看下他是如何实现的
export default function compose(...funcs) {
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
当然 compose 函数对于新人来说可能比较难理解,你只需要他是做什么的就行啦!
有时候我们创建 store 的时候不传 initState,我们怎么用?
const store = createStore(reducer, {}, rewriteCreateStoreFunc);
redux 允许我们这样写
const store = createStore(reducer, rewriteCreateStoreFunc);
我们仅需要改下 createStore 函数,如果第二个参数是一个object,我们认为他是 initState,如果是 function,我们就认为他是 rewriteCreateStoreFunc。
function craeteStore(reducer, initState, rewriteCreateStoreFunc){
if (typeof initState === 'function'){
rewriteCreateStoreFunc = initState;
initState = undefined;
}
...
}
reducer 拆分后,和组件是一一对应的。我们就希望在做按需加载的时候,reducer也可以跟着组件在必要的时候再加载,然后用新的 reducer 替换老的 reducer。
const createStore = function (reducer, initState) {
...
function replaceReducer(nextReducer) {
reducer = nextReducer
/*刷新一遍 state 的值,新来的 reducer 把自己的默认状态放到 state 树上去*/
dispatch({ type: Symbol() })
}
...
return {
...
replaceReducer
}
}
我们来尝试使用下
const reducer = combineReducers({
counter: counterReducer
});
const store = createStore(reducer);
/*生成新的reducer*/
const nextReducer = combineReducers({
counter: counterReducer,
info: infoReducer
});
/*replaceReducer*/
store.replaceReducer(nextReducer);
replaceReducer 示例源码见 demo-5
bindActionCreators 我们很少很少用到,一般只有在 react-redux 的 connect 实现中用到。
他是做什么的?他通过闭包,把 dispatch 和 actionCreator 隐藏起来,让其他地方感知不到 redux 的存在。
我们通过普通的方式来 隐藏 dispatch 和 actionCreator 试试,注意最后两行代码
const reducer = combineReducers({
counter: counterReducer,
info: infoReducer
});
const store = createStore(reducer);
/*返回 action 的函数就叫 actionCreator*/
function increment() {
return {
type: 'INCREMENT'
}
}
function setName(name) {
return {
type: 'SET_NAME',
name: name
}
}
const actions = {
increment: function () {
return store.dispatch(increment.apply(this, arguments))
},
setName: function () {
return store.dispatch(setName.apply(this, arguments))
}
}
/*注意:我们可以把 actions 传到任何地方去*/
/*其他地方在实现自增的时候,根本不知道 dispatch,actionCreator等细节*/
actions.increment(); /*自增*/
actions.setName('九部威武'); /*修改 info.name*/
我眼睛一看,这个 actions 生成的时候,好多公共代码,提取一下
const actions = bindActionCreators({ increment, setName }, store.dispatch);
来看一下 bindActionCreators 的源码,超级简单(就是生成了刚才的 actions)
/*核心的代码在这里,通过闭包隐藏了 actionCreator 和 dispatch*/
function bindActionCreator(actionCreator, dispatch) {
return function () {
return dispatch(actionCreator.apply(this, arguments))
}
}
/* actionCreators 必须是 function 或者 object */
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error()
}
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
bindActionCreators 示例源码见 demo-8
完整的示例源码见 demo-9,你可以和 redux 源码做一下对比,你会发现,我们已经实现了 redux 所有的功能了。
当然,为了保证代码的理解性,我们少了一些参数验证。比如 createStore(reducer)
的参数 reducer 必须是 function 等等。
什么是纯函数?
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
通俗来讲,就两个要素
/*不是纯函数,因为同样的输入,输出结果不一致*/
function a( count ){
return count + Math.random();
}
/*不是纯函数,因为外部的 arr 被修改了*/
function b( arr ){
return arr.push(1);
}
let arr = [1, 2, 3];
b(arr);
console.log(arr); //[1, 2, 3, 1]
/*不是纯函数,以为依赖了外部的 x*/
let x = 1;
function c( count ){
return count + x;
}
我们的 reducer 计划函数,就必须是一个纯函数!
只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
到了最后,我想把 redux 中关键的名词列出来,你每个都知道是干啥的吗?
createStore
创建 store 对象,包含 getState, dispatch, subscribe, replaceReducer
reducer
reducer 是一个计划函数,接收旧的 state 和 action,生成新的 state
action
action 是一个对象,必须包含 type 字段
dispatch
dispatch( action )
触发 action,生成新的 state
subscribe
实现订阅功能,每次触发 dispatch 的时候,会执行订阅函数
combineReducers
多 reducer 合并成一个 reducer
replaceReducer
替换 reducer 函数
middleware
扩展 dispatch 函数!
你再看 redux 流程图,是不是大彻大悟了?
(redux 流程图)说到 web3,很多人觉得这是骗局,是割韭菜。是因为大部分介绍 web3 的文章都离不开 NFT、数字货币、区块链、比特币、以太坊、元宇宙等概念,玄之又玄,脱离我们的生活,没解决我们的痛点。一般文章最后还教给我们怎么炒币,怎么买卖 NFT,妥妥的割韭菜套路,所以大部分觉得 web3 就是在忽悠人。
说实话,很多东西我也觉得是在炒作,我也不信。
一个图片卖几百万美金?
一个空气币要卖我钱?
元宇宙?可能要等我儿子长大了玩吧?
关于 web3 是什么,可能每篇文章介绍的都不一样,好像现在大家对 web3 是什么还没达成共识。但对于普通人来讲,我们只关心 web3 有没有解决我的痛点,有没有给我带来价值。
本篇文章,我会通过大白话来介绍我认可的 web3 形态,极大解决了我们的痛点,我愿称之为未来!
本文不会涉及任何韭菜币等相关概念(我也不懂),请放心食用。
通常大家说的 web1,是指门户时代,内容由各大门户网站创作,普通用户只是作为浏览方。数据由门户网站产出,收益当然归他们所有,价值流向正确。总结:平台创造、平台所有、平台控制、平台受益。
通常大家说的 web2,就是我们当下的互联网形态:用户创造、平台所有、平台控制、平台分配。
举一些典型的例子:
web2 的问题有很多:
就我个人而言,我对 web2 中的很多点已经达到忍无可忍的程度,如果 web3 能解决这些问题,那我就是 web3 的粉丝。
基于市场经济,web3 的宗旨必须是用户创造、用户所有、用户控制、协议分配。
在 web3 中,用户的所有数据都归属用户个人,用户可以授权其它平台访问自己的某类数据。想象一下这种场景:
总结就是,哪个软件让我不爽,我就无缝切换到另外一个软件,让软件供应商去内卷吧。
能随意炒微信的鱿鱼,想想都开心。以前不可能,但 web3 确实带来了这种机会。我认为 web3 的实现思路应该是这样的:
针对数据存储部分的技术解决方案,我们要求能解决信任问题。即用户完全信任数据存储方,非用户授权,任何人无法访问和修改数据。中心化的数据存储肯定不能符合要求,从目前来看,「区块链」去中心化技术就非常适合充当这里的可信任数据存储解决方案。这也是为什么 web3 总是和区块链绑定在一起。其实如果有其它技术能解决信任问题,也是 OK 的。
针对软件提供方,未来可能会更轻量。比如很多软件可能都不需要数据库了,数据完全由用户自带。软件需要做的就是帮用户管理数据,比如播放音乐、管理文章、聊天等。软件提供方必须用优质的体验来吸引用户,想想都幸福。
大家快来学前端吧 😏,按我讲的,前端就是未来呀!欢迎关注我的公众号《前端技术砖家》跟我学前端。哈哈哈。
哪里有压迫,哪里就有革命。web3 让我们可以完全控制自己的数据,可以让我们用上更好的软件,用户会用脚投票的,为何不会成功呢?所以我还是笃定 web3 这个方向的,虽然这条路真的很难,需要无数人去探索,但互联网的未来一定属于 web3!
如果下次还有谁介绍 web3 的时候,提到数字货币、NFT 啥的,那就妥妥的是割韭菜了。
最后需要说明的是,本篇文章关于 web3 的定义不一定准确,纯属我一个菜鸡的 YY,另外文中的很多观点和话术来自网络,感谢互联网。
老哥可否出一个typescript版本的react环境啊
请问明年还可以找你内推吗?😄
关于微前端是什么,以及微前端落地方案,社区遍地都是,本篇文章不会再赘述这些基础知识。当然如果你没了解过上述知识,也可以直接读下这篇文章,足够浅显易懂。
这篇文章通过实现一个商城后台,介绍了基于 umi 框架的微前端落地方案,通过这篇文章,你可以收获
一些技术栈
- umi 插件化的企业级前端应用框架,帮助你更好更快的开发 React 应用
- qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统
- @umijs/plugin-qiankun umi 的 qiankun 插件,极其方便的在 umi 框架中使用 qiankun 的微前端能力
PS:想做 qiankun、umi、antd 就来蚂蚁金服体验技术部,我们目前正在大力招前端!!见《宇宙第三前端团队招人啦!》
本篇的文章的示例源码见 umi-micro-apps。
假如我们有一个超大型的中台,不同的模块是由不同团队维护的,那我们完全可以让各自团队维护自己的前端,然后通过微前端把它们组合起来。
比如我们要做一个大型的商城后台,商铺管理和用户管理分别由两个团队开发维护,那我们可以将前端应用拆为主应用(layout)、商铺管理应用(shop)、用户管理应用(user),然后通过微前端组织起来,最终效果如下图。
我们根据 umi 教程,初始化三个前端应用(源码见这里 umi-micro-apps)
/shop
开头,包含商铺列表和商铺详情/user
开头,包含用户列表和用户详情umi 提供了一个 plugin-qiankun 插件,可以非常方便的使用 qiankun 的微前端能力。
@umijs/plugin-qiankun
npm install @umijs/plugin-qiankun -S
// .umirc.ts
qiankun: {
slave: {}
},
// .umirc.ts
{
qiankun: {
// qiankun.master.apps 配置了子应用的唯一名称,及入口 html 地址
master: {
apps: [{
name: 'shop',
entry: 'http://localhost:8001'
}, {
name: 'user',
entry: 'http://localhost:8002'
}]
},
},
routes: [
{
path: '/',
component: '@/layouts/index',
routes: [
{ path: '/', component: '@/pages/index' },
{ path: '/shop', microApp: 'shop' }, // 所有以 /shop 开头的路径,加载 shop 应用
{ path: '/user', microApp: 'user' }, // 所有以 /user 开头的路径,加载 shop 应用
]
}
],
}
上述配置非常简单,我们声明了要嵌套的子应用的名称和入口 html 地址,然后在路由上配置了某个路径加载相应的子应用。
通过三步,我们已经完成了一个总分结构的微前端商城开发,步骤是真的真的超级简单,为 umi 和 qiankun 打 call~~~
总分中台是最常见的微前端落地场景,可以完美的拆分大型中台项目,也可以平滑的升级历史应用。
如果我们今天的文章只是介绍“总分式的中台应用”,那这篇文章的意义就很小了,毕竟社区上把这一套都玩烂了。
想象这个场景:我们想管理某个商铺的用户,也就是在商铺详情中,我们需要查看并管理商铺名下的用户列表,以及用户详情。如下图所示:
商铺名下的用户展示、数据来源与“用户管理”模块一致,我们肯定不可能重复写一套。那有没有办法实现复用呢?
我们知道,shop 应用 与 user 应用的页面都是通过浏览器的 url 来驱动跳转定位的,所以同时只能存活一个应用, url 为 /shop
时展示 shop 应用,为 /user
时展示 user 应用,两个应用是不能同时激活的,否则 url 要打架了。
但困难难不倒聪明的劳动人民,我们知道 history 分为 brower
、 hash
、 memory
三种,其中 memory
路由是不依赖浏览器的 url 的,如果我们能在运行时动态的将 user 应用的 history 改为 memory 类型,那完全可以实现 shop 和 user 同时存在。
plugin-qiankun 提供了一个组件 MicroAppWithMemoHistory
,该组件可以在运行时,修改子应用为 memory 路由。
要实现上述套娃,我们只需要在 shop 应用中做简单配置即可:
qiankun.master
配置,声明子应用信息,将 user 应用作为嵌套子应用引入// .umirc.ts
{
qiankun: {
master: {
apps: [{
name: 'user',
entry: 'http://localhost:8002'
}]
},
slave: {}
},
}
MicroAppWithMemoHistory
组件使用子应用import { MicroAppWithMemoHistory } from 'umi';
<Drawer>
<MicroAppWithMemoHistory name="user" url='/' />
</Drawer>
只需极其简单的代码,我们就能实现任意套娃,给中台项目的组合带来非常大的想象空间。
微前端为大型中台项目带来了福音,我们可以非常灵活的进行应用拆分和组合。基于这一套玩法,我们不仅可以完成“总分”形式的组合,也可以实现“任意套娃”,极大的提升了中台应用的灵活性。
我想象中未来的中台前端也是微服务化,每个小组维护自己的数据和页面,通过“总分”和“套娃”组成一个大型中台应用。
其实不应该有后记的,但是我还是想介绍个东西,不介绍心里堵得慌,这么优秀的东西应该让大家知道😋。
关于通信,我们可以像使用普通的 React 组件一样,实现父子应用的通信,超级舒服。
在父级应用中,我们可以使用 MicroAppWithMemoHistory
嵌入子应用,同时可以像普通的 React 组件一样,传递 props 进去,比如我们传递一个 shopId 给子应用。
<MicroAppWithMemoHistory
name="user"
url='/'
shopId={1}
/>
在子应用中,我们通过一个高阶组件 connectMaster
包裹组件,即可从 props 中拿到父级传递过来的 shopId。
import React from 'react';
import { connectMaster } from 'umi';
const UserList = (props)=>{
const {shopId} = props;
...
}
export default connectMaster(UserList);
UserList 也可以在子组件中使用,接收 shopId。对于 UserList 组件来讲,它根本不关心是微前端在使用,还是自己内部在使用,完全不用为微前端定制任何逻辑。
<UserList shopId={1} />
通信的心智成本相当低,我们可以像使用 React 组件一样使用其它应用,实现无限套娃。
心动不如行动,如果你想做微前端,何不选择如此简单的 umi 系列呢?
最后再打个招聘广告:《宇宙第三前端团队招人啦!》
大佬这是你的博客吗,感觉有人抄袭
https://www.jianshu.com/p/b9ac8fa849f1
webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead.
如题。。博主来解释解释。
说到状态管理器,轮子满天飞。在 Class 时代,redux 与 mobx 几乎占据了全部市场,几乎没有没用过 redux 的同学。随着 Hooks 的诞生,新的一批轮子应运而生,其中有代表性的有 unstated-next、constate 等等。
当然无论什么轮子,要解决的问题都是一样的:**跨组件状态共享。**在解决这个核心问题的同时,需要尽可能的满足以下几个特性:
最近,facebook 官方出了一个状态管理器解决方案 Recoil,我们来体验一下。
使用 Recoil,我们需要在项目最外层包一个 RecoilRoot
,这个和大部分状态管理器一致,通过 context 来跨组件传递数据。
import React from 'react';
import { RecoilRoot } from 'recoil';
function App() {
return (
<RecoilRoot>
...
</RecoilRoot>
);
}
状态最简单的就是定义和使用。在 Recoil 中,通过 atom
来定义一个状态。
const inputValueState = atom({
key: "inputValue",
default: ""
});
如上面的代码所示,我们定义了一个 inputValue
状态,它的默认值是空字符串。
需要注意的是 key
字段,它应该是全局唯一的。这个 key
主要为了 debug 方便,持久化数据(数据恢复时的唯一标识),以及可以方便的看到全局 atoms 树。
消费状态也比较简单,通过 useRecoilState
来消费状态。
import React from "react";
import { useRecoilState } from "recoil";
import { inputValue } from "../store";
const InputA = () => {
const [value, setValue] = useRecoilState(inputValueState);
return <input value={value} onChange={e => setValue(e.target.value)} />;
};
export default InputA;
是不是很简单?Recoil 的基础用法就是这样的。我在这里写了一个 demo,你可以体验下。
有些状态需要依赖其它状态,这时候就要用 selector
来定义这个状态了。
比如,我们需要定义一个新的状态 filterdInputValue
,它是过滤 inputValue
中的数字后的值。
const filterdInputValue = selector({
key: "filterdInputValue",
get: ({get}) => {
// 通过 get 可以读取其它状态
const inputValue = get(inputValueState);
return inputValue.replace(/[0-9]/ig, "");
},
});
selector
比较简单,就是为了实现状态的依赖。你可以在这个 demo 体验下。
良好的异步请求支持是状态管理器必不可少的。Recoil 提供了一个 useRecoilValueLoadable
来处理异步请求。直接上例子:
const currentUserNameQuery = selector({
key: "CurrentUserName",
get: async () => {
const response = await queryUserInfo();
return response.name;
}
});
我们需要通过 selector
来定义异步状态,如果 get
函数是一个 Promise,则代表该状态为异步状态,需要使用 useRecoilValueLoadable
来消费该状态。
const UserName = () => {
const userNameLoadable = useRecoilValueLoadable(currentUserNameQuery);
switch (userNameLoadable.state) {
case "hasValue":
return <div>{userNameLoadable.contents}</div>;
case "loading":
return <div>Loading...</div>;
case "hasError":
throw userNameLoadable.contents;
}
};
从上面例子可以看到, useRecoilValueLoadable
返回的状态,可以通过 state
字段读取到异步请求的状态。我写了个 demo,你可以体验下。
当然通过 useRecoilValueLoadable
来消费异步状态,比较符合我们当前的习惯。但 Recoil 更推荐通过 React.Suspense
来消费异步状态,这里就仁者见仁了,虽然 Suspense
可能是方向,但用起来是还不太习惯。
const UserName = () => {
const userName = useRecoilValue(currentUserNameQuery);
return <>{userName}</>
}
};
function MyApp() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<UserName />
</React.Suspense>
);
}
当前 Recoil 还处于开发阶段,文档都还不是很全。基于现状,说几点我的感受。
这点我很惊讶,也是写这个文章的时候才发现的,很奇怪。讲道理 Recoil 支持 typescript 应该是顺手的事情,可能后期需要来个 @types/recoil
吧。
这个特性应该是必备的,应该不会彻底抛弃 Class 组件。估计下个版本肯定会支持的这个特性的。实现成本较低,不支持的话就太反人类了。
各类 API 一共有 19 个,偏复杂了。感觉很多都是可以合并的,比如 atom
和 selector
合并成一个等等(也可能是我考虑不成熟)。建议官方可以考虑精简精简,本来是一个很简单的东西,搞的太复杂了。
我们需要消费一个状态的时候,需要 import 两个东西,比较繁琐。
import { useRecoilState } from "recoil";
import { inputValueState } from "../store";
// 用法
useRecoilState(inputValueState);
本来应该可以直接通过字符串 key
消费的,但这样和 redux 问题一样了,无法支持 ts。
import { useRecoilState } from "recoil";
useRecoilState('inputValueState');
无论如果,import 两个东西不是一个好的用法。
没有看到让人眼前一亮的东西,没有使用冲动。静观发展~
Recoil 整体看下来,比较中庸,需要静观发展。
另外推荐一下我目前正在用的最简单的 React 状态管理器 hox,只有一个 API,非常符合直觉,没有任何上手成本,完全拥抱 Hooks 😋。
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
useRequst 文档:https://hooks.umijs.org/zh-CN/async
Umi Hooks Github 地址:https://github.com/umijs/hooks
useRequest 是一个超级强大,且生产完备的网络请求 Hooks,目前已经成为蚂蚁中台最佳实践内置网络请求方案。在蚂蚁内部中台应用,写网络请求,都推荐用 useRequest。
useRequest 可能是目前社区中最强大,最接地气的请求类 Hooks 了。可以覆盖 99% 的网络请求场景,无论是读还是写,无论是普通请求还是分页请求,无论是缓存还是防抖节流,通通都能支持。只有你想不到,没有它做不到(吹牛🐂~)。
在组件开发中,要实现一个健壮的网络请求,并不是一个简单的事情。正如我上一篇文章《Umi Hooks - 助力拥抱 React Hooks》举的例子,实现一个网络请求,我们需要考虑 loading、竞态处理、组件卸载等等方面。
当然通过 React Hooks 的逻辑封装能力,我们可以将网络请求相关的逻辑封装起来。Umi Hooks 中的 useAsync 就做了这个事情,一行代码就可以实现网络请求,提效非常明显。
但日常工作中,只用一个 useAsync 还是不够的,Umi Hooks 中和网络请求相关的 Hooks 就有非常多。比如和分页请求相关的 usePagination,请求自带防抖的 useSearch,内置 umi-request 的 useAPI,加载更多场景的 useLoadMore,等等等等。
目前已有 Hooks 有几个很明显的缺点:
同时随着 zeit/swr 的诞生,给了我们很多灵感,原来网络请求还可以这么玩!swr 有非常多好用,并且我们想不到的能力。比如:
这里我简单科普下 swr。swr 是
stale-while-revalidate
的简称,最主要的能力是:我们在发起网络请求时,会优先返回之前缓存的数据,然后在背后发起新的网络请求,最终用新的请求结果重新触发组件渲染。swr 特性在特定场景,对用户非常友好。
基于上面两点,经过内部多次讨论,最终决定,我们要做一个能力强大,覆盖所有场景的网络请求 Hooks!useRequest 诞生了!它不仅囊括了当前 Umi Hooks 中所有和网络请求相关的 Hooks 的能力,也大量借鉴了 swr 的优秀特性,香的不得了。
import { useRequest } from '@umijs/hooks';
function getUsername() {
return Promise.resolve('jack');
}
export default () => {
const { data, error, loading } = useRequest(getUsername)
if (error) return <div>failed to load</div>
if (loading) return <div>loading...</div>
return <div>Username: {data}</div>
}
这是一个最简单的网络请求示例。在这个例子中 useRequest
接收了一个 Promise 函数。在组件初始化时,会自动触发 getUsername
执行,并自动管理 data
、 loading
、 error
等数据,我们只需要根据状态来写相应的 UI 实现即可。
对于“写”请求,我们一般需要手动触发,比如添加用户,编辑信息,删除用户等等。 useRequest
只需要配置 manual = true
,即可阻止初始化执行。只有触发 run
时才会开始执行。
import { useRequest } from '@umijs/hooks';
export default () => {
const { run, loading } = useRequest(changeUsername, {manual: true})
return (
<Button onClick={() => run('new name')} loading={loading}>
Edit
</Button>
)
}
对于需要保持新鲜度的数据,我们通常需要不断发起网络请求以更新数据。 useRequest
只要配置 poilingInterval
即可自动定时发起网络请求。
import { useRequest } from '@umijs/hooks';
export default () => {
const { data } = useRequest(getUsername, { pollingInterval: 1000 })
return <div>Username: {data}</div>
}
同时通过设置 pollingWhenHidden
,我们可以智能的实现在屏幕隐藏时,暂停轮询。等屏幕恢复可见时,继续请求,以节省资源。
当然你也可以通过 run/cancel
来手动控制定时器的开启和关闭。
什么是并行请求?看了下图应该就明白了,也就是同一个接口,我们需要维护多个请求状态。
示例中的并行请求有几个特点:
useRequest
通过设置 fetchKey
,即可对请求进行分类。相同分类的请求,只会维护一份状态。不同分类的请求,则会维护多份状态。在下面的代码中,我们通过 userId
将请求进行分类,同时我们可以通过 fetches[userId]
拿到当前分类的请求状态!
export default () => {
const { run, fetches } = useRequest(deleteUser, {
manual: true,
fetchKey: id => id, // 不同的 ID,分类不同
});
return (
<div>
<Button loading={fetches.A?.loading} onClick={() => { run('A') }}>删除 1</Button>
<Button loading={fetches.B?.loading} onClick={() => { run('B') }}>删除 2</Button>
<Button loading={fetches.C?.loading} onClick={() => { run('C') }}>删除 3</Button>
</div>
);
};
通常在边输入边搜索的场景中,我们会用到防抖功能,以节省不必要的网络请求。通过 useRequest
,只需要配置一个 debounceInterval
,就可以非常简单的实现对网络请求的节流操作。
在下面的例子中,无论调用了多少次 run
,只会在输入停止后,发送一次请求。
import { useRequest } from '@umijs/hooks';
export default () => {
const { data, loading, run, cancel } = useRequest(getEmail, {
debounceInterval: 500,
manual: true
});
return (
<div>
<Select onSearch={run} loading={loading}>
{data && data.map(i => <Option key={i} value={i}>{i}</Option>)}
</Select>
</div>
);
};
节流与防抖是同样的道理,只需要配置了 throttleInterval
,即可实现节流功能。
在前面我讲了什么是 SWR,在 SWR 场景下,我们会对接口数据进行缓存,当下次请求该接口时,我们会先返回缓存的数据,同时,在背后发起新的网络请求,待新数据拿到后,重新触发渲染。
对于一些数据不是经常变化的接口,使用 SWR 后,可以极大提高用户使用体验。比如下面的图片例子,当我们第二次访问该文章时,直接返回了缓存的数据,没有任何的等待时间。同时,我们可以看到“最新访问时间”在 2 秒后更新了,这意味着新的请求数据返回了。
useRequest
通过配置 cacheKey
,即可进入 SWR 模式,相当简单。
const { data, loading } = useRequest(getArticle, {
cacheKey: 'articleKey',
});
同时需要注意,同一个 cacheyKey
的数据是全局共享的。通过这个特性,我们可以实现“预加载”功能。比如鼠标 hover
到文章标题时,我们即发送读取文章详情的请求,这样等用户真正点进文章时,数据早已经缓存好了。
通过配置 refreshOnWindowFocus
,我们可以实现,在屏幕重新聚焦或可见时,重新发起网络请求。这个特性有什么用呢?它可以保证多个 tab 间数据的同步性。也可以解决长间隔之后重新打开网站的数据新鲜度问题。
这里借用 swr 的一个图来说明问题。
考虑到使用便捷性, useRequest
集成了 umi-request。如果第一个参数不是 Promise,我们会通过 umi-request
来发起网络请求。
当然如果你想用 axios,也是可以的,通过 requstMethod
即可定制你自己的请求方法。
// 用法 1
const { data, error, loading } = useRequest('/api/userInfo');
// 用法 2
const { data, error, loading } = useRequest({
url: '/api/changeUsername',
method: 'post',
});
// 用法 3
const { data, error, loading } = useRequest((userId)=> `/api/userInfo/${userId}`);
// 用法 4
const { loading, run } = useRequest((username) => ({
url: '/api/changeUsername',
method: 'post',
data: { username },
}));
中台应用中最多的就是表格和表单了。对于一个表格,我们要处理非常多的请求逻辑,包括不限于:
useRequest
通过配置 paginated = true
,即可进入分页模式,自动帮你处理表格常见逻辑,同时我们对 antd Table
做了特殊支持,只用简单几行代码,就可以实现下面图中这样复杂的逻辑,提效百倍。
import {useRequest} from '@umijs/hooks';
export default () => {
const [gender, setGender] = useState('male');
const { tableProps } = useRequest((params)=>{
return getTableData({...params, gender})
}, {
paginated: true,
refreshDeps: [gender]
});
const columns = [];
return (
<Table columns={columns} rowKey="email" {...tableProps}/>
);
};
加载更多的场景也是日常开发中常见的需求。在加载场景中,我们一般需要处理:
useRequest
通过设置 loadMore = true
,即可进入加载更多模式,配合其它参数,可以帮你处理上面所有的逻辑。
const { data, loading, loadMore, loadingMore } = useRequest((d) => getLoadMoreList(d?.nextId, 3), {
loadMore: true,
cacheKey: 'loadMoreDemoCacheId',
fetchKey: d => `${d?.nextId}-`,
});
当然我前面也说了, useReqeust
的功能只有你想不到,没有它没有的。哈哈哈~
除了上面的特性,我们还有一些其它的能力,可以在文档中发现。比如 loadingDelay
。
通过设置 loadingDelay
,延迟 loading
变为 true
的时间,当请求很快响应时,可以有效避免 loading
变化导致的抖动。
虽然 useRequest
的功能很多,也避免不了有些你想用的特性它不支持。但不用担心,你可以很方面的基于 useRequest
去扩展。我们的分页模式,及加载更多模式均是基于底层能力扩展实现的。你可以参考它们的代码,实现自己的特有能力。
通过 useRequest
,可以解决日常 99% 的网络请求需求。奥利给!奥利给!
最后打个招聘广告,蚂蚁金服体验技术部招聘前端啦!要求 P6 及以上!有兴趣的同学可以发简历到
我会帮您跟进面试进度的,期待您的加入~
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
目前,Hooks 应该是 React 中最火的概念了,在阅读这篇文章之前,希望你已经了解了基本的 Hooks 用法。
在使用 Hooks 的时候,我们可能会有很多疑惑
这篇文章我们不会讲解 Hooks 的概念和用法,而是会带你从零实现一个 tiny hooks,知其然知其所以然。
最简单的 useState 用法是这样的:
demo1: https://codesandbox.io/s/v0nqm309q3
function Counter() {
var [count, setCount] = useState(0);
return (
<div>
<div>{count}</div>
<Button onClick={() => { setCount(count + 1); }}>
点击
</Button>
</div>
);
}
基于 useState 的用法,我们尝试着自己实现一个 useState:
demo2:https://codesandbox.io/s/myy5qvoxpp
function useState(initialValue) {
var state = initialValue;
function setState(newState) {
state = newState;
render();
}
return [state, setState];
}
这时我们发现,点击 Button 的时候,count 并不会变化,为什么呢?我们没有存储 state,每次渲染 Counter 组件的时候,state 都是新重置的。
自然我们就能想到,把 state 提取出来,存在 useState 外面。
demo3:https://codesandbox.io/s/q9wq6w5k3w
var _state; // 把 state 存储在外面
function useState(initialValue) {
_state = _state || initialValue; // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
function setState(newState) {
_state = newState;
render();
}
return [_state, setState];
}
到目前为止,我们实现了一个可以工作的 useState,至少现在来看没啥问题。
接下来,让我们看看 useEffect 是怎么实现的。
useEffect 是另外一个基础的 Hook,用来处理副作用,最简单的用法是这样的:
demo4:https://codesandbox.io/s/93jp55qyp4
useEffect(() => {
console.log(count);
}, [count]);
我们知道 useEffect 有几个特点:
我们来实现一个 useEffect
demo5:https://codesandbox.io/s/3kv3zlvzl1
let _deps; // _deps 记录 useEffect 上一次的 依赖
function useEffect(callback, depArray) {
const hasNoDeps = !depArray; // 如果 dependencies 不存在
const hasChangedDeps = _deps
? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
: true;
/* 如果 dependencies 不存在,或者 dependencies 有变化*/
if (hasNoDeps || hasChangedDeps) {
callback();
_deps = depArray;
}
}
到这里,我们又实现了一个可以工作的 useEffect,似乎没有那么难。
此时我们应该可以解答一个问题:
Q:为什么第二个参数是空数组,相当于 componentDidMount
?
A:因为依赖一直不变化,callback 不会二次执行。
到现在为止,我们已经实现了可以工作的 useState 和 useEffect。但是有一个很大的问题:它俩都只能使用一次,因为只有一个 _state 和 一个 _deps。比如
const [count, setCount] = useState(0);
const [username, setUsername] = useState('fan');
count 和 username 永远是相等的,因为他们共用了一个 _state,并没有地方能分别存储两个值。我们需要可以存储多个 _state 和 _deps。
如 《React hooks: not magic, just arrays》所写,我们可以使用数组,来解决 Hooks 的复用问题。
demo6:https://codesandbox.io/s/50ww35vkzl
代码关键在于:
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
我们用图来描述 memoizedState 及 cursor 变化的过程。
到这里,我们实现了一个可以任意复用的 useState 和 useEffect。
同时,也可以解答几个问题:
Q:为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用。
A:memoizedState 数组是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。
Q:自定义的 Hook 是如何影响使用它的函数组件的?
A:共享同一个 memoizedState,共享同一个顺序。
Q:“Capture Value” 特性是如何产生的?
A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。
虽然我们用数组基本实现了一个可用的 Hooks,了解了 Hooks 的原理,但在 React 中,实现方式却有一些差异的。
React 中是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook。
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null,// UpdateQueue 通过
next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
}
type Effect = {
tag: HookEffectTag, // effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
create: () => mixed, // 初始化 callback
destroy: (() => mixed) | null, // 卸载 callback
deps: Array<mixed> | null,
next: Effect, // 同上
};
memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?
我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
招聘:《宇宙第三前端团队招人啦!》
招聘:《宇宙第三前端团队招人啦!》
招聘:《宇宙第三前端团队招人啦!》
原创 for:前端技术砖家 公众号
性能优化不是一个简单的事情,但在 95% 以上的 React 项目中,是不需要考虑的,按自己的想法奔放的使用就可以了。
我认为性能优化最好的时候是项目启动时。在项目启动时,需要充分考虑页面的复杂度,如果非常复杂,则必须提前制定各种措施,防止出现性能问题。如果前期评估页面不复杂,那大概率不会出现什么性能问题。最惨的事情就是前期没有评估,中后期碰到了性能问题,解决起来就相当棘手了。
这篇文章会分享 React 项目常见的性能分析手段及优化手段,碰到性能问题的同学可以看看,没碰到性能问题的同学也需要提前预警了。
说到性能分析,当然要有一些指标,来度量现在网页“卡”的程度,并指导我们持续改进。chrome 自带的 Performance,一般就足够我们进行分析了。
我写了一个简单的卡顿的例子,我们尝试通过 Performance 来分析出这个例子中哪一行代码卡。首先你可以打开这个示例页面,在这个页面的 input 框中输入的时候,你能明显感觉到非常卡顿。
从上面的动图可以看到,最后上面一栏出现很多红线,这就代表性能出问题了。
我们看下 Frames(帧) 这一栏,能看到红框中在一次输入中,776.9 ms 内都是 1 fps 的。这代表什么意思?我们知道正常网页刷新频率一般是 60 帧,也就是 16.67ms(1s/60)必须要刷新一次,否则就会有卡顿感,刷新时间越长,就越卡顿,在当前例子中,我们输入字符后,776.9 ms 后才触发更新,可以说是相当相当卡了。
我们知道 JS 是单线程的,也就是执行代码与绘制是同一个线程,必须等代码执行完,才能开始绘制。那具体是那一块代码执行时间长了呢?这里我们就要看 Main 这一栏,这一栏列出了 JS 调用栈。
在 Main 这一栏中,可以看到我们的 KeyPress 事件执行了 771.03ms,然后往上托动,就能看到 KeyPress 中 JS 的执行栈,能找到每个函数的执行时间。
拖动到最下面,你可以看到 onChange 函数执行了很长时间,点击它,你可以在下面看到这个函数的具体信息,点击 demo1.js:7
甚至能看到每一行执行了多长时间。
罪魁祸首找到了,第九行代码执行了 630ms,找到问题所在,就好解决了。
这是一个最简单的例子,这种由单个地方引起的性能问题,也是比较好解决的。找到它、修改它、解决它!
React.Profiler 是 React 提供的,分析组件渲染次数、开始时间及耗时的一个 API,你可以在官网找到它的文档。
当然我们不需要每个组件都去加一个 React.Profiler 包裹,在开发环境下,React 会默记录每个组件的信息,我们可以通过 Chrome Profiler Tab 整体分析。
当然我们的 Chrome 需要安装 React 扩展,才能在工具栏中找到 Profiler
的 Tab。
Profiler 的用法和 Performance 用法差不多,点击开始记录,操作页面,然后停止记录,就会产出相关数据。
我找了一张比较复杂的图来做个示例,图中的数字分别表示:本次操作 React 做了 26 次 commit,第 14 次 commit 耗时最长,该次 commit 从 3.4s 时开始,消耗了 89.1 ms。
同时我们切换到 Ranked 模式,可以看到该次 commit,每个组件的耗时排名。比如下图表示 MarkdownText
组件耗时最长,达到 13.7 ms。
通过 React.Profiler,我们可以清晰的看到 React 组件的执行次数及时间,为我们优化性能指明了方向。
但我们需要注意的是,React.Profiler 记录的是 commit 阶段的数据。React 的执行分为两个阶段:
componentDidMount
和 componentDidUpdate
之类的生命周期函数。所以 React.Profiler 的分析范围是有限的,比如我们最开始的 input 示例,通过 React Profiler 是分析不出来性能问题的。
如果所有的性能问题都像上面这么简单就好了。某个点耗时极长,找到它并改进之,皆大欢喜。但在 React 项目中,最容易出现的问题是组件太多,每个组件执行 1ms,一百个组件就执行了 100ms,怎么优化?没有任何一个突出的点可以攻克,我们也不可能把一百个组件都优化成 0.01 ms。
class App extend React.Component{
constructor(props){
super(props);
this.state={
count: 0
}
}
render(){
return (
<div>
<A />
<B />
<C />
<D />
<Button onClick={()=>{ this.setState({count: 1}) }}>click</Button>
</div>
)
}
}
就像上面这个组件一样,当我们点击 Button 更新 state 时,A/B/C/D 四个组件均会执行一次 render 计算,这些计算完全是无用的。当我们组件够多时,会逐渐成为性能瓶颈!我们目标是减少不必要的 render。
说到避免 Render,当然第一时间想到的就是 ShouldComponentUpdate 这个生命周期,该生命周期通过判断 props 及 state 是否变化来手动控制是否需要执行 render。当然如果使用 PureComponent,组件会自动处理 ShouldComponentUpdate。
使用 PureComponent/ShouldComponentUpdate 时,需要注意几点:
React.memo 与 PureComponent 一样,但它是为函数组件服务的。React.memo 会对 props 进行浅比较,如果一致,则不会再执行了。
const App = React.memo(()=>{
return <div></div>
});
当然,如果你的数据不是 immutable 的,你可以通过 React.memo 的第二个参数来手动进行深比较,同样极其不推荐。
React.memo 对 props 的变化做了优化,避免了无用的 render。那 state 要怎么控制呢?
const [state, setState] = useState(0);
React 函数组件的 useState,其 setState 会自动做浅比较,也就是如果你在上面例子中调用了 setState(0)
,函数组件会忽略这次更新,并不会执行 render 的。一般在使用的时候要注意这一点,经常有同学掉进这个坑里面。
React.useMemo 是 React 内置 Hooks 之一,主要为了解决函数组件在频繁 render 时,无差别频繁触发无用的昂贵计算 ,一般会作为性能优化的手段之一。
const App = (props)=>{
const [boolean, setBoolean] = useState(false);
const [start, setStart] = useState(0);
// 这是一个非常耗时的计算
const result = computeExpensiveFunc(start);
}
在上面例子中, computeExpensiveFunc
是一个非常耗时的计算,但是当我们触发 setBoolean
时,组件会重新渲染, computeExpensiveFunc
会执行一次。这次执行是毫无意义的,因为 computeExpensiveFunc
的结果只与 start
有关系。
React.useMemo 就是为了解决这个问题诞生的,它可以指定只有当 start
变化时,才允许重新计算新的 result
。
const result = useMemo(()=>computeExpensiveFunc(start), [start]);
我建议 React.useMemo 要多用,能用就用,避免性能浪费。
在函数组件中,React.useCallback 也是性能优化的手段之一。
const OtherComponent = React.memo(()=>{
...
});
const App = (props)=>{
const [boolan, setBoolean] = useState(false);
const [value, setValue] = useState(0);
const onChange = (v)=>{
axios.post(`/api?v=${v}&state=${state}`)
}
return (
<div>
{/* OtherComponent 是一个非常昂贵的组件 */}
<OtherComponent onChange={onChange}/>
</div>
)
}
在上面的例子中, OtherComponent
是一个非常昂贵的组件,我们要避免无用的 render。虽然 OtherComponent
已经用 React.memo 包裹起来了,但在父组件每次触发 setBoolean
时, OtherComponent
仍会频繁 render。
因为父级组件 onChange
函数在每一次 render 时,都是新生成的,导致子组件浅比较失效。通过 React.useCallback,我们可以让 onChange 只有在 state 变化时,才重新生成。
const onChange = React.useCallback((v)=>{
axios.post(`/api?v=${v}&state=${state}`)
}, [state])
通过 useCallback 包裹后, boolean
的变化不会触发 OtherComponent
,只有 state
变化时,才会触发,可以避免很多无用的 OtherComponent
执行。
但是仔细想想, state
变化其实也是没有必要触发 OtherComponent
的,我们只要保证 onChange
一定能访问到最新的 state
,就可以避免 state
变化时,触发 OtherComponent
的 render。
const onChange = usePersistFn((v)=>{
axios.post(`/api?v=${v}&state=${state}`)
})
上面的例子,我们使用了 Umi Hooks 的 usePersistFn,它可以保证函数地址永远不会变化,无论何时, onChange
地址都不会变化,也就是无论何时, OtherComponent
都不会重新 render 了。
Context 是跨组件传值的一种方案,但我们需要知道,我们无法阻止 Context 触发的 render。
不像 props 和 state,React 提供了 API 进行浅比较,避免无用的 render,Context 完全没有任何方案可以避免无用的渲染。
有几点关于 Context 的建议:
Redux 中的一些细节,稍不注意,就会触发无用的 render,或者其它的坑。
const App = (props)=>{
return (
<div>
{props.project.id}
</div>
)
}
export default connect((state)=>{
layout: state.layout,
project: state.project,
user: state.user
})(App);
在上面的例子中,App 组件显示声明依赖了 redux 的 layout
、 project
、 user
数据,在这三个数据变化时,都会触发 App 重新 render。
但是 App 只需要监听 project.id
的变化,所以精细化依赖可以避免无效的 render,是一种有效的优化手段。
const App = (props)=>{
return (
<div>
{props.projectId}
</div>
)
}
export default connect((state)=>{
projectId: state.project.id,
})(App);
我们经常会不小心直接操作 redux 源数据,导致意料之外的 BUG。
我们知道,JS 中的 数组/对象 是地址引用的。在下面的例子中,我们直接操作数组,并不会改变数据的地址。
const list = ['1'];
const oldList = list;
list.push('a');
list === oldList; //true
在 Redux 中,就经常犯这样的错误。下面的例子,当触发 PUSH
后,直接修改了 state.list
,导致 state.list
的地址并没有变化。
let initState = {
list: ['1']
}
function counterReducer(state, action) {
switch (action.type) {
case 'PUSH':
state.list.push('2');
return {
list: state.list
}
default:
return state;
}
}
如果组件中使用了 ShouldComponentUpdate
或者 React.memo
,浅比较 props.list === nextProps.list
,会阻止组件更新,导致意料之外的 BUG。
所以如果大量使用了 ShouldComponentUpdate
与 React.meo
,则一定要保证依赖数据的不可变性!建议使用 immer.js 来操作复杂数据。
在项目初期,一定要考虑项目的复杂度,及早采取有效的措施,防止产生性能问题。如果在中后期才考虑性能问题,则难度会增加数十倍不止。
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
`
const path = require('path');
module.exports = {
/*入口*/
entry: path.join(__dirname, 'src/index.js'),
/*输出到dist文件夹,输出文件名字为bundle.js*/
output: {
path: path.join(__dirname, './dist'),
filename: 'bundle.js'
}
};
`
webpack.dev.config.js 文件
Originally posted by @ntuwang in #1 (comment)
本着学习的目的,在react-family框架基础上,做了最小的修改,使框架兼容了IE8浏览器。
预览地址:https://brickspert.github.io/react-family-ie8/index.html
源码地址:react-family-ie8
这是一个痛苦的过程,不过看到结果还是非常开心的。
下面就让我们开始吧。
第一部分,我们先修改开发坏境及开发配置文件~
react降级
npm install [email protected] [email protected] --save
webpack降级到v1
备注:网上也有很多人说,webpack v3也能兼容IE8,但是我试了很长时间也没搞好。被迫降版本了,这里应该是我的问题。
npm install [email protected] [email protected] --save-dev
babel-loader降级
webpack 1.x 对应 babel-loader <= 6.x
npm install [email protected] --save-dev
修改webpack配置文件,至兼容v1的状态。在我们的项目中,主要修改
webpack.common.config.js
和webpack.dev.config.js
里面的module->rules
,改回loaders
。
删除react-hot-loader
相关的代码
webpack.dev.config.js
直接删除entry
,删除尾部的webpack.merge
的自定义函数。new webpack.HashedModuleIdsPlugin()
删除。因为这不是webpack v1的。
现在就可以npm start
启动了,IE8浏览器打开来,你发现是空白的。没关系,打开调试器,看什么错误。
这里注意下,我用虚拟机通过局域网IP访问的,直接打不开页面,报错Invalid Host header
。
修改package.json->start
命令,增加--public 192.168.x.x
,后面的IP为你的局域网IP就可以啦。
接着说错误。现在我们看到的脚本错误应该是“缺少标识符”。
参考这里,我们使用es3ify
。
npm install --save es3ify-webpack-plugin
webpack.commn.config.js
使用插件。
再npm start
,发现错误换了,“对象不支持此属性或方法”。
这次我们使用es5-shim
。
npm install --save es5-shim
webpack.common.config.js
修改入口entry->app
app: [
"es5-shim", "es5-shim/es5-sham",
"babel-polyfill",
path.join(__dirname, 'src/index.js')
]
这里我有个问题,必须删除entry
里面的vendor
和plugins
里面CommonsChunkPlugin
相关的代码。
不删除IE8就一直报错,我开始猜是要把es5-shim/shame/babel-polyfill
等提取出来,独立于app
和vendor
。但是提取了还是不行唉,学艺不精~只能先删除了。
继续执行,又是错误“例外被抛出且未被接住”。
npm install export-from-ie8 --save
然后webpack.common.config.js
使用
postLoaders: [
{
test: /\.js$/,
loaders: ['export-from-ie8/loader']
}
]
现在你再执行,发现IE8可以正常访问了。嘿嘿嘿。最好我们把BrowserRouter
改成HashRouter
,这样路由切换页面就不会刷新啦。
到目前为止,开发坏境已经OK了,下面就是修改生产坏境的配置文件了。
extract-text-webpack-plugin
插件降级。
npm install --save-dev [email protected]
然后按文档修改配置文件。
uglifyjs-webpack-plugin
增加兼容IE8参数。
做了这两项,你执行npm run bundle
,发现生产坏境也OK了。
虽然最终做出了兼容IE8的版本,但是还有很多地方搞不太懂的。后续会继续学习,改进,更新的。
做这个任务的时候,参考了很多很多的文章,我就不一一列举了,感谢!
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
alias里的别名设置少了引号,似乎会不生效
工作中需要实现尾部红色警告的一个圆环倒计时,网上搜了一圈,同时满足css
,单边颜色渐变
,圆形
的案例还真没有,光单边颜色渐变
的案例都几乎没有。那我自己实现一个吧,不做不知道,一做吓一跳,竟然花了好几个小时才完成,特此记录一下,有缘人拿去。
直接上结果图
这个进度条可以拆解成两部分
思考下思路:一个盒子,三个边是绿色,一个边是绿色到红色的渐变色,然后用border-radius
弯曲成一个圆。
哈哈,这么一想,好简单啊。
but,but,只有单边颜色渐变用css
是没法实现的。吐血~,不信你去试试,去查查。
难点就在如何实现单边颜色渐变这里。
follow me~
这步非常简单
<div class='box'>
<div class='green-border'></div>
</div>
<style>
*{
box-sizing: border-box;
}
.box {
width: 240px;
height: 240px;
}
.green-border {
width: 100%;
height: 100%;
border-radius: 50%;
border: 20px solid #00a853;
border-bottom-color: transparent;
transform: rotate(45deg);
}
</style>
难点就在这里,我们画一个从上到下渐变的方块,放在空白圆环那里。
<div class='red-gradients'></div>
<style>
.box{
position: relative;
}
.red-gradients {
width: 120px;
height: 120px;
background: linear-gradient(to right, #00a853, #F04134);
position: absolute;
bottom: 0;
left: 0;
z-index: 1;
}
</style>
接下来我们要覆盖多余的内容,圆内放一个div,盖住多余的部分。外面的通过box
的overflow:hidden
来隐藏。
<div class='inner-circle'></div>
<style>
.box{
border-radius: 50%;
overflow: hidden;
}
.inner-circle {
width: 200px;
height: 200px;
border-radius: 50%;
position: absolute;
z-index: 2;
top: 20px;
left: 20px;
background-color: white;
}
</style>
大功告成了,真是机智!
接下来我们讲讲如何实现灰色动态进度条。
算了,不写了~网上讲圆环进度条的一大堆,我就不重复讲了,随便找个例子推荐下:https://www.xiabingbao.com/css/2015/07/27/css3-animation-circle.html
完整源码在这里,祝你好运!
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
虽然 antd 提供了大量的 Icon 图标,但是在平时的设计稿中,会出现各种设计师自定义的 Icon,对于这类图标,怎样处理更好呢?
svg 也是一种图片,可以作为 img
的 src
使用。
通过配置 url-loader
的配置,我们可以直接引用 svg 资源。
{
test: /\.(png|jpg|gif|svg)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}
import customSvg from '../assets/images/xxx.svg';
<img src={customSvg} />
color
, font-size
等。参见官方文档
在 antd v3.9.0 之后,提供了一个 createFromIconfontCN
方法,方便开发者调用在 iconfont.cn 上自行管理的图标。
首先你需要在 iconfont 上创建自己的图标库,并上传自定义的 Icon,然后就可以通过自定义组件使用了。
iconfont 上新建图标库比较简单,我就不赘述了。我讲一下在上传自定义 Icon 时的几个坑。
Q:自定义 SVG 上传后,显示为空白。
如果 SVG 图标不是封闭的,上传到 iconfont 之后,会显示为空白。那什么是封闭呢?大概就是图标有缺口~
比如下面的线条就是非封闭的,上传到 iconfont 就变成空白了。
我们只需要选中该图标,进行“轮廓化”处理即可。
Q:自定义 SVG 上传后,图标显示不全。
如果 SVG 图标由多个部分组成,但是没有进行“轮廓化”,那上传到 iconfont 之后,可能会显示不全。比如下图的图标,上传到 iconfont 后,图标显示不全了。
我们选中该图标多个部分,进行“轮廓化”即可。
**Q:自定义 SVG 上传后,无法通过 color
属性设置颜色。
我们从 sketch 导出的 svg,一般都是自带颜色的,我们在上传时,选择去除颜色并提交即可。
经过上面的步骤,我们已经把需要的 Icon 上传到 iconfont 了,同时我们可以拿到字体的链接。
然后通过 antd 的 createFromIconfontCN
包装下即可使用。
const MyIcon = Icon.createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js', // 在 iconfont.cn 上生成
});
/* 使用起来还比较简单 */
<MyIcon type="xxx"/>
通过 svgr 我们可以将一个普通的 svg 图片,转成 React 组件。
参见官方文档
// webpack.config.js
// umijs 配置见 https://github.com/umijs/umi/issues/1078
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'babel-loader',
},
{
loader: '@svgr/webpack',
options: {
babel: false,
icon: true,
},
},
],
}
import { Icon } from 'antd';
import MessageSvg from 'path/to/message.svg'; // path to your '*.svg' file.
ReactDOM.render(<Icon component={MessageSvg} />, mountNode);
对于带颜色的图标,loader 并不知道 svg 上哪个颜色是需要自定义的,所以需要手动将 svg 中写死的 color 值改成 currentColor
。
svgr 可以通过 cli,将 SVG 转换为组件,同时可以将 svg 中的颜色,设置成变量。
正常情况下,我们导出的 SVG 长这样:
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 58 (84663) - https://sketch.com -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<g id="组件" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="一级导航" transform="translate(-1197.000000, -18.000000)">
<g id="Group" transform="translate(1197.000000, 18.000000)">
<circle id="Oval" fill="#2F54EB" cx="10" cy="10" r="10"></circle>
<g id="plus" transform="translate(3.000000, 3.000000)" fill-rule="nonzero">
<rect id="Rectangle-path" fill="#000000" opacity="0" x="0" y="0" width="14" height="14"></rect>
<path d="M11.59375,6.48046875 L7.51953125,6.48046875 L7.51953125,2.078125 L6.48046875,2.078125 L6.48046875,6.48046875 L2.40625,6.48046875 C2.34609375,6.48046875 2.296875,6.5296875 2.296875,6.58984375 L2.296875,7.41015625 C2.296875,7.4703125 2.34609375,7.51953125 2.40625,7.51953125 L6.48046875,7.51953125 L6.48046875,11.921875 L7.51953125,11.921875 L7.51953125,7.51953125 L11.59375,7.51953125 C11.6539063,7.51953125 11.703125,7.4703125 11.703125,7.41015625 L11.703125,6.58984375 C11.703125,6.5296875 11.6539063,6.48046875 11.59375,6.48046875 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</g>
</g>
</g>
</svg>
通过 svgr 的命令行尝试转一下:
npx @svgr/cli --icon --replace-attr-values "#2F54EB=currentColor" message.svg
输出结果为:
import React from "react";
const MessageSvg = props => (
<svg width="1em" height="1em" viewBox="0 0 20 20" {...props}>
<g fill="none" fillRule="evenodd">
<circle fill="currentColor" cx={10} cy={10} r={10} />
<path
d="M14.594 9.48H10.52V5.078H9.48V9.48H5.406a.11.11 0 00-.11.11v.82c0 .06.05.11.11.11H9.48v4.402h1.04V10.52h4.074c.06 0 .11-.05.11-.11v-.82a.11.11 0 00-.11-.11z"
fill="#FFF"
fillRule="nonzero"
/>
</g>
</svg>
);
export default MessageSvg;
good job~ 现在我们获得了一个纯 React 组件,直接使用即可。
import { Icon } from 'antd';
import MessageSvg from './MessageSvg';
ReactDOM.render(<Icon component={MessageSvg} />, mountNode);
上面的几种方案我有在多个项目中尝试过,目前觉得 通过 webpack 将 SVG 转成组件 方案不错。
color
, font-size
等属性控制样式。关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
react-router 中没有return返回
import React from 'react';
import {
BrowserRouter as Router,
Route,
Switch,
Link
} from 'react-router-dom';
import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';
const getRouter = () => {
return (
<Router>
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/page1">Page1</Link></li>
</ul>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/page1" component={Page1}/>
</Switch>
</div>
</Router>
)
}
export default getRouter;
有时候人们很喜欢造一些名字很吓人的名词,让人一听这个名词就觉得自己不可能学会,从而让人望而却步。但是其实这些名词背后所代表的东西其实很简单。来自React.js 小书
a higher-order component is a function that takes a component and returns a new component.
翻译:高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
理解了吗?看了定义似懂非懂?继续往下看。
我们通过普通函数来理解什么是高阶组件哦~
welcome
,一个goodbye
。两个函数先从localStorage
读取了username
,然后对username
做了一些处理。function welcome() {
let username = localStorage.getItem('username');
console.log('welcome ' + username);
}
function goodbey() {
let username = localStorage.getItem('username');
console.log('goodbey ' + username);
}
welcome();
goodbey();
我们发现两个函数有一句代码是一样的,这叫冗余唉。不好不好~(你可以把那一句代码理解成平时的一大堆代码)
我们要写一个中间函数,读取username
,他来负责把username
传递给两个函数。
function welcome(username) {
console.log('welcome ' + username);
}
function goodbey(username) {
console.log('goodbey ' + username);
}
function wrapWithUsername(wrappedFunc) {
let newFunc = () => {
let username = localStorage.getItem('username');
wrappedFunc(username);
};
return newFunc;
}
welcome = wrapWithUsername(welcome);
goodbey = wrapWithUsername(goodbey);
welcome();
goodbey();
好了,我们里面的wrapWithUsername
函数就是一个“高阶函数”。
他做了什么?他帮我们处理了username
,传递给目标函数。我们调用最终的函数welcome
的时候,根本不用关心username
是怎么来的。
我们增加个用户study
函数。
function study(username){
console.log(username+' study');
}
study = wrapWithUsername(study);
study();
这里你是不是理解了为什么说wrapWithUsername
是高阶函数?我们只需要知道,用wrapWithUsername
包装我们的study
函数后,study
函数第一个参数是username
。
我们写平时写代码的时候,不用关心wrapWithUsername
内部是如何实现的。
高阶组件就是一个没有副作用的纯函数。
我们把上一节的函数统统改成react
组件。
welcome
函数转为react
组件。
import React, {Component} from 'react'
class Welcome extends Component {
constructor(props) {
super(props);
this.state = {
username: ''
}
}
componentWillMount() {
let username = localStorage.getItem('username');
this.setState({
username: username
})
}
render() {
return (
<div>welcome {this.state.username}</div>
)
}
}
export default Welcome;
goodbey
函数转为react
组件。
import React, {Component} from 'react'
class Goodbye extends Component {
constructor(props) {
super(props);
this.state = {
username: ''
}
}
componentWillMount() {
let username = localStorage.getItem('username');
this.setState({
username: username
})
}
render() {
return (
<div>goodbye {this.state.username}</div>
)
}
}
export default Goodbye;
按照上一节wrapWithUsername
函数的思路,我们来写一个高阶组件(高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件)。
import React, {Component} from 'react'
export default (WrappedComponent) => {
class NewComponent extends Component {
constructor() {
super();
this.state = {
username: ''
}
}
componentWillMount() {
let username = localStorage.getItem('username');
this.setState({
username: username
})
}
render() {
return <WrappedComponent username={this.state.username}/>
}
}
return NewComponent
}
这样我们就能简化Welcome
组件和Goodbye
组件。
import React, {Component} from 'react';
import wrapWithUsername from 'wrapWithUsername';
class Welcome extends Component {
render() {
return (
<div>welcome {this.props.username}</div>
)
}
}
Welcome = wrapWithUsername(Welcome);
export default Welcome;
import React, {Component} from 'react';
import wrapWithUsername from 'wrapWithUsername';
class Goodbye extends Component {
render() {
return (
<div>goodbye {this.props.username}</div>
)
}
}
Goodbye = wrapWithUsername(Goodbye);
export default Goodbye;
看到没有,高阶组件就是把username
通过props
传递给目标组件了。目标组件只管从props
里面拿来用就好了。
到这里位置,高阶组件就讲完了。你再返回去理解下定义,是不是豁然开朗~
你现在理解react-redux
的connect
函数~
把redux
的state
和action
创建函数,通过props
注入给了Component
。
你在目标组件Component
里面可以直接用this.props
去调用redux state
和action
创建函数了。
ConnectedComment = connect(mapStateToProps, mapDispatchToProps)(Component);
相当于这样
// connect是一个返回函数的函数(就是个高阶函数)
const enhance = connect(mapStateToProps, mapDispatchToProps);
// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store
// 关联起来的新组件
const ConnectedComment = enhance(Component);
antd
的Form也是一样的
const WrappedNormalLoginForm = Form.create()(NormalLoginForm);
参考地址:
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
当我们使用react-router v3
的时候,我们想跳转路由,我们一般这样处理
react-router
导出browserHistory
。browserHistory.push()
等等方法操作路由跳转。类似下面这样
import browserHistory from 'react-router';
export function addProduct(props) {
return dispatch =>
axios.post(`xxx`, props, config)
.then(response => {
browserHistory.push('/cart'); //这里
});
}
but!! 问题来了,在react-router v4
中,不提供browserHistory
等的导出~~
那怎么办?我如何控制路由跳转呢???
withRouter
高阶组件,提供了history
让你使用~
import React from "react";
import {withRouter} from "react-router-dom";
class MyComponent extends React.Component {
...
myFunction() {
this.props.history.push("/some/Path");
}
...
}
export default withRouter(MyComponent);
这是官方推荐做法哦。但是这种方法用起来有点难受,比如我们想在redux
里面使用路由的时候,我们只能在组件把history
传递过去。。
就像问题章节的代码那种场景使用,我们就必须从组件中传一个history
参数过去。。。
react-router v4
在 Router
组件中通过Contex
暴露了一个router
对象~
在子组件中使用Context
,我们可以获得router
对象,如下面例子~
import React from "react";
import PropTypes from "prop-types";
class MyComponent extends React.Component {
static contextTypes = {
router: PropTypes.object
}
constructor(props, context) {
super(props, context);
}
...
myFunction() {
this.context.router.history.push("/some/Path");
}
...
}
当然,这种方法慎用~尽量不用。因为react不推荐使用contex
哦。在未来版本中有可能被抛弃哦。
其实分析问题所在,就是v3
中把我们传递给Router
组件的history
又暴露出来,让我们调用了~~
而react-router v4
的组件BrowserRouter
自己创建了history
,
并且不暴露出来,不让我们引用了。尴尬~
我们可以不使用推荐的BrowserRouter
,依旧使用Router
组件。我们自己创建history
,其他地方调用自己创建的history
。看代码~
history
// src/history.js
import createHistory from 'history/createBrowserHistory';
export default createHistory();
Router
组件// src/index.js
import { Router, Link, Route } from 'react-router-dom';
import history from './history';
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
...
</Router>
</Provider>,
document.getElementById('root'),
);
import history from './history';
export function addProduct(props) {
return dispatch =>
axios.post(`xxx`, props, config)
.then(response => {
history.push('/cart'); //这里
});
}
BrowserRouter
确实,react-router v4
推荐使用BrowserRouter
组件,而在第三个解决方案中,我们抛弃了这个组件,又回退使用了Router
组件。
怎么办。 你去看看BrowserRouter
的源码,我觉得你就豁然开朗了。
源码非常简单,没什么东西。我们完全自己写一个BrowserRouter
组件,然后替换第三种解决方法中的Router
组件。嘿嘿。
讲到这里也结束了,我自己目前在使用第三种方法,虽然官方推荐第一种,我觉得用着比较麻烦唉。~
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
模块热替换保存状态的部分,按照您这边的教程写的,但是没有实现出来。
点击加号之后数字更新,这时候修改home.js文件,就会重新更新了。
作为 React Hooks 库 ahooks 的作者,我应该算一个非常非常资深的 React Hooks 用户。在两年多的 React Hooks 使用过程中,我越来越发现大家(包括我自己)对 React Hooks 的使用姿势存在很大误区,归根到底是官方文档的教程很不严谨,存在错误的指引。
对于所有的 React Hooks 用户,都有一个共识:“useEffect 中使用到外部变量,都应该放到第二个数组参数中”,同时我们会安装 eslint-plugin-react-hooks 插件,来提醒自己是不是忘了某些变量。
以上共识来自官方文档:
我愿称该条规则为万恶之源,这条规则以高亮展示,所有的新人都很重视,包括我自己。然而在实际的开发中,发现事情并不是这样的。
下面举一个比较简单的例子,要求如下:当 props.count
和 count
变化时,上报当前所有数据。
这个例子比较简单,先贴下源码:
function Demo(props) {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const [a, setA] = useState('');
useEffect(() => {
monitor(props.count, count, text, a);
}, [props.count, count]);
return (
<div>
<button
onClick={() => setCount(c => c + 1)}
>
click
</button>
<input value={text} onChange={e => setText(e.target.value)} />
<input value={a} onChange={e => setA(e.target.value)} />
</div>
)
}
我们能看到示例代码中,useEffect 是不符合 React 官方建议的,text
和 a
变量没有放到依赖数组中,ESLint 警告如下:
那如果按照规范,我们把依赖项都放到第二个数组参数中,会怎样呢?
useEffect(() => {
monitor(props.count, count, text, a);
}, [props.count, count, text, a]);
如上的代码虽然符合了 React 官方的规范,但不满足我们的业务需求了,当 text
和 a
变化时,也触发了函数执行。
此时陷入了困境,当满足 useEffect 使用规范时,业务需求就不能满足了。当满足业务需求时,useEffect 就不规范了。
我的建议为:
eslint-plugin-react-hooks
插件,或者可以选择性忽略该插件的警告。假如完全按第二个建议来写代码,很多人又担心,会不会造成一些不必要的闭包问题?我的结论是:闭包问题和 useEffect 的 deps 参数没有太大关系。
比如我有一个这样的需求:当进入页面 3s 后,输出当前最新的 count。代码如下:
function Demo() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log(count)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
return (
<button
onClick={() => setCount(c => c + 1)}
>
click
</button>
)
}
以上代码,实现了初始化 3s 后,输出 count。但很遗憾,这里肯定会出闭包问题,哪怕进来之后我们多次点击了 button,输出的 count 仍然为 0。
那假如我们把 count
放到 deps 中,是不是就好了?
useEffect(() => {
const timer = setTimeout(() => {
console.log(count)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [count])
如上代码,此时确实没有闭包问题了,但在每次 count
变化时,定时器卸载并重新开始计时了,不满足我们的最初需求了。
要解决的唯一办法为:
const [count, setCount] = useState(0);
// 通过 ref 来记忆最新的 count
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setTimeout(() => {
console.log(countRef.current)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
虽然上面的代码,很绕,但确实,只有这个解决方案。请记住这段代码,功能真的很强大。
const countRef = useRef(count);
countRef.current = count;
上面的例子,可以发现,闭包问题是不能仅仅通过遵守 React 规则来避免的。我们必须清晰的知道,在什么场景下会出现闭包问题。
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const c = a + b;
useEffect(()=>{
console.log(a, b, c)
}, [a]);
useEffect(()=>{
console.log(a, b, c)
}, [b]);
useEffect(()=>{
console.log(a, b, c)
}, [c]);
在一般的使用过程中,是不会有闭包问题的,如上代码中,完全不会有闭包问题,和 deps 怎么写没有任何关系。
在延迟调用的场景下,一定会存在闭包问题。 什么是延迟调用?
const getUsername = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('John');
}, 3000);
})
}
function Demo() {
const [count, setCount] = useState(0);
// setTimeout 会造成闭包问题
useEffect(() => {
const timer = setTimeout(() => {
console.log(count);
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
// setInterval 会造成闭包问题
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 3000);
return () => {
clearInterval(timer);
}
}, [])
// Promise.then 会造成闭包问题
useEffect(() => {
getUsername().then(() => {
console.log(count);
});
}, [])
// useEffect 卸载函数会造成闭包问题
useEffect(() => {
return () => {
console.log(count);
}
}, []);
return (
<button
onClick={() => setCount(c => c + 1)}
>
click
</button>
)
}
在以上示例代码中,四种情况均会出现闭包问题,永远输出 0
。这四种情况的根因都是一样的,我们看一下代码的执行顺序:
count = 0
count=0
的引用关系可以看到,闭包问题均是出现在延迟调用的场景下。解决办法如下:
const [count, setCount] = useState(0);
// 通过 ref 来记忆最新的 count
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setTimeout(() => {
console.log(countRef.current)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
......
通过 useRef
来保证任何时候访问的 countRef.current
都是最新的,以解决闭包问题。
到这里,我重申下我对 useEffect 的建议:
我建议在项目中尽量不要用 useCallback,大部分场景下,不仅没有提升性能,反而让代码可读性变的很差。
useCallback 可以记住函数,避免函数重复生成,这样函数在传递给子组件时,可以避免子组件重复渲染,提高性能。
const someFunc = useCallback(()=> {
doSomething();
}, []);
return <ExpensiveComponent func={someFunc} />
基于以上认知,很多同学(包括我自己)在写代码时,只要是个函数,都加个 useCallback,是你么?反正我以前是。
但我们要注意,提高性能还必须有另外一个条件,子组件必须使用了 shouldComponentUpdate
或者 React.memo
来忽略同样的参数重复渲染。
假如 ExpensiveComponent
组件只是一个普通组件,是没有任何用的。比如下面这样:
const ExpensiveComponent = ({ func }) => {
return (
<div onClick={func}>
hello
</div>
)
}
必须通过 React.memo
包裹 ExpensiveComponent
,才会避免参数不变的情况下的重复渲染,提高性能。
const ExpensiveComponent = React.memo(({ func }) => {
return (
<div onClick={func}>
hello
</div>
)
})
所以,useCallback 是要和 shouldComponentUpdate/React.memo
配套使用的,你用对了吗?当然,我建议一般项目中不用考虑性能优化的问题,也就是不要使用 useCallback 了,除非有个别非常复杂的组件,单独使用即可。
我看到过一些代码,使用 useCallback 后,大概长这样:
const someFuncA = useCallback((d, g, x, y)=> {
doSomething(a, b, c, d, g, x, y);
}, [a, b, c]);
const someFuncB = useCallback(()=> {
someFuncA(d, g, x, y);
}, [someFuncA, d, g, x, y]);
useEffect(()=>{
someFuncB();
}, [someFuncB]);
在上面的代码中,变量依赖一层一层传递,最终要判断具体哪些变量变化会触发 useEffect 执行,是一件很头疼的事情。
我期望不要用 useCallback,直接裸写函数就好:
const someFuncA = (d, g, x, y)=> {
doSomething(a, b, c, d, g, x, y);
};
const someFuncB = ()=> {
someFuncA(d, g, x, y);
};
useEffect(()=>{
someFuncB();
}, [...]);
在 useEffect 存在延迟调用的场景下,可能造成闭包问题,那通过咱们万能的方法就能解决:
const someFuncA = (d, g, x, y)=> {
doSomething(a, b, c, d, g, x, y);
};
const someFuncB = ()=> {
someFuncA(d, g, x, y);
};
+ const someFuncBRef = useRef(someFuncB);
+ someFuncBRef.current = someFuncB;
useEffect(()=>{
+ setTimeout(()=>{
+ someFuncBRef.current();
+ }, 1000)
}, [...]);
对 useCallback 的建议就一句话:没事别用 useCallback。
相较于 useCallback 而言,useMemo 的收益是显而易见的。
// 没有使用 useMemo
const memoizedValue = computeExpensiveValue(a, b);
// 使用 useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
如果没有使用 useMemo,computeExpensiveValue
会在每一次渲染的时候执行。如果使用了 useMemo
,只有在 a
和 b
变化时,才会执行一次 computeExpensiveValue
。
这笔账大家应该都会算,所以我建议 useMemo 可以适当使用。
当然也不是无节制的使用,在很简单的基础类型计算时,可能 useMemo 并不划算。
const a = 1;
const b = 2;
const c = useMemo(()=> a + b, [a, b]);
比如上面的例子,请问计算 a+b
的消耗大?还是记录 a/b
,并比较a/b
是否变化的消耗大?
明显 a+b
消耗更小。
const a = 1;
const b = 2;
const c = a + b;
这笔账大家可以自己算,我建议简单的基础类型计算,就不要用 useMemo 了~
useState 应该算最简单的一个 Hooks,但在使用中,也有很多技巧可循,如果严格按照以下几点,代码可维护性直接翻倍。
一个 state 必须不能通过其它 state/props 直接计算出来,否则就不用定义 state。
const SomeComponent = (props) => {
const [source, setSource] = useState([
{type: 'done', value: 1},
{type: 'doing', value: 2},
])
const [doneSource, setDoneSource] = useState([])
const [doingSource, setDoingSource] = useState([])
useEffect(() => {
setDoingSource(source.filter(item => item.type === 'doing'))
setDoneSource(source.filter(item => item.type === 'done'))
}, [source])
return (
<div>
.....
</div>
)
}
上面的示例中,变量 doneSource
和 doingSource
可以通过变量 source
计算出来,那就不要定义 doneSource
和 doingSource
了!
const SomeComponent = (props) => {
const [source, setSource] = useState([
{type: 'done', value: 1},
{type: 'doing', value: 2},
])
const doneSource = useMemo(()=> source.filter(item => item.type === 'done'), [source]);
const doingSource = useMemo(()=> source.filter(item => item.type === 'doing'), [source]);
return (
<div>
.....
</div>
)
}
一般在项目中此类问题都比较隐晦,层层传递,在 Code Review 中很难一眼看出。如果能把变量定义清楚,那事情就成功了一半。
在项目中同一个数据,保证只存储在一个地方。
不要既存在 redux 中,又在组件中定义了一个 state 存储。
不要既存在父级组件中,又在当前组件中定义了一个 state 存储。
不要既存在 url query 中,又在组件中定义了一个 state 存储。
function SearchBox({ data }) {
const [searchKey, setSearchKey] = useState(getQuery('key'));
const handleSearchChange = e => {
const key = e.target.value;
setSearchKey(key);
history.push(`/movie-list?key=${key}`);
}
return (
<input
value={searchKey}
placeholder="Search..."
onChange={handleSearchChange}
/>
);
}
在上面的示例中,searchKey
存储在两个地方,既在 url query 上,又定义了一个 state。完全可以优化成下面这样:
function SearchBox({ data }) {
const searchKey = parse(localtion.search)?.key;
const handleSearchChange = e => {
const key = e.target.value;
history.push(`/movie-list?key=${key}`);
}
return (
<input
value={searchKey}
placeholder="Search..."
onChange={handleSearchChange}
/>
);
}
在实际项目开发中,此类问题也是比较隐晦,编码时应注意。
项目中有木有写过这样的代码:
const [firstName, setFirstName] = useState();
const [lastName, setLastName] = useState();
const [school, setSchool] = useState();
const [age, setAge] = useState();
const [address, setAddress] = useState();
const [weather, setWeather] = useState();
const [room, setRoom] = useState();
反正我最开始是写过,useState 拆分过细,导致代码中一大片 useState。
我建议,同样含义的变量可以合并成一个 state,代码可读性会提升很多:
const [userInfo, setUserInfo] = useState({
firstName,
lastName,
school,
age,
address
});
const [weather, setWeather] = useState();
const [room, setRoom] = useState();
当然这种方式我们在变更变量时,一定不要忘记带上老的字段,比如我们只想修改 firstName
:
setUserInfo(s=> ({
...s,
fristName,
}))
其实如果是 React Class 组件,state 是会自动合并的:
this.setState({
firstName
})
在 Hooks 中,可以有这种用法吗?其实是可以的,我们自己封装一个 Hooks 就可以,比如 ahooks 的 useSetState,就封装了类似的逻辑:
const [userInfo, setUserInfo] = useSetState({
firstName,
lastName,
school,
age,
address
});
// 自动合并
setUserInfo({
firstName
})
我自己在项目中大量使用了 useSetState
来代替 useState
,来管理复杂类型的 state,妈妈更爱我了。
作为资深的 React Hooks 用户,我很认可 React Hooks 带来的提效,这也是我这几年完全拥抱 Hooks 的原因。同时我也越来越觉得 React Hooks 难驾驭,尤其随着 React 18 的 concurrent mode 的到来,不知道会带来什么坑。
最后再给大家三个建议:
先介绍下背景
非211,985本科毕业。一年半PHP经验,一年半前端经验,前端一直在做React
开发。
半年之前,我是一个前端小小小白。多么小白呢?
css
调样式全靠试。BFC
,啥是BFC
?为啥外边距会合并?有些会合并,有些不会合并,这都是啥玩意?z-index
为啥有时候有效果,有时候没效果?为啥有时候小的值还在大的上面?js
就会基础使用,稍微复杂的一脸懵逼。prototype
,立马头疼,这都是啥!什么原型,继承,离我远点!arguments
,作用域链等等都是啥?我都不想去思考这些问题,啊,,,头疼,这都是什么?我都不会啊!
这样的我,怎么出去面试?别人随便问个问题,我都不会!
我又去网上看了别人的面试题,娘的哟,这是啥?这又是啥?好像会点,但是说不出来~~
不行不行,我得赶紧学习了。但是我要怎么去准备呢?好像js,html,css,http
都没系统学过啊?好像react,webpack
这些玩意也没系统整理过啊。好多啊!
废话不多说,我们开始吧~~
吭哧
吭哧
吭哧
…...
经过半年的准备,我成功面试进了BAT
!
所以相信自己,从现在开始!你应该比我厉害吧?
我看到很多像我之前一样迷茫的人,我觉得我的经历是可以复制的。就写下来,共勉!
过程分为:
1. 系统学习基础知识
2. 面试题提高
3. 项目
基础知识通过看书来系统学习。
《JavaScript高级程序设计(第3版)》
系统学习一遍js,看这本书是非常痛苦的过程。预计时间在2个月左右。
不要心急,一章一章的过,每个知识点都要去理解,做笔记。
碰到看不懂的,反复去读,去网上查,一定要搞懂!
《CSS权威指南(第三版)》
系统学习css
,预计时间2星期。记得做笔记,不会的点,反复去读,去查。
css3教程学习
这个去网上找份教程,过一遍。每个新属性都去写下。
《html5与css3权威指南》
这个快速过下前面html
部分就好了。
1个星期左右读完。
《图解HTTP》
非常重要!系统学习HTTP
。半个月时间~
《ECMAScript 6 入门》
阮一峰写的。系统学习ES6
。这个因人而异,时间长短不一,不过真的很重要!
好了,到这里为止,我们已经系统的学习了前端的各种知识了。应该花费了3个多月了。如果你能坚持下来,绝对会有一个质的飞跃。
下面我的策略是,找面试题,把网上能找到的面试都记下来,每个题都去深入研究!
注意,是深入研究,不是浅显的知道答案就行了。
面试题大部分来源于这里https://github.com/h5bp/Front-end-Developer-Interview-Questions/tree/master/Translations/Chinese,还有很多很多其他的面经,我就不一一贴出来了,反正能搜到的我都看了~
我把我整理的面试题列出来,希望每个题你都能深入去研究。
DOCTYPE
(文档类型)的作用是什么?
浏览器标准模式 (standards mode
) 、几乎标准模式(almost standards mode
)和怪异模式 (quirks mode
) 之间的区别是什么?
使用 data-
属性的好处是什么?
可以去实践下data-*
的使用啦
如果把 HTML5
看作做一个开放平台,那它的构建模块有哪些?
研究下HTML5
的所有模块
cookies
、sessionStorage
和 localStorage
的区别
请解释 <script>
、<script async>
和 <script defer>
的区别。
为什么通常推荐将 CSS <link>
放置在 <head></head>
之间,而将 JS <script>
放置在 </body>
之前?你知道有哪些例外吗?
什么是渐进式渲染 (progressive rendering
)?
HTML
和 XHTML
有什么区别?
HMTL5
新标签
CSS
中类 (class
) 和 ID
的区别
请问 "resetting" 和 "normalizing" CSS 之间的区别?你会如何选择,为什么?
https://github.com/necolas/normalize.css
https://github.com/shannonmoeller/reset-css/blob/master/reset.css
请解释浮动 (Floats
) 及其工作原理
这个非常重要,前面读的书上有这个,一定要完全搞懂。
清除浮动
重要
描述z-index
和叠加上下文是如何形成的?
重要,书上有,先理解。然后我推荐两个文章
https://www.w3cplus.com/css/what-no-one-told-you-about-z-index.html
http://www.zhangxinxu.com/wordpress/2016/01/understand-css-stacking-context-order-z-index/
请描述 BFC(Block Formatting Context
) 及其如何工作?
理解BFC的特性及如何触发BFC
CSS sprites
优点,缺点
图片替换文字方案
你会如何解决特定浏览器的样式问题?
如何为有功能限制的浏览器提供网页?
渐进增强,优雅降级等等等等
有哪些的隐藏内容的方法?
栅格系统 (grid system
)
你用过媒体查询,或针对移动端的布局CSS
吗?
如何优化网页的打印样式?
在书写高效 CSS
时会有哪些问题需要考虑?
使用 CSS
预处理器的优缺点有哪些?
如果设计中使用了非标准的字体,你该如何去实现?
请解释浏览器是如何判断元素是否匹配某个 CSS
选择器?
请描述伪元素 (pseudo-elements
) 及其用途
伪元素,伪类等等都去研究下
请解释你对盒模型的理解,以及如何在 CSS
中告诉浏览器使用不同的盒模型来渲染你的布局
请罗列出你所知道的 display
属性的全部值
请解释 inline
和 inline-block
的区别
请解释 relative
、fixed
、absolute
和 static
元素的区别
请问你有尝试过 CSS Flexbox
或者 Grid
标准规格吗
flex很重要,每个属性都要知道。建议去读阮一峰的flex文章
为什么响应式设计 (responsive design
) 和自适应设计 (adaptive design
) 不同?
你有兼容 retina
屏幕的经历吗?如果有,在什么地方使用了何种技术?
移动端开发必须知道!
请问为何要使用 translate()
而非 absolute position
,或反之的理由?为什么?
如果实现一个高性能的CSS
动画效果?
IFC
css3
动画
各种属性熟悉下
布局之:左边定宽,右边自适应
圣杯布局,双飞翼布局
实现垂直居中和水平居中
事件代理
请解释 JavaScript
中 this
是如何工作的
javascript
继承
这个不多说,十分十分重要。建议按照《高程三》的继承那里,仔细理解哦。
javascript
模块化
理解模块化发展过程,理解 commonJS
,AMD
,CMD
,ES6
模块化
IIFE
立即执行函数
null undefined
区别
闭包 与 作用域
非常重要,书上有!
匿名函数
你是如何组织自己的代码?是使用模块模式,还是使用经典继承的方法?
宿主对象 (host objects
) 和原生对象 (native objects
)
请指出以下代码的区别:function Person(){}
、var person = Person()
、var person = new Person()
?
apply call bind
深入到源码如何实现这三个功能的。
new
源码如何实现的?
document.write()
特性检测 特性推断 UA字符串嗅探
Ajax
工作原理
着重理解XMLHttpRequest
!!
跨域
图片ping
, JSONP
, CORS
。
这是面试必问的点。注意一定要完全理解,完全!
变量声明提升
冒泡机制
attribute
和 property
document load
和 document DOMContentLoaded
非常重要哦
==
和 ===
有什么不同
同源策略 (same-origin policy
)
Cookie
,iframe
,AJAX
同源
strict
模式
为何通常会认为保留网站现有的全局作用域 (global scope
) 不去改变它,是较好的选择
为何你会使用 load
之类的事件 (event
)?此事件有缺点吗?你是否知道其他替代品,以及为何使用它们?
请解释什么是单页应用 (single page app
), 以及如何使其对搜索引擎友好 (SEO-friendly
)
相当重要
Promise
怎么用?源码如何实现的?
推荐文章:xieranmaya/blog#3
使用一种可以编译成 JavaScript
的语言来写 JavaScript
代码有哪些优缺点?
javascript
调试工具
对象遍历 和 数组遍历
可变对象和不可变对象
什么是事件循环 (event loop
)
非常重要,面试必问。
深入原理,宏任务,微任务等等
let var const
数组的方法
web worker
柯里化
创建对象的三种方法
深拷贝和浅拷贝
可以实现手写深拷贝
图片懒加载
咋实现的?
网页各种高度
这个好难记,我也没记住~
实现页面加载进度条
箭头函数ES5如何实现
虚拟DOM
是啥?以及diff算法原理
react
事件绑定
生命周期
函数式编程,纯函数
React
创建组件的方式
组件性能优化
shuouldComponentUpdate
pureComponent
不可变数据
key
等等优化方法,每一点的优点和缺点
如何设计一个好组件
哪里进行网络请求?为什么
调用setState
之后发生了什么
refs
react16
新特性
尤其理解time slice
和suspense
在 React
当中 Element
和 Component
有何区别
容器组件和展示组件
props.children
路由实现原理
react
的setState
同步还是异步?
Redux
,react-redux
等原理
如何实现异步网络请求的?
组件间通信
高阶组件是什么和常见的高阶组件
React key
是干嘛的?
loader
自己如何写一个loader
plugin
自己如何写一个plugin
webpack
原理之普通打包
webpack
原理之多文件打包
webpack
原理之提取公共文件
webpack
如何做到 tree shaking
webpack
配置文件基本概念
webpack
构建流程
前端模块化的理解
打包很慢,怎么解决?
打包出来的文件很大,怎么解决?
xss
csrf
为什么传统上利用多个域名来提供网站资源会更有效
Long-Polling
、Websockets
和 Server-Sent Event
常见的请求头和响应头
和缓存有关的HTTP
首部字段
相当重要。如何应用的?
HTTP method
HTTP
状态码
https
加密过程
http2
新特性
你会用什么工具来查找代码中的性能问题?
增强网站的页面滚动效能
重排,重绘,合成
相当相当重要
合成层
我在这里理解了一个多星期,静下心来去理解。
前端优化方法
css3
动画和js
动画对比
babel
原理面试问的最多的,除了基础知道,就是项目了。
必须对自己做的项目,有十足的掌握。做项目的时候,有主人翁意识~
项目业务理解,性能优化等等~
行了,到这里就结束了。如果你能坚持下来,我觉得你自己就有很足的自信了!
从一个弱鸡,成长为一个合格的前端菜鸟啦!
路漫漫其修远兮~加油!
一堆胡言乱语,希望对你有帮助。
我再强调一下:基础很重要!项目很重要!
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
devServer: {
port: 8081,
contentBase: path.join(__dirname, './dist')
}
设置了port,webpack-dev-server启动以后,也提示running at http://localhost:8081/,但是打开页面,是cannot get /
请教您一个问题,就是 react-router 4.0 + 可以实现嵌套路由吗,网上的资料确实有限,目前还没发现一个标准答案,嘿嘿。
ahooks 是由蚂蚁 umi 团队、淘系 ice 团队以及阿里体育团队共同建设的 React Hooks 工具库。ahooks 基于 React Hooks 的逻辑封装能力,提供了大量常见好用的 Hooks,可以极大降低代码复杂度,提升开发效率。
ahooks 致力成为和 antd/fusion 一样的 React 基础设施,帮助开发者在逻辑层面省去大量的重复工作。
ahooks 的前身是蚂蚁开源的 @umijs/hooks,可以说 ahooks 是 umi hooks 的 2.0 版本。
umi hooks 从 2019年9月 发布 v1.0 之后,一路前行,得到了不少用户的青睐。截至当前,umi hooks 在社区收获了 2.2k star,npm 周下载量最高 7000+,tnpm 周下载量 8000+。
同时在蚂蚁内部,umi hooks 也已经成为标准 React Hooks 库,截至当前,能统计到的项目中有 600+ 项目依赖了 umi hooks。并且 useRequest 也已经成为 umi3 内置请求方案。
但 umi hooks 半年来的野蛮生长,也带来了一些副作用。
随着 React Hooks 的发展,各个团队都开始尝试使用 Hooks 代替 Class,Hooks 正逐渐成为 React 组件的主流写法。得益于 Hooks 的逻辑封装能力,我们可以将常见的逻辑封装起来,以减少代码复杂度。或者使用社区上别人封装的 Hooks,比如 react-use 等。
当然出于种种原因,很多团队希望建设自己的 Hooks 库。但在建设过程中,能发现各个 Hooks 库提供的 Hooks 大同小异,尤其是基础类 Hooks 几乎都是一样的。
基于避免重复建设的目的,以及 umi hooks 的积累,我们与集团 ice 团队,阿里体育团队一拍即合,决定基于 umi hooks 共同建设 React Hooks 工具库,ahooks 随即诞生。
经过一个半月的改造,ahooks 已经发布了 v1.0 版本,并开源在 https://github.com/alibaba/hooks 仓库,你可以放心的在生产环境使用。
ahooks 相较于 umi hooks,有了自己的 API 规范,我们基于这套规范,重新整理了所有 Hooks 的 API,你可以在这里找到升级详情。
在 ahooks 的开发过程中,集团内也有其它很多部门参与进来,出谋划策,感谢大家。
如前面所说,ahooks 致力成为向 antd/fusion 一样的 React 基础设施。为了达到这个目标,我们正在全力开发更多的 Hooks,同时我们也期望大家能将日常封装的 Hooks 贡献到 ahooks 中,一起来帮助 ahooks 成长。
除了 Hooks 库,我们也在准备 React Hooks 系列教程。不得不承认,虽然 React Hooks 很好用,但其中确实有有不少的明坑暗坑,我们希望通过系列教程,减少大家在使用 Hooks 时的困惑,避免走弯路。
推荐之前的几篇文章,可以帮助你对 umi hooks/ahooks 有一个更深入的认识:
应该很多人想问,为什么不直接用 react-use,而是要自己建设 React Hooks 库呢?
服务端渲染(Server-Side Rendering),是指由服务侧完成页面的 HTML 结构拼接的页面处理技术。一般用于解决 SEO 问题和首屏加载速度问题。
由于 SSR 是在非浏览器环境执行 JS 代码,所以会出现很多问题。本文主要介绍 React Hooks 在 SSR 模式下常见问题及解决方案。
更多关于 SSR 的介绍可以看 UmiJS 的文档《服务端渲染(SSR)》。
SSR 是在 node 环境下运行 React 代码,而此时 window、document、navigator 等全局属性没有。如果直接使用了这些属性,就会报错 window is not defined, document is not defined, navigator is not defined
等。
常见的错误用法是在 Hooks 执行过程中,直接使用了 document 等全局属性。
import React, { useState } from 'react';
export default () => {
const [state, setState] = useState(document.visibilityState);
return state;
}
import React, { useState, useEffect } from 'react';
export default () => {
const [state, setState] = useState();
useEffect(()=>{
setState(document.visibilityState);
}, []);
return state;
}
import React, { useState } from 'react';
function isBrowser() {
return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
}
export default () => {
const [state, setState] = useState(isBrowser() && document.visibilityState);
return state;
}
如果使用了 useLayoutEffect
,在 SSR 模式下,会出现以下警告
⚠️ Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.
import { useLayoutEffect, useEffect } from 'react';
const useIsomorphicLayoutEffect = isBrowser() ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
isBrowser
判断是否在浏览器环境执行import React, { useState } from 'react';
import { useEventListener } from 'ahooks';
export default () => {
const [value, setValue] = useState(0);
const clickHandler = () => {
setValue(value + 1);
};
useEventListener(
'click',
clickHandler,
{
- target: document.getElemenetById('click-btn')
+ target: () => document.getElemenetById('click-btn')
}
);
return (
<button id="click-btn" type="button">
You click {value} times
</button>
);
};
useIsomorphicLayoutEffect
来代替 useLayoutEffect
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
源码地址:https://github.com/brickspert/react-family
提问反馈:blog
axios
和middleware
优化API请求webpack
公共配置 webpack-common-config(2017-09-04)当我第一次跟着项目做react
项目的时候,由于半截加入的,对框架了解甚少,只能跟着别人的样板写。对整个框架没有一点了解。
做项目,总是要解决各种问题的,所以每个地方都需要去了解,但是对整个框架没有一个整体的了解,实在是不行。
期间,我也跟着别人的搭建框架的教程一步一步的走,但是经常因为自己太菜,走不下去。在经过各种蹂躏之后,对整个框架也有一个大概的了解,
我就想把他写下来,让后来的菜鸟能跟着我的教程对react
全家桶有一个全面的认识。
我的这个教程,从新建根文件夹开始,到成型的框架,每个文件为什么要建立?建立了干什么?每个依赖都是干什么的?一步一步写下来,供大家学习。
当然,这个框架我以后会一直维护的,也希望大家能一起来完善这个框架,如果您有任何建议,欢迎在这里留言,欢迎fork
源码react-family。
我基于该框架react-family
又做了一个兼容IE8的版本,教程在这里react-family框架兼容IE8教程。
cd src/pages
mkdir Home
│ .babelrc #babel配置文件
│ package-lock.json
│ package.json
│ README.MD
│ webpack.config.js #webpack生产配置文件
│ webpack.dev.config.js #webpack开发配置文件
│
├─dist
├─public #公共资源文件
└─src #项目源码
│ index.html #index.html模板
│ index.js #入口文件
│
├─component #组建库
│ └─Hello
│ Hello.js
│
├─pages #页面目录
│ ├─Counter
│ │ Counter.js
│ │
│ ├─Home
│ │ Home.js
│ │
│ ├─Page1
│ │ │ Page1.css #页面样式
│ │ │ Page1.js
│ │ │
│ │ └─images #页面图片
│ │ brickpsert.jpg
│ │
│ └─UserInfo
│ UserInfo.js
│
├─redux
│ │ reducers.js
│ │ store.js
│ │
│ ├─actions
│ │ counter.js
│ │ userInfo.js
│ │
│ ├─middleware
│ │ promiseMiddleware.js
│ │
│ └─reducers
│ counter.js
│ userInfo.js
│
└─router #路由文件
Bundle.js
router.js
创建文件夹并进入
mkdir react-family && cd react-family
init npm
npm init
按照提示填写项目基本信息
安装 webpack
npm install --save-dev webpack@3
Q: 什么时候用--save-dev
,什么时候用--save
?
A: --save-dev
是你开发时候依赖的东西,--save
是你发布之后还依赖的东西。看这里
根据webpack文档编写最基础的配置文件
新建webpack
开发配置文件 touch webpack.dev.config.js
webpack.dev.config.js
const path = require('path');
module.exports = {
/*入口*/
entry: path.join(__dirname, 'src/index.js'),
/*输出到dist文件夹,输出文件名字为bundle.js*/
output: {
path: path.join(__dirname, './dist'),
filename: 'bundle.js'
}
};
学会使用webpack
编译文件
新建入口文件
mkdir src && touch ./src/index.js
src/index.js
添加内容
document.getElementById('app').innerHTML = "Webpack works"
现在我们执行命令 webpack --config webpack.dev.config.js
webpack如果没有全局安装,这里会报错哦。命令建议全局安装,如果编译有问题看这里 #1 (comment)
我们可以看到生成了dist
文件夹和bundle.js
。
现在我们测试下~
dist
文件夹下面新建一个index.html
touch ./dist/index.html
dist/index.html
填写内容
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="./bundle.js" charset="utf-8"></script>
</body>
</html>
用浏览器打开index.html
,可以看到Webpack works
!
现在回头看下,我们做了什么或者说webpack
做了什么。
把入口文件 index.js
经过处理之后,生成 bundle.js
。就这么简单。
Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。
通俗的说,就是我们可以用ES6, ES7等来编写代码,Babel会把他们统统转为ES5。
babel-core 调用Babel的API进行转码
babel-preset-es2015 用于解析 ES6
babel-preset-react 用于解析 JSX
babel-preset-stage-0 用于解析 ES7 提案
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0
新建babel
配置文件.babelrc
touch .babelrc
.babelrc
{
"presets": [
"es2015",
"react",
"stage-0"
],
"plugins": []
}
修改webpack.dev.config.js
,增加babel-loader
!
/*src文件夹下面的以.js结尾的文件,要使用babel解析*/
/*cacheDirectory是用来缓存编译结果,下次编译加速*/
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader?cacheDirectory=true'],
include: path.join(__dirname, 'src')
}]
}
现在我们简单测试下,是否能正确转义ES6~
修改 src/index.js
/*使用es6的箭头函数*/
var func = str => {
document.getElementById('app').innerHTML = str;
};
func('我现在在使用Babel!');
执行打包命令webpack --config webpack.dev.config.js
浏览器打开index.html
,我们看到正确输出了我现在在使用Babel!
然后我们打开打包后的bundle.js
,翻页到最下面,可以看到箭头函数被转换成普通函数了!
Q: babel-preset-state-0
,babel-preset-state-1
,babel-preset-state-2
,babel-preset-state-3
有什么区别?
A: 每一级包含上一级的功能,比如 state-0
包含state-1
的功能,以此类推。state-0
功能最全。具体可以看这篇文章:babel配置-各阶段的stage的区别
参考地址:
npm install --save react react-dom
修改 src/index.js
使用react
import React from 'react';
import ReactDom from 'react-dom';
ReactDom.render(
<div>Hello React!</div>, document.getElementById('app'));
执行打包命令webpack --config webpack.dev.config.js
打开index.html
看效果。
我们简单做下改进,把Hello React
放到组件里面。体现组件化~
cd src
mkdir component
cd component
mkdir Hello
cd Hello
touch Hello.js
按照React语法,写一个Hello组件
import React, {Component} from 'react';
export default class Hello extends Component {
render() {
return (
<div>
Hello,React!
</div>
)
}
}
然后让我们修改src/index.js
,引用Hello
组件!
src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import Hello from './component/Hello/Hello';
ReactDom.render(
<Hello/>, document.getElementById('app'));
在根目录执行打包命令
webpack --config webpack.dev.config.js
打开index.html
看效果咯~
Q:每次打包都得在根目录执行这么一长串命令webpack --config webpack.dev.config.js
,能不打这么长吗?
A:修改package.json
里面的script
,增加dev-build
。
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev-build": "webpack --config webpack.dev.config.js"
}
现在我们打包只需要执行npm run dev-build
就可以啦!
参考地址:
http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html
npm install --save react-router-dom
新建router
文件夹和组件
cd src
mkdir router && touch router/router.js
按照react-router
文档编辑一个最基本的router.js
。包含两个页面home
和page1
。
src/router/router.js
import React from 'react';
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';
const getRouter = () => (
<Router>
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/page1">Page1</Link></li>
</ul>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/page1" component={Page1}/>
</Switch>
</div>
</Router>
);
export default getRouter;
新建页面文件夹
cd src
mkdir pages
新建两个页面 Home
,Page1
cd src/pages
mkdir Home && touch Home/Home.js
mkdir Page1 && touch Page1/Page1.js
填充内容:
src/pages/Home/Home.js
import React, {Component} from 'react';
export default class Home extends Component {
render() {
return (
<div>
this is home~
</div>
)
}
}
Page1.js
import React, {Component} from 'react';
export default class Page1 extends Component {
render() {
return (
<div>
this is Page1~
</div>
)
}
}
现在路由和页面建好了,我们在入口文件src/index.js
引用Router。
修改src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import getRouter from './router/router';
ReactDom.render(
getRouter(), document.getElementById('app'));
现在执行打包命令npm run dev-build
。打开index.html
查看效果啦!
那么问题来了~我们发现点击‘首页’和‘Page1’没有反应。不要惊慌,这是正常的。
我们之前一直用这个路径访问index.html
,类似这样:file:///F:/react/react-family/dist/index.html
。
这种路径了,不是我们想象中的路由那样的路径http://localhost:3000
~我们需要配置一个简单的WEB服务器,指向
index.html
~有下面两种方法来实现
Nginx
, Apache
, IIS
等配置启动一个简单的的WEB服务器。webpack-dev-server
来配置启动WEB服务器。下一节,我们来使用第二种方法启动服务器。这一节的DEMO,先放这里。
参考地址
简单来说,webpack-dev-server
就是一个小型的静态文件服务器。使用它,可以为webpack
打包生成的资源文件提供Web服务。
npm install webpack-dev-server@2 --save-dev
2017.11.16补充:这里webpack-dev-server需要全局安装,要不后面用的时候要写相对路径。需要再执行这个
npm install webpack-dev-server@2 -g
修改webpack.dev.config.js
,增加webpack-dev-server
的配置。
webpack.dev.config.js
devServer: {
contentBase: path.join(__dirname, './dist')
}
现在执行
webpack-dev-server --config webpack.dev.config.js
浏览器打开http://localhost:8080,OK,现在我们可以点击首页
,Page1
了,
看URL地址变化啦!我们看到react-router
已经成功了哦。
Q: --content-base
是什么?
A:URL的根目录。如果不设定的话,默认指向项目根目录。
重要提示:webpack-dev-server编译后的文件,都存储在内存中,我们并不能看见的。你可以删除之前遗留的文件dist/bundle.js
,
仍然能正常打开网站!
每次执行webpack-dev-server --config webpack.dev.config.js
,要打很长的命令,我们修改package.json
,增加script->start
:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev-build": "webpack --config webpack.dev.config.js",
"start": "webpack-dev-server --config webpack.dev.config.js"
}
下次执行npm start
就可以了。
既然用到了webpack-dev-server
,我们就看看它的其他的配置项。
看了之后,发现有几个我们可以用的。
console
中打印彩色日志404
响应都被替代为index.html
。有什么用呢?你现在运行npm start
,然后打开浏览器,访问http://localhost:8080
,然后点击Page1
到链接http://localhost:8080/page1
,404
了。为什么?dist
文件夹里面并没有page1.html
,当然会404
了,所以我们需要配置historyApiFallback
,让所有的404
定位到index.html
。host
,默认是localhost
。如果你希望服务器外部可以访问,指定如下:host: "0.0.0.0"
。比如你用手机通过IP访问。Webpack
的模块热替换特性。关于热模块替换,我下一小节专门讲解一下。8080
端口。localhost:3000
上有后端服务的话,你可以这样启用代理: proxy: {
"/api": "http://localhost:3000"
}
根据这几个配置,修改下我们的webpack-dev-server
的配置~
webpack.dev.config.js
devServer: {
port: 8080,
contentBase: path.join(__dirname, './dist'),
historyApiFallback: true,
host: '0.0.0.0'
}
CLI ONLY
的需要在命令行中配置
package.json
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress"
现在我们执行npm start
看看效果。是不是看到打包的时候有百分比进度?在http://localhost:8080/page1
页面刷新是不是没问题了?
用手机通过局域网IP是否可以访问到网站?
参考地址:
到目前,当我们修改代码的时候,浏览器会自动刷新,不信你可以去试试。(如果你的不会刷新,看看这个调整文本编辑器)
我相信看这个教程的人,应该用过别人的框架。我们在修改代码的时候,浏览器不会刷新,只会更新自己修改的那一块。我们也要实现这个效果。
我们看下webpack模块热替换教程。
我们接下来要这么修改
package.json
增加 --hot
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"
src/index.js
增加module.hot.accept()
,如下。当模块更新的时候,通知index.js
。
src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import getRouter from './router/router';
if (module.hot) {
module.hot.accept();
}
ReactDom.render(
getRouter(), document.getElementById('app'));
现在我们执行npm start
,打开浏览器,修改Home.js
,看是不是不刷新页面的情况下,内容更新了?惊不惊喜?意不意外?
做模块热替换,我们只改了几行代码,非常简单的。纸老虎一个~
现在我需要说明下我们命令行使用的--hot
,可以通过配置webpack.dev.config.js
来替换,
向文档上那样,修改下面三处。但我们还是用--hot
吧。下面的方式我们知道一下就行,我们不用。同样的效果。
const webpack = require('webpack');
devServer: {
hot: true
}
plugins:[
new webpack.HotModuleReplacementPlugin()
]
HRM
配置其实有两种方式,一种CLI
方式,一种Node.js API
方式。我们用到的就是CLI
方式,比较简单。
Node.js API
方式,就是建一个server.js
等等,网上大部分教程都是这种方式,这里不做讲解了。
你以为模块热替换到这里就结束了?nonono~
上面的配置对react
模块的支持不是很好哦。
例如下面的demo
,当模块热替换的时候,state
会重置,这不是我们想要的。
修改Home.js
,增加计数state
src/pages/Home/Home.js
import React, {Component} from 'react';
export default class Home extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
_handleClick() {
this.setState({
count: ++this.state.count
});
}
render() {
return (
<div>
this is home~<br/>
当前计数:{this.state.count}<br/>
<button onClick={() => this._handleClick()}>自增</button>
</div>
)
}
}
你可以测试一下,当我们修改代码的时候,webpack
在更新页面的时候,也把count
初始为0了。
为了在react
模块更新的同时,能保留state
等页面中其他状态,我们需要引入react-hot-loader~
Q: 请问webpack-dev-server
与react-hot-loader
两者的热替换有什么区别?
A: 区别在于webpack-dev-serve
r自己的--hot
模式只能即时刷新页面,但状态保存不住。因为React
有一些自己语法(JSX)是HotModuleReplacementPlugin
搞不定的。
而react-hot-loader
在--hot
基础上做了额外的处理,来保证状态可以存下来。(来自segmentfault)
下面我们来加入react-hot-loader v3
,
安装依赖
npm install react-hot-loader@next --save-dev
根据文档,
我们要做如下几个修改~
.babelrc
增加 react-hot-loader/babel
.babelrc
{
"presets": [
"es2015",
"react",
"stage-0"
],
"plugins": [
"react-hot-loader/babel"
]
}
webpack.dev.config.js
入口增加react-hot-loader/patch
webpack.dev.config.js
entry: [
'react-hot-loader/patch',
path.join(__dirname, 'src/index.js')
]
src/index.js
修改如下src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import getRouter from './router/router';
/*初始化*/
renderWithHotReload(getRouter());
/*热更新*/
if (module.hot) {
module.hot.accept('./router/router', () => {
const getRouter = require('./router/router').default;
renderWithHotReload(getRouter());
});
}
function renderWithHotReload(RootElement) {
ReactDom.render(
<AppContainer>
{RootElement}
</AppContainer>,
document.getElementById('app')
)
}
现在,执行npm start
,试试。是不是修改页面的时候,state
不更新了?
参考文章:
做到这里,我们简单休息下。做下优化~
在之前写的代码中,我们引用组件,或者页面时候,写的是相对路径~
比如src/router/router.js
里面,引用Home.js
的时候就用的相对路径
import Home from '../pages/Home/Home';
webpack提供了一个别名配置,就是我们无论在哪个路径下,引用都可以这样
import Home from 'pages/Home/Home';
下面我们来配置下,修改webpack.dev.config.js
,增加别名~
webpack.dev.config.js
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
component: path.join(__dirname, 'src/component'),
router: path.join(__dirname, 'src/router')
}
}
然后我们把之前使用的绝对路径统统改掉。
src/router/router.js
import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
src/index.js
import getRouter from 'router/router';
我们这里约定,下面,我们会默认配置需要的别名路径,不再做重复的讲述哦。
接下来,我们就要就要就要集成redux
了。
要对redux
有一个大概的认识,可以阅读阮一峰前辈的Redux 入门教程(一):基本用法
如果要对redux
有一个非常详细的认识,我推荐阅读中文文档,写的非常好。读了这个教程,有一个非常深刻的感觉,redux
并没有任何魔法。
不要被各种关于 reducers, middleware, store 的演讲所蒙蔽 ---- Redux 实际是非常简单的。
当然,我这篇文章是写给新手的,如果看不懂上面的文章,或者不想看,没关系。先会用,多用用就知道原理了。
开始整代码!我们就做一个最简单的计数器。自增,自减,重置。
先安装redux
npm install --save redux
初始化目录结构
cd src
mkdir redux
cd redux
mkdir actions
mkdir reducers
touch reducers.js
touch store.js
touch actions/counter.js
touch reducers/counter.js
先来写action
创建函数。通过action
创建函数,可以创建action
~
src/redux/actions/counter.js
/*action*/
export const INCREMENT = "counter/INCREMENT";
export const DECREMENT = "counter/DECREMENT";
export const RESET = "counter/RESET";
export function increment() {
return {type: INCREMENT}
}
export function decrement() {
return {type: DECREMENT}
}
export function reset() {
return {type: RESET}
}
再来写reducer
,reducer
是一个纯函数,接收action
和旧的state
,生成新的state
.
src/redux/reducers/counter.js
import {INCREMENT, DECREMENT, RESET} from '../actions/counter';
/*
* 初始化state
*/
const initState = {
count: 0
};
/*
* reducer
*/
export default function reducer(state = initState, action) {
switch (action.type) {
case INCREMENT:
return {
count: state.count + 1
};
case DECREMENT:
return {
count: state.count - 1
};
case RESET:
return {count: 0};
default:
return state
}
}
一个项目有很多的reducers
,我们要把他们整合到一起
src/redux/reducers.js
import counter from './reducers/counter';
export default function combineReducers(state = {}, action) {
return {
counter: counter(state.counter, action)
}
}
到这里,我们必须再理解下一句话。
reducer
就是纯函数,接收state
和 action
,然后返回一个新的 state
。
看看上面的代码,无论是combineReducers
函数也好,还是reducer
函数也好,都是接收state
和action
,
返回更新后的state
。区别就是combineReducers
函数是处理整棵树,reducer
函数是处理树的某一点。
接下来,我们要创建一个store
。
前面我们可以使用 action
来描述“发生了什么”,使用action
创建函数来返回action
。
还可以使用 reducers
来根据 action
更新 state
。
那我们如何提交action
?提交的时候,怎么才能触发reducers
呢?
store
就是把它们联系到一起的对象。store
有以下职责:
state
;getState()
方法获取 state
;dispatch(action)
触发reducers
方法更新 state
; subscribe(listener)
注册监听器;subscribe(listener)
返回的函数注销监听器。src/redux/store.js
import {createStore} from 'redux';
import combineReducers from './reducers.js';
let store = createStore(combineReducers);
export default store;
到现在为止,我们已经可以使用redux
了~
下面我们就简单的测试下
cd src
cd redux
touch testRedux.js
src/redux/testRedux.js
import {increment, decrement, reset} from './actions/counter';
import store from './store';
// 打印初始状态
console.log(store.getState());
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
// 发起一系列 action
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(reset());
// 停止监听 state 更新
unsubscribe();
当前文件夹执行命令
webpack testRedux.js build.js
node build.js
是不是看到输出了state
变化?
{ counter: { count: 0 } }
{ counter: { count: 1 } }
{ counter: { count: 0 } }
{ counter: { count: 0 } }
做这个测试,就是为了告诉大家,redux
和react
没关系,虽说他俩能合作。
到这里,我建议你再理下redux
的数据流,看看这里。
store.dispatch(action)
提交action
。redux store
调用传入的reducer
函数。把当前的state
和action
传进去。reducer
应该把多个子 reducer
输出合并成一个单一的 state
树。Redux store
保存了根 reducer
返回的完整 state
树。就是酱紫~~
这会webpack.dev.config.js
路径别名增加一下,后面好写了。
webpack.dev.config.js
alias: {
...
actions: path.join(__dirname, 'src/redux/actions'),
reducers: path.join(__dirname, 'src/redux/reducers'),
redux: path.join(__dirname, 'src/redux')
}
把前面的相对路径都改改。
下面我们开始搭配react
使用。
写一个Counter
页面
cd src/pages
mkdir Counter
touch Counter/Counter.js
src/pages/Counter/Counter.js
import React, {Component} from 'react';
export default class Counter extends Component {
render() {
return (
<div>
<div>当前计数为(显示redux计数)</div>
<button onClick={() => {
console.log('调用自增函数');
}}>自增
</button>
<button onClick={() => {
console.log('调用自减函数');
}}>自减
</button>
<button onClick={() => {
console.log('调用重置函数');
}}>重置
</button>
</div>
)
}
}
修改路由,增加Counter
src/router/router.js
import React from 'react';
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';
const getRouter = () => (
<Router>
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/page1">Page1</Link></li>
<li><Link to="/counter">Counter</Link></li>
</ul>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/page1" component={Page1}/>
<Route path="/counter" component={Counter}/>
</Switch>
</div>
</Router>
);
export default getRouter;
npm start
看看效果。
下一步,我们让Counter
组件和Redux
联合起来。使Counter
能获得到Redux
的state
,并且能发射action
。
当然我们可以使用刚才测试testRedux
的方法,手动监听~手动引入store
~但是这肯定很麻烦哦。
react-redux
提供了一个方法connect
。
容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。你可以手工来开发容器组件,但建议使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。
connect
接收两个参数,一个mapStateToProps
,就是把redux
的state
,转为组件的Props
,还有一个参数是mapDispatchToprops
,
就是把发射actions
的方法,转为Props
属性函数。
先来安装react-redux
npm install --save react-redux
src/pages/Counter/Counter.js
import React, {Component} from 'react';
import {increment, decrement, reset} from 'actions/counter';
import {connect} from 'react-redux';
class Counter extends Component {
render() {
return (
<div>
<div>当前计数为{this.props.counter.count}</div>
<button onClick={() => this.props.increment()}>自增
</button>
<button onClick={() => this.props.decrement()}>自减
</button>
<button onClick={() => this.props.reset()}>重置
</button>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
counter: state.counter
}
};
const mapDispatchToProps = (dispatch) => {
return {
increment: () => {
dispatch(increment())
},
decrement: () => {
dispatch(decrement())
},
reset: () => {
dispatch(reset())
}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
下面我们要传入store
所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。
建议的方式是使用指定的 React Redux 组件 来 魔法般的 让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。
src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import store from './redux/store';
import getRouter from 'router/router';
/*初始化*/
renderWithHotReload(getRouter());
/*热更新*/
if (module.hot) {
module.hot.accept('./router/router', () => {
const getRouter = require('router/router').default;
renderWithHotReload(getRouter());
});
}
function renderWithHotReload(RootElement) {
ReactDom.render(
<AppContainer>
<Provider store={store}>
{RootElement}
</Provider>
</AppContainer>,
document.getElementById('app')
)
}
到这里我们就可以执行npm start
,打开localhost:8080/counter看效果了。
但是你发现npm start
一直报错
ERROR in ./node_modules/react-redux/es/connect/mapDispatchToProps.js
Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\node_modules\react-redux\es\connect'
ERROR in ./src/redux/store.js
Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\src\redux'
WTF?这个错误困扰了半天。我说下为什么造成这个错误。我们引用redux
的时候这样用的
import {createStore} from 'redux'
然而,我们在webapck.dev.config.js
里面这样配置了
resolve: {
alias: {
...
redux: path.join(__dirname, 'src/redux')
}
}
然后webapck
编译的时候碰到redux
都去src/redux
去找了。但是找不到啊。所以我们把webpack.dev.config.js
里面redux
这一行删除了,就好了。
并且把使用我们自己使用redux
文件夹的地方改成相对路径哦。
现在你可以npm start
去看效果了。
这里我们再缕下(可以读React 实践心得:react-redux 之 connect 方法详解)
Provider
组件是让所有的组件可以访问到store
。不用手动去传。也不用手动去监听。connect
函数作用是从 Redux state
树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。也传递dispatch(action)
函数到props
。接下来,我们要说异步action
参考地址: http://cn.redux.js.org/docs/advanced/AsyncActions.html
想象一下我们调用一个异步get
请求去后台请求数据:
isLoading
置为true
。isLoading
置为false
,data
填充数据。isLoading
置为false
,显示错误信息。下面,我们以向后台请求用户基本信息为例。
user.json
,等会请求用,相当于后台的API接口。cd dist
mkdir api
cd api
touch user.json
dist/api/user.json
{
"name": "brickspert",
"intro": "please give me a star"
}
action
创建函数。cd src/redux/actions
touch userInfo.js
src/redux/actions/userInfo.js
export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";
function getUserInfoRequest() {
return {
type: GET_USER_INFO_REQUEST
}
}
function getUserInfoSuccess(userInfo) {
return {
type: GET_USER_INFO_SUCCESS,
userInfo: userInfo
}
}
function getUserInfoFail() {
return {
type: GET_USER_INFO_FAIL
}
}
我们创建了请求中,请求成功,请求失败三个action
创建函数。
reducer
再强调下,reducer
是根据state
和action
生成新state
的纯函数。
cd src/redux/reducers
touch userInfo.js
src/redux/reducers/userInfo.js
import {GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL} from 'actions/userInfo';
const initState = {
isLoading: false,
userInfo: {},
errorMsg: ''
};
export default function reducer(state = initState, action) {
switch (action.type) {
case GET_USER_INFO_REQUEST:
return {
...state,
isLoading: true,
userInfo: {},
errorMsg: ''
};
case GET_USER_INFO_SUCCESS:
return {
...state,
isLoading: false,
userInfo: action.userInfo,
errorMsg: ''
};
case GET_USER_INFO_FAIL:
return {
...state,
isLoading: false,
userInfo: {},
errorMsg: '请求错误'
};
default:
return state;
}
}
这里的...state
语法,是和别人的Object.assign()
起同一个作用,合并新旧state。我们这里是没效果的,但是我建议都写上这个哦
组合reducer
src/redux/reducers.js
import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';
export default function combineReducers(state = {}, action) {
return {
counter: counter(state.counter, action),
userInfo: userInfo(state.userInfo, action)
}
}
action
,有了reducer
,我们就需要调用把action
里面的三个action
函数和网络请求结合起来。
dispatch getUserInfoRequest
dispatch getUserInfoSuccess
dispatch getUserInfoFail
src/redux/actions/userInfo.js
增加
export function getUserInfo() {
return function (dispatch) {
dispatch(getUserInfoRequest());
return fetch('http://localhost:8080/api/user.json')
.then((response => {
return response.json()
}))
.then((json) => {
dispatch(getUserInfoSuccess(json))
}
).catch(
() => {
dispatch(getUserInfoFail());
}
)
}
}
我们这里发现,别的action
创建函数都是返回action
对象:
{type: xxxx}
但是我们现在的这个action
创建函数 getUserInfo
则是返回函数了。
为了让action
创建函数除了返回action
对象外,还可以返回函数,我们需要引用redux-thunk
。
npm install --save redux-thunk
这里涉及到redux
中间件middleware
,我后面会讲到的。你也可以读这里Middleware。
简单的说,中间件就是action
在到达reducer
,先经过中间件处理。我们之前知道reducer
能处理的action
只有这样的{type:xxx}
,所以我们使用中间件来处理
函数形式的action
,把他们转为标准的action
给reducer
。这是redux-thunk
的作用。
使用redux-thunk
中间件
我们来引入redux-thunk
中间件
src/redux/store.js
import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
import combineReducers from './reducers.js';
let store = createStore(combineReducers, applyMiddleware(thunkMiddleware));
export default store;
到这里,redux
这边OK了,我们来写个组件验证下。
cd src/pages
mkdir UserInfo
cd UserInfo
touch UserInfo.js
src/pages/UserInfo/UserInfo.js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {getUserInfo} from "actions/userInfo";
class UserInfo extends Component {
render() {
const {userInfo, isLoading, errorMsg} = this.props.userInfo;
return (
<div>
{
isLoading ? '请求信息中......' :
(
errorMsg ? errorMsg :
<div>
<p>用户信息:</p>
<p>用户名:{userInfo.name}</p>
<p>介绍:{userInfo.intro}</p>
</div>
)
}
<button onClick={() => this.props.getUserInfo()}>请求用户信息</button>
</div>
)
}
}
export default connect((state) => ({userInfo: state.userInfo}), {getUserInfo})(UserInfo);
这里你可能发现connect
参数写法不一样了,mapStateToProps
函数用了es6
简写,mapDispatchToProps
用了react-redux
提供的简单写法。
增加路由
src/router/router.js
import React from 'react';
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';
import UserInfo from 'pages/UserInfo/UserInfo';
const getRouter = () => (
<Router>
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/page1">Page1</Link></li>
<li><Link to="/counter">Counter</Link></li>
<li><Link to="/userinfo">UserInfo</Link></li>
</ul>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/page1" component={Page1}/>
<Route path="/counter" component={Counter}/>
<Route path="/userinfo" component={UserInfo}/>
</Switch>
</div>
</Router>
);
export default getRouter;
现在你可以执行npm start
去看效果啦!
到这里redux
集成基本告一段落了,后面我们还会有一些优化。
redux
提供了一个combineReducers
函数来合并reducer
,不用我们自己合并哦。写起来简单,但是意思和我们
自己写的combinReducers
也是一样的。
src/redux/reducers.js
import {combineReducers} from "redux";
import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';
export default combineReducers({
counter,
userInfo
});
现在我们发现一个问题,代码哪里写错了,浏览器报错只报在build.js
第几行。
这让我们分析错误无从下手。看这里。
我们增加webpack
配置devtool
!
webpack.dev.config.js
增加
devtool: 'inline-source-map'
这次看错误信息是不是提示的很详细了?
同时,我们在srouce
里面能看到我们写的代码,也能打断点调试哦~
先说这里为什么不用scss
,因为Windows
使用node-sass
,需要先安装 Microsoft Windows SDK for Windows 7 and .NET Framework 4。
我怕有些人copy这份代码后,没注意,运行不起来。所以这里不用scss
了,如果需要,自行编译哦。
npm install css-loader style-loader --save-dev
css-loader
使你能够使用类似@import
和 url(...)
的方法实现 require()
的功能;
style-loader
将所有的计算后的样式加入页面中; 二者组合在一起使你能够把样式表嵌入webpack
打包后的JS文件中。
webpack.dev.config.js
rules
增加
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
我们用Page1
页面来测试下
cd src/pages/Page1
touch Page1.css
src/pages/Page1/Page1.css
.page-box {
border: 1px solid red;
}
src/pages/Page1/Page1.js
import React, {Component} from 'react';
import './Page1.css';
export default class Page1 extends Component {
render() {
return (
<div className="page-box">
this is page1~
</div>
)
}
}
好了,现在npm start
去看效果吧。
npm install --save-dev url-loader file-loader
webpack.dev.config.js
rules
增加
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}
options limit 8192
意思是,小于等于8K的图片会被转成base64
编码,直接插入HTML中,减少HTTP
请求。
我们来用Page1
测试下
cd src/pages/Page1
mkdir images
给images
文件夹放一个图片。
修改代码,引用图片
src/pages/Page1/Page1.js
import React, {Component} from 'react';
import './Page1.css';
import image from './images/brickpsert.jpg';
export default class Page1 extends Component {
render() {
return (
<div className="page-box">
this is page1~
<img src={image}/>
</div>
)
}
}
可以去看看效果啦。
为什么要实现按需加载?
我们现在看到,打包完后,所有页面只生成了一个build.js
,当我们首屏加载的时候,就会很慢。因为他也下载了别的页面的js
了哦。
如果每个页面都打包了自己单独的JS,在进入自己页面的时候才加载对应的js,那首屏加载就会快很多哦。
在 react-router 2.0
时代, 按需加载需要用到的最关键的一个函数,就是require.ensure()
,它是按需加载能够实现的核心。
在4.0版本,官方放弃了这种处理按需加载的方式,选择了一个更加简洁的处理方式。
根据官方示例,我们开搞
npm install bundle-loader --save-dev
bundle.js
cd src/router
touch Bundle.js
src/router/Bundle.js
import React, {Component} from 'react'
class Bundle extends Component {
state = {
// short for "module" but that's a keyword in js, so "mod"
mod: null
};
componentWillMount() {
this.load(this.props)
}
componentWillReceiveProps(nextProps) {
if (nextProps.load !== this.props.load) {
this.load(nextProps)
}
}
load(props) {
this.setState({
mod: null
});
props.load((mod) => {
this.setState({
// handle both es imports and cjs
mod: mod.default ? mod.default : mod
})
})
}
render() {
return this.props.children(this.state.mod)
}
}
export default Bundle;
src/router/router.js
import React from 'react';
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Bundle from './Bundle';
import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
import Page1 from 'bundle-loader?lazy&name=page1!pages/Page1/Page1';
import Counter from 'bundle-loader?lazy&name=counter!pages/Counter/Counter';
import UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';
const Loading = function () {
return <div>Loading...</div>
};
const createComponent = (component) => (props) => (
<Bundle load={component}>
{
(Component) => Component ? <Component {...props} /> : <Loading/>
}
</Bundle>
);
const getRouter = () => (
<Router>
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/page1">Page1</Link></li>
<li><Link to="/counter">Counter</Link></li>
<li><Link to="/userinfo">UserInfo</Link></li>
</ul>
<Switch>
<Route exact path="/" component={createComponent(Home)}/>
<Route path="/page1" component={createComponent(Page1)}/>
<Route path="/counter" component={createComponent(Counter)}/>
<Route path="/userinfo" component={createComponent(UserInfo)}/>
</Switch>
</div>
</Router>
);
export default getRouter;
现在你可以npm start
,打开浏览器,看是不是进入新的页面,都会加载自己的JS的~
但是你可能发现,名字都是0.bundle.js
这样子的,这分不清楚是哪个页面的js
呀!
我们修改下webpack.dev.config.js
,加个chunkFilename
。chunkFilename
是除了entry
定义的入口js
之外的js
~
output: {
path: path.join(__dirname, './dist'),
filename: 'bundle.js',
chunkFilename: '[name].js'
}
现在你运行发现名字变成home.js
,这样的了。棒棒哒!
那么问题来了home
是在哪里设置的?webpack
怎么知道他叫home
?
其实在这里我们定义了,router.js
里面
import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
看到没。这里有个name=home
。嘿嘿。
参考地址:
想象一下这个场景~
我们网站上线了,用户第一次访问首页,下载了home.js
,第二次访问又下载了home.js
~
这肯定不行呀,所以我们一般都会做一个缓存,用户下载一次home.js
后,第二次就不下载了。
有一天,我们更新了home.js
,但是用户不知道呀,用户还是使用本地旧的home.js
。出问题了~
怎么解决?每次代码更新后,打包生成的名字不一样。比如第一次叫home.a.js
,第二次叫home.b.js
。
文档看这里
我们照着文档来
webpack.dev.config.js
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[hash].js',
chunkFilename: '[name].[chunkhash].js'
}
每次打包都用增加hash
~
现在我们试试,是不是修改了文件,打包后相应的文件名字就变啦?
但是你可能发现了,网页打开报错了~因为你dist/index.html
里面引用js
名字还是bundle.js
老名字啊,改成新的名字就可以啦。
啊~那岂不是我每次编译打包,都得去改一下js名字?欲知后事如何,且看下节分享。
这个插件,每次会自动把js插入到你的模板index.html
里面去。
npm install html-webpack-plugin --save-dev
新建模板index.html
cd src
touch index.html
src/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
修改webpack.dev.config.js
,增加plugin
var HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/index.html')
})],
npm start
运行项目,看看是不是能正常访问啦。~
说明一下:npm start
打包后的文件存在内存中,你看不到的。~ 你可以把遗留dist/index.html
删除掉了。
想象一下,我们的主文件,原来的bundle.js
里面是不是包含了react
,redux
,react-router
等等
这些代码??这些代码基本上不会改变的。但是,他们合并在bundle.js
里面,每次项目发布,重新请求bundle.js
的时候,相当于重新请求了
react
等这些公共库。浪费了~
我们把react
这些不会改变的公共库提取出来,用户缓存下来。从此以后,用户再也不用下载这些库了,无论是否发布项目。嘻嘻。
webpack
文档给了教程,看这里
webpack.dev.config.js
var webpack = require('webpack');
entry: {
app: [
'react-hot-loader/patch',
path.join(__dirname, 'src/index.js')
],
vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
}
/*plugins*/
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
})
把react
等库生成打包到vendor.hash.js
里面去。
但是你现在可能发现编译生成的文件app.[hash].js
和vendor.[hash].js
生成的hash
一样的,这里是个问题,因为呀,你每次修改代码,都会导致vendor.[hash].js
名字改变,那我们提取出来的意义也就没了。其实文档上写的很清楚,
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[hash].js', //这里应该用chunkhash替换hash
chunkFilename: '[name].[chunkhash].js'
}
但是无奈,如果用chunkhash
,会报错。和webpack-dev-server --hot
不兼容,具体看这里。
现在我们在配置开发版配置文件,就向webpack-dev-server
妥协,因为我们要用他。问题先放这里,等会我们配置正式版webpack.config.js
的时候要解决这个问题。
开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
文档看这里
我们要开始做了~
touch webpack.config.js
在webpack.dev.config.js
的基础上先做以下几个修改~
webpack-dev-server
相关的东西~devtool
的值改成cheap-module-source-map
hash
改成chunkhash
webpack.config.js
const path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpack = require('webpack');
module.exports = {
devtool: 'cheap-module-source-map',
entry: {
app: [
path.join(__dirname, 'src/index.js')
],
vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
},
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js'
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader'],
include: path.join(__dirname, 'src')
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader']
}, {
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/index.html')
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
})
],
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
component: path.join(__dirname, 'src/component'),
router: path.join(__dirname, 'src/router'),
actions: path.join(__dirname, 'src/redux/actions'),
reducers: path.join(__dirname, 'src/redux/reducers')
}
}
};
在package.json
增加打包脚本
"build":"webpack --config webpack.config.js"
然后执行npm run build
~看看dist
文件夹是不是生成了我们发布要用的所有文件哦?
接下来我们还是要优化正式版配置文件~
webpack
使用UglifyJSPlugin
来压缩生成的文件。
npm i --save-dev uglifyjs-webpack-plugin
webpack.config.js
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
plugins: [
new UglifyJSPlugin()
]
}
npm run build
发现打包文件大小减小了好多。
许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。例如,当不处于生产环境中时,某些 library 为了使调试变得容易,可能会添加额外的日志记录(log)和测试(test)。其实,当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。我们可以使用 webpack 内置的 DefinePlugin 为所有的依赖定义这个变量:
webpack.config.js
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
})
]
}
npm run build
后发现vendor.[hash].js
又变小了。
刚才我们把[name].[hash].js
变成[name].[chunkhash].js
后,npm run build
后,
发现app.xxx.js
和vendor.xxx.js
不一样了哦。
但是现在又有一个问题了。
你随便修改代码一处,例如Home.js
,随便改变个字,你发现home.xxx.js
名字变化的同时,
vendor.xxx.js
名字也变了。这不行啊。这和没拆分不是一样一样了吗?我们本意是vendor.xxx.js
名字永久不变,一直缓存在用户本地的。~
官方文档推荐了一个插件HashedModuleIdsPlugin
plugins: [
new webpack.HashedModuleIdsPlugin()
]
现在你打包,修改代码再试试,是不是名字不变啦?错了,现在打包,我发现名字还是变了,经过比对文档,我发现还要加一个runtime
代码抽取,
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
加上这句话就好了~为什么呢?看下解释。
注意,引入顺序在这里很重要。CommonsChunkPlugin 的 'vendor' 实例,必须在 'runtime' 实例之前引入。
想象一个场景,我们的静态文件放在了单独的静态服务器上去了,那我们打包的时候,如何让静态文件的链接定位到静态服务器呢?
看文档Public Path
webpack.config.js
output
中增加一个publicPath
,我们当前用/
,相对于当前路径,如果你要改成别的url
,就改这里就好了。
output: {
publicPath : '/'
}
你现在打开dist
,是不是发现好多好多文件,每次打包后的文件在这里混合了?我们希望每次打包前自动清理下dist
文件。
npm install clean-webpack-plugin --save-dev
webpack.config.js
const CleanWebpackPlugin = require('clean-webpack-plugin');
plugins: [
new CleanWebpackPlugin(['dist'])
]
现在npm run build
试试,是不是之前的都清空了。当然我们之前的api
文件夹也被清空了,不过没关系哦~本来就是测试用的。
目前我们的css
是直接打包进js
里面的,我们希望能单独生成css
文件。
我们使用extract-text-webpack-plugin来实现。
npm install --save-dev extract-text-webpack-plugin
webpack.config.js
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}
]
},
plugins: [
new ExtractTextPlugin({
filename: '[name].[contenthash:5].css',
allChunks: true
})
]
}
npm run build
后发现单独生成了css
文件哦
axios
和middleware
优化API请求先安装下axios
npm install --save axios
我们之前项目的一次API请求是这样写的哦~
action
创建函数是这样的。比我们现在写的fetch
简单多了。
export function getUserInfo() {
return {
types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
promise: client => client.get(`http://localhost:8080/api/user.json`)
afterSuccess:(dispatch,getState,response)=>{
/*请求成功后执行的函数*/
},
otherData:otherData
}
}
然后在dispatch(getUserInfo())后,通过redux
中间件来处理请求逻辑。
中间件的教程看这里
我们想想中间件的逻辑
dispatch
REQUEST
请求。dispatch
SUCCESS
请求,如果定义了afterSuccess()
函数,调用它。dispatch
FAIL
请求。来写一个
cd src/redux
mkdir middleware
cd middleware
touch promiseMiddleware.js
src/redux/middleware/promiseMiddleware.js
import axios from 'axios';
export default store => next => action => {
const {dispatch, getState} = store;
/*如果dispatch来的是一个function,此处不做处理,直接进入下一级*/
if (typeof action === 'function') {
action(dispatch, getState);
return;
}
/*解析action*/
const {
promise,
types,
afterSuccess,
...rest
} = action;
/*没有promise,证明不是想要发送ajax请求的,就直接进入下一步啦!*/
if (!action.promise) {
return next(action);
}
/*解析types*/
const [REQUEST,
SUCCESS,
FAILURE] = types;
/*开始请求的时候,发一个action*/
next({
...rest,
type: REQUEST
});
/*定义请求成功时的方法*/
const onFulfilled = result => {
next({
...rest,
result,
type: SUCCESS
});
if (afterSuccess) {
afterSuccess(dispatch, getState, result);
}
};
/*定义请求失败时的方法*/
const onRejected = error => {
next({
...rest,
error,
type: FAILURE
});
};
return promise(axios).then(onFulfilled, onRejected).catch(error => {
console.error('MIDDLEWARE ERROR:', error);
onRejected(error)
})
}
修改src/redux/store.js
来应用这个中间件
import {createStore, applyMiddleware} from 'redux';
import combineReducers from './reducers.js';
import promiseMiddleware from './middleware/promiseMiddleware'
let store = createStore(combineReducers, applyMiddleware(promiseMiddleware));
export default store;
修改src/redux/actions/userInfo.js
export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";
export function getUserInfo() {
return {
types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
promise: client => client.get(`http://localhost:8080/api/user.json`)
}
}
是不是简单清新很多啦?
修改src/redux/reducers/userInfo.js
case GET_USER_INFO_SUCCESS:
return {
...state,
isLoading: false,
userInfo: action.result.data,
errorMsg: ''
};
action.userInfo
修改成了action.result.data
。你看中间件,请求成功,会给action
增加一个result
字段来存储响应结果哦~不用手动传了。
npm start
看看我们的网络请求是不是正常哦。
使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具有“安全写入”功能,可能会影响重新编译。
要在一些常见的编辑器中禁用此功能,请查看以下列表:
webpack
公共配置想象一个场景,现在我想给webpack
增加一个css modules
依赖,你会发现,WTF?我即要修改webpack.dev.config.js
,又要修改webpack.config.js
~
这肯定不行啊。所以我们要把公共的配置文件提取出来。提取到webpack.common.config.js
里面~
webpack.dev.config.js
和webpack.config.js
写自己的特殊的配置。
这里我们需要用到webpack-merge来合并公共配置和单独的配置。
这样说一下,应该看代码就能看懂了。下次公共配置直接就写在webpack.common.config.js
里面啦。
这里偷偷说下,我修改了
CleanWebpackPlugin
的参数,不让他每次构建都删除api
文件夹了。要不每次都得复制进去。麻烦~
npm install --save-dev webpack-merge
touch webpack.common.config.js
webpack.common.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
commonConfig = {
entry: {
app: [
path.join(__dirname, 'src/index.js')
],
vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
},
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js',
publicPath: "/"
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader?cacheDirectory=true'],
include: path.join(__dirname, 'src')
}, {
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/index.html')
}),
new webpack.HashedModuleIdsPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
],
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
components: path.join(__dirname, 'src/components'),
router: path.join(__dirname, 'src/router'),
actions: path.join(__dirname, 'src/redux/actions'),
reducers: path.join(__dirname, 'src/redux/reducers')
}
}
};
module.exports = commonConfig;
webpack.dev.config.js
const merge = require('webpack-merge');
const path = require('path');
const commonConfig = require('./webpack.common.config.js');
const devConfig = {
devtool: 'inline-source-map',
entry: {
app: [
'react-hot-loader/patch',
path.join(__dirname, 'src/index.js')
]
},
output: {
/*这里本来应该是[chunkhash]的,但是由于[chunkhash]和react-hot-loader不兼容。只能妥协*/
filename: '[name].[hash].js'
},
module: {
rules: [{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}]
},
devServer: {
contentBase: path.join(__dirname, './dist'),
historyApiFallback: true,
host: '0.0.0.0',
}
};
module.exports = merge({
customizeArray(a, b, key) {
/*entry.app不合并,全替换*/
if (key === 'entry.app') {
return b;
}
return undefined;
}
})(commonConfig, devConfig);
webpack.config.js
const merge = require('webpack-merge');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const commonConfig = require('./webpack.common.config.js');
const publicConfig = {
devtool: 'cheap-module-source-map',
module: {
rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}]
},
plugins: [
new CleanWebpackPlugin(['dist/*.*']),
new UglifyJSPlugin(),
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new ExtractTextPlugin({
filename: '[name].[contenthash:5].css',
allChunks: true
})
]
};
module.exports = merge(commonConfig, publicConfig);
现在我们优化下目录结构,把router
和nav
分开,新建根组件App
。
component
改名为components
,因为是复数。。。注意修改引用的地方哦。components/App/APP.js
import React, {Component} from 'react';
import Nav from 'components/Nav/Nav';
import getRouter from 'router/router';
export default class App extends Component {
render() {
return (
<div>
<Nav/>
{getRouter()}
</div>
)
}
}
components/Nav/Nav
组件,把router/router.js
里面的nav
提出来。components/Loading/Loading
组件,把router/router.js
里面的Loading
提出来。src/index.js
修改import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import store from './redux/store';
import {BrowserRouter as Router} from 'react-router-dom';
import App from 'components/App/App';
renderWithHotReload(App);
if (module.hot) {
module.hot.accept('components/App/App', () => {
const NextApp = require('components/App/App').default;
renderWithHotReload(NextApp);
});
}
function renderWithHotReload(RootElement) {
ReactDom.render(
<AppContainer>
<Provider store={store}>
<Router>
<RootElement/>
</Router>
</Provider>
</AppContainer>,
document.getElementById('app')
)
}
pages/NotFound/NotFound
组件。router/router.js
,增加404
import NotFound from 'bundle-loader?lazy&name=notFound!pages/NotFound/NotFound';
<Route component={createComponent(NotFound)}/>
先来说说babel-plugin-transform-runtime
在转换 ES2015 语法为 ECMAScript 5 的语法时,babel 会需要一些辅助函数,例如 _extend。babel 默认会将这些辅助函数内联到每一个 js 文件里,这样文件多的时候,项目就会很大。
所以 babel 提供了 transform-runtime 来将这些辅助函数“搬”到一个单独的模块 babel-runtime 中,这样做能减小项目文件的大小。
npm install --save-dev babel-plugin-transform-runtime
修改`.babelrc`配置文件,增加配置
.babelrc
```javascript
"plugins": [
"transform-runtime"
]
```
Q: 为什么要集成babel-polyfill
?
A:
Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。
举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。
网上很多人说,集成了transform-runtime
就不用babel-polyfill
了,其实不然,看看官方怎么说的:
NOTE: Instance methods such as "foobar".includes("foo") will not work since that would require modification of existing built-ins (Use babel-polyfill for that).
所以,我们还是需要babel-polyfill
哦。
npm install --save-dev babel-polyfill
修改webpack两个配置文件。
webpack.common.config.js
app: [
"babel-polyfill",
path.join(__dirname, 'src/index.js')
]
webpack.dev.config.js
app: [
'babel-polyfill',
'react-hot-loader/patch',
path.join(__dirname, 'src/index.js')
]
参考地址:
Q: 这是啥?为什么要用它?
他有很多很多的插件,我们举几个例子~
Autoprefixer这个插件,可以自动给css属性加浏览器前缀。
/*编译前*/
.container{
display: flex;
}
/*编译后*/
.container{
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
postcss-cssnext 允许你使用未来的 CSS 特性(包括 autoprefixer)
当然,它有很多很多的插件可以用,你可以去官网详细了解。我们今天只用postcss-cssnext
。(它包含了autoprefixer)
npm install --save-dev postcss-loader
npm install --save-dev postcss-cssnext
修改webpack
配置文件,增加postcss-loader
webpack.dev.config.js
rules: [{
test: /\.(css|scss)$/,
use: ["style-loader", "css-loader", "postcss-loader"]
}]
webpack.config.js
rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ["css-loader", "postcss-loader"]
})
}]
根目录增加postcss
配置文件。
touch postcss.config.js
postcss.config.js
module.exports = {
plugins: {
'postcss-cssnext': {}
}
};
现在你运行代码,然后写个css,去浏览器审查元素,看看,属性是不是生成了浏览器前缀?
今天突然发现,当修改reducer代码的时候,页面会整个刷新,而不是局部刷新唉。
这不行,就去查了webpack文档,果然是要配置的。看这里
代码修改起来也简单,增加一段监听reducers变化,并替换的代码。
src/redux/store.js
if (module.hot) {
module.hot.accept("./reducers", () => {
const nextCombineReducers = require("./reducers").default;
store.replaceReducer(nextCombineReducers);
});
}
哦了~
每个改进都是为了解决问题。
现在我在开发中碰到了问题,我先描述下问题:
我们现在做前后端完全分离的应用,前端写前端的,后端写后端的,他们通过API接口连接。
前端同学心理路程:"后端同学接口写的好慢,我都没法调试了。"
是不是有这个问题呢?一般我们怎么解决?
第一种:自己这边随便造点数据,等后端接口写好了之后,再小修改,再调试。
第二种:想想我们之前获得用户信息的dist/api/user.json
,我们可以用这种方式来调试。
但是想象下,我们要模拟一个文章列表,就要手动写几十列。oh~no!
并且,后端接口一般都不带.json
,到时候对接,是不是还得改代码?
好了,下面介绍下今天的主角Mock.js。
他会做一件事情:拦截AJAX请求,返回需要的数据!
我们写AJAX请求的时候,正常写,Mock.js会自动拦截的。
Mock.js提供各种随机生成数据。具体可以去官网看~
下面我们就在项目中集成咯:
npm install mockjs --save-dev
新建mock文件夹
touch mock
模拟一个我们之前用到的/api/user
接口
cd mock
touch mock.js
mock/mock.js
import Mock from 'mockjs';
let Random = Mock.Random;
Mock.mock('/api/user', {
'name': '@cname',
'intro': '@word(20)'
});
上面代码的意思就是,拦截/api/user
,返回随机的一个中文名字,一个20个字母的字符串。
我知道你看不懂,你去看看Mock.js文档就能看懂啦!
与我们的项目连接。到目前为止,刚才定义的接口和我们的项目还没有关系。
先来做,在src/index.js
里面增加一行代码:
src/index.js
import '../mock/mock';
现在我们删除dist/api
文件夹,然后修改之间的接口路径,把.json
去掉。
rm -rf dist/api
src/redux/actions/userInfo.js
/*promise: client => client.get(`/api/user.json`)*/
promise: client => client.get(`/api/user`)
现在我们运行npm start
,到获取用户信息界面,看每次获取用户信息都会变化呀?
到这里还没完,我们还要配置:只有在开发坏境下,才引入mock
,在生产坏境,不引入。
跟着我做:
先给mock
文件夹加个别名,这个我就不单独介绍了:
webpack.common.config.js
resolve: {
alias: {
...
mock: path.join(__dirname, 'mock')
}
}
webpack.dev.config.js
增加
const webpack = require('webpack');
plugins:[
new webpack.DefinePlugin({
MOCK: true
})
]
然后修改src/index.js
刚才加的那句话为下面这样
if (MOCK) {
require('mock/mock');
}
这样,就只会在npm start
开发模式下,才会应用mock,如果你不想用,就把MOCK改成false就好了。
哦了,到这里就结束了~回头缕下:
我们定义了mock,在index.js引入。
mock的工作就是,拦截AJAX请求,返回模拟数据。
参考文章:
http://www.jianshu.com/p/dd23a6547114
https://segmentfault.com/a/1190000005793320
关于什么是CSS Modules
,我这里不介绍。
可以去看阮一峰的文章CSS Modules 用法教程
修改以下几个地方:
webpack.dev.config.js
module: {
rules: [{
test: /\.css$/,
use: ["style-loader", "css-loader?modules&localIdentName=[local]-[hash:base64:5]", "postcss-loader"]
}]
}
webpack.config.js
module: {
rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ["css-loader?modules&localIdentName=[local]-[hash:base64:5]", "postcss-loader"]
})
}]
}
src/pages/Page1/page1.css
.box {
border: 1px solid red;
}
src/pages/Page1/Page1.js
import React, {Component} from 'react';
import style from './Page1.css';
import image from './images/brickpsert.jpg';
export default class Page1 extends Component {
render() {
return (
<div className={style.box}>
this is page1~
<img src={image}/>
</div>
)
}
}
enjoy it!
json-server和Mock.js
一样,都是用来模拟接口数据的。
json-server
功能更强大,支持分页,排序,筛选等等,具体的可以去看文档。
我们用json-server
代替之前的Mock.js
。
删除Mock.js
相关代码。
一共两处,webpack.dev.config.js
,src/index.js
npm install --save-dev json-server
写个demo,我们生成虚假数据还是用mockjs
。
mock/mock.js
let Mock = require('mockjs');
var Random = Mock.Random;
module.exports = function () {
var data = {};
data.user = {
'name': Random.cname(),
'intro': Random.word(20)
};
return data;
};
package.json
"mock": "json-server mock/mock.js --watch --port 8090",
"mockdev": "npm run mock & npm start"
json-server
服务器去。 devServer: {
...
proxy: {
"/api/*": "http://localhost:8090/$1"
}
}
哦了,你可以npm run mockdev
启动项目,然后访问我们之前的用户信息接口,试试啦。
问题:windows
不支持命令并行执行&
,你可以分开执行,或者使用npm-run-all
举例:在首页中,当我们计数加上去,然后修改代码,计数又恢复成0了。也就是热模块加载的时候,重置了react
的state
。
如果我们不使用code splitting
,是没有这个问题的。但是我们就是要用,哼!
解决问题参考这里:https://github.com/gaearon/react-hot-loader/tree/next#code-splitting
解决步骤:
npm install react-hot-loader@next
import {hot} from 'react-hot-loader';
...
export default hot(module)(Home);
其他模块如果需要,可以自己同理修改哦。
你好,我想问一下在用的时候不需要npm install --save history 这个包就可以直接用吗?谢谢
阿斯顿参数
组件是 UI + 逻辑的复用,但逻辑复用能力等于 0。
一个 React 项目,是由无数个大大小小的组件组合而成的。在 React 的世界中,组件是一等公民。而我们平时拆分组件的依据无非是:尽量的复用代码。
组件是 UI + 逻辑的复用,你不能将 UI 和逻辑拆开。比如 Antd 的 Cascader 级联选择 组件,内置了样式和级联选择的逻辑,用户使用的时候相当于一个黑盒,只管用就行了。但是有一个很现实的问题,当该组件的样式不能满足我们需求的时候,我们需要从 0 重新实现一个组件,重写 UI + 逻辑,哪怕逻辑真的一模一样。组件的逻辑复用能力等于 0。我可以想到一个可怕的事实,社区上的同类组件,大部分的逻辑都是可以复用的,只是在样式上有差异,但逻辑共享在社区上并没有很流行。
HOC 与 Render Props 可以把逻辑抽出来复用,但并没有让逻辑复用流行起来。
当然,我上面说的不能复用有点夸张,React 提供了 HOC 与 Render Props 两种方式来解决逻辑复用的问题。比如下面的监听鼠标位置的逻辑,我们就可以通过 Render Props 来复用。
<Mouse>
(position)=> <OurComponent />
</Mouse>
同类逻辑包括监听 window size,监听 scroll 位置等等。但是我们一般很少用 render props 来封装逻辑,更少去和其它项目去共享逻辑。为什么呢?想想多个逻辑复用会怎么样,你就知道多可怕了。
<WindowSize>
(size)=> (
<Mouse>
(position)=> <OurComponent size={size} mouse={position}/>
</Mouse>
)
</WindowSize>
嵌套地狱的代码是我们不能忍受的,同时 HOC 也存在类似的问题,这可能是导致逻辑复用不能流行起来的一个重要原因。
React Hooks 很好的解决了逻辑复用的问题,同时社区中诞生了一批比较好的 React Hooks 库。
React Hooks 是今年 React 的一个重磅炸弹,在社区引起了激烈的回响。随着 Hooks 的诞生,我们可以通过 Custom Hooks 很方便的封装逻辑,逻辑共享也成为了潮流。比如上面的例子,我们就可以通过 react-use 很方便的实现。
import {useMouse, useWindowSize, useScroll} from 'react-use'
function Demo(){
const mousePosition = useMouse();
const windowSize = useWindowSize();
}
react-use 是社区中比较优秀的 Hooks 库,封装了很多常用的基础逻辑,在日常开发中必不可少。但是只用 react-use 就够了吗?显然不是。react-use 中的 Hooks 粒度比较小,类似于工具库。而在中台产品中,有很多特定的场景逻辑,需要多个 Hooks 进行组合,或者定制特定的逻辑。基于此,我们创建了 @umijs/hooks ,定位为为中台场景服务的 Hooks 库。
@umijs/hooks 是面向中台应用场景的 Hooks 库,封装了中台常见场景的逻辑,让中台开发变得超级简单。@umijs/hooks 已经在蚂蚁金服多个产品中落地,口碑很好,提效明显。当然,你可能不信,口说无凭,那就用例子来说话。
中台开发中,table 页面应该算最多的一个了,我们一般会使用 Antd 的 Table 组件来搭建,但是其中还是有很多逻辑,我们是无法避免的。
上面的逻辑,我们在几乎所有的 table 页是必须要处理的,想想都可怕。useAntdTable 至少封装了上面 7 个逻辑,列表页开发从未变得如此简单。
const { table } = useAntdTable(asyncFn);
const columns = [];
return (
<Table columns={columns} rowKey="id" {...table} />
)
常见的异步搜索场景,我们一般要集成:
现在一切变得如此简单:
const { data, loading, onChange } = useSearch(asyncFn);
<Select
onSearch={onChange}
loading={loading}
>
{data.map((item)=><Option />)}
</Select>
当然,我们还有更多极大提效的 CustomHooks,你能想象不用写一行逻辑,就能实现异步 loadmore 功能吗?
你能想象不用写一行逻辑,就能实现动态增删,排序的表单吗?
各种常见场景,通通不用写逻辑,通通不用写逻辑。
umi hooks 让中台开发变得如此简单,我能想象,不久的将来,中台开发可以不用写一行逻辑,这也是我们为之奋斗的目标。
同时,我也知道,大家平时工作中也积累了很多常用的 Hooks,我们非常希望大家能参与进来,共建 umi hooks,无论对我们,还是用户,都是最好的福音。
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.