# 核心原理

# 1.什么是前端路由

在SPA(single page application)中,描述URLUI的映射关系。url的改变引起ui的变化,并且无需刷新页面。(即局部刷新

# 2.如何实现前端路由

  1. 如何改变页面而不引起刷新?
  2. 如何检测url的变化?

# hash实现

# hash概述

  • hash是url后面(#)的部分,

  • 常用hash(锚点)来做页面导航,

  • 改变url后面的hash部分不会引起页面刷新

  • URL的变化会触发hashChange

改变url的方式

  • 浏览器前进后退
  • window.location

# history实现

# history概述

  • h5提供可以添加历史记录,而不引起页面刷新的api
  • pushState (name,title,url)
  • replaceState(name,title,url)
  • popState 详解
    • 1.浏览器前进后退会触发popState
    • pushState和replaceState和不会触发
    • 可以拦截 pushState replaceState 调用 和a的点击 来 检测
    • go back forward 会触发

# 3.实现简单前端路由

hash实现

<body>
  <ul>
    <li><a href="#/foo">foo</a></li>
    <li><a href="#/home">home</a></li>
  </ul>

  <div id="content">内容</div>

  <script>
    /* 
    就是监听hash的改变,去改变视图
    */
    var divv = content
    window.addEventListener('hashchange', function (e) {
      divv.textContent = window.location.hash
    })
    window.addEventListener('DOMContentLoaded', function (e) {
      if (!window.location.hash) {
        location.hash = '/'
      }else {
        divv.textContent = window.location.hash
      }
    })
  </script>
</body>

history实现

<body>
  <ul>
    <li><a href="/foo">foo</a></li>
    <li><a href="/home">home</a></li>
  </ul>

  <div id="content">内容</div>
  <script>
    var divv = content
    // push replace a
    // go back forward
    window.addEventListener('popState', function (e) {
      // hash pathname host ...
      divv.textContent = location.pathname
    })
    window.addEventListener('DOMContentLoaded', function (e) {
      divv.textContent = location.pathname
      let linkList = document.querySelectorAll('a[href]')
      linkList.forEach(item => {
        item.addEventListener('click', function (e) {
            e.preventDefault()
          //也就是说history实现 :就多一步 我们自己操作记录
          history.pushState(null, '', item.getAttribute("href"))
          divv.textContent = location.pathname
        })
      })
    })
  </script>
</body>

# 实现

# 使用1:路由的引入

Vue.use(VueRouter)
//use需要VueRouter实现install进行注册

实现install

//引入2个组件
import View from './components/view'
import Link from './components/link'

let Vue=null
class VueRouter {
    
}
VueRouter.install=function(v) {
  //保存Vue
    Vue=v
    
  //注册2个组件
    Vue.component('router-link',Link
    })
    Vue.component('router-view',View
    })
}

完善install

$router$route 有什么区别?

  • router是VueRouter的实例对象,全局对象

  • route是当前路由对象,route是router的一个属性

  • 每个组件添加的 router是同一个,每个组件添加的route也是同一个的

那如何给每个实例都添加上router``route?

# 处理$router

VueRouter.install=function(v){
    //添加的代码
    Vue.mixin({
        beforeCreate(){
        //给根组件添加_router属性,
        //每个子组件通过_root._router拿到全局router对象    
        if(this.$options&& this.$options.router) {
			 		//如果是根组件,
            this._root=this
            this._router=this.$options.router
        }else {
            this._root=this.$parent&&this.$parent._router
        }
           Object.defineProperty(this,'$router',{
               get:function(){
                   return this._root._router
               }
           } 
          Object.defineProperty(this,'$route',{
               get:function(){
               //注意:history在构造函数中根据mode模式创建的
                   return this._root._router.history.current
               }
           }                     
		) 
            
        },
        destory(){
            
        }
    })
}

# 使用2:路由对象的配置

new VueRouter ({
    route: [
        {
            name:'foo',
            path:'/foo',
            component:Foo
        }
    ],
    mode:'hash' 
})

完善构造函数

class VueRouter {
    constructor(options) {
        this.mode=options.mode||hash
       //处理路由配置
       this.matcher = createMatcher(options.routes || [], this)
    }
}

# 处理route

//  /src/createMatcher.js
// 改方法返回多个处理函数
export function createMatcher(routes,router){
    
    const {pathList,pathMap,nameMap}= createRouteMap(routes)
	
    function addRoute(parentOrRoute,route){}
    
    function addRoutes(route){}

    function match(){}
    
    function getRoutes(){}
    
    return {
        match,
        addRoute,
        getRoutes,
        addRoutes
    }
 }

接下来看 createRouteMap

  • 创建映射表
  • 将每一项路由配置 转换成 record
  • 并且递归子配置,扁平化所有配置到引射表
  • 如果 配置项有别名,同样生成另一条配置转换为record,加入map
pathList:路由路径列表
pathMap:路径到路由的映射
nameMap:名字到路由的映射
export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>,
  parentRoute?: RouteRecord
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  //控制路径匹配的优先级
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  //遍历路由记录(toute) 调用addRouteRecord
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
  })

  // 确保通配符在最后面
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }
	...
  return {
    pathList,
    pathMap,
    nameMap
  }
}

