2020vue-b阶段课程(架构)\第2章 手写Vue-Router
使用 vue/cli 初始化一个带 vue-router 的项目
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
Vue.use(VueRouter); // 使用Vue-Router插件
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue"),
},
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});
export default router; // 创建Vue-router实例,将实例注入到main.js中
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
实现 install 方法
Vue.use = funciton(plugin, options){
plugin.install(this, options)
}
实现 install 方法
export default function install(Vue) {
_Vue = Vue;
Vue.mixin({
// 给每个组件添加_routerRoot属性
beforeCreate() {
if (this.$options.router) { // 如果有 router 属性说明是根实例
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this); // 初始化路由,这里的 this 指向的是根实例
} else {
// 儿子找爸爸
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
}
})
}
为了让所有子组件都有_routerRoot
(根实例),所有组件都可以通过this._routerRoot._router
获取用户的实例化路由对象。
生成路由表
class VueRouter {
constructor(options) {
// 生成路由映射表
// match 匹配方法
// addRoutes 动态添加路由
this.matcher = createMatcher(options.routes || []);
}
}
VueRouter.install = install;
import { createRouteMap } from "./create-route-map";
export function createMatcher(routes) {
// 路径和记录匹配 / record
let { pathMap } = createRouteMap(routes); // 创建映射表 v
function match(path) {
return pathMap[path];
};
function addRoutes(routes) {
createRouteMap(routes, pathMap);
}
return {
addRoutes,
match
}
}
export function createRouteMap(routes, oldPathMap) {
// 如果有oldPathMap 我需要将 routes格式化后放到 oldPathMap 中
// 如果没有传递 需要生成一个映射表
let pathMap = oldPathMap || {}
routes.forEach(route => {
addRouteRecord(route, pathMap);
})
return {
pathMap
}
}
function addRouteRecord(route, pathMap, parent) {
let path = parent ? `${parent.path}/${route.path}` : route.path;
// 将记录 和 路径关联起来
let record = { // 最终路径 会匹配到这个记录,里面可以自定义属性等
path,
component: route.component, // 组件
props: route.props || {},
parent
}
pathMap[path] = record;
route.children && route.children.forEach(childRoute => {
addRouteRecord(childRoute, pathMap, record); // 在循环儿子的时候将父路径也同时传入,目的是为了在子路由添加的时候可以拿到父路径
})
}
路由模式
this.mode = options.mode || 'hash';
switch (this.mode) {
case 'hash':
this.history = new Hash(this)
break
case 'history':
this.history = new HTML5History(this);
break
}
// ...
init(app) {
const history = this.history;
// 初始化时,应该先拿到当前路径,进行匹配逻辑
// 让路由系统过度到某个路径
const setupHashListener = () => {
history.setupListener(); // 监听路径变化
}
history.transitionTo( // 父类提供方法负责跳转
history.getCurrentLocation(), // 子类获取对应的路径
// 跳转成功后注册路径监听,为视图更新做准备
setupHashListener
)
}
hash
export default class Hash extends History {
constructor(router) {
super(router);
// hash路由初始化的时候 需要增加一个默认hash值 #/
ensureHash();
}
getCurrentLocation() {
return getHash();
}
setUpListener() {
window.addEventListener('hashchange', () => {
// hash 值变化 再去切换组件 渲染页面
this.transitionTo(getHash());
})
}
}
function ensureHash() {
if (!window.location.hash) {
window.location.hash = '/';
}
}
function getHash() {
return window.location.hash.slice(1);
}
高版本浏览器可以用 popstate 代替 hashchange 事件,性能更好
h5
import History from './base'
export default class HTML5History extends History {
constructor(router) {
super(router);
}
getCurrentLocation() {
return window.location.pathname;// 获取路径
}
setUpListener() {
window.addEventListener('popstate', () => { // 监听前进和后退
this.transitionTo(window.location.pathname);
})
}
pushState(location) {
history.pushState({}, null, location);
}
}
// 路由公共的方法都放在这 大家共用
function createRoute(record, location) { // 创建路由
const matched = [];
// 不停的去父级查找
if (record) {
while (record) {
matched.unshift(record);
record = record.parent;
} // /about/a => [about,aboutA]
}
return {
...location,
matched
}
}
export default class History {
constructor(router) {
this.router = router;
// 有一个数据来保存路径的变化
// 当前没有匹配到记录
this.current = createRoute(null, {
path: '/'
}); // => {path:'/',matched:[]}
}
transitionTo(path, cb) {
// 前端路由的实现原理 离不开hash h5
let record = this.router.match(path); // 匹配到后
this.current = createRoute(record, { path });
// 路径变化 需要渲染组件 响应式原理
// 我们需要将currrent属性变成响应式的,这样后续更改current 就可以渲染组件了
// Vue.util.defineReactive() === defineReactive
// 我可以在router-view组件中使用current属性,如果路径变化就可以更新router-view了
cb && cb(); // 默认第一次cb是hashchange
}
}
init( )时 先跳转路径,然后开始监听路径变化
transitionTo
根据路径进行组件的渲染
transitionTo(path, cb) { // {path:'/',matched:[record]}
// 前端路由的实现原理 离不开hash h5
let record = this.router.match(path); // 匹配到后
let route = createRoute(record, { path });
// 1.保证跳转的路径 和 当前路径一致
// 2.匹配的记录个数 应该和 当前的匹配个数一致 说明是相同路由
if (path === this.current.path && route.matched.length === this.current.matched.length) {
return
}
// 在跳转前 我需要先走对应的钩子
// 修改current _route 实现跳转的
let queue = this.router.beforeHooks;
const iterator = (hook,next) =>{ // 此迭代函数可以拿到对应的hook
hook(route,this.current,next);
}
runQueue(queue,iterator,()=>{
this.updateRoute(route);
cb && cb(); // 默认第一次cb是hashchange
// 后置的钩子
})
// 更新current 需要重新渲染视图
// Vue.util.defineReactive();
// 如果 两次路由一致 不要跳转了
}
createRoute 返回的结果 {path:'about/a',matched:[{...'about' },{...'about/a' }]}
嵌套路由时 要写两层 router-view
才可以,先渲染about
再渲染about/a
需要将 current 属性变化成响应式的,后续 current 变化会更新视图
// vuex中的 state 在哪里使用就会收集对应的 watcher
// current 里面的属性在哪使用,就会收集对应的 watcher
Vue.util.defineReactive(this,'_route',this._router.history.current);
要改变_route需要传回调函数进去,对_route重新赋值
history.listen((route)=>{
// 监听 监听如果current变化了 就重新的给 _route赋值
app._route = route;
})
组件
Object.defineProperty(Vue.prototype,'$router',{ // 方法
get(){
return this._routerRoot._router
}
})
Object.defineProperty(Vue.prototype,'$route',{ // 属性
get(){
return this._routerRoot._route
}
});
Vue.component('router-link',RouterLink)
Vue.component('router-view',RouterView)
router-view
export default {
functional:true,
render(h,{parent,data}){ // current = {matched:[]} .$route // data里面我可以增加点标识
// 内部current变成了响应式的
// 真正用的是$route this.$route = current; current = xxx
let route = parent.$route; // 获取current对象
// 依次的将matched 的结果赋予给每个router-view
// 父 * 父 * -> 父 * -> 子 *
let depth = 0;
while (parent) { // 1.得是组件 <router-view></router-view> <app></app>
if(parent.$vnode && parent.$vnode.data.routerView ){
depth++;
}
parent = parent.$parent; // 不停的找父亲
}
// 两个router-view [ /about /about/a] /about/a
let record = route.matched[depth]; // 默认肯定先渲染第一层
if(!record){
return h() // 空
}
// 渲染匹配到的组件,这里一直渲染的是第一个
data.routerView = true;
return h(record.component, data); // <router-view routeView=true></router-view>
}
}
嵌套路由,record 是数组,通过找有几级父亲及渲染标致,判断该渲染 record 第几条记录
router-link
export default {
functional: true, // 函数式组件, 会导致render函数中没有this了
// 正常组件是一个类 this._init() 如果是函数式组件就是一个普通函数
props: { // 属性校验
to: {
type: String,
required: true
}
},
// render的第二个函数 是内部自己声明一个对象
render(h, { props, slots, data, parent }) { // render 方法和 template 等价的 -> template语法需要被编译成render函数
const click = () => {
// 组件中的$router
parent.$router.push(props.to)
}
// jsx 和 react语法一样 < 开头的表示的是html {} js属性
return <a onClick = { click } > { slots().default } </a>
}
}
钩子
function runQueue(queue,iterator,cb){
function step(index){
if(index >= queue.length) return cb();
let hook = queue[index];
iterator(hook,()=>step(index+1)); // 第二个参数什么时候调用就走下一次的
}
step(0);
}