原理分析之路由管理

252 阅读3分钟

Vue Router

基本使用

引入vue插件:

vue add router
Vue.use(router)

配置router.js:

//routes
{ 
    path: '/', name: 'home', component: Home, children : []    //children中配置嵌套路由
    path: '/about', name: 'about', component: About 
}
new Router(routers)

路由出口:

<router-view />

导航链接:

<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>

添加到vue选项:

// main.js 
new Vue({ 
    router,
    render: h => h(App) 
}).$mount('#app')

vue-router源码中到底做了什么事情呢?

分析:
1.首先要定义一个Router类,在这个类中,要解析我们的routes配置,生成对应的map,以便导航栏切换了url后,可以很方便的找到要渲染的组件;其次是要对url做响应式处理,并监听浏览器url变化并添加监听函数处理响应逻辑;同时我们在全局中可以使用router-link和router-view,所以还需要全局注册这两个组件。
2.我们使用Vue.use(router)添加router,说明router是一个插件,调用use方法时,会调用插件的静态方法install,所有还需要在类中实现install的静态方法,然后mixin到Vue中,并挂载全局对象$router。

开始写源码

//导出我们自己实现的router
export default class VueRouter{
    constructor(options){
        this.$options = options    //保存我们的参数,后续要用
        this.routerMap = {}    //保存地址和组件之间的映射关系
        this.app = new Vue({    //对url作响应式处理
            data:{ url:'/' }
        })
    }
    //初始化router的函数,new Vue的时候调用
    init(){
        //监听浏览器的url变化,这里我们使用hash模式(带#号的)
        this.bindEvents()
        //生成routerMap映射关系
        this.createRouterMap()
        //全局注册router-link,router-view
        this.initComponents()
    }
    bindEvents(){
        window.addListener('hashChange',this.onHashChange.bind(this))
        window.addListener('load',this.onHashChange.bind(this))
    }
    onHashChange(e){
        //这个一变,用到url的地方就会变
        this.app.url = window.location.hash.slice(1)    //#index,只需要#后面的内容
    }
    createRouterMap(){
        //递归函数
        function getMap(routes){
            routes.forEach(item=>{
                this.routeMap[item.path] = item
                if(item.children){    //嵌套路由
                    getMap(item.children)
                }
            })
        }
        getMap(this.$options.routes)
    }
    initComponents(){
        //全局注册router-link
        Vue.component('router-link',{
            //有一个属性to
            props:{
                to:String
            }
            //实则是一个a标签
            render(h){
               // return <a href={this.to}>{this.$slots.default}<a>  
               // h就是createELement函数,参数是tag,data,children
               return h('a',{attrs:{href:'#'+this.to}},[this.$slots.default])
            }
        })
        //全局注册router-view
        Vue.component('router-view',{
            render(h){
                //拿出要渲染的组件,url变化router-view就会变化
               const component = this.routerMap[this.app.url].component
               return h(component)
            }
        })
    }
    //实现插件,即实现install的静态方法,Vue.use的时候调用,参数是Vue的构造函数
    VueRouter.install = function(Vue){
        //混入到Vue的生命周期中,执行初始化router并挂载$router
        Vue.mixin({
            //该生命周期比较早,且只会在new Vue的时候调用
            beforeCreate(){
                if(this.$options.router){
                    //vue的实例就可以使用vue.$router
                    Vue.prototype.$router = this.$options.router
                    //初始化router
                    this.$options.router.init()
                }
            }
        })
    }
}

嵌套路由的解决方式

以上代码并没有解决rouer-view嵌套问题,源码中的实现方式是在router-view的render函数中将data的routerView属性设为true,然后去找父级中的data属性中找存不存在routerView=true的情况,找到了就将其属性depth加1,然后根据depth去route中match对应的组件并渲染。

React Router

基本使用

react-router中奉⾏⼀切皆组件的思想,路由器Router、链接Link、路由Route、独占路由Switch、重定向Redirect都以组件形式存在。
Route渲染优先级:children>component>render
创建RouterPage.js:

