首頁 > 軟體

Vue.js 前端路由和非同步元件介紹

2022-09-12 18:00:24

文章目標

P6

  • 針對 react/vue 能夠根據業務需求口噴 router 的關鍵設定,包括但不限於:路由的匹配規則、路由守衛、路由分層等;
  • 能夠描述清楚 history 的主要模式,知道 history 和 router 的邊界;

P6+ ~ P7

  • 在沒有路由的情況下,也可以根據也無需求,實現一個簡單的路由;
  • 讀過 router 底層的原始碼,不要求每行都讀,可以口噴關鍵程式碼即可;

一、背景

遠古時期,當時前後端還是不分離的,路由全部都是由伺服器端控制的,前端程式碼和伺服器端程式碼過度融合在一起。

使用者端 --> 前端發起 http 請求 --> 伺服器端 --> url 路徑去匹配不同的路由 --> 返回不同的資料。

這種方式的缺點和優點都非常明顯:

  • 優點:因為直接返回一個 html,渲染了頁面結構。SEO 的效果非常好,首屏時間特別快;
    • 在瀏覽器輸入一個 url 開始到頁面任意元素載入出來/渲染出來 --> 首屏時間;
  • 缺點:前端程式碼和伺服器端程式碼過度融合在一起,開發協同非常的亂。伺服器壓力大,因為把構建 html 的工作放在的伺服器端;

後來 ...隨之 ajax 的流行,非同步資料請求可以在瀏覽器不重新整理的情況下進行。

後來 ...出現了更高階的體驗 —— 單頁應用

  •  --> HTML 檔案
  • 單頁 --> 單個 HTML 檔案

在單頁應用中,不僅在頁面中的互動是不重新整理頁面的,就連頁面跳轉也都是不重新整理頁面的。

單頁應用的特點:

  • 頁面中的互動是不重新整理的頁面的,比如點選按鈕,比如點選出現一個彈窗;
  • 多個頁面間的互動,不需要重新整理頁面(a/b/ca-> b -> c);載入過的公共資源,無需再重複載入;

而支援起單頁應用這種特性的,就是 前端路由

二、前端路由特性

前端路由的需求是什麼?

  • 根據不同的 url 渲染不同內容;
  • 不重新整理頁面;

也就是可以在改變 url 的前提下,保證頁面不重新整理。

三、面試!!!

Hash 路由和 History 路由的區別?

  • hash 有 #history 沒有 #
  • hash 的 # 部分內容不會給伺服器端,主要一般是用於錨點, history 的所有內容都會給伺服器端;
  • hash 路由是不支援 SSR 的,history 路由是可以的;
  • hash 通過 hashchange 監聽變化,history 通過 popstate 監聽變化;

四、Hash 原理及實現

1、特性

hash 的出現滿足了這個需求,他有以下幾種特徵:

  • url 中帶有一個 # 符號,但是 # 只是瀏覽器端/使用者端的狀態,不會傳遞給伺服器端;
    • 使用者端路由地址 www.baidu.com/#/user --> 通過 http 請求 --> 伺服器端接收到的 www.baidu.com/
    • 使用者端路由地址 www.baidu.com/#/list/detail/1 --> 通過 http 請求 --> 伺服器端接收到的 www.baidu.com/
  • hash 值的更改,不會導致頁面的重新整理;
location.hash = '#aaa';
location.hash = '#bbb';
// 從 #aaa 到 #bbb,頁面是不會重新整理的
  • 不同 url 會渲染不同的頁面;
  • hash 值的更改,會在瀏覽器的存取歷史中新增一條記錄,所以我們才可以通過瀏覽器的返回、前進按鈕來控制 hash 的切換;
  • hash 值的更改,會觸發 hashchange 事件;
location.hash = '#aaa';
location.hash = '#bbb';

window.addEventLisenter('hashchange', () => {});

2、如何更改 hash

我們同樣有兩種方式來控制 hash 的變化:

  • location.hash 的方式:
location.hash = '#aaa';
location.hash = '#bbb';
  • html 標籤的方式:
<a href="#user" rel="external nofollow" > 點選跳轉到 user </a>

<!-- 等同於下面的寫法 -->
location.hash = '#user';

3、手動實現一個基於 hash 的路由

  • ./index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="./index.css" rel="external nofollow"  />
  </head>
  <body>
    <div class="container">
      <a href="#gray" rel="external nofollow" >灰色</a>
      <a href="#green" rel="external nofollow" >綠色</a>
      <a href="#" rel="external nofollow" >白色</a>
      <button onclick="window.history.go(-1)">返回</button>
    </div>

    <script type="text/javascript" src="index.js"></script>
  </body>
</html>
  • ./index.css
.container {
  width: 100%;
  height: 60px;
  display: flex;
  justify-content: space-around;
  align-items: center;

  font-size: 18px;
  font-weight: bold;

  background: black;
  color: white;
}

a:link,
a:hover,
a:active,
a:visited {
  text-decoration: none;
  color: white;
}
  • ./index.js
/* 
  期望看到的效果:點選三個不同的 a 標籤,頁面的背景顏色會隨之變化
*/
class BaseRouter {
  constructor() {
    this.routes = {}; // 儲存 path 以及 callback 的對應關係
    this.refresh = this.refresh.bind(this); // 如果不 bind 的話,refresh 方法中的 this 指向 window
    // 處理頁面 hash 變化,可能存在問題:頁面首次進來可能是 index.html,並不會觸發 hashchange 方法
    window.addEventListener('hashchange', this.refresh);

    // 處理頁面首次載入
    window.addEventListener('load', this.refresh);
  }

  /**
   * route
   * @param {*} path 路由路徑
   * @param {*} callback 回撥函數
   */
  route(path, callback) {
    console.log('========= route 方法 ========== ', path);
    // 向 this.routes 儲存 path 以及 callback 的對應關係
    this.routes[path] = callback || function () {};
  }

  refresh() {
    // 重新整理頁面
    const path = `/${location.hash.slice(1) || ''}`;
    console.log('========= refresh 方法 ========== ', path);
    this.routes[path]();
  }
}

const body = document.querySelector('body');
function changeBgColor(color) {
  body.style.backgroundColor = color;
}

const Router = new BaseRouter();

Router.route('/', () => changeBgColor('white'));
Router.route('/green', () => changeBgColor('green'));
Router.route('/gray', () => changeBgColor('gray'));

五、History 原理及實現

hash 有個 # 符號,不美觀,伺服器端無法接受到 hash 路徑和引數。

歷史的車輪無情攆過 hash,到了 HTML5 時代,推出了 History API

1、HTML5 History 常用的 API

window.history.back(); // 後退

window.history.forward(); // 前進

window.history.go(-3); // 接收 number 引數,後退 N 個頁面

window.history.pushState(null, null, path);

window.history.replaceState(null, null, path);

其中最主要的兩個 API 是 pushState 和 replaceState,這兩個 API 都可以在不重新整理頁面的情況下,操作瀏覽器歷史記錄。

不同的是,pushState 會增加歷史記錄,replaceState 會直接替換當前歷史記錄。

2、pushState/replaceState 的引數

  • pushState:頁面的瀏覽記錄裡新增一個歷史記錄;
  • replaceState:替換當前歷史記錄;

他們的引數是⼀樣的,三個引數分別是:

  • state:是一個物件,是一個與指定網址相關的物件,當 popstate 事件觸發的時候,該物件會傳入回撥函數;
  • title:新頁面的標題,瀏覽器支援不一,建議直接使用 null
  • url:頁面的新地址;

3、History 的特性

History API 有以下幾個特性:

  • 沒有 #
  • history.pushState() 或 history.replaceState() 不會觸發 popstate 事件,這時我們需要手動觸發頁面渲染;
  • 可以使用 history.popstate 事件來監聽 url 的變化;
  • 只有使用者點選瀏覽器 倒退按鈕 和 前進按鈕,或者使用 JavaScript 呼叫 backforwardgo 方法時才會觸發 popstate

4、面試!!!

  • pushState 時,會觸發 popstate 嗎?

    • pushState/replaceState 並不會觸發 popstate 事件,這時我們需要手動觸發頁面的重新渲染;
  • 我們可以使用 popstate 來監聽 url 的變化;

  • popstate 到底什麼時候才能觸發:

    • 點選瀏覽器後退按鈕;
    • 點選瀏覽器前進按鈕;
    • js 呼叫 back 方法;
    • js 呼叫 forward 方法;
    • js 呼叫 go 方法;

5、手動實現一個基於 History 的路由

  • ./index.html
  • ./index.css
.container {
  width: 100%;
  height: 60px;
  display: flex;
  justify-content: space-around;
  align-items: center;

  font-size: 18px;
  font-weight: bold;

  background: black;
  color: white;
}

a:link,
a:hover,
a:active,
a:visited {
  text-decoration: none;
  color: white;
}
  • ./index.js
class BaseRouter {
  constructor() {
    this.routes = {};

    // location.href; => hash 的方式
    console.log('location.pathname ======== ', location.pathname); // http://127.0.0.1:8080/green ==> /green
    this.init(location.pathname);
    this._bindPopState();
  }

  init(path) {
    // pushState/replaceState 不會觸發頁面的渲染,需要我們手動觸發
    window.history.replaceState({ path }, null, path);
    const cb = this.routes[path];
    if (cb) {
      cb();
    }
  }

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

  // ! 跳轉並執行對應的 callback
  go(path) {
    // pushState/replaceState 不會觸發頁面的渲染,需要我們手動觸發
    window.history.pushState({ path }, null, path);
    const cb = this.routes[path];
    if (cb) {
      cb();
    }
  }
  // ! 演示一下 popstate 事件觸發後,會發生什麼
  _bindPopState() {
    window.addEventListener('popstate', e => {
      /* 
        觸發條件:
          1、點選瀏覽器前進按鈕
          2、點選瀏覽器後退按鈕
          3、js 呼叫 forward 方法
          4、js 呼叫 back 方法
          5、js 呼叫 go 方法
      */
      console.log('popstate 觸發了');
      const path = e.state && e.state.path;
      console.log('path >>> ', path);
      this.routes[path] && this.routes[path]();
    });
  }
}
const Router = new BaseRouter();
const body = document.querySelector('body');
const container = document.querySelector('.container');

function changeBgColor(color) {
  body.style.backgroundColor = color;
}
Router.route('/', () => changeBgColor('white'));
Router.route('/gray', () => changeBgColor('gray'));
Router.route('/green', () => changeBgColor('green'));

container.addEventListener('click', e => {
  if (e.target.tagName === 'A') {
    e.preventDefault();
    console.log(e.target.getAttribute('href')); // /gray  /green 等等
    Router.go(e.target.getAttribute('href'));
  }
});

六、Vue-Router

1、router 使用

使用 Vue.js,我們已經可以通過組合元件來組成應用程式,當你要把 Vue Router 新增進來,我們需要做的是,將元件(components)對映到路由(routes),然後告訴 Vue Router 在哪裡渲染它們。

舉個例子:

<!-- 路由匹配到的元件將渲染在這裡 -->
<div id="app">
  <router-view></router-view>
</div>
// 如果使用模組化機制程式設計,匯入 Vue 和 VueRouter,要呼叫 Vue.use(VueRouter)

// 1、定義(路由)元件
// 可以從其他檔案 import 進來
const Foo = { template: '<div>foo</div>' };
const Bar = { template: '<div>bar</div>' };

// 2、定義路由
//每個路由應該對映一個元件,其中 component 可以是通過 Vue.extend() 建立的元件構造器,或者只是一個元件設定物件
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar },
];

// 3、建立 router 範例,然後傳 routes 設定
const router = new VueRouter({
  routes,
});

// 4、建立和掛載根範例
// 記得要通過 router 設定引數注入路由,從而讓整個應用都有路由功能
const app = new Vue({
  router,
}).$mount('#app');

2、動態路由匹配

我們經常需要把某種模式匹配到的所有路由,全部對映到同個元件,比如使用者資訊元件,不同使用者使用同一個元件。

可以通過 $route.params.id 或者引數。

const router = new VueRouter({
  routes: [
    // 動態路徑引數,以冒號開頭
    { path: '/user/:id', component: User },
  ],
});

const User = {
  template: '<div>User: {{ $route.params.id }}</div>',
};

3、響應路由引數的變化

複用元件時,想對 路由引數 的變化作出響應的話,可以使用 watch 或者 beforeRouteUpdate

舉個例子:

const User = {
  template: '...',
  watch: {
    $route(to, from) {
      // 對路由變化作出響應...
    },
  },
};

const User = {
  template: '...',
  beforeRouteUpdate(to, from, next) {
    // 對路由變化作出響應...
    // don't forget to call next()
  },
};

4、捕獲所有路由或 404 Not found 路由

當時用萬用字元路由時,請確保路由的順序是正確的,也就是說含有萬用字元的路由應該在 最後

舉個例子:

5、導航守衛

vue-router 提供的導航守衛主要用來通過跳轉或取消的方式守衛導航。有多種方式植入路由導航過程中:

  • 全域性的
    • 全域性前置守衛:router.beforeEach
    • 全域性解析守衛:router.beforeResolve
    • 全域性後置勾點:router.afterEach
  • 單個路由獨享的
    • 路由獨享守衛:beforeEnter
  • 元件級的
    • beforeRouteEnter
    • beforeRouteUpdate
    • beforeRouteLeave

6、完整的導航解析流程

  • 導航被觸發;
  • 在失活的元件裡呼叫離開守衛(前一個元件的 beforeRouteLeave);
  • 呼叫全域性的 beforeEach 守衛;
  • 在重用的元件裡呼叫 beforeRouteUpdate 守衛;
  • 在路由設定裡呼叫 beforeEnter
  • 解析非同步路由元件;
  • 在被啟用的元件裡呼叫 beforeRouterEnter
  • 呼叫全域性的 beforeResolve 守衛;
  • 導航被確認;
  • 呼叫全域性的 afterEach 勾點;
  • 觸發 DOM 更新;
  • 用建立好的範例呼叫 beforeRouterEnter 守衛中傳給 next 的回撥函數;

舉個例子:

// 全域性
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

// 全域性的導航守衛
router.beforeEach((to, from, next) => {
  console.log(`Router.beforeEach => from=${from.path}, to=${to.path}`);
  // 可以設定頁面的 title
  document.title = to.meta.title || '預設標題';
  // 執行下一個路由導航
  next();
});

router.afterEach((to, from) => {
  console.log(`Router.afterEach => from=${from.path}, to=${to.path}`);
});

// 路由獨享
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // 設定陣列裡針對單個路由的導航守衛
        console.log(`TestComponent route config beforeEnter => from=${from.path}, to=${to.path}`);
        next();
      },
    },
  ],
});

// 元件
const Foo = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
    // 在渲染該元件的對應路由被 comfirm 前呼叫
    // 不!能!獲取元件範例 this,因為當守衛執行前,元件範例還沒被呼叫
  },
  beforeRouteUpdate(to, from, next) {
    // 在當前路由改變,但是該元件被複用時呼叫
    // 舉個例子來說,對於一個帶有動態引數的路徑 /foo/:id,在 /foo/1 和 /foo/2 之間跳轉的時候
    // 由於會渲染同樣的 Foo 元件,因此元件範例會被複用。而這個勾點就會在這個情況下被呼叫
    // 可以存取元件範例 this
  },
  beforeRouteLeave(to, from, next) {
    // 導航離開該元件的對應路由時呼叫
    // 可以存取元件範例 this
  },
};

next 必須呼叫:

  • next():進行管道中的下一個勾點。如果全部勾點執行完了,則導航的狀態就是 confirmed(確認的)。
  • next(false):中斷當前的導航。如果瀏覽器的 URL 改變了(可能是使用者手動或者瀏覽器後退按鈕),那麼 URL 地址會重置到 from 路由對應的地址。
  • next("/") 或者 next({ path: "/" }):跳轉到一個不同的地址。當前的導航被中斷,然後進行一個新的導航。可以向 next 傳遞任意位置物件,且允許設定諸如 replace: truename: "home" 之類的選項以及任何用在 router-link 的 to prop 或 router.push 中的選項。
  • next(error):如果傳入 next 的引數是一個 Error 範例,則導航會被終止且該錯誤會被傳遞給 router.onError() 註冊過的回撥。

7、導航守衛執行順序(面試!!!)

  • 【元件】前一個元件的 beforeRouteLeave
  • 【全域性】的 router.beforeEach
    • 【元件】如果是路由引數變化,觸發 beforeRouteUpdate
  • 【組態檔】裡,下一個的 beforeEnter
  • 【元件】內部宣告的 beforeRouteEnter
  • 【全域性】的 router.afterEach

8、捲動行為(面試!!!)

vue-router 裡面,怎麼記住前一個頁面的卷軸的位置???

使用前端路由,當切換到新路由時,想要頁面捲動到頂部,或者是保持原先的捲動位置,就像重新載入頁面那樣。

Vue-router 能做到,而且更好,它讓你可以自定義路由切換時頁面如何捲動。

【注意】:這個功能只在支援 history.pushState 的瀏覽器中可用。

scrollBehavior 生效的條件:

  • 瀏覽器支援 history API
  • 頁面間的互動是通過 goforwardback 或者 瀏覽器的前進/返回按鈕
window.history.back(); // 後退
window.history.forward(); // 前進
window.history.go(-3); // 接收 number 引數,後退 N 個頁面

舉個例子

// 1. 記住:手動點選瀏覽器返回或者前進按鈕,記住卷軸的位置,基於 history API 的,其中包括:go、back、forward、手動點選瀏覽器返回或者前進按鈕
// 2. 沒記住:router-link,並沒有記住卷軸的位置

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
  scrollBehavior: (to, from, savedPosition) => {
    console.log(savedPosition); // 已儲存的位置資訊
    return savedPosition;
  },
});

9、路由懶載入

當打包構建應用時,JavaScript 包會變得非常大,影響頁面載入。如果我們能把不同路由對應的元件分割成不同的程式碼塊,然後當路由被存取的時候才載入對應元件,這樣就更加高效了。

舉個例子:

const Foo = () => import(/* webpackChunkName: "foo" */ './Foo.vue');

const router = new VueRouter({
  routes: [{ path: '/foo', component: Foo }],
});

到此這篇關於Vue.js 前端路由和非同步元件介紹的文章就介紹到這了,更多相關Vue.js 非同步元件內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com