第22章-前端核心技術-VUE生態系統
學習目標
- 掌握
vue
cli
的特征 - 掌握
vue
router
的使用重點
難點
- 掌握
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
創建的項目中。是構建於 webpack
和 webpack-dev-server
之上的。它包含了: serve
、build
和 inspect
命令。
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
創建的項目相同的默認設置 (webpack
、Babel
、PostCSS
和 ESLint
)。它會在當前目錄自動推導入口文件——入口可以是 main.js
、index.js
、App.vue
或 app.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
代碼中識別和報告模式匹配的工具,它的目標是保證代碼的一致性和避免錯誤。在許多方面,它和 JSLint
、JSHint
相似。官網
安裝ESLint
之后會在你的文件夾中自動創建 .eslintrc
文件。可以在 .eslintrc
文件中看到許多像這樣的規則:
1
2
3
4
5
6
{
"rules": { "semi": ["error", "always"], // 是否必須使用分號 "quotes": ["error", "double"] // 引號只能使用雙引號 } }
"semi"
和 "quotes"
是 ESLint
中 規則 的名稱。第一個值是錯誤級別,可以使下面的值之一:
"off"
or0
- 關閉規則"warn"
or1
- 將規則視為一個警告(不會影響退出碼)"error"
or2
- 將規則視為一個錯誤 (退出碼為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" } }
可以通過 npm
或 Yarn
調用這些 script
:
1
2
3
4
5
npm run serve
npm run build
# OR
yarn serve
yarn build
如果你可以使用 npx
(最新版的 npm
應該已經自帶),也可以直接這樣調用命令。
npx
是npm5.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
Router
是 Vue.js
的官方路由。它與 Vue.js
核心深度集成,讓用 Vue.js
構建單頁應用變得輕而易舉。使用 CLI
創建項目的時候可以選擇添加到項目中。
Route
使用過程
(1)模板中添加路由元素
如:
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.$router
或 this.$route
。
作為替代,可以使用 useRouter
和 useRoute
函數。
但是在模板中仍然可以直接使用 $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(
() => route.params,
async newParams => {
userData.value = await fetchUser(newParams.id)
}
)
},
}
Router
對象屬性
Router
中有兩個只讀屬性currentRoute
和options
:
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) => {
setTimeout(() => {
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
。 所有的 alias
和 path
值必須共享相同的參數。
“別名”的功能讓你可以自由地將 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
)。而 params
、query
和 hash
一定不要這樣,因為它們會被路由編碼。
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 。包括一個包含任何現有 base
的 href
屬性。
函數簽名:
1
2
3
resolve(to: RouteLocationRaw): RouteLocation & { href: string }
參數
參數 | 類型 | 描述 |
---|---|---|
to | RouteLocationRaw |
要解析的原始路由地址 |
Vue
Vuex
Vuex
是一個專為 Vue.js
應用程序開發的**狀態管理模式 + 庫**。它采用集中式存儲管理應用的所有組件的狀態。以一個全局單例模式來存儲狀態數據。非持久。
簡言之就是存儲狀態的地方。
每一個 Vuex
應用的核心就是 store
(倉庫)。“store
”基本上就是一個容器,它包含着你的應用中大部分的**狀態 (state
)**。Vuex
和單純的全局對象有以下兩點不同:
Vuex
的狀態存儲是響應式的。當Vue
組件從store
中讀取狀態的時候,若store
中的狀態發生變化,那么相應的組件也會相應地得到高效更新。- 你不能直接改變
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
修改
更改 Vuex
的 store
中的狀態的唯一方法是提交 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.state
和 context.getters
來獲取 state
和 getters
。
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
)**。每個模塊擁有自己的 state
、mutation
、action
、getter
、甚至是嵌套子模塊——從上至下進行同樣方式的分割:
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 的狀態
模塊的局部狀態
對於模塊內部的 mutation
和 getter
,接收的第一個參數是**模塊的局部狀態對象**。
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 => 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>