第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>