import React, { Component } from "react";
import { BrowserRouter, Link, Route } from "react-router-dom";
import HomePage from "./HomePage";
import UserPage from "./UserPage";
export default class RouterPage extends Component {
  render() {
     return (
         <div>
             <h1>RouterPage</h1>
             <BrowserRouter>
                 <nav>
                     <Link to="/">⾸⻚</Link>
                     <Link to="/user">⽤户中⼼</Link>
                 </nav>
                 //根路由要添加exact,实现精确匹配,动态路由使⽤:id的形式定义动态路由
                 <Route exact path="/" component= {HomePage} />
                 <Route path="/user" component= {UserPage} />
             </BrowserRouter>
         </div>
     );
  }
}

嵌套路由

Route组件嵌套在其他⻚⾯组件中就产⽣了嵌套关系
Detail.js:

function Detail() {
   return (
     <div>
         <h1>Detail</h1>
     </div>
   );
}

嵌套了Detail.js的Search.js:

function Search({ match, history, location }) {
 const { id } = match.params;
 return (
     <div>
         <h1>Search: {id}</h1>
         <nav>
             <Link to="/search/add">新增</Link>
             <Link to={"/search/detail/" + id}>详情</Link>
         </nav>
         <Route path="/search/add" component={()=> <h1>add</h1>} />
         <Route path={"/search/detail/:" + id} component={Detail} />
     </div>
  );
}

404⻚⾯

设定⼀个没有path的路由在路由列表最后⾯,表示⼀定匹配

<Switch>    //独占路由表示只会匹配一个
 {/* 根路由要添加exact,实现精确匹配 */}
 <Route exact path="/" component={HomePage} />
 <Route path="/user" component={UserPage} />
 <Route path="/search/:id" component={Search} />
 <Route component={() => <h1>404</h1>} />
</Switch>

react-router源码中到底做了什么事情呢?

分析:
react-router秉承⼀切皆组件,因此实现的核⼼就是BrowserRouter、Route、Link

实现BrowserRouter

BrowserRouter:历史记录管理对象history初始化,及向下传递,location变更监听,本身
无内容,负责显示子组件的内容即可(组件复合,或者叫插槽)。

import { createBrowserHistory} from 'history';    //切换组件的库,保存着我们的路由状态
const RouterContext = React.createContext()    //用于多层级组件之间的传值
export class BrowserRouter extends React.Component{
    construtor(pros){
        super(props)
        this.history = createBrowserHistory(props)
        //响应式数据,location改变时渲染对应的组件
        this.state = { location : this.history.location }
    }
    //监听history中location改变事件
    this.unListen = this.history.listen(location=>{
        this.setState({ location })
    })
    //组件卸载时移除监听
    componentWillUnmount(){
        this.unListen && this.unListen()
    }
}
render(){
    return (
        //要向下级传值
        <ReactContext.Provider
            //传值的对象
            children = {this.props.children || null }
            value = {{
                history:this.history,
                location:this.state.location
            }}
        >
    )
}

实现Route

路由配置,匹配检测,内容渲染

//这里我们使用函数式组件
export function Route(props){
    const ctx = useContext(ReactContext)    //函数式组件中获取context的方法
    const {path,components:Cmp} = props    //获取参数
    //这个组件负责渲染,所以需要根据location去获取对应的组件
    const { location } = ctx
    let match = path == location.pathname
    //匹配到了就返回该组件
    return match ? <Cmp />:null
}

实现Link

link负责跳转链接,本质是a标签,需要禁用a标签的默认行为。

//这里我们使用类组件
export class Link extends Component{
    constructor(props){
        super(props)
        this.props = props
    }
    handleClick(event,history){
        //先阻止a标签的默认行为,防止浏览器刷新
        event.preventDefault()
        //切换组件,会引发history.listen的回调事件触发,进而改变location,渲染对应的Route
        history.push(this.prosps.to)
    }
    render(){
        //<link to='/'>首页</Link>,to=/,children=首页
        const {to,children} = this.props
        return <ReactContext.Consumer>    //类组件使用Consumer获取Context中的参数
            context=>{
                return <a href={to} 
                    onclick={event=>this.handleClick(event,context.history)}>
                    {children}
                 </a>
            }
        </ReactContext.Consumer>
    }
}