接下来看addRouteRecord

  • 生成 record
  • 添加nameMap和pathMap
function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  const { path, name } = route

  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }
//生成record
  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    alias: route.alias
      ? typeof route.alias === 'string'
        ? [route.alias]
        : route.alias
      : [],
    instances: {},
    enteredCbs: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  }
	//递归子配置项
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
	//加入pathMap
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

    
    }
  }
  //配置项别名 也生成一条新的record
   if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i]
      if (process.env.NODE_ENV !== 'production' && alias === path) {
        warn(
          false,
          `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
        )
        // skip in dev to make it work
        continue
      }

      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    }
  }
 //加入nameMap
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
          `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}

# 处理 mode

HTML5History HashHistory AbstractHistory 用来记录路由的历史记录

class VueRouter {
    constructor(options){
        this.mode=options.mode
        
         switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
    }
}

# 使用3: 导航守卫

const route= new VueRouter({
    route:[
        {
            name:'foo',
            path:'/foo',
            //独享
    		beforeEnter:()=>{}        
        }
    ]
})
//全局
route.beforeEach(from,to,next) {
    
}
route.beforeEach(from,to,next) {
    
}
route.beforeEach(from,to,next) {
    
}

# 处理路由改变时导航守卫的调用

路由改变 
window.addEventListener('popState',handleRoutingEvent)

//handleRoutingEvent调用transitionTo
//在路由改变时被  handleRoutingEvent()调用
//执行按顺序路由守卫方法
function transitionTo() {
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )
  
//按解析顺序放入队列中
const queue:Array<?NavigationGuard> = [].concat(
2 extractLeaveGuards(deactivated)
3 this.router.beforeHooks,
4 extractUpdateHooks(updated),
5 activated.map(m=>m.beforeEnter)
6 resolveAsyncComponents(activated)
)

//尝试执行和做tryCatch处理
iterator(){}

runQueue(queue,iterator,()=>{
   //7,8
    const enterGuards=extractEnterGuards(activated)
    const queue = enterGuards.concat(this.router.resolveHooks)
	runQueue(queue,iterator , ()=>{
        9 确认  onComplete(route)
        ///10 afterEach在哪执行?
         if (this.router.app) {
          this.router.app.$nextTick(() => {
          11 更新视图  handleRouteEntered(route)
          })
        }

    })
    })   
}  

class VueRouter {
    constructor(options){
        
      // 存放 各种全局路由守卫的函数钩子的队列
      this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    }
      beforeEach (fn: Function): Function {
    return registerHook(this.beforeHooks, fn)
  }

  beforeResolve (fn: Function): Function {
    return registerHook(this.resolveHooks, fn)
  }

  afterEach (fn: Function): Function {
    return registerHook(this.afterHooks, fn)
  }

}

下面看registerHook

  • 将钩子函数添加到函数队列
  • 返回删除钩子函数的方法
function registerHook (list: Array<any>, fn: Function): Function {
  list.push(fn)
  return () => {
    const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}

# 使用4:Router-view

<router-link to='/foo' />
//渲染 foo组件    
<router-view >    

# 实现Router-View

每个组件都有_root, 可以通过_root拿到_router,再拿到history 最后拿到current

// View组件 
export default {
    name:'RouterView',
    functional:true,
    prop:{
        name:{
            type:String,
            default:'default'
        }
    },
    render(){
     //从history获取current
     //根据routeMap获取对应的组件
     //return h( routeMap[current] )   
    }    
}

Q:当current路径变化,不能响应式修改视图。所以需要将`histroy`变成响应式数据
//在install方法里
Vue.mixin({
    beforeCreate(){
        if (this.$options && this.$options.router){ 
            this._root = this; 
            this._router = this.$options.router;
           //!! 关键 新增代码
            Vue.util.defineReactive(this,"_route",this._router.history)
        }else { 
            this._root= this.$parent && this.$parent._root
        }
        Object.defineProperty(this,'$router',{
            get(){
                return this._root._router
            }
        });
        Object.defineProperty(this,'$route',{
            get(){
                //要将 this._router.history变成响应式
                return this._root._route.current
            }
        })
    }
})
<router-link to='/foo' />
//渲染 foo组件    
<router-view >    
export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
   //简单实现
     let to =  this._root.$router.mode =='hash' ? `#${this.to}`:this.to
     return h('a',{attr:{href:to}},this.$slots.default)
  }
}