前言

实现一个简单版本的react-router, 揭秘路由的神秘面纱

  1. ![](https://cdn.nlark.com/yuque/0/2020/gif/1225858/1600775247766-b26a934d-34e2-4632-8a25-2e5e417269dc.gif#align=left&display=inline&height=240&margin=%5Bobject%20Object%5D&originHeight=240&originWidth=360&size=0&status=done&style=none&width=360)

思考

前端路由本质上是什么

前端路由里的一些坑和注意点

hash路由和history路由的区别

Router组件和Route组件分别做了什么

路由的本质

浏览器端的路由不是真实的网页跳转,和服务器没有任何交互,本质上就是对url进行监听,让某个dom节点显示对应的视图
路由的区别

路由的区别

一般来说,前端路由分为两种
1、hash 路由, 特征是url后面会有 # 号, 如 baidu.com/#foo/bar/baz
2、history 路由, url和普通路由没有差异。 如 baidu.com/foo/bar/baz

实际上只要搞清楚两种路由分别是如何改变,并且组件是如何完成视图的展示的

hash

通过location.hash = 'foo' 这样的语法来改变, 路径, 路径就会由baidu.com变成baidu.com/#foo
通过window.addEventListener('hashchange')这个事件监听到hash值的变化

- history

通过window.history.pushState(data, title, targetURL)

  • @状态对象:传给目标路由的信息,可为空
  • @页面标题:目前所有浏览器都不支持,填空字符串即可
  • @可选url:目标url,不会检查url是否存在,且不能跨域。如不传该项,即给当前url添加data

通过
history.pushState({}, '', 'foo'),可以让 baidu.com 变化为 baidu.com/foo

!坑

history路由的监听,浏览器虽然提供了window.addEventListener(‘popstate事件’),但是只能监听浏览器回退和前进产生的路由变化,对于主动的pushState却监听不到

基于history版本从零到1实现 react-mini-router

实现一个history

1、history.push
2、history.listen

利用观察者模式封装简单的listen API。让用户监听到history.push 产生的路径变化

  1. // 参考 https://github.com/sl1673495/react-mini-router
  2. let listeners = [];
  3. function listen(fn){
  4. listeners.push(fn)
  5. }
  6. function push(to, state) {
  7. // 解析用户传入的 url
  8. // 调用原生 history 的方法改变路由
  9. window.history.pushState(state, '', to);
  10. // 执行用户传入的监听函数
  11. listeners.forEach(fn => fn(location));
  12. }
  13. // 用于处理浏览器前进后退操作
  14. window.addEventListener('popstate', () => {
  15. listeners.forEach(fn => fn(location));
  16. });
  17. history.listen = listen
  18. history.push = push
  19. history.location = location

简单实现

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <title>React Router History</title>
  7. </head>
  8. <body>
  9. <div id="app"><div>
  10. </body>
  11. <script src="https://unpkg.com/react@16/umd/react.production.min.js" crossorigin></script>
  12. <script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" crossorigin></script>
  13. <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  14. <script>
  15. // 参考 https://github.com/sl1673495/react-mini-router
  16. let listeners = [];
  17. function listen(fn){
  18. listeners.push(fn)
  19. }
  20. function push(to, state) {
  21. // 解析用户传入的 url
  22. // 调用原生 history 的方法改变路由
  23. window.history.pushState(state, '', to);
  24. // 执行用户传入的监听函数
  25. listeners.forEach(fn => fn(location));
  26. }
  27. // 用于处理浏览器前进后退操作
  28. window.addEventListener('popstate', () => {
  29. listeners.forEach(fn => fn(location));
  30. });
  31. history.listen = listen
  32. history.push = push
  33. history.location = location
  34. </script>
  35. <script type="text/babel">
  36. class App extends React.Component{
  37. constructor(props){
  38. super(props)
  39. this.state={
  40. showFoo: false
  41. }
  42. }
  43. componentDidMount(){
  44. history.listen(location => {
  45. console.log(location,'location');
  46. const {pathname} = location;
  47. if(pathname == '/Router/foo'){
  48. this.setState({
  49. showFoo: true
  50. })
  51. }else{
  52. this.setState({
  53. showFoo: false
  54. })
  55. }
  56. });
  57. }
  58. changeRouter(to){
  59. history.push(to, {});
  60. }
  61. render(){
  62. const {showFoo} = this.state;
  63. return(
  64. <div>
  65. <button onClick={()=>{this.changeRouter('foo')}}>展示foo组件</button>
  66. <button onClick={()=>{this.changeRouter('index')}}>回到首页</button>
  67. {!showFoo && <div>首页</div>}
  68. {showFoo && <div>我是Foo</div>}
  69. </div>
  70. )
  71. }
  72. }
  73. ReactDOM.render(<App/>,document.getElementById('app'));
  74. </script>
  75. </html>

实现 Router

Router的核心原理就是通过Provider把location和history等路由关键信息传递给子组件,并切在路由发生变化的时候让子组件可以感知

  1. const RouterContext = React.createContext(null)
  2. class Router extends React.Component{
  3. constructor(props){
  4. super(props)
  5. this.state={
  6. location: location
  7. }
  8. }
  9. componentDidMount(){
  10. history.listen(location => {
  11. this.setState({
  12. location
  13. })
  14. });
  15. }
  16. render(){
  17. const { location } = this.state
  18. return(
  19. <div>
  20. <RouterContext.Provider value={{ history, location }}>
  21. {this.props.children}
  22. </RouterContext.Provider>
  23. </div>
  24. )
  25. }
  26. }

实现 Route

Route 组件接受 pathchildren两个 prop ,本质上就决定了在某个路径下需要渲染什么组件,我们又可以通过 Router 的 Provider 传递下来的 location 信息拿到当前路径,所以这个组件需要做的就是判断当前的路径是否匹配,渲染对应组件

  1. const Route = ({ path, children }) => {
  2. let {history,location} = React.useContext(RouterContext);
  3. let { pathname } = location
  4. console.log(pathname,path,'iii');
  5. if(pathname === path){
  6. return children
  7. }
  8. return null
  9. };

实现 Link

Link 组件接受to和name两个参数,通过 Router 的 Provider 传递下来的 history 拿到当前的push方法,点击的时候去触发

  1. const Link = ({name,to})=>{
  2. let {history} = React.useContext(RouterContext);
  3. function changeRouter(to){
  4. history.push(to, {});
  5. }
  6. return (
  7. <div>
  8. <button onClick={()=>{changeRouter(to)}}>{name}</button>
  9. </div>
  10. )
  11. }

完整代码

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <title>React Router History</title>
  7. </head>
  8. <body>
  9. <div id="app"><div>
  10. </body>
  11. <script src="https://unpkg.com/react@16/umd/react.production.min.js" crossorigin></script>
  12. <script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" crossorigin></script>
  13. <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  14. <script src="./history.js"></script>
  15. <script type="text/babel">
  16. const RouterContext = React.createContext(null)
  17. class Router extends React.Component{
  18. constructor(props){
  19. super(props)
  20. this.state={
  21. location: location
  22. }
  23. }
  24. componentDidMount(){
  25. history.listen(location => {
  26. this.setState({
  27. location
  28. })
  29. });
  30. }
  31. render(){
  32. const { location } = this.state
  33. return(
  34. <div>
  35. <RouterContext.Provider value={{ history, location }}>
  36. {this.props.children}
  37. </RouterContext.Provider>
  38. </div>
  39. )
  40. }
  41. }
  42. const Route = ({ path, children }) => {
  43. let {history,location} = React.useContext(RouterContext);
  44. let { pathname } = location
  45. console.log(pathname,path,'iii');
  46. if(pathname === path){
  47. return children
  48. }
  49. return null
  50. };
  51. const Link = ({name,to})=>{
  52. let {history} = React.useContext(RouterContext);
  53. function changeRouter(to){
  54. history.push(to, {});
  55. }
  56. return (
  57. <div>
  58. <button onClick={()=>{changeRouter(to)}}>{name}</button>
  59. </div>
  60. )
  61. }
  62. class App extends React.Component{
  63. constructor(props){
  64. super(props)
  65. }
  66. render(){
  67. return(
  68. <div>
  69. <Router>
  70. <Link to="/foo" name="展示foo组件"/>
  71. <Link to="/index" name="回到首页"/>
  72. <Route path="/index">
  73. <Index/>
  74. </Route>
  75. <Route path="/foo">
  76. <Foo/>
  77. </Route>
  78. </Router>
  79. </div>
  80. )
  81. }}
  82. class Foo extends React.Component{
  83. render(){
  84. return <div>我是foo</div>
  85. }
  86. }
  87. class Index extends React.Component{
  88. render(){
  89. return <div>我是index1</div>
  90. }
  91. }
  92. ReactDOM.render(<App/>,document.getElementById('app'));
  93. </script>
  94. </html>

参考

hooks+ts实现的版本

总结

react-router 前端路由本质上是通过地址栏的切换,展示隐藏页面上的dom元素, 前端路由一般分为两种,hash模式和history模式, hash模式可以通过location.hash 切换,通过浏览器的hashchange进行事件监听, history模式可以通过history.pushState({},’’,’foo’) 切换地址, 通过浏览器的popstate事件监听(这里需要注意的坑是popstate只能监听到浏览器前进后退事件不能监听到history.pushState的切换,需要通过观察者模式来注册回调的方式进行监听)