1 Vue Router 基础使用步骤

1 脚手架搭建 vue 项目,如果未使用 vue-router,再手动安装 vue-router,创建几个路由视图
1610071110(1).jpg
2 新建router 模块

  1. // router/index.js
  2. import Vue from 'vue';
  3. import VueRouter from 'vue-router';
  4. import Index from '../views/Index.vue';
  5. Vue.use(VueRouter);
  6. const routes = [
  7. {
  8. path: '/',
  9. name: 'Index',
  10. component: Index,
  11. },
  12. {
  13. path: '/blog',
  14. name: 'Blog',
  15. component: () =>
  16. import(/** webpackChunkName: "blog" */ '../views/Blog.vue'),
  17. },
  18. {
  19. path: '/photo',
  20. name: 'Photo',
  21. component: () =>
  22. import(/** webpackChunkName: "blog" */ '../views/Photo.vue'),
  23. },
  24. {
  25. path: '/hello',
  26. name: 'HelloWorld',
  27. component: () =>
  28. import(/** webpackChunkName: "blog" */ '../components/HelloWorld.vue'),
  29. },
  30. ];
  31. const router = new VueRouter({
  32. routes,
  33. });
  34. export default router;

3 在模板中添加路由出口

  1. <template>
  2. <div id="app">
  3. <div>
  4. <img alt="Vue logo" src="./assets/logo.png">
  5. </div>
  6. <!-- <HelloWorld msg="Welcome to Your Vue.js App"/> -->
  7. <div id="nav">
  8. <router-link to="/" >Index</router-link>|
  9. <router-link to="/blog" >Blog</router-link>|
  10. <router-link to="/photo" >Photo</router-link>
  11. </div>
  12. <router-view></router-view>
  13. </div>
  14. </template>
  15. <script>
  16. // import HelloWorld from './components/HelloWorld.vue'
  17. export default {
  18. name: 'App',
  19. components: {
  20. // HelloWorld
  21. }
  22. }
  23. </script>
  24. <style>
  25. #app {
  26. font-family: Avenir, Helvetica, Arial, sans-serif;
  27. -webkit-font-smoothing: antialiased;
  28. -moz-osx-font-smoothing: grayscale;
  29. text-align: center;
  30. color: #2c3e50;
  31. margin-top: 60px;
  32. }
  33. </style>

4 vue 实例中添加router

  1. import Vue from 'vue'
  2. import App from './App.vue'
  3. import router from './router'
  4. Vue.config.productionTip = false
  5. var app = new Vue({
  6. router, // 此处引入router,就会在创建实例的时候,添加$router、$route对象,$route是当前路由的数据(规则、路径、参数等)
  7. // $router 路由对象,是router的一个实例,其中提供了一些路由相关的方法,路由对象的信息,以及currentRoute,当前路由的数据
  8. render: h => h(App),
  9. }).$mount('#app')
  10. console.log(app)

2 动态路由

在路由 path 中添加占位符,来传递参数

  1. ...
  2. {
  3. path: '/about/:id',
  4. name: 'About',
  5. props: true, // 开启 props, 会把 URL 中的参数传递给组件 在组件中通过 props 来接受 URL 参数
  6. // 懒加载
  7. component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  8. }
  9. ...

接收参数时两种方式,直接通过 $route 对象,$route.params.id 获取;或者通过在路由中开启 props,此时 URL 中的参数会传递给组件,组件中通过 props 来接收;

  1. <template>
  2. <div class="about">
  3. <h1>This is an about page: {{$route.params.id}}</h1>
  4. <br>
  5. <h1>通过开启 props 获取: {{id}}</h1>
  6. </div>
  7. </template>
  8. <script>
  9. export default {
  10. name: 'About',
  11. props: ['id']
  12. }
  13. </script>

3 嵌套路由
通过在路由中添加 children 实现嵌套路由

  1. ...
  2. {
  3. path: '/',
  4. component: Layout,
  5. children: [
  6. {
  7. name: 'index',
  8. path: '/',
  9. component: Index
  10. },
  11. {
  12. name: 'About',
  13. path: '/about/:id',
  14. props: true,
  15. component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue')
  16. }
  17. ]
  18. },
  19. ...

4 编程式导航
通过方法动态操作 $router 封装的方法

  1. ...
  2. this.$router.push('/')
  3. ...
  4. this.$router.replace('/login')
  5. ...
  6. this.$router.push({name: 'About', params: {id: 1}})
  7. ...
  8. this.$router.go(-2)
  9. ...

5 Hash 模式和 History 模式的区别
Hash 模式是基于锚点,以及 Onhashchange 事件
History 模式是基于 HTML5 中的 History API: history.pushState() (ie10以后才支持) history.replaceState()
6 History 模式
History 模式需要服务器的支持,在服务端应该除了静态资源外都返回单页应用的index.html
在 node + express 的服务中,需要添加 connect-history-api-fallback 模块作为中间件

  1. ...
  2. const history = require('connect-history-api-fallback')
  3. ...
  4. // 注册处理history模式的中间件
  5. app.use(history())
  6. ...

