路由及vue-router - 04

281 阅读7分钟

Vue 路由及异步组件

背景

后端路由 koa express

前后端开发未分离的时候,路由基本上都是由服务端控制

前端/客户端 发起http 请求 -> 服务端 -> 根据url 路径去匹配不同的路由 / 返回不同的数据(Restful接口)

优点:

  • 类似于现在的ssr,直接返回一个html,渲染了页面结构
  • seo 的效果好,首屏时间耗时短

首屏时间:浏览器地址栏输入一个url 开始 -> 页面上任意元素展示出来(白屏时间)

缺点: 前端和后端代码融合在一起,开发协同很乱,构建html的过程放在服务器上

现代前端:打包 -> webpack上传到oss或者cdn,对外直接暴露个地址,通过 nginx 代理到html上

单页应用

SPA

页 -> html 单页 -> 只有一个html文件

多页面彼此之间数据不能交互

多页 -> 一个团队维护多个业务,业务又存在一些共同点没办法拆成多个项目 a -> a.html 单页应用 b -> b.html 单页应用

特点:

  1. 页面中的交互是不刷新页面的,比如从 /home -> /about,并不会刷新页面,比如点击按钮;内存中的变量都可以访问的
  2. 加载过的公共资源无需重复加载。除了首页之外的其他页面,都通过异步组件的方式注册(只有在访问其他页面的时候才会加载资源)

前端路由 router 原理 及其表现

vue -> hash, history

react -> hash, history

底层都会依托浏览器提供的API

特点:

  1. 页面间的交互不会刷新页面
  2. 不同 url/路径/路由 会渲染不同的内容

面试:hash 和 history 的区别?

  1. hash 有 #,history 没有
  2. hash 的 # 部分内容不会传给服务端,history的所有url内容服务端都可以获取到
  3. history 路由,应用在部署时,需要注意 html 文件的访问:

单页应用,只有一个index.html 用history 路由/a /b /c 都会访问index.html 文件,用户在输入index.html 之内的路径,无论那个路径,都指向 index.html

hash 路由在 html 文件后 有index.html#a, index.html#b,本身就是一个文件,不需要用nginx 指向index.html

  1. 监听变化:hash 通过 hashchange 监听,history 是通过 popstate 监听变化

Hash

特性

  1. url 中带有一个#,# 知识浏览器/客户端的状态,不会传给服务端

www.baidu.com/#/user -> 发起 http 请求,-> 服务端只能拿到 www.baidu.com www.baidu.com/#/list/deta… -> 发起 http 请求,-> 服务端只能拿到 www.baidu.com

  1. hash 值的改变,不会导致页面的刷新
  location.hash = '#aaa'
  location.hash = '#bbb'

页面不刷新,url会变

  1. hash 值的更改,会在浏览器访问历史中添加一条记录。可以通过浏览器的返回、前进按钮 来控制 hash 的切换(其实 返回,就是上一条hash 记录,同理 前进)

  2. hash 值的更改,会触发hashchange时间

监听:

  location.hash = '#aaa'
  location.hash = '#bbb'

  window.addEventListener('hashchange', () => {})
  1. 如何更改hash

5.1 location.hash = '#aaa'

5.2 点击跳转到user

History (nginx 部署)

特性

  1. hash有个符号,不美观,服务端无法接收到hash部分
  window.history.back() // 后退
  window.history.forward() // 前进
  window.history.go(-3) // 正数前进,负数后退
  window.history.pushState() // 相当于location.href(会刷新页面,但是history不会) 页面的浏览记录里会添加一个历史记录
  window.history.replaceState() //相当于location.replace(会刷新页面,但是history不会) 页面的浏览记录里会替换当前的历史记录

pushState / replaceState() 的参数

window.history.pushState(null,'new',path)

  1. state, 是一个对象,是一个与指定网址相关的对象
  2. title, 新的页面地址
  3. url, 页面的新地址

面试题

pushState 时候,会触发popState 事件吗?

pushState: 往页面里推入一个新的页面栈,新的历史记录 popState: 监听页面的弹出等事件

  1. pushState/replaceState 都不会触发popState 事件,需要手动触发页面的重新渲染。 --> 这里和hash 不同,hash 只要改变里 就会触发 hashchange

popState 什么时候才会触发?

  1. 点击浏览器后退按钮
  2. 点击浏览器前进按钮
  3. js back
  4. js forward
  5. js go

history 想要部署的时候,如何配置 nginx?

