我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情
前言
我们都见过离开前页面前的提示弹窗,比如"您确定要离开当前页面吗?"
或者 提示是”当前设定修改后未保存,是否确定离开"
等等,
推测肯定是拦截了浏览器的回退功能,但很好奇具体是怎么实现的?现在,就让我们像剥洋葱一样一层一层剥开它的心~~
一、使用方式
- 先看下基本使用
// users.js
import React from "react"
import { Prompt } from "react-router";
const Users = () => {
return (
<div>
<div style={{color: '#990', fontSize: '40px'}}> users 页面</div>
<Prompt message="你真的要离开吗?" />
</div>
);
}
export default Users
当点击回退按钮之后,页面出现提示
- 参数说明
export interface PromptProps {
message: string | ((location: H.Location, action: H.Action) => string | boolean);
when?: boolean | undefined;
}
export class Prompt extends React.Component<PromptProps, any> {}
可以看到,<Prompt>
只接收两个参数,一个必传的message
,就是弹窗消息内容,message本身既可以是string(上例),还可以是个函数;另一个可选的when
,可以控制出现提示的触发时机,接收布尔值或undefined
。
二、剥源码
2.1 一睹为快
下面是react-router V5.3.3的<prompt>
源码
可以看到,
<Prompt>
组件使用订阅消费模式,拿到context对象,然后<Lifecycle>
组件传入了从context传过来的history.block
方法,最后在不同的生命周期调用。看起来就是这么简单!嗯,看起来~
2.2 【1】RouterContext.Consumer 和 【2】Context
<ReactContext.Consumer>{context => {...}</ReactContext.Consumer>
其实就是react 原生的React.createContext
————一个使用订阅消费模式实现的跨组件共享数据机制。
有Consumer
就一定会有Provider
,凭借修炼已久的顺藤摸瓜大法,在Router.js
中我找了它
其中,RouterContext.Provider
的value值,就是<Prompt>
中的context。
我们再来看下这里:
(warning除外)用到context的地方只有两处:
- context.staticContext 主要用于测试和
服务器渲染场景,上面那行
if
判断也能看出来,如果when是false
或者context.staticContext 是 true
是不会拦截的,想了解的童鞋也可自行看下<StaticRouter>
组件 - context.history.block 这里才是拦截的实现,下面单独来看
2.3【3】context.history.block
- 首先,在
history
里找到这个方法(当前用的是V4.10.1版本)
- 然后看下它的实现(
createBrowserHistory
和createHashHistory
是一模一样的实现)
可以看到,block的代码里关键是标注的
[1]
和[2]
- 先看下
【1】createTransitionManager
由于上面block
里用到的是这个方法setPrompt
,其余干扰代码我就删掉了,这简单的几行代码里做了三件事:
- 一是
history
一次只支持传入一个prompt
的提示,warning
函数第一个参数为false的时候触发提示。 - 二是 在调用
setPrompt
的时候通过闭包给外层的prompt
赋值为传入的提示内容 - 三是
setPrompt
返回一个函数,可在调用这个返回函数的时候,把变量prompt
重新置为null
。(所以,前面block的第一行,会给返回函数起名为unblock
)
- 再看
[2] checkDOMListeners
这个方法嵌套的调用有点多(关于这个方法,后面我单独写一篇,感兴趣的童鞋,敬请期待),只能放部分思路代码了
checkDOMListeners
这个函数事实上做了两件事:
- 用一个
listenerCount
的值是0还是1来控制监听/取消相应事件 - 在触发
BrowserHistory
的时候如果同时触发了HashHistory
,也要监听/取消相应事件。
这里我们以看上面圈出来的方法handlePopState
为代表
先是一个忽略 WebKit 中无关的 popstate 事件
的判断,然后调用了一个handlePop(getDOMLocation(event.state))
, getDOMLocation(event.state)
返回的是location对象
(不再展开细说),我们看handlePop
的实现:
第一次调用的时候会走到else逻辑
,关键方法是t ransitionManager.confirmTransitionTo
,坚持✊一下,马上就能看到曙光了~,继续看这个方法:
该方法里面的prompt
就是我们使用时传入的提示内容,比如是个string——您确定要离开当前页面吗?
,那标注[1]
处的result
此时就是这个字符串。而标注[2]
处的getUserConfirmation
长这个样子,终于见到庐山真面目了~
这个函数的第二参数是个回调,参数是window.confirm(message)
,这里就直接看出来了,浏览器拦截弹窗用的是window.confirm
而非window.alert
。而在getUserConfirmation(result, callback);
调用中的callBack
是confirmTransitionTo
的第四个参数:
如果在拦截弹窗里点true,那就跳走,否则走revertPop
至此,history
相关内容告一段落,接下来,我们回到Prompt
代码,看下标注的最后一点【4】Lifecycle
2.4 【4】Lifecycle
有了上面的铺垫,<Lifecycle>
就是洒洒水啦~,组件本身也非常简单,直接上代码:
可以看到,就是继承了
React.Component
的三个生命周期,并在各自的生命周期里调用从props传入的方法,返回一个null
。
再来回忆下我们Prompt
里这段代码:
他调的method
就是history.block
, message
就是我们的提示内容。onMount、onUpdate、onUnmount
分别对应React.component
的componentDidMount、componentDidUpdate、componentWillUnmount
。
再做下简单解释,当onMount的时候会执行method(message)
,并把它挂到self.releas
上,等到update的时候,判断下前后两次的message是否一致,不一致的话, 执行selef.relase()
,(也即是unblock
---history.block
的返回值),然后再执行一遍self.release = method(message)
,最后就是Unmount
的卸载了。
细心的童鞋可能会发现,<Lifecycle>
明明没有接收message
参数,为什么<Prompt>
还要传个message={message}
呢?是不是去掉也可以呢?
答案是~ 当然不可以,<Lifecycle>
的确明明没有接收message
,但是他暗戳戳的接收了呀,在componentDidUpdate
里有个prevProps
,就是用于获取上次的message
的。
结语
俗话说,一图胜千言,我们就来画个简图:
最后,祝大家月饼节快乐,希望感觉有一丢丢收获的童鞋可以不吝赐赞,快乐加倍🦆!!。像我一样因为疫情回不了家的,也要好好犒劳自己一顿😄。