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