nginx 配置

  1. index.html 文件存在服务器本地 -> 如 nginx 部署在 a 服务器,以及 index.html 也在 a 服务器

如要访问 www.lubai.com/main/

配置 nginx

location /main/ {
  try_files $uri $uri/ /home/dist/index.html
}
// 当要访问 main 时,会去查找 当前目录下的 index.html 文件

此方法 无论是访问www.lubai.com/main/a 还是 www.lubai.com/main/b 都会指向index.html

如要访问 www.lubai.com/ 根路由

配置 nginx

location / {
  try_files $uri $uri/ /home/dist/index.html
}
  1. index.html 存在于远程地址,oss/cdn 如 nginx 配置在a 服务器,index.html 被传到了cdn 上

如要访问 www.lubai.com/main/a/

cdn: www.lubai-cdn.com/file/index.…

location /main/ {
  rewrite ^ /file/index.html break; // 重写访问到的文件全部写成/file/index.html
  proxy_pass https://www.lubai-cdn.com; //代理访问
}

mock hash router

实现不同路由不同颜色

  <div class="container">
      <a href="#gray">灰色</a>
      <a href="#green">绿色</a>
      <a href="#">白色</a>
      <button onclick="window.history.go(-1)">返回</button>
  </div>

注册路由:实例.route('/', function() {})

构造函数用:

  1. 来存储path及callback的对应关系

  2. 页面加载完毕后首次会触发路由!! => window.addEventListener('load', this.refresh) 渲染当前路径

  3. hash 路由需要用hashchange 监听改变,调用刷新

  4. this 指向改变

  class BaseRouter {
    constructor() {
      this.routes = {}; // 存储path及callback的对应关系
      this.refresh = this.refresh.bind(this);
      window.addEventListener('load', this.refresh);
      window.addEventListener('hashchange', this.refresh);
    }

    route(path, callback) {
      this.routes[path] = callback || function () {}
    }


    // 渲染当前路径对应的操作
    refresh() {

    }
}

route 方法:存储 path 和 callback

refresh 方法

  1. 拿到当前路径的hash
  const path = `/${location.hash.slice(1) || ''}` // slice(1) 去掉 #
  1. 执行path
  this.routes[path]()

整体代码

class BaseRouter {
  constructor() {
      this.routes = {}; // 存储path以及callback的对应关系
      this.refresh = this.refresh.bind(this);
      // window.addEventListener('load', this.refresh); // 处理页面首次加载
      window.addEventListener('hashchange', this.refresh); // 处理页面hash的变化
  }

  /**
   * route
   * @param {string} path 路由路径
   * @param {function} callback 回调函数
   */
  route(path, callback) {
      // 向this.routes存储path以及callback的对应关系
      this.routes[path] = callback || function () {};
  }

  refresh() {
      // 刷新页面
      const path = `/${location.hash.slice(1) || ''}`;
      console.log(location.hash);
      this.routes[path]();
  }

}

// 期望看到的是,点击三个不同的a标签,页面的背景色会随之改变

const body = document.querySelector('body');

function changeBgColor(color) {
  body.style.backgroundColor = color;
}


const Router = new BaseRouter();

Router.route('/', function () {
  changeBgColor('white');
});
Router.route('/green', function () {
  changeBgColor('green');
});
Router.route('/gray', function () {
  changeBgColor('gray');
});

注意事项:

  1. 如果一个路径的改变会有多个callback,那么存储的时候:
  this.routes = {
    key: array
  }
  1. 监听load事件后refresh,在green 路由中刷新后,还是白色,与路由不符

mock history router

实现不同路由不同颜色

  <div class="container">
      <a href="/gray">灰色</a>
      <a href="/green">绿色</a>
      <a href="/">白色</a>
      <button onclick="window.history.go(-1)">返回</button>
  </div>

history 路由点击/grey 会直接无法访问 index.html 因为 点击 a标签 相当于 执行location.href = '/grey'

image.png

因此要在容器父元素.container 中阻止location.href 默认行为,而执行我们自己的router行为

container.addEventListener('click', e => {
  if (e.target.tagName === 'A') {
    // 阻止默认 location.href 行为
    e.preventDefault();
    // 执行我们的go 方法
    Router.go(e.target.getAttribute('href')); //Router.go('/green')
  }
})

构造函数:

route方法:

class BaseRouter {
  constructor() {
    this.routes = {};
  }

  route(path, callback) {
    this.routes[path] =  callback || function () {};
  }

不同:监听popState事件

bindPopStat() {
    window.addEventListener('popstate', e => {
      const path = e.state && e.state.path;
      this.routes[path] && this.routes[path];
    })
  }

go 方法:

go(path) {
    window.history.pushState({
      path
    }, null, path);
    // pushState 时候不会触发popState,因此需要手动执行
    const cb = this.routes[path];
    if (cb) {
      cb();
    }
  }

同样需要考虑初始情况

constructor() {
    this.routes = {};
    this.init(location.pathname);
    this.bindPopState();
  }

init(path) {
  window.history.replaceState({
    path
  }, null, path);
  // 执行一下回调
  const cb = this.routes[path];
    if (cb) {
      cb();
    }
}

封装后整体代码

class BaseRouter {
  constructor() {
    this.routes = {};
    this.init(location.pathname);
    this.bindPopState();
  }

  init(path) {
    window.history.replaceState({
      path
    }, null, path);
    // 执行一下回调
    // const cb = this.routes[path];
    //   if (cb) {
    //     cb();
    //   }
    this.invokePathCallBack(path);
  }

  route(path, callback) {
    this.routes[path] =  callback || function () {};
  }

  go(path) {
    window.history.pushState({
      path
    }, null, path);
    // const cb = this.routes[path];
    // if (cb) {
    //   cb();
    // }
    // 封装
    this.invokePathCallBack(path);
  }

  invokePathCallBack(path) {
    const cb = this.routes[path];
    if (cb) {
      cb();
    }
  }

  // 初始化后及监听popState事件
  bindPopState() {
    window.addEventListener('popstate', e => {
      const path = e.state && e.state.path;
      // this.routes[path] && this.routes[path];
      this.invokePathCallBack(path);
    })
  }
}

const body = document.querySelector('body');
const container = document.querySelector('.container');

function changeBgColor(color) {
  body.style.backgroundColor = color;
}


const Router = new BaseRouter();

Router.route('/', function () {
  changeBgColor('white');
});
Router.route('/green', function () {
  changeBgColor('green');
});
Router.route('/gray', function () {
  changeBgColor('gray');
});

container.addEventListener('click', e => {
  if (e.target.tagName === 'A') {
    // 阻止默认 location.href 行为
    e.preventDefault();
    // 执行我们的go 方法
    Router.go(e.target.getAttribute('href')); //Router.go('/green')
  }
})

vue-router

main.js 中引入一个 router 并挂载在实例上

// main.js
import router from "./router/scroll";

// 把整个vue实例挂在到#app元素上
new Vue({
    router,
    store,
    render: h => h(App)
}).$mount("#app");

//index.js
// 通过use方法,引入vueRouter插件,进而使用vueRouter
Vue.use(VueRouter);

路由配置文件, 可以配置多个

// 路由匹配的顺序是由上而下
const routes: Array<RouteConfig> = [
  {
      path: "/", // 路径
      name: "Home", // 名称
      component: Home // 路径对应的组件/页面
  },
  {
      path: "/about",
      name: "About",
      component: () =>
          import(/* webpackChunkName: "about" */ "../views/About.vue")
  },
  {
      path: "*", // 这个路由可以匹配所有的路径 /list
      name: "ErrorPage",
      component: () =>
          import(/* webpackChunkName: "errorPage" */ "../views/ErrorPage.vue")
  }
];

Home 路径直接import 导入,但是about路径,component 为一个函数,指向导入一个views/About.vue 组件

在访问home组件时,只有app.js 以及chunk-vendors.js 用来存资源包,并没要引入 about 路由的文件

当访问 /about 路由时,会有 about.js

因此,初次加载只会通过 import 方式引入主页组件,当访问/about 组件时,通过component: () => import("../views/ErrorPage.vue") 异步引入相关资源文件,=> 大大缩短了首屏时间

path: "*", 通配符兜底所有没有的路由,返回404

路由守卫执行问题: 新建 routeguard.ts,引入

  1. 导航守卫执行前 router.beforeEach 三个参数 (去哪,来自于,下一个)
// 全局的导航守卫
router.beforeEach((to, from, next) => {
    console.log(`Router.beforeEach => from=${from.path}, to=${to.path}`);
    document.title = to.meta.title || "默认标题";
    // 执行下一个路由导航
    next();
});

这里的 to.meta.title 可以在路由的 item 里定义 ``js { path: "/", name: "Home", component: Home, meta: { title: "首页" } }, { path: "/about", name: "About", component: AboutComponent, meta: { title: "关于" } },


2. 导航守卫执行后
```js
router.afterEach((to, from) => {
    console.log(`Router.afterEach => from=${from.path}, to=${to.path}`);
});

单个路由的导航守卫,可以放在路由的 item 里

{
  path: "/test",
  name: "Test",
  component: TestComponent,
  meta: {
      title: "测试导航守卫"
  },
  children: [
      {
          path: "",
          component: TestComponent
      },
      {
          path: ":id",
          component: TestComponent
      }
  ],
  beforeEnter: (to, from, next) => {
      // 配置数组里针对单个路由的导航守卫
      console.log(
          `TestComponent route config beforeEnter => from=${from.path}, to=${to.path}`
      );
      next();
  }
}

单个路由的导航守卫,可以放在组件里:使用@Component 装饰器

// Test.vue
@Component({
  components: {},
  beforeRouteEnter: (to, from, next) => {
      // 这里无法通过this来获取页面各种属性和方法
      console.log(
          `TestComponent beforeRouteEnter => from=${from.path}, to=${to.path}`
      );
      next();
  },
  beforeRouteUpdate: (to, from, next) => {
      // /test/1 ->? /test/2
      // 此行为会触发beforeRouteUpdate
      console.log(
          `TestComponent beforeRouteUpdate => from=${from.path}, to=${to.path}`
      );
      next();
  },
  beforeRouteLeave: (to, from, next) => {
      console.log(
          `TestComponent beforeRouteLeave => from=${from.path}, to=${to.path}`
      );
      next();
  }
})

导航守卫执行顺序

  1. 【组件】前一个组件的 beforeRouteLeave
  2. 【全局】的 router.beforeEach
  3. 【组件】如果有路由参数变化, /test => /test/1,触发 beforeRouteUpdate
  4. 【配置文件】里, 下一个的 beforeEnter
  5. 【组件】内部声明的 beforeRouteEnter
  6. 【全局】的 router.afterEach

一些参数获取

如在 item 中获取 id

{
  // router/routeguard.js
  path: "/dynamic",
  name: "Dynamic",
  component: DynamicComponent,
  meta: {
      title: "动态传参"
  },
  children: [
      {
          path: "",
          component: DynamicComponent
      },
      {
          path: ":id",
          component: DynamicComponent
      },
      {
          path: ":id/:name",
          component: DynamicComponent
      }
  ]
},

在组件中获取

export default class Test extends Vue {
  get id() {
    return this.$route.params.id;
  }
}

一般监听路由变化在组件中监听

// App.vue
export default class Index extends Vue {
  @Watch("$route")
  handler(to: Route, from: Route) {
      console.log(from.path);
      console.log(to.path);
  }
}

路由动画效果

使用 包裹

<transition name="fade">
    <router-view class="child-view"></router-view>
</transition>
.fade-enter-active,
.fade-leave-active {
    transition: opacity 0.75s ease;
}
.fade-enter,
.fade-leave-active {
    opacity: 0;
}
.child-view {
    position: absolute;
    transition: all 0.75s cubic-bezier(0.55, 0, 0.1, 1);
}

scrollBehavior 滚动条

// List.vue
<div class="list">
  <h1>This is an list page</h1>
  <div v-for="item in list" :key="item" class="item">
      <router-link :to="item">{{ item }}</router-link>
  </div>
</div>

// Detail.vue
<div>
  <h1>{{ id }}</h1>
  <router-link to="/list">返回列表</router-link>
</div>
// List.vue
export default class List extends Vue {
  get list() {
      return new Array(100).fill("").map((item, index) => `/detail/${index}`);
  }
}

// Detail.vue
export default class Detail extends Vue {
  get id() {
      return this.$route.params.id;
  }
}

Router 注册

// scroll.js
{
  path: "/detail/:id",
  name: "Detail",
  component: DetailComponent
},
{
  path: "/list",
  name: "List",
  component: ListComponent
},

image.png

需求:用户访问某详情页,之后回到主页滚动位置

VueRouter 对象中有个属性:scrollBehavior 可以实现

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
  scrollBehavior: (to, from, savedPosition) => {
      console.log(savedPosition); // 已报错的位置信息
      return savedPosition;
  }
});

返回列表页的问题:

  1. 手动点击浏览器返回或者前进按钮 记住滚动条的位置,基于history,go,back,forward
  2. router-link,并没有记住滚动条的位置