在 nginx 中,需要在 location 中添加 try_file 属性,并配置找不到资源时的路径

  1. ...
  2. server {
  3. listen 80;
  4. server_name localhost;
  5. location / {
  6. root html;
  7. index index.html index.htm;
  8. try_files $uri $uri/ /index.html;
  9. }

7 VueRouter 实现原理
Hash 模式
URL 中 # 后面的内容作为路径地址
监听 hashchange 事件
根据当前路由地址找到对应组件重新渲染
History 模式
通过 history.pushState() 方法改变地址栏
监听 popstate 事件
根据当前路由地址找到对应组件重新渲染
8 VueRouter 模拟实现
1 分析
Vue.use() 方法可以传入对象或者方法,如果传入函数,Vue.use 会首先调用这个函数,如果传入对象的话,会调用这个对象的 install 方法
定义个 VueRouter 类,类中要有 install 的静态方法,其构造函数方法传入的参数是个对象,对象中有个 routes 数组,里面就是路由规则,路径,名称组件等;
新加一个 Vue 实例,在实例的构造函数中传入了一个 VueRouter 实例;
2 实现静态方法 install
首先判断当前插件是否已经被安装;其次,把vue的实例记录到全局变量当中;再次把创建 vue 实例时传入的router 对象注入到 Vue 实例中

  1. let _Vue = null
  2. export default class VueRouter {
  3. static install(Vue) {
  4. // 1. 判断当前插件是否已经被安装
  5. if(VueRouter.install.installed) {
  6. return
  7. }
  8. VueRouter.install.installed = true
  9. // 2. 把 vue 的构造函数记录到全局变量中来
  10. _Vue = Vue
  11. // 3. 把创建 Vue 实例时传入的router 对象注入到 Vue 实例上
  12. // 混入
  13. _Vue.mixin({
  14. beforeCreate() {
  15. if(this.$options.router) {
  16. _Vue.prototype.$router = this.$options.router
  17. }
  18. },
  19. })
  20. }
  21. }

3 实现构造方法

  1. ...
  2. constructor(options) {
  3. this.options = options // 记录构造函数中传入的对象
  4. this.routerMap = {} // 记录构造函数中传入对象的routes(路由规则)解析后的结果,键值对(路由地址=>路由组件)
  5. this.data = _Vue.observable({ // 响应式的对象
  6. current: '/' // 存储当前的路由地址,默认为斜线
  7. })
  8. }
  9. ...

4 createRouterMap

  1. ...
  2. createRouterMap() {
  3. // 遍历所有的路由规则,并解析成键值对,存储到 routerMap 中
  4. this.options.routes.forEach(route => {
  5. this.routerMap[route.path] = route.component
  6. })
  7. }
  8. ...

5 initComponents,创建router-link组件

  1. ...
  2. initComponents(Vue) {
  3. Vue.component('router-link', {
  4. props: {
  5. to: String
  6. },
  7. template:'<a :href="to"><slot></slot></a>'
  8. })
  9. }
  10. ...

6 包装方法并调用

  1. _Vue.mixin({
  2. beforeCreate() {
  3. if(this.$options.router) {
  4. _Vue.prototype.$router = this.$options.router
  5. this.$options.router.init() // 调用init 方法
  6. }
  7. },
  8. })
  9. ...
  10. init() {
  11. this.createRouterMap()
  12. this.initComponents(_Vue)
  13. }
  14. ...

此时用当前自定义的vuerouter 取代原因的 vuerouter,运行,报错 runtime 版本的vue,编译器不支持 template,有两种解决方案,一种通过 配置 vue.config.js,运行完整版本的vue的编译器

  1. // vue.config.js
  2. module.exports = {
  3. runtimeCompiler: true
  4. }

另一种,通过render函数,改写 initComponents 方法

  1. initComponents(Vue) {
  2. Vue.component('router-link', {
  3. name: 'RouterLink',
  4. props: {
  5. to: String
  6. },
  7. // template:'<a :href="to"><slot></slot></a>',
  8. render: function(h) {
  9. return h('a', {
  10. attrs: {
  11. href: this.to
  12. }
  13. }, [this.$slots.default])
  14. }
  15. })
  16. }

7 route-view 组件

  1. ...
  2. const self = this
  3. Vue.component('router-view', {
  4. render(h) {
  5. // 当前路由地址
  6. const component = self.routerMap[self.data.current]
  7. return h(component)
  8. }
  9. })
  10. ...

此时打开浏览器,点击home、about,页面有刷新但是内容没发生变化;页面闪动说明有请求服务器,但是我们希望的是在客户端处理,所以要在router-link里面去阻止浏览器的默认行为,并且点击后调用 pushstate 方法,将地址更新到浏览器历史中,然后更新 $router.data.current,因为data 是个响应式对象,变化以后会自动加载对应的组件并且渲染到视图上

  1. ...
  2. Vue.component('router-link', {
  3. name: 'RouterLink',
  4. props: {
  5. to: String
  6. },
  7. // template:'<a :href="to"><slot></slot></a>',
  8. render(h) {
  9. return h('a', {
  10. attrs: {
  11. href: this.to
  12. },
  13. on: {
  14. click: this.clickHandler
  15. }
  16. }, [this.$slots.default])
  17. },
  18. methods: {
  19. clickHandler (e) {
  20. // 调用 pushstate 方法改变地址栏的地址
  21. history.pushState({},'',this.to)
  22. // 更换视图
  23. this.$router.data.current = this.to
  24. // 阻止浏览器的默认行为
  25. e.preventDefault();
  26. }
  27. },
  28. })
  29. ...

到此为止,点击router-link后切换视图并且添加地址到浏览器历史中已经实现,但是在用浏览器的前进后退按钮时,视图并未发生变化,此时就要去用popstate方法监听浏览器的变化

  1. initEvent() {
  2. window.addEventListener('popstate', () => {
  3. this.data.current = window.location.pathname
  4. })
  5. }