整理一些重点:
  • 组件化和MVVM
  • 响应式原理
  • vdom和diff算法
  • 模板编译
  • 组件渲染过程
  • 前端路由

一、组件化基础

在组件化基础上,实现了数据驱动视图(MVVM)
传统组件只是静态渲染,更新还要依赖于操作DOM
而MVVM——不用操作DOM 直接修改数据就能够改变视图 
结合代码分析view如何修改model
整理一下自己的理解:
组件化是一个较早之前就有的理念,但传统的组件化,只是做静态渲染,更新依赖于直接操作DOM
而Vue在组件化的基础上,加入了创新——数据驱动视图MVVM
通过MVVM,我们不再需要直接操控DOM元素,而是来操作model(也就是data),通过指令来更新View视图层,相应的,视图层的DOM也可以通过聆听来改变model数据

二、Vue响应式

重点!!!
组件data的数据一旦变化,立刻触发视图的更新,这也是时间数据驱动试图的第一步

核心API-Object.defineProperty

但Vue3.0启用了Proxy来启动响应式了(因为Object.defineProperty存在一些缺点)

基本使用:

Object.defineProperty() 方***直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
其语法:
Object.defineProperty(obj,prop,descriptor)
obj——要定义属性的对象  prop——要定义或修改的属性的名称或 Symbol   descriptor——要定义或修改的属性描述符
const data={}
const name='zhangsan'
  Object.defineProperty(data, "name", {
    get: function() {
      console.log('get!');
      return name
    },
    set: function(newVal) {
      console.log('set!');
         name=newVal;
    }
  });

  //通过以上代码 就可以get 和 set data中的name属性了
基于Object.defineProperty这个函数,写最简单的监听函数:
// 触发更新视图
function updateView() {
    console.log('视图更新')
}
// 核心监听函数
function defineReactive(target, key, value) {
    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                value = newValue
                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 如果传进来的不是对象或数组 就直接返回
        return target
    }
    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
}
 监听数据
observer(data)

// 测试
 data.name = 'lisi'//相当于调用了set函数 触发了更新视图函数updateView()
 data.age = 21

深度监听:

但上述函数无法深度监听 我们只能监听到key这一层 
// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // value很可能是个对象 之前普通监听没有value的判断 所以调用observer 可以进行判断
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 设置newVal的时候可能设置了一个对象做新值 也需要深度监听
                observer(newValue)

           // 设置新值
           // 注意,value一直在闭包中,此处设置完之后,再get时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    }
}

// 监听数据
observer(data)

// 测试
data.name = 'lisi'
data.age = 21
data.info.address = '上海' // 深度监听
说白了,我们做深度监听的时候,一定要保证,每个key 都被Object.defineProperty挂上了value 这样key值发生set时才能被监听到
但有此时还有很多缺点:
  • 深度监听递归到底,一次性计算量大,可能会导致卡死
  • 新增属性和删除属性时无法监听(Vue.set Vue.delete)
  • 无法原生监听数组 需要特殊梳理
监听数组:
// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建一个空新对象,原型指向oldArrayProperty,对arrProto扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
//一次性扩展常用的方法
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
                //去调用真正的数组原型方法 使用真的方法
        //等同于 Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
       // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 如果是数组
    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
data.nums.push(4) // 监听数组
代码很长,我把最核心的改动部分再单独拿出来一下:
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建一个空新对象,原型指向oldArrayProperty,对arrProto扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
//一次性扩展常用的方法
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
                //去调用真正的数组原型方法 使用真的方法
        //等同于 Array.prototype.push.call(this, ...arguments)
    }
})
重点思路就是这一块:要重新定义一个数组原型,不能直接使用数组原型,会污染全局数据。

三、虚拟DOM(virtual  DOM)

VDOM是实现VUE的基础,diff就是VDOM的最核心、最关键的部分。
VDOM就是用JS来模拟DOM结构,因为JS执行速度很快,这样可以计算出最小的变更,来操作DOM

用JS模拟DOM结构

传统JS结构:

用JS模拟DOM:主要就是 tag props children 三大元素 
注:class-classname
以上代码通过JS实现了vnode 但这只是一个节点 还不是咱们需要的VDOM
VDOM可以进行新旧vnode的对比,得出最小的更新范围,最后更新DOM
学习VDOM可以先了解snabbdomhttps://github.com/snabbdom/snabbdom:h函数 vnode数据结构 patch函数
vue就是基础snabbdom实现的VDOM 它是一个简介强大的vdom库 非常简单

**diff算法

前文提到了,VDOM可以进行新旧vnode的对比,得出最小的更新范围,最后更新DOM
这个更新过程就是diff算法 

diff算法概述

先来了解一下diff算法概述

原本树diff的时间复杂度非常高  达到了O(n^3) 
因此,对diff进行了优化,时间复杂度优化到了O(n)
  • 只比较同一层级
  • 比较tag 若tag不同 就不比较了 直接删掉重建拉到
  • tag和key都相同,则认为是相同节点,也不再做深度比较



要记住:diff的作用,就是以代价最小的方式来更新DOM ,所以会尽可能的复用DOM。

vue的VDOM是在snabbdom的基础上实现的,下面就是品味snabbdom的源码,领会这个过程:
h函数:
h函数就是vue中的createElement方法,这个函数作用就是创建虚拟dom,追踪dom变化的 它返回的就是Vnode
说白了 就是生成vnode的函数
接受三个参数:
  •  tag(标签名)、组件的选项对象、函数(必选);
  •  一个对象,标签的属性对应的数据(可选);
  •   子级虚拟节点,字符串形式或数组形式,子级虚拟节点也需要使用createElement构建。

h函数返回的也是一个函数

vnode函数返回的也是一个vnode结构的对象


patch函数:

patch就是实现虚拟DOM渲染的这个方法
通过patch可以进行新旧两个vnode的差异对比,但这只是手段,不是目的,它的最终目的还是渲染视图
patch函数有两种用法:
patch(container,vnode)

patch(vnode,newVnode)
  • 首次渲染,vnode把结构全部塞给container里面去
  • 当数据改变时,第一个参数vnode是之前的结构,newVnode是改变后的结构,首先这两者先进行对比,找出不同的地方,只渲染不相同的地方,减少DOM的操作。
前文中一直强调的diff算法,就是patch在进行vnode对比时使用的算法,也就是我们学习的重点
看一下源码,发现patch是由.init函数返回的:

去找这个snabbdom.init函数:

发现其返回结果就是一个patch函数:

这个patch函数 也就是我们要重点分析 接下来通过分析源码 领会这个patch函数:
  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    // 执行 pre hook
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

    // 第一个参数不是 vnode
    if (!isVnode(oldVnode)) {
      // 创建一个空的 vnode ,关联到这个 DOM 元素
      oldVnode = emptyNodeAt(oldVnode);
    }

    // 相同的 vnode(key 和 sel 都相等) 进行patchVnode对比
    if (sameVnode(oldVnode, vnode)) {
      // vnode 对比
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    
    // 不同的 vnode ,直接删掉重建
    } else {
      elm = oldVnode.elm!;
      parent = api.parentNode(elm);

      // 重建
      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }
那么如何对比呢? 首先进行sameVnode的判断  如果都相等 就进行patchVnode的对比
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  // key 和 sel 都相等
  // undefined === undefined // true
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
 function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // 执行 prepatch hook 就相当于一个生命周期钩子
    const hook = vnode.data?.hook;
    hook?.prepatch?.(oldVnode, vnode);

    // 设置新的vnode.elem 因为此时新的vnode是断线风筝 不知道更新哪个element
    const elm = vnode.elm = oldVnode.elm!;
  
    // 旧 children
    let oldCh = oldVnode.children as VNode[];
    // 新 children
    let ch = vnode.children as VNode[];

    if (oldVnode === vnode) return;
  
    // hook 相关 先不看
    if (vnode.data !== undefined) {
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      vnode.data.hook?.update?.(oldVnode, vnode);
    }

    // vnode.text === undefined (意味着一般情况下 vnode.children有值)
    if (isUndef(vnode.text)) {
      // 新旧都有children
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
      // 新children有,旧children无 (旧text有)
      } else if (isDef(ch)) {
        // 清空 text
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        // 添加 children
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      // 旧child有,新child无
      } else if (isDef(oldCh)) {
        // 移除 children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      // 旧 text 有 新text无
      } else if (isDef(oldVnode.text)) {
               //清空text 
        api.setTextContent(elm, '');
      }


    // else: vnode.text有值(也就是vnode.children无值)
       //新旧text值不相等
    } else if (oldVnode.text !== vnode.text) {
      // 移除旧 children
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      }
      // 设置新 text
      api.setTextContent(elm, vnode.text!);
    }
    hook?.postpatch?.(oldVnode, vnode);
  }


其中重点 就是新旧都是children时 进行updateChildren


指针向中间移动,当两个指针重合的时候,循环结束
那么如何对比呢? 
         // 开始和开始对比
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      
      // 结束和结束对比
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];

      // 开始和结束对比
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];

      // 结束和开始对比
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
如果sameVnode 就命中啦!! (这种对比方式不要较真 因为这是细节问题 可能不一样)
如果以上都没命中
 // 以上四个都未命中
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 拿当前新节点的key 看它能否对应上oldCh中的某个节点的key
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
  
        // 没对应上 就新建 插入
        if (isUndef(idxInOld)) { // 
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          newStartVnode = newCh[++newStartIdx];
        
        // 对应上了
        } else {
          // 拿到oldCh中对应上 key 的节点
          elmToMove = oldCh[idxInOld];

          // 判断sel 是否相等(sameVnode 的条件)sel不相等 就新建
          if (elmToMove.sel !== newStartVnode.sel) {
            // New element
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          
          // sel 相等,key 相等
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
          newStartVnode = newCh[++newStartIdx];
        }
      }

如果不使用key
如果使用index作为key的话,当children有顺序上的变化时,就不好判断了

一个小结:

判断是否为sameVnode
就是看key和tag是否相同
相同的话 进行patchVnode 进一步比较
不同的话 直接替换成新的 没有比较必要
patchVnode
只有文本不一样 修改文本
旧的有子节点 新的没有子节点 删除子节点  removeVnodes
旧的没有子节点 新的有子节点 新增子节点 addVnodes
新的旧的都有子节点 且子节点不相同 进行updateChildren
updateChildren
新旧两组子节点 首尾都置指针
比较过程参考上面复制的那篇文章

三、模板编译

模板是vue中最常用的部分,看起来很像html 但又不是html 浏览器无法识别 他有指令有插值有JS表达式  还能做循环和片段
html就是一个标签语言 只有JS才能够实现这些逻辑
因此,模板一定是转成了某些JS代码

先要学习JS的with语法
(并不常用,但是模板编译需要用到)

也就是改变{ }内自由变量的查找规则 当作obj的属性来查找 查不到匹配的obj属性就会报错 他打破了作用域的规则

在with的基础上 我们来学习 vue模板究竟被编译成了什么?

模板编译

with(this)就相当于h函数,返回了一个vnode
this就是vue实例
当只有一个插值时:
// 插值
 const template = `<p>{{message}}</p>`
// 输出结果:with(this){return _c('p',[_v(_s(message))])}
//相当于 with(this){return createElement('p',[createTextVNode(toString(message))])}

当表达式时:
/ 表达式
 const template = `<p>{{flag ? message : 'no message found'}}</p>`
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
属性和动态属性时
// 属性和动态属性
 const template = `
     <div id="div1" class="container">
         <img :src="imgUrl"/>
     </div> `
// with(this){return _c('div',
//      {staticClass:"container",attrs:{"id":"div1"}},
//      [
//          _c('img',{attrs:{"src":imgUrl}})])}
条件 把条件判断写成了一个三元表达式
 const template = `
    <div>
         <p v-if="flag === 'a'">A</p>
         <p v-else>B</p>
     </div>/ `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

剩下的累了 直接一起看吧
// 循环
 const template = `
     <ul>
       <li v-for="item in list" :key="item.id">{{item.title}}</li>
     </ul>
 `
// with(this){return _c('ul',_l((list),
 //function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}

// 事件
 const template = `
     <button @click="clickHandler">submit</button>
 `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
//with(this){return _c('input',
//{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],
//attrs:{"type":"text"},domProps:{"value":(name)},
//on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
//我们在渲染时 已经绑定了一个input事件

// render 函数
// 返回 vnode
// patch

// 编译
const res = compiler.compile(template)
console.log(res.render)

// ---------------分割线--------------

// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
//     target._o = markOnce;
//     target._n = toNumber;
//     target._s = toString;
//     target._l = renderList;
//     target._t = renderSlot;
//     target._q = looseEqual;
//     target._i = looseIndexOf;
//     target._m = renderStatic;
//     target._f = resolveFilter;
//     target._k = checkKeyCodes;
//     target._b = bindObjectProps;
//     target._v = createTextVNode;
//     target._e = createEmptyVNode;
//     target._u = resolveScopedSlots;
//     target._g = bindObjectListeners;
//     target._d = bindDynamicKeys;
//     target._p = prependModifier;
// }
以上那些生成的类似vnode函数,都被叫做render函数 会返回一个vnode
模板编译过程说白了就
  • 将模板编译为render函数,执行render函数返回vnode
  • 在生成vnode后,再执行patch和diff
  • 实际项目中,使用webpack vue-loader 会在开发环境下编译模板
在vue组件中,可以使用render代替template 

很显然 template更好
但React一直都用的render

四、渲染更新

最后一部分来了!!!!
分为三个部分
  • 初次渲染过程
  • 更新过程
  • 异步渲染

初次渲染过程

  • 解析模板为render函数(或在开发环境中已经完成 vue-loader)
  • 触发响应式 监听data属性 getter setter
  • 执行render函数 生成vnode 执行patch(elem,vnode)

第二步看起来没有用,但其实不然,在执行render函数 就会触发getter

更新过程

  • 修改data 触发stter  但要看一下这个触发了setter的函数是否在之前的getter中已经监听 (说白了 就是看这个修改了的data模板有没有用到,和视图有没有关系,如果是像上图代码中的city 修改了也没事 它和视图无关啊!不用重新渲染)
  • 重新执行render 生成newVnode
  • patch(vnode,newVnode)
品一品这个流程图

*异步渲染

非常非常重要!和$nextTick结合着看
异步渲染会汇总data的修改,一次性更新视图,从而减少DOM操作次数,提高性能

小结:

VUE高级原理最重要的就是响应式、模板编译、VDOM、渲染这四大块。他们是息息相关的
要牢牢记住:
  • 渲染和响应式的关系
  • 渲染和模板编译的关系
  • 渲染和VDOM的关系


五、路由

路由原理

回顾vue-router的路由模式
hash 
H5 history
首先分析一下网页url组成部分

hash方法

  • hash变化会触发网页跳转 即浏览器的前进后退
  • hash变化不会刷新页面 这也是SPA必须的特点
  • hash永远不会提交到server端 在前端自生自灭
hash的变化有三种方式:
  •          JS 修改 url
  •          手动修改 url 的 hash
  •         浏览器前进、后退

代码演示:
通过JS设置hash
通过window.onhashchange这个API监听hash变化
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>hash test</title>
</head>
<body>
    <p>hash test</p>
    <button id="btn1">修改 hash</button>

    <script>
        // hash 变化,包括:
        // a. JS 修改 url
        // b. 手动修改 url 的 hash
        // c. 浏览器前进、后退
        window.onhashchange = (event) => {
            console.log('old url', event.oldURL)
            console.log('new url', event.newURL)

            console.log('hash:', location.hash)
        }

        // 页面初次加载,获取 hash
        document.addEventListener('DOMContentLoaded', () => {
            console.log('hash:', location.hash)
        })

        // JS 修改 url
        document.getElementById('btn1').addEventListener('click', () => {
            location.href = '#/user'
                 
        })
    </script>
</body>
</html>

H5 history

  • 用url规范的路由,但是跳转时不刷新页面(与hasn不一样)
  • 通过history.pushState window.onpopstate实现
  • 需要server端的配合 

正常页面浏览时:

H5 history

代码演示:
重点是切换路由的API
history.pushState(state, '', 'page1')
以及 监听路由变化的API
window.onpopstate = (event) => { // 重要!!
            console.log('onpopstate', event.state, location.pathname)
        }
整体代码:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>history API test</title>
</head>
<body>
    <p>history API test</p>
    <button id="btn1">修改 url</button>

    <script>
        // 页面初次加载,获取 path
        document.addEventListener('DOMContentLoaded', () => {
            console.log('load', location.pathname)
        })

        // 打开一个新的路由
        // 【注意】用 pushState 方式,浏览器不会刷新页面
        document.getElementById('btn1').addEventListener('click', () => {
            const state = { name: 'page1' }
            console.log('切换路由到', 'page1')
            history.pushState(state, '', 'page1') // 重要!!
        })

        // 监听浏览器前进、后退
        window.onpopstate = (event) => { // 重要!!
            console.log('onpopstate', event.state, location.pathname)
        }

        // 需要 server 端配合,可参考
        // https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
    </script>
</body>
</html>

需要后端的配合 不管访问什么路由 都返回一个index.html的路由 再从这个index.html页面,通过history.pushState跳转到需要的路由

选择:
to B(后台)的系统推荐用hash 简单易用
to C的系统,可以考虑H5 history
还是hash更简单