22-前端核心技術-VUE生態系統


第22章-前端核心技術-VUE生態系統

學習目標

  1. 掌握vue cli的特征
  2. 掌握vue router的使用 重點 難點
  3. 掌握vue vuex的使用 重點

Vue Cli

Cli 介紹

Vue CLI 是一個基於 Vue.js 進行快速開發的完整系統。Vue CLI 致力於將 Vue 生態中的工具基礎標准化。

Vue CLI 有幾個獨立的部分:

  • CLI

CLI (@vue/cli) 提供了終端里的 vue 命令。可以通過 vue create 快速搭建一個新項目,或者通過 vue serve 運行vue文件

也可以通過 vue ui 通過一套圖形化界面管理你的所有項目。

  • CLI 服務

CLI 服務 (@vue/cli-service) 是一個開發環境依賴。它是一個 npm 包,局部安裝在每個 @vue/cli 創建的項目中。是構建於 webpackwebpack-dev-server 之上的。它包含了: servebuildinspect 命令。

  • CLI 插件

CLI 插件是向你的 Vue 項目提供可選功能的 npm 包,例如 Babel/TypeScript 轉譯、ESLint 集成、單元測試和 end-to-end 測試等。

當你在項目內部運行 vue-cli-service 命令時,它會自動解析並加載 package.json 中列出的所有 CLI 插件。

Cli 常用命令

vue create 創建項目(重要)

安裝:

1
2
3
npm install -g @vue/cli
# 或者
yarn global add @vue/cli

如果安裝時出現如下報錯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
npm ERR! gyp info find Python using Python version 3.9.6 found at "C:\Users\star\AppData\Local\Programs\Python\Python39\python.exe"
npm ERR! gyp ERR! find VS
npm ERR! gyp ERR! find VS msvs_version not set from command line or npm config
npm ERR! gyp ERR! find VS VCINSTALLDIR not set, not running in VS Command Prompt
npm ERR! gyp ERR! find VS could not use PowerShell to find Visual Studio 2017 or newer, try re-running with '--loglevel silly' for more details
npm ERR! gyp ERR! find VS looking for Visual Studio 2015
npm ERR! gyp ERR! find VS - not found
npm ERR! gyp ERR! find VS not looking for VS2013 as it is only supported up to Node.js 8
npm ERR! gyp ERR! find VS
npm ERR! gyp ERR! find VS **************************************************************
npm ERR! gyp ERR! find VS You need to install the latest version of Visual Studio
npm ERR! gyp ERR! find VS including the "Desktop development with C++" workload.
npm ERR! gyp ERR! find VS For more information consult the documentation at:
npm ERR! gyp ERR! find VS https://github.com/nodejs/node-gyp#on-windows
npm ERR! gyp ERR! find VS **************************************************************
npm ERR! gyp ERR! find VS
npm ERR! gyp ERR! configure error

解決方法:裝 Visual Studio和node-gyp 以管理員的身份打開CMD,開始菜單右擊選擇即可:

1
2
npm install -g node-gyp
npm install --global --production windows-build-tools

如果還是沒有解決,可能是npm版本和node版本不一致,改變版本,根據官網來,如:長期維護版: 14.17.6 (包含 npm 6.14.15)

npm install npm@6.14.15 -g

如果以前安裝,最好強制覆蓋式安裝

npm install -g @vue/cli --force

檢查版本:

vue --version

升級

1
2
3
npm update -g @vue/cli
# 或者
yarn global upgrade --latest @vue/cli

創建一個新項目:

1
2
3
vue create --help # 查看命令幫助文檔
vue create hello-world # 創建一個 hello-world 項目
vue ui # 圖形化界面創建項目

運行項目

npm run serve

vue serve 單文件單獨運行

可以使用 vue serve 命令對單個 *.vue 文件進行快速原型開發,不過這需要先額外安裝一個全局的擴展:

1
2
npm install -g @vue/cli-service-global
npm install -g @vue/compiler-sfc

vue serve 的缺點就是它需要安裝全局依賴,這使得它在不同機器上的一致性不能得到保證。因此這只適用於快速原型開發。

vue serve 語法:

1
2
3
4
5
6
serve [options] [file]
# 在開發環境模式下零配置為 .js 或 .vue 文件啟動一個服務器
# Options 參數:
  -o, --open  打開瀏覽器
  -c, --copy  將本地 URL 復制到剪切板
  -h, --help  輸出用法信息

如:有一個 App.vue 文件:

1
2
3
<template>
  <h1>Hello!</h1>
</template>

然后在這個 App.vue 文件所在的目錄下運行:

vue serve

vue serve 使用了和 vue create 創建的項目相同的默認設置 (webpackBabelPostCSSESLint)。它會在當前目錄自動推導入口文件——入口可以是 main.jsindex.jsApp.vueapp.vue 中的一個。你也可以顯式地指定入口文件:

vue serve MyComponent.vue

vue build 構建項目

1
2
3
4
5
6
7
8
build [options] [file]
# 在生產環境模式下零配置構建一個 .js 或 .vue 文件

Options:
-t, --target <target> 構建目標 (app | lib | wc | wc-async, 默認值:app)
-n, --name <name> 庫的名字或 Web Components 組件的名字 (默認值:入口文件名)
-d, --dest <dir> 輸出目錄 (默認值:dist)
-h, --help 輸出用法信息

你也可以使用 vue build 將目標文件構建成一個生產環境的包並用來部署:

vue build MyComponent.vue

vue build 也提供了將組件構建成為一個庫或一個組件的能力。

vue add 安裝插件

每個 CLI 插件都會包含一個 (用來創建文件的) 生成器和一個 (用來調整 webpack 核心配置和注入命令的) 運行時插件。當你使用 vue create 來創建一個新項目的時候,有些插件會根據你選擇的特性被預安裝好。如果你想在一個已經被創建好的項目中安裝一個插件,可以使用 vue add 命令,如:

vue add eslint
Babel

Babel 是一個 JavaScript 編譯器,主要用於將采用 ECMAScript 2015+ 語法編寫的代碼轉換為向后兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環境中。官網

如:

1
2
3
4
5
6
7
// Babel 輸入: ES2015 箭頭函數
[1, 2, 3].map((n) => n + 1);

// Babel 輸出: ES5 語法實現的同等功能
[1, 2, 3].map(function(n) {
return n + 1;
});

ESLint

ESLint 是在 ECMAScript/JavaScript 代碼中識別和報告模式匹配的工具,它的目標是保證代碼的一致性和避免錯誤。在許多方面,它和 JSLintJSHint 相似。官網

安裝ESLint之后會在你的文件夾中自動創建 .eslintrc 文件。可以在 .eslintrc 文件中看到許多像這樣的規則:

1
2
3
4
5
6
{
    "rules": { "semi": ["error", "always"], // 是否必須使用分號 "quotes": ["error", "double"] // 引號只能使用雙引號 } }

"semi""quotes"ESLint規則 的名稱。第一個值是錯誤級別,可以使下面的值之一:

  • "off" or 0 - 關閉規則
  • "warn" or 1 - 將規則視為一個警告(不會影響退出碼)
  • "error" or 2 - 將規則視為一個錯誤 (退出碼為1)

這三個錯誤級別可以允許你細粒度的控制 ESLint 是如何應用規則(更多關於配置選項和細節的問題,請查看配置文件

Jest

Jest 是一個令人愉快的 JavaScript 測試框架,專注於 簡潔明快。官網

下面我們開始給一個假定的函數寫測試,這個函數的功能是兩數相加。首先創建 sum.js 文件:

1
2
3
4
function sum(a, b) {
  return a + b;
}
module.exports = sum;

接下來,創建名為 sum.test.js 的文件。這個文件包含了實際測試內容:

1
2
3
4
5
const sum = require('./sum');

test('期望(expect) 1 + 2 等於(toBe) 3', () => {
expect(sum(1, 2)).toBe(3);
});

expect 對結構進行包裝,表示期望

toBe 用來來檢測兩個值是否完全相同。

若要了解其它使用 Jest 可以測試的內容,請參閱使用匹配器(Matcher)

將如下代碼添加到 package.json 中:

1
2
3
4
5
{
  "scripts": {
    "test": "jest"
  }
}

最后,運行npm run test ,Jest 將輸出如下信息:

1
2
PASS  ./sum.test.js
✓ 期望(expect) 1 + 2 等於(toBe) 3 (5ms)

CLI 服務

在一個 Vue CLI 項目中,@vue/cli-service 安裝了一個名為 vue-cli-service 的命令。

可以在 npm 命令 中以 vue-cli-service、或者從終端中以 ./node_modules/.bin/vue-cli-service 訪問這個命令。

默認的項目的 package.json

1
2
3
4
5
6
{
  "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build" } }

可以通過 npmYarn 調用這些 script

1
2
3
4
5
npm run serve
npm run build
# OR
yarn serve
yarn build

如果你可以使用 npx (最新版的 npm 應該已經自帶),也可以直接這樣調用命令。

npxnpm5.2之后發布的一個命令。官網說它是“execute npm package binaries”,就是執行npm依賴包的二進制文件,簡而言之,就是我們可以使用npx來執行各種命令。

使用npx可以在命令行直接執行本地已安裝的依賴包命令,不用在scripts腳本寫入命令

1
2
3
npx vue-cli-service serve
# 或者
npx vue-cli-service build

vue-cli-service serve

vue-cli-service serve 命令會啟動一個 基於 webpack-dev-server 的開發服務器,並附帶開箱即用的模塊**熱重載** (Hot-Module-Replacement)。

1
2
3
4
5
6
7
8
9
10
# 語法:
vue-cli-service serve [options] [entry]

# options 選項:
--open 在服務器啟動時打開瀏覽器
--copy 在服務器啟動時將 URL 復制到剪切版
--mode 指定環境模式 (默認值:development)
--host 指定 host (默認值:0.0.0.0)
--port 指定 port (默認值:8080)
--https 使用 https (默認值:false)

命令行參數 [entry] 將被指定為唯一入口,而非額外的追加入口。

vue-cli-service build

1
2
3
4
5
6
7
8
9
10
11
12
13
# 語法:
vue-cli-service build [options] [entry|pattern]

# 選項:
--mode 指定環境模式 (默認值:production)
--dest 指定輸出目錄 (默認值:dist)
--modern 面向現代瀏覽器帶自動回退地構建應用
--target app | lib | wc | wc-async (默認值:app)
--name 庫或 Web Components 模式下的名字 (默認值:package.json 中的 "name" 字段或入口文件名)
--no-clean 在構建項目之前不清除目標目錄
--report 生成 report.html 以幫助分析包內容
--report-json 生成 report.json 以幫助分析包內容
--watch 監聽文件變化

vue-cli-service build 會在 dist/ 目錄產生一個可用於生產環境的包,帶有 JS/CSS/HTML 的壓縮。

這里還有一些有用的命令參數:

  • --modern 為現代瀏覽器交付原生支持的 ES2015 代碼,並生成一個兼容老瀏覽器的包用來自動回退。
  • --target 允許你將項目中的任何組件以一個庫或 Web Components 組件的方式進行構建。
  • --report--report-json 會根據構建統計生成報告,它會幫助你分析包中包含的模塊們的大小。

vue-cli-service inspect

1
2
3
4
5
#語法:
vue-cli-service inspect [options] [...paths]

選項:
--mode 指定環境模式 (默認值:development)

可以使用 vue-cli-service inspect 來審查一個 Vue CLI 項目的 webpack config

Vue Router

Vue RouterVue.js 的官方路由。它與 Vue.js 核心深度集成,讓用 Vue.js 構建單頁應用變得輕而易舉。使用 CLI 創建項目的時候可以選擇添加到項目中。

Route 使用過程

(1)模板中添加路由元素

image-20210928172005311

如:

1
2
3
4
5
6
7
<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
  </div>
  <router-view />
</template>

(2)綁定路由和組件

當然還有配合路由配置文件才能發揮作用,路由配置文件將 url 綁定到某個組件上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { createRouter, createWebHashHistory } from "vue-router";
import Home from "../views/Home.vue"; // 立即加載

// 定義一些路由,每個路由都需要映射到一個組件上。
const routes = [
{
path: "/",
name: "Home",
component: Home, // 映射到 Home 組件上
},
{
path: "/about",
name: "About",
component: () => import("../views/About.vue"), // 當訪問該路由時,它將被延遲加載。
},
];

// 創建路由實例
const router = createRouter({
history: createWebHashHistory(), // 使用 hash 模式的 history 的實現。
routes, // 將路由映射列表傳遞到 routes 路由對象中。
});

export default router;

(3)全局應用路由

入口文件中引入路由 router

1
2
3
4
5
6
7
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router"; // 引入路由 router 對象
import store from "./store";

// vue中全局使用 router 對象
createApp(App).use(store).use(router).mount("#app");

(4)使用路由

通過調用 app.use(router),就可以在任意組件中以 this.$router 的形式訪問它,並且以 this.$route 的形式訪問當前正在訪問的頁面的路由

如:

普通API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
  computed: {
    username() {
      // 我們很快就會看到 `params` 是什么
      return this.$route.params.username
    },
  },
  methods: {
    goToDashboard() {
      if (isAuthenticated) {
        this.$router.push('/dashboard')
      } else {
        this.$router.push('/login')
      }
    },
  },
}

組合式API

因為我們在 setup 里面沒有訪問 this,所以我們不能再直接訪問 this.$routerthis.$route

作為替代,可以使用 useRouteruseRoute 函數。

但是在模板中仍然可以直接使用 $router$route而不需要在 setup 中返回。

route 對象是一個響應式對象,所以它的任何屬性都可以被監聽,但你應該**避免監聽整個 route** 對象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useRouter, useRoute } from 'vue-router'

export default {
setup() {
const router = useRouter()
const route = useRoute()

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pushWithQuery</span><span class="hljs-params">(query)</span> </span>{
  router.push({
    name: <span class="hljs-string">'search'</span>,
    query: {
      ...route.query,
    },
  })
}

<span class="hljs-comment">// 當參數更改時獲取用戶信息</span>
watch(
  () =&gt; route.params,
  async newParams =&gt; {
    userData.value = await fetchUser(newParams.id)
  }
)

},
}

Router 對象屬性

Router中有兩個只讀屬性currentRouteoptions

  • currentRoute - 當前路由地址。只讀的。
  • options - 創建 Router 時傳遞的原始配置對象。只讀的。

RouterOptions屬性

history

用於路由實現歷史記錄。大多數 web 應用程序都應該使用 createWebHistory,但它要求正確配置服務器。

所以通常還可以使用 createWebHashHistory 的基於 hash 的歷史記錄,它不需要在服務器上進行任何配置,但是搜索引擎根本不會處理它,在 SEO 上表現很差。

示例

1
2
3
const router = createRouter({
  history: createWebHashHistory(),
});
linkActiveClass

用於模糊匹配(匹配的地址可以有多個)的路由被激活的 RouterLink 的默認class類。如果什么都沒提供,則會使用默認的 router-link-active

示例

1
2
3
4
const router = createRouter({
  	history: createWebHashHistory(),
    linkActiveClass: "active",
});
linkExactActiveClass

用於精准匹配(匹配的地址只有一個)的路由被激活的 RouterLink 的默認 class 類。如果什么都沒提供,則會使用 router-link-exact-active

示例

1
2
3
4
5
const router = createRouter({
  	history: createWebHashHistory(),
    linkActiveClass: "active",
    linkExactActiveClass?: "exact-active",
});
routes

應該添加到路由的初始路由列表。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createRouter, createWebHashHistory } from "vue-router";
import Home from "../views/Home.vue";

const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
linkActiveClass: "active",
component: () => import("../views/About.vue"),
},
];

const router = createRouter({
history: createWebHashHistory(),
linkActiveClass: "active",
linkExactActiveClass?: "exact-active",
routes,
});

scrollBehavior

在頁面之間導航時控制滾動的函數。可以返回一個 Promise 來延遲滾動。

使用前端路由,當切換到新路由時,想要頁面滾到頂部,或者是保持原先的滾動位置,就像重新加載頁面那樣。 vue-router 能做到,而且更好,它讓你可以自定義路由切換時頁面如何滾動。

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

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const router = createRouter({
  	history: createWebHashHistory(),
    linkActiveClass: "active",
    linkExactActiveClass?: "exact-active",
    routes,
    scrollBehavior (to, from, savedPosition) {
    	// `to` 和 `from` 都是路由地址
  		// `savedPosition` 可以為空,如果沒有的話。
    	// return 期望滾動到哪個的位置
    	// 案例1:始終滾動到頂部
    	return { top: 0 }
	<span class="hljs-comment">// 案例2:始終在元素 #main 上方滾動 10px</span>
	<span class="hljs-keyword">return</span> {
  		<span class="hljs-comment">// 也可以這么寫</span>
  		<span class="hljs-comment">// el: document.getElementById('main'),</span>
  		el: <span class="hljs-string">'#main'</span>,
  		top: -<span class="hljs-number">10</span>,
	}

	<span class="hljs-comment">// 案例3:返回 savedPosition,在按下 后退/前進 按鈕時,就會像瀏覽器的原生表現那樣</span>
	<span class="hljs-keyword">if</span> (savedPosition) {
  		<span class="hljs-keyword">return</span> savedPosition
	} <span class="hljs-keyword">else</span> {
 	 	<span class="hljs-keyword">return</span> { top: <span class="hljs-number">0</span> }
	}
	
	<span class="hljs-comment">// 案例4:模擬 “滾動到錨點” 的行為</span>
	<span class="hljs-keyword">if</span> (to.hash) {
  		<span class="hljs-keyword">return</span> {
    		el: to.hash,
    		behavior: <span class="hljs-string">'smooth'</span>,
        }
  	}
	
	<span class="hljs-comment">// 案例5:延遲滾動</span>
	<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Promise((resolve, reject) =&gt; {
  		setTimeout(() =&gt; {
    		resolve({ left: <span class="hljs-number">0</span>, top: <span class="hljs-number">0</span> })
  		}, <span class="hljs-number">500</span>)
	})
}

});

RouteRecordRaw 屬性

RouteRecordRaw 是一個記錄單個路由的對象,包括如下屬性

path
  • 類型string

  • 詳細內容

記錄的路徑。應該以 / 開頭,除非該記錄是另一條記錄的子記錄。可以定義參數:/users/:id 匹配 /users/1 以及 /users/posva

動態路由匹配規則

1
2
3
4
5
6
const routes = [
	// 動態段以冒號開始
    // /users/johnny 和 /users/jolyne 這樣的 URL 都會映射到同一個路由。
	// 路徑參數用 :xxx 表示。在每個組件中用 this.$route.params.xxx 獲取。
	{ path: '/users/:id', component: User },
]

可以在同一個路由中設置有多個 路徑參數,它們會映射到 $route.params 上的相應字段。例如:

匹配模式 匹配路徑 $route.params
/users/:username /users/eduardo { username: 'eduardo' }
/users/:username/posts/:postId /users/eduardo/posts/123 { username: 'eduardo', postId: '123' }

路徑參數 + 正則表達式 提取參數

1
2
3
4
5
6
const routes = [
	// 將匹配所有內容並將其放在 `$route.params.abcd` 下
	{ path: '/:abcd(.*)*', name: 'NotFound', component: NotFound },
	// 將匹配以 `/user-` 開頭的所有內容,並將其放在 `$route.params.xyz` 下
	{ path: '/user-:xyz(.*)', component: UserGeneric },
]

在參數中自定義正則匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const routes = [
  	// 匹配 /o/3549
  	{ path: '/o/:orderId' },
 	// 匹配 /p/books
  	{ path: '/p/:productName' },
	// /:orderId -> 僅匹配數字
 	{ path: '/:orderId(\\d+)' },
 	// /:productName -> 匹配其他任何內容
 	{ path: '/:productName' },
    // /:chapters -> 匹配 /one, /one/two, /one/two/three, 等
  	{ path: '/:chapters+' },
  	// /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等
  	{ path: '/:chapters*' },
    // 僅匹配數字
  	// 匹配 /1, /1/2, 等
  	{ path: '/:chapters(\\d+)+' },
  	// 匹配 /, /1, /1/2, 等
  	{ path: '/:chapters(\\d+)*' },
    // 匹配 /users 和 /users/posva
  	{ path: '/users/:userId?' },
  	// 匹配 /users 和 /users/42
  	{ path: '/users/:userId(\\d+)?' },
]
redirect
  • 類型RouteLocationRaw | (to: RouteLocationNormalized) => RouteLocationRaw (可選)

  • 詳細內容

如果路由是直接匹配的,那么重定向到的路徑。

重定向發生在所有導航守衛之前,並以新的目標位置觸發一個新的導航。

也可以是一個接收目標路由地址並返回我們應該重定向到的位置的函數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const router = new VueRouter({
	routes: [
        // 從 /a 重定向到 /b:
        { path: '/a', redirect: '/b' },
        // 重定向的目標也可以是一個命名的路由
        { path: '/a', redirect: { name: 'foo' }}
        // 甚至是一個方法,動態返回重定向目標
        { path: '/a', redirect: to => {
        	const { hash, params, query } = to
        	if (query.to === 'foo') {
          		return { path: '/foo', query: null }
        	}
        	if (hash === '#baz') {
          		return { name: 'baz', hash: '' }
        	}
        	if (params.id) {
          		return '/with-params/:id'
        	} else {
          		return '/bar'
            }
		}
    ]
})
children
  • 類型RouteRecordRaw 數組 (可選)

  • 詳細內容

當前記錄的嵌套路由。

要將組件渲染到組件中嵌套的 router-view 中,我們需要在路由中配置 children

換句話說,想要使用嵌套 children 路由,必須也沒中嵌套 router-view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        // 當 /user/:id/profile 匹配成功 
        // UserProfile 將被渲染到 User 的 <router-view> 內部
        path: 'profile',
        component: UserProfile,
      },
      {
        // 當 /user/:id/posts 匹配成功
        // UserPosts 將被渲染到 User 的 <router-view> 內部
        path: 'posts',
        component: UserPosts,
      },
    ],
  },
]
alias
  • 類型string | string[] (可選)

  • 詳細內容

路由的別名。允許定義類似記錄副本的額外路由。這使得路由可以簡寫為像這種 /users/:id/u/:id所有的 aliaspath 值必須共享相同的參數

“別名”的功能讓你可以自由地將 UI 結構映射到任意的 URL,而不是受限於配置的嵌套路由結構。

1
2
3
4
5
6
7
// /a 的別名是 /b,意味着,當用戶訪問 /b 時,URL 會保持為 /b,但是路由匹配則為 /a,就像用戶訪問 /a 一樣。
// 上面對應的路由配置為:
const router = new VueRouter({
  routes: [
    { path: '/a', component: A, alias: '/b' }
  ]
})
name
  • 類型string | symbol (可選)

  • 詳細內容

路由記錄獨一無二的名稱。

1
2
3
4
5
const router = new VueRouter({
  routes: [
    { path: '/a', component: A, alias: '/b', name: new Symbol('a') }
  ]
})
props
  • 類型boolean | Record<string, any> | (to: RouteLocationNormalized) => Record<string, any> (可選)

  • 詳細內容

允許將參數作為 props 傳遞給由 router-view 渲染的組件。當傳遞給一個*多視圖記錄*時,它應該是一個與組件具有相同鍵的對象,或者是一個應用於每個組件的布爾值

在組件中使用 $route 會與路由緊密耦合,這限制了組件的靈活性,可以通過 props 配置來解除這種行為:

1
2
3
4
5
const User = {
  props: ['id'], // 直接使用路由中的參數 id
  template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }] // 開啟 props: true
meta
  • 類型RouteMeta (可選)

  • 詳細內容

在記錄上附加自定義數據。

有時,可能希望將任意信息附加到路由上,如過渡名稱、誰可以訪問路由等。這些事情可以通過接收屬性對象的meta屬性來實現,並且它可以在路由地址和導航守衛上都被訪問到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes = [
  {
    path: '/posts',
    component: PostsLayout,
    children: [
      {
        path: 'new',
        component: PostsNew,
        // 只有經過身份驗證的用戶才能創建帖子
        // 通過$router.meta.requiresAuth 來獲取
        meta: { requiresAuth: true }
      },
    ]
  }
]
beforeEnter
  • 類型NavigationGuard | NavigationGuard[\] (可選)

  • 詳細內容

在進入特定於此記錄的守衛之前。注意如果記錄有重定向屬性,則 beforeEnter 無效。

Rputer 對象函數

beforeEach 前置守衛

可以使用 router.beforeEach 注冊一個全局前置守衛。

當一個導航被觸發時,全局前置守衛按照創建順序調用。

守衛是異步解析執行,此時導航在所有守衛 resolve 完之前一直處於**等待中**。

每個守衛方法接收兩個參數:

  • to: 即將要進入的目標
  • from: 當前導航正要離開的路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { createRouter, createWebHashHistory } from "vue-router";
import Home from "../views/Home.vue";

const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
component: () => import("../views/About.vue"),
},
];

const router = createRouter({
history: createWebHashHistory(),
routes,
});

// 全局前置守衛
// 當一個導航觸發時,按照所有路由創建順序依次被 循環 調用。
router.beforeEach((to, from, next) => {
// to :即將要進入的目標
// from :當前導航正要離開的路由
// next :回調函數,用以驗證導航

// 返回 false 取消導航,路由顯示的頁面不顯示
// return false

// next({ name: 'Login' }) 等價於 router.push('/login')
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// next() 依次執行
else next()
})

export default router;

beforeResolve 解析守衛

router.beforeResolve 也可以注冊一個全局守衛。

router.beforeEach 類似,在 每次導航時都會觸發。

但是必須在導航被確認之前,**同時在所有組件內守衛和異步路由組件被解析之后,解析守衛就被正確調用**。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 處理錯誤,然后取消導航
        return false
      } else {
        // 意料之外的錯誤,取消導航並把錯誤傳給全局處理器
        throw error
      }
    }
  }
})

afterEach 后置守衛

添加一個導航鈎子,在**每次導航后執行**。返回一個刪除注冊鈎子的函數。

函數簽名:

afterEach(guard: NavigationHookAfter): () => void

參數

參數 類型 描述
guard NavigationHookAfter 要添加的導航鈎子

示例

1
2
3
4
5
router.afterEach((to, from, failure) => {
  if (isNavigationFailure(failure)) {
    console.log('failed navigation', failure)
  }
})

addRoute

添加一條新的路由記錄作為現有路由的子路由。如果路由有一個 name,並且已經有一個與之名字相同的路由,它會先刪除之前的路由。

函數簽名:

addRoute(parentName: string | symbol, route: RouteRecordRaw): () => void

參數

參數 類型 描述
parentName string | symbol 父路由記錄,route 應該被添加到的位置
route RouteRecordRaw 要添加的路由記錄

addRoute

添加一條新的路由記錄到路由。如果路由有一個 name,並且已經有一個與之名字相同的路由,它會先刪除之前的路由。

函數簽名:

addRoute(route: RouteRecordRaw): () => void

參數

參數 類型 描述
route RouteRecordRaw 要添加的路由記錄

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
router.addRoute({ path: '/about', name: 'about', component: About })
// 這將會刪除之前已經添加的路由,因為他們具有相同的名字且名字必須是唯一的
router.addRoute({ path: '/other', name: 'about', component: Other })

router.addRoute({ path: '/about', name: 'about', component: About })
// 刪除路由
router.removeRoute('about')

// 要將嵌套路由添加到現有的路由中,
// 可以將路由的 name 作為第一個參數傳遞給 router.addRoute(),
// 這將有效地添加路由,就像通過 children 添加的一樣:
router.addRoute({ name: 'admin', path: '/admin', component: Admin }) // or
router.addRoute('admin', { path: 'settings', component: AdminSettings }) // or
router.addRoute({
name: 'admin',
path: '/admin',
component: Admin,
children: [{ path: 'settings', component: AdminSettings }],
})

removeRoute

通過名稱刪除現有路由。

函數簽名:

removeRoute(name: string | symbol): void

參數

參數 類型 描述
name string | symbol 要刪除的路由名稱

push

通過在歷史堆棧中推送一個 entry,以編程方式導航到一個新的 URL

函數簽名:

push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>

參數

參數 類型 描述
to RouteLocationRaw 要導航到的路由地址

路由地址可以是一個 字符串,比如 /users/posva#bio,也可以是一個對象:

1
2
3
4
5
6
7
8
9
10
// 這三種形式是等價的
router.push('/users/posva#bio')
router.push({ path: '/users/posva', hash: '#bio' })
router.push({ name: 'users', params: { username: 'posva' }, hash: '#bio' })
// 只改變 hash
router.push({ hash: '#bio' })
// 只改變 query
router.push({ query: { page: '2' } })
// 只改變 param
router.push({ params: { username: 'jolyne' } })

注意 path 必須以編碼方式提供(例如,phantom blood 變為 phantom%20blood)。而 paramsqueryhash 一定不要這樣,因為它們會被路由編碼。

replace

通過替換歷史堆棧中的當前 entry,以編程方式導航到一個新的 URL

replace()進行頁面跳轉不會形成history,不可返回到上一層

函數簽名:

replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>

參數

參數 類型 描述
to RouteLocationRaw 要導航到的路由地址

原始路由地址還支持一個額外的配置 replace 來調用導航守衛中的 router.replace(),而不是 router.push()。請注意,即使在調用 router.push()時,它也會在內部調用 router.replace()

1
2
3
router.push({ hash: '#bio', replace: true })
// 相當於
router.replace({ hash: '#bio' })

back

如果可能的話,通過調用 history.back() 回溯歷史。相當於 router.go(-1)

函數簽名:

back(): void

forward

如果可能的話,通過調用 history.forward() 在歷史中前進。相當於 router.go(1)

函數簽名:

forward(): void

go

允許你在歷史中前進或后退。

函數簽名:

go(delta: number): void

參數

參數 類型 描述
delta number 相對於當前頁面,你要移動到的歷史位置

hasRoute

確認是否存在指定名稱的路由。

函數簽名:

hasRoute(name: string | symbol): boolean

參數

參數 類型 描述
name string | symbol 要確認的路由名稱

isReady

當路由器完成初始化導航時,返回一個 Promise,這意味着它已經解析了所有與初始路由相關的異步輸入鈎子和異步組件。如果初始導航已經發生了,那么 promise 就會立即解析。這在服務器端渲染中很有用,可以確保服務器和客戶端的輸出一致。需要注意的是,在服務器端,你需要手動推送初始位置,而在客戶端,路由器會自動從 URL 中獲取初始位置。

函數簽名:

isReady(): Promise<void>

onError

添加一個錯誤處理程序,在導航期間每次發生未捕獲的錯誤時都會調用該處理程序。這包括同步和異步拋出的錯誤、在任何導航守衛中返回或傳遞給 next 的錯誤,以及在試圖解析渲染路由所需的異步組件時發生的錯誤。

函數簽名:

onError(handler: (error: any, to: RouteLocationNormalized, from: RouteLocationNormalized) => any): () => void

參數

參數 類型 描述
handler (error: any, to: RouteLocationNormalized, from: RouteLocationNormalized) => any error

resolve

將路由地址轉成 RouteLocation 。包括一個包含任何現有 basehref 屬性。

函數簽名:

1
2
3
resolve(to: RouteLocationRaw): RouteLocation & { href: string }

參數

參數 類型 描述
to RouteLocationRaw 要解析的原始路由地址

Vue Vuex

Vuex 是一個專為 Vue.js 應用程序開發的**狀態管理模式 + 庫**。它采用集中式存儲管理應用的所有組件的狀態。以一個全局單例模式來存儲狀態數據。非持久。

簡言之就是存儲狀態的地方。

每一個 Vuex 應用的核心就是 store(倉庫)。“store”基本上就是一個容器,它包含着你的應用中大部分的**狀態 (state)**。Vuex 和單純的全局對象有以下兩點不同:

  1. Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那么相應的組件也會相應地得到高效更新。
  2. 你不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地**提交 (commit) mutation**。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地了解我們的應用。

創建 vuex 非常簡單

1
2
3
4
5
6
7
8
9
import { createStore } from "vuex";

export default createStore({
state: {}, // Vuex 狀態
mutations: {}, // 暴露接口給外部組件調用
actions: {}, // 調用 mutations 中的方法等,也暴露接口給外部組件調用
getter: {}, // 從 store 中的 state 中派生出一些狀態屬性
modules: {}, // state 數據很大時,將 store 分割成模塊(module)
});

詳細案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { createStore } from "vuex";

export default createStore({
// 1、狀態集合
// state: {} 等價如下
state: () => ({
count: 1,
arrays: [1, 2, 3],
objects: {
x: 1,
y: [4, 5, 6],
},
}),

// 2、自定義函數式修改狀態的地方
mutations: {
// 修改 count狀態,外部通過 store.commit("increment"); 調用
increment(state) {
state.count++;
},
// ES6 的參數解構來簡化代碼
increment2({ commit }) {
commit("increment");
},
},

// 3、Action 類似於 mutation,不同在於:
// Action 提交的是 mutation,而不是直接變更狀態
// Action 函數接受一個與 store 實例具有相同方法和屬性的 context 對象
// ction 通過 store.dispatch 方法觸發 store.dispatch("increment");
actions: {
increment(context) {
context.commit("increment");
},
// ES6 的參數解構來簡化代碼
increment2({ commit }) {
commit("increment");
},
},

// 4、從 store 中的 state 中派生出一些狀態
getters: {
doneTodos: (state) => {
return state.arrays.filter((e) => e % 2 == 0).length;
},
},

// 5、state 數據很大時,將 store 分割成模塊(module)。
// 每個模塊擁有自己的 state、mutation、action、getter
modules: {
// 使用 store.state.a;
a: { state: () => ({}), mutations: {}, actions: {}, getters: {} },
// 使用 store.state.a;
b: { state: () => ({}), mutations: {}, actions: {}, getters: {} },
},
});

state 狀態

由於 Vuex 的狀態存儲是響應式的,從 store 實例中讀取狀態最簡單的方法就是在 計算屬性 中返回某個狀態:

state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createStore } from "vuex";

export default createStore({
// 1、狀態集合
// state: {} 等價如下
state: () => ({
count: 1,
arrays: [1, 2, 3],
objects: {
x: 1,
y: [4, 5, 6],
},
}),
});

組件中使用

1
2
3
4
5
6
7
8
9
// 創建一個 Counter 組件
const Counter = {
  template: `<div>{{ count }}</div>`, computed: { count () { return this.$store.state.count // 只讀 } } } 

每當 store.state.count 變化的時候, 都會重新求取計算屬性,並且觸發更新相關聯的 DOM

mutation 修改

更改 Vuexstore 中的狀態的唯一方法是提交 mutation

Vuex 中的 mutation 非常類似於事件:每個 mutation 都有一個字符串的**事件類型 (type)**和一個**回調函數 (handler)**。

這個回調函數就是我們實際進行狀態更改的地方,並且它會接受 state 作為第一個參數:

1
2
3
4
5
6
7
8
9
10
11
const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 變更狀態
      state.count++
    }
  }
})

不能直接調用一個 mutation 處理函數。調用此函數。需要調用 store.commit 方法:

store.commit('increment')

提交載荷(Payload)額外參數

你可以向 store.commit 傳入額外的參數,即 mutation 的**載荷(payload)**:

1
2
3
4
5
6
mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)

在大多數情況下,載荷應該是一個對象,這樣可以包含多個字段並且記錄的 mutation 會更易讀:

1
2
3
4
5
6
7
8
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
store.commit('increment', {
  amount: 10
})

action 異步操作

Action 類似於 mutation,不同在於:

  • Action 提交的是 mutation,而不是直接變更狀態。
  • Action 可以包含任意異步操作。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 函數接受一個與 store 實例具有相同方法和屬性的 context 對象,因此你可以調用 context.commit 提交一個 mutation,或者通過 context.statecontext.getters 來獲取 stategetters

context 對象不是 store 實例本身

經常用到 ES2015 的參數解構來簡化代碼(特別是我們需要調用 commit 很多次的時候):

1
2
3
4
5
actions: {
  increment ({ commit }) { commit('increment') } } 

也不能直接調用一個 action 處理函數。調用此函數。需要調用 store.dispatch 方法:

store.dispatch('increment')

乍一眼看上去感覺多此一舉,我們直接分發 mutation 豈不更方便?

實際上並非如此,還記得 mutation 必須同步執行這個限制么?

Action 就不受約束!我們可以在 action 內部執行**異步**操作:

1
2
3
4
5
6
7
actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

Actions 支持同樣的載荷方式和對象方式進行分發:

1
2
3
4
5
6
7
8
9
10
// 以載荷形式分發
store.dispatch('incrementAsync', {
  amount: 10
})

// 以對象形式分發
store.dispatch({
type: 'incrementAsync',
amount: 10
})

來看一個更加實際的購物車示例,涉及到**調用異步 API** 和**分發多重 mutation**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
actions: {
  checkout ({ commit, state }, products) {
    // 把當前購物車的物品備份起來
    const savedCartItems = [...state.cart.added]
    // 發出結賬請求,然后樂觀地清空購物車
    commit(types.CHECKOUT_REQUEST)
    // 購物 API 接受一個成功回調和一個失敗回調
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失敗操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

注意我們正在進行一系列的異步操作,並且通過提交 mutation 來記錄 action 產生的副作用(即狀態變更)。

getter

有時候我們需要從 store 中的 state 中派生出一些狀態,例如對列表進行過濾並計數:

1
2
3
4
5
computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

如果有多個組件需要用到此屬性,我們要么復制這個函數,或者抽取到一個共享函數然后在多處導入它——無論哪種方式都不是很理想。

Vuex 允許我們在 store 中定義“getter”(可以認為是 store 的計算屬性)。

Getter 接受 state 作為其第一個參數:

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = createStore({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: (state) => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

Getter 會暴露為 store.getters 對象,你可以以屬性的形式訪問這些值:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

Getter 也可以接受其他 getter 作為第二個參數:

1
2
3
4
5
6
7
8
9
getters: {
  doneTodos: (state) => {
    return state.todos.filter(todo => todo.done)
  },
  doneTodosCount (state, getters) {
    return getters.doneTodos.length
  }
}
store.getters.doneTodosCount // -> 1

可以很容易地在任何組件中使用它:

1
2
3
4
5
computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

注意,getter 在通過屬性訪問時是作為 Vue 的響應式系統的一部分緩存其中的。

通過方法訪問

你也可以通過讓 getter 返回一個函數,來實現給 getter 傳參。在你對 store 里的數組進行查詢時非常有用。

1
2
3
4
5
6
7
getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

注意,getter 在通過方法訪問時,每次都會去進行調用,而不會緩存結果。

module

由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常復雜時,store 對象就有可能變得相當臃腫。

為了解決以上問題,Vuex 允許我們將 store 分割成**模塊(module)**。每個模塊擁有自己的 statemutationactiongetter、甚至是嵌套子模塊——從上至下進行同樣方式的分割:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}

const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})

store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態

模塊的局部狀態

對於模塊內部的 mutationgetter,接收的第一個參數是**模塊的局部狀態對象**。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 這里的 `state` 對象是模塊的局部狀態
      state.count++
    }
  },

getters: {
doubleCount (state) {
return state.count * 2
}
}
}

根節點狀態

同樣,對於模塊內部的 action,局部狀態通過 context.state 暴露出來,根節點狀態則為 context.rootState

1
2
3
4
5
6
7
8
9
10
const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

對於模塊內部的 getter,根節點狀態會作為第三個參數暴露出來:

1
2
3
4
5
6
7
8
const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

組合式函數

useStore

  • useStore<S = any>(injectKey?: InjectionKey<Store<S>> | string): Store<S>;

setup 鈎子函數中調用該方法可以獲取注入的 store。當使用組合式 API 時,可以通過調用該方法檢索 store

1
2
3
4
5
6
7
import { useStore } from 'vuex'

export default {
setup () {
const store = useStore()
}
}

輔助函數

mapState

當一個組件需要獲取多個狀態的時候,可以使用 mapState 輔助函數幫助我們生成計算屬性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在單獨構建的版本中輔助函數為 Vuex.mapState
import { mapState } from 'vuex'

export default {
computed: mapState({
// 箭頭函數可使代碼更簡練
count: state => state.count,

<span class="hljs-comment">// 傳字符串參數 'count' 等同於 `state =&gt; state.count`</span>
countAlias: <span class="hljs-string">'count'</span>,

<span class="hljs-comment">// 為了能夠使用 `this` 獲取局部狀態,必須使用常規函數</span>
countPlusLocalState (state) {
  <span class="hljs-keyword">return</span> state.count + <span class="hljs-keyword">this</span>.localCount
}

})
}

mapState 函數返回的是一個對象。我們如何將它與局部計算屬性混合使用呢?通常,我們需要使用一個工具函數將多個對象合並為一個,以使我們可以將最終對象傳給 computed 屬性。但是自從有了對象展開運算符,我們可以極大地簡化寫法:

1
2
3
4
5
6
7
computed: {
  localComputed () { /* ... */ },
  // 使用對象展開運算符將此對象混入到外部對象中
  ...mapState({
    // ...
  })
}

mapMutations

可以使用 mapMutations 輔助函數將組件中的 methods 映射為 store.commit 調用(需要在根節點注入 store)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { mapMutations } from 'vuex'

export default {
// ...
methods: {
...mapMutations([
'increment', // 將 this.increment() 映射為 this.$store.commit('increment')

  <span class="hljs-comment">// `mapMutations` 也支持載荷:</span>
  <span class="hljs-string">'incrementBy'</span> <span class="hljs-comment">// 將 `this.incrementBy(amount)` 映射為 `this.$store.commit('incrementBy', amount)`</span>
]),
...mapMutations({
  add: <span class="hljs-string">'increment'</span> <span class="hljs-comment">// 將 `this.add()` 映射為 `this.$store.commit('increment')`</span>
})

}
}

mapActions

使用 mapActions 輔助函數將組件的 methods 映射為 store.dispatch 調用(需要先在根節點注入 store):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { mapActions } from 'vuex'

export default {
// ...
methods: {
...mapActions([
'increment', // 將 this.increment() 映射為 this.$store.dispatch('increment')

  <span class="hljs-comment">// `mapActions` 也支持載荷:</span>
  <span class="hljs-string">'incrementBy'</span> <span class="hljs-comment">// 將 `this.incrementBy(amount)` 映射為 `this.$store.dispatch('incrementBy', amount)`</span>
]),
...mapActions({
  add: <span class="hljs-string">'increment'</span> <span class="hljs-comment">// 將 `this.add()` 映射為 `this.$store.dispatch('increment')`</span>
})

}
}

作業

參照cli自動生成的vue3項目從零創建一個項目

    </article>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM