Vue.js 是一个用于创建用户界面的开源 JavaScript 框架,也是一个创建单页应用的 Web 应用框架。目前正式版迭代到了 v2。值得一提的是,Vue 的作者是国人尤雨溪,最大的好处就是文档的中文支持非常快,目前还在 v3.x-beta 的教程仅支持中英文。

Vuetify 是一个纯手工精心打造的 Material 样式的 Vue UI 组件库。目前迭代到了 v2。

我开发的第一个(目前也是唯一一个) Vue 应用使用的组件库就是 Vuetify。可以说,Vue + Vuetify 让我入了前端的门。在本文中,我将聊聊作为一个前端萌新,在使用 Vue 和 Vuetify 的过程中,学到的前端技巧。

Vue 教程?

零基础开始学习 Vue,我自然想到的是看官方教程。然而官方教程并不是那么好懂:刚看到安装部分,教程给出的数种安装方法就让我傻了眼,无从下手。

于是尝试去找别的教程,居然发现了尤雨溪在知乎上给出的学习路线:新手向:Vue 2.0 的建议学习顺序,其中第二点的 就只用最简单的 <script>,把教程里的例子模仿一遍,理解用法。不推荐上来就直接用 vue-cli 构建项目,尤其是如果没有 Node/Webpack 基础。 这句,对真·零基础新手非常友好。

Vue 开发环境

我刚入坑时,想试试使用 Typescript 来开发 Vue。有人说 WebStorm 对 TS 的支持不行,得用 VS Code,于是我就用 VS Code 写了几天 Typescript。最后,还没写完账户管理部分,就放弃 Typescript 了。

Typescript 的静态类型确实很不错,但是写 Typescript 的时候,是各种 TSLint 报类型错。我也尝试写 .d.ts 文件,但是又遇到了没法引入等等问题。还有就是 TSLint 认为 this.$routerundefined,所以 this.$router.push() 等等都会报错。

上面这些问题或许有方法解决,但是问题实在太多,我最后还是选择使用 JavaScript + WebStorm 开发。

回来发现,WebStorm 还对 Vue Router 和 Vuex 有支持,输入 store.commit() 的时候居然能够提示函数名。真香。

WebStorm 对 Vuex 的支持
WebStorm 对 Vuex 的支持

部署 Vue 应用

如果你的 Vue App 使用了 vue-router 的 history 模式,目前 (2021.2) 是不能使用 GitHub Pages 部署的。

从 uri 的角度理解,是因为 GitHub Pages 服务器会默认将对 /activities/create 的请求理解为 /activities/create/index.html,而 vue-router history 模式想要服务器理解为 /index.html。于是, GitHub Pages 会返回 404。

我尝试了网上的 hack 404.html200.html 的方法,但均无效。

一个解决方案是改用 hash 模式,此模式下链接会变为 /#/activities/create,这个 #(hash) 的存在,会让服务器将这个链接理解为 /index.html#/activities/create。但此法会让链接变丑。

另一个解决方案是用一台服务器来部署前端。Vue Router 文档中针对不同服务器提供了部署方法。不过对于 Caddy,我使用 Vue Router 文档提供的 rewrite 法不能正常运行。于是改用了 try_files,只对 Not Found 的文件进行 rewrite:

1
2
3
4
5
example.com {
root * /path/to/dist
try_files {path} /index.html
file_server
}

最后一个解决方案是我最后采用的,就是白嫖 Azure 的静态 Web 应用。Microsoft Learn 还写了一篇 教程 供大家参考。与 GitHub Pages 不同的是,Azure Static Web App 允许使用路由,只需要在开发 Vue 应用时,将以下 route.json 放在 /public/ 目录下即可(抄 MS Learn 的作业真爽)。

1
2
3
4
5
6
7
8
9
{
"routes": [
{
"route": "/*",
"serve": "/index.html",
"statusCode": 200
}
]
}

后来了解到,Vercel App 也支持部署。

Vue 页面间数据传递

页面间需要传递数据,该怎么办呢?

对于 C 语言,不同函数传递数据无非是两个办法,使用全局变量和传递参数。

在 Vue 中也是类似的思想:全局变量(window 或 Vuex.store)和传递参数(params 或 query)。

1. window

window 就是 DOM 中的全局变量,你可以通过赋值,把任何数据(当前用户信息)、甚至函数和对象(如 Vue 组件)挂到 window 下。

2. Vuex

Vuex

Vuex.Store 类似于 window,可以在不同的页面中使用、更改这些里面的数据。那为什么还需要这个工具呢?

个人理解,Store 更强调的是状态。因此,它和 window 有两个区别:

  1. Store 存储的状态是需要在定义 Store 时就给出的,而不是像 window 一样,随时都可以为其添加新的数据、删除数据。
  2. Store 可以随心所欲地读取,但是不能随心所欲地修改。为了防止对数据的错误修改,Store 要求将修改数据的操作,作为函数提前写入 Store 里(这些操作函数被称为 mutation),然后要求页面调用这些 mutation 来修改数据。在严格模式下,无论何时发生数据变更且不是由 mutation 函数引起的,将会抛出错误。

3. Vue Router 路由时添加 params 参数

可以传递任何对象,但参数不会出现在链接中(除非在 Router 中专门设置了)。

发送方:

1
2
3
4
5
6
7
8
let profile = {id: 1};
$this.router.push({
name: 'NextPage',
params: {
user_profile: profile,
user_id: 233
}
})

接收方:

1
2
$this.route.params.user_id // 233
$this.route.params.user_profile // {id: 1}

如果 RouteConfig 中定义如下:

1
2
3
4
5
6
7
8
const routes = [
{
path: '/user/:user_id',
name: 'UserDetail',
component: UserDetail,
props: true, // props 表示 activityId 参数可以传到组件
},
]

url 会变为 /user/233

4. Vue router 路由时添加 query 参数

和上一种方法类似,但是这种方法的参数会出现在 url 中,且只能传递字符串。

发送方:

1
2
3
4
5
6
$this.router.push({
name: 'NextPage',
query: {
id: '1'
}
})

接收方:

1
$this.route.params.query.id // 1

同时链接会变为:原url?id=1

Vue 生命周期之迷惑钩子函数

生命周期,最大的作用,就是进入一个页面的时候,对这个页面进行初始化;还有就是,从别的页面切换到这个页面的时候,对这个页面的数据初始化。

如果是一个没有缓存的页面,每进入一个页面都需要进行创建并挂载,那么 createdmounted 这两个生命周期钩子函数,二选一,在里面写上自己的初始化代码就可以了。

而对于 Vue 来说,如果使用 <keep-alive> 包裹组件,它会缓存不活动的组件实例,等再次调用时(即使不是同一个 url)并不会发生第二次 createdmounted
这可能会导致下面这种场景:对于用户信息页面,我们进入用户 1 的页面,此时发生 createdmounted,页面信息显示为用户 1 的信息;然后我们切换到用户 2 的页面,此时并没有发生 createdmounted,页面仍显示为用户 1 的信息。

因此,我们要采取别的方法。对于 <keep-alive> 包裹的组件,官方文档直接指明使用 activateddeactivated。的确,把初始化代码放在 activated 里,就能代替 created 的初始化功能了。


不过有个地方很奇怪,如果用 <keep-alive> 包裹了一个带有 <v-tab> 的组件(tab 下有几个子组件)。我发现,第一次加载这个页面以及子组件时,子组件并不会触发 activated

经过测试后发现,第一次加载这个页面以及子组件时,子组件并不会触发 activated只会触发 created;而在后面几次进入时,每次都会触发 activated,而不会触发 created

目前尚不清楚不清楚为什么会这样。不过这样的话,可以改用 createdactivated:将初始化函数封装为组件的一个 method,然后把 createdactivated 写成调用这个 method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default {
//...

methods: {
//...

fetchData() {
// 初始化代码
// ...
// ...
}
},

created() {
this.fetchData();
},

activated() {
this.fetchData();
}
};

在单向数据流中实现双向同步数据

父组件使用 .sync 修饰符:

1
<text-document v-bind:title.sync="myTitle"></text-document>

子组件使用 computed

1
2
3
4
5
6
7
8
9
10
computed: {
indexInternal: {
get() {
return this.index;
},
set(val) {
this.emit('update:index', val);
}
},
}

子组件也可以使用 watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

watch: {
// 外部变量
index() {
if (this.indexInternal !== this.index) {
this.indexInternal = this.index;
}
},
// 内部变量
indexInternal() {
if (this.indexInternal !== this.index) {
this.$emit('update:index', this.indexInternal);
}
},
}

Vuex 模块化太难用?

尝试一下 vuex-pathify!其核心语法就如下图:

vuex-pathify
vuex-pathify

另外还直接提供了 get(类似于 mapGetters 的单向数据流)和 sync(这是 Vuex 没有的双向数据流!)。


但是啊但是,Webstorm 完全不支持对 vuex-pathify 进行自动补全,重构代码太麻烦了,再加上不熟悉 vuex-pathify,最终还是放弃了 vuex-pathify。快进到放弃 Vue 2

Vuetify 间距系统

在抄 Vuetify 示例组件的时候常常看到 class = "ma-0" 等此类标识,这个是什么呢?

Vuetify 深受 Bootstrap 的启发,在这部分也一样。我在 Google 找到了 Bootstrap 的间距。大概意思是:

ma-0

  • m 表示 margin;也可以取 p 表示 padding
  • a 表示 all;也可以取 t b l r 分别表示四边;也可以使用 xy 分别表示左右和上下;
  • 0 表示设置为 0;也可以取 1~5,间距从 0 依次增大;还可以取 auto

所以,可以理解为 class="ma-0"style="margin: 0 !important" 的简写。


而 Vuetify 的 ma-1 和 Bootstrap 有一点不同,在最后的数字上:

  • Vuetify 的数字取值范围在 0~16,且每个单位都代表 4px(即 16 代表 64px),还可以使用 n16 表示负 64px;而 Bootstrap 的 0 到 5 是从 0$spacer * 3 不均匀增加的。

vuetify.css 也能看到这部分的定义:

vuetify.css
vuetify.css

Vuetify 网格系统

Vuetify 网格深受 Bootstrap 网格的启发,所以如果有 Bootstrap 基础,Vuetify 上手应该会很快,反之亦然。可惜我都没有

Vuetify 网格系统包含四个核心子组件:

  • v-container 代表一个网格
  • v-row 代表一行 (row)
  • v-col 代表一列 (column)。注意 v-col 必须是 v-row 的子组件,即一定是 v-container > v-row > v-col 的顺序
  • v-spacer:可以在 v-row 之间或 v-col 之间放置一个 v-spacer,这样父子组件之间的剩余宽度就会被分配到这里

下面我将借助两个实际的例子聊聊网格系统的作用。


网格系统 例 1
网格系统 例 1

问:上图中用个几个网格 v-container

答案是两个。

我刚接触 Vuetify 的时候,也觉得只需要一个网格将表单框起来、每个 row 对应一个输入项就可以了。

然而,当我只使用一个 v-container 时,效果图是这样的:

使用一个网格系统的效果
使用一个网格系统的效果

可以看到,最外层的 v-card 和边框合为一体了。而如果在 v-card 外面加一层 v-container v-row v-col,就可以把 v-card 框在中间的位置。

第一张图的部分代码如下:

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
<template>
<!-- 外层的 container、row、col 是为了限制 card 的布局 -->
<v-container>
<v-row class="justify-center">
<v-col xs="12" md="8">
<v-card>
<v-card-text>
<v-form @submit.prevent="register">
<!-- 内层的 container、col 是为了限制 form 的布局 -->
<v-container>

<v-col>
<v-text-field/>
</v-col>

<!-- 更多 text-field -->

<v-col>
<v-text-field/>
</v-col>

</v-container>
</v-form>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>

完整代码

顺便一提,Vuetify 网格系统文档中介绍到,Vuetify 配备了一个使用 flexbox 构建的 12 格网格系统。<v-col xs="12" md="8"> 表示,对于 md 的屏幕,这列将占 8 格,也就是 2/3;对于 xs 的屏幕,这列将占据 12 格,也就是全部。

这和 Bootstrap 是也类似的,所以如果在 Vuetify 中遇到不懂的概念,除了在 Vuetify 文档中搜索,还可以尝试在 Bootstrap 里搜索。


再来一个例子,下图中用了几个网格系统?

网格系统 例 2
网格系统 例 2

答案是 3 个,在下图中有标出:

三个网格系统
三个网格系统

完整代码:

把别人的组件封装成自己的

无脑复制

大多数后端项目的缩进都是 4 spaces,但前端则是使用 2 spaces。因为前端实在是太容易套娃了,随随便便就能搞个五层、十层以上。上面的例子用了三个网格系统,每一个网格系统对应一个 v-containerv-rowv-column,光是网格系统也有九层缩进了……

所以,将别人的组件封装成自己的,不仅能减少重复代码量,还能大幅减少缩进。

比如,上面的网格部分提到,最外层的 v-card 都需要加一个网格系统。所以我做了如下封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- /src/components/ui/base/simple-card.vue -->
<template>
<v-container>
<v-row class="justify-center">
<v-col xs="12" md="8">
<v-card>
<v-card-text>
<slot/> <!-- 这里会把调用 SimpleCard 处的 <SimpleCard> </SimpleCard> 之间的代码插入 -->
</v-card-text>
</v-card>
</v-col>
</v-row>

</v-container>
</template>

<script>
export default {
};
</script>

这样,网格系统例 1 的部分代码就可以缩减为如下:

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
<template>
<SimpleCard>
<v-form @submit.prevent="register">
<!-- 内层的 container、col 是为了限制 form 的布局 -->
<v-container>

<v-col>
<v-text-field/>
</v-col>

<!-- 更多 text-field -->

<v-col>
<v-text-field/>
</v-col>

</v-container>
</v-form>
</SimpleCard>
</template>

<script>
import SimpleCard from "@/components/ui/base/simple-card";

export default {
components: {SimpleCard},
}
</script>

直接少了 4 层缩进。

把父组件 props 传递给子组件 props

现在的需求是,有些页面不想用 xs="8",而想用 xs="6"。用编程中的函数来讲,就是想要默认参数。我们想要使用:

1
2
3
4
5
6
7
8
<template>
<
SimpleCard
md="6"
>
<!-- ... -->
<SimpleCard>
</template>

就能在子组件中渲染出 <v-col md="6">。而在其他时候,依然渲染 <v-col md="8">

只需要使用在子组件的 component 定义中添加 props 即可。

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
<!-- /src/components/ui/base/simple-card.vue -->
<template>
<v-container>
<v-row class="justify-center">
<v-col :xs="xs" :md="md">
<v-card>
<v-card-text>
<slot/> <!-- 这里会把调用 SimpleCard 处的 <SimpleCard> </SimpleCard> 之间的代码插入 -->
</v-card-text>
</v-card>
</v-col>
</v-row>

</v-container>
</template>

<script>
export default {
props: {
xs: {
type: Number,
default: 12
},
md: {
type: Number,
default: 8
}
}
};
</script>

props 就类似于函数的参数,default 即是默认参数。

将父组件所有多余的 props 自动传给子组件

这个问题出现在我想要封装 tooltipsfab 的时候。

首先我们依照 Vuetify 给的模板编写的可用的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<!-- 鼠标放置按钮之上可以看到提示 -->
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<v-btn
fab
large
buttom
absolute
right
v-bind="attrs"
v-on="on"
>
<v-icon>mdi-home</v-icon>
</v-btn>
</template>
<span>Tooltips</span>
</v-tooltip>
</template>

我们先按上文,将 Tooltips 和 图标的内容改为 props:

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
<!-- /src/components/ui/base/floating-action-button.vue -->
<template>
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-btn
fab
large
buttom
absolute
right
v-bind="attrs"
v-on="on"
>
<v-icon>mdi-home</v-icon>
</v-btn>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>
</template>

<script>
export default {
props: {
icon: {
type: String,
default: 'mdi-plus'
},
tooltip: {
type: String,
default: ''
}
},
};
</script>

这样就可以通过下面的形式调用这个组件了。

1
2
3
4
5
6
7
8
9
10
11
<template>
<FloatingActionButton />
</template>

<script>
import FloatingActionButton from "@/components/ui/base/floating-action-button";

export default {
components: {FloatingActionButton},
}
</script>

自定义了 tooltipicon 后我们发现,由于 v-btn 上能设置的属性可就太多了(如 disabled color loading 等等),把这些一个一个写到 props 里,实在不美观。有没有简洁的方法,使得我写 <FloatingActionButton loading disabled/> 就能把这两个参数传给子组件的 v-btn 中呢?


Vue 文档中提到了这种情况。Vue 把这里的 disabled color 等称为非 Prop 的 Attribute

一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 prop 定义的 attribute。

文档指出,这些非 prop 的 attribute,会默认替换/合并根组件已有的 attribute。不巧的是,我们的 <FloatingActionButton> 的根组件是 <v-tooltips>,所以这些 attribute 被默认放到了 <v-tooltips> 上。

为了打破这种默认情况,我们需要先在 component 定义中加入 inheritAttrs: false 禁止继承给根组件 <v-tooltips>,然后手动给需要的组件绑定 $attrs,类似于下面:

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
<!-- /src/components/ui/base/floating-action-button.vue -->
<template>
<!-- 鼠标放置按钮之上可以看到提示 -->
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<v-btn
fab
large
buttom
absolute
right
v-bind="attrs"
v-bind="$attrs"
v-on="on"
>
<v-icon>{{ icon }}</v-icon>
</v-btn>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>
</template>

<script>
export default {
inheritAttrs: false,
// 不让组件的根元素继承 attribute,而手动将 $attrs 赋给 v-btn
props: {
icon: {
type: String,
default: 'mdi-plus'
},
tooltip: {
type: String,
default: ''
}
},
};
</script>

对于其他组件,就是这个效果。然而,对于这个组件,问题在于 Vuetify 的模板代码包含了一个 v-bind="attrs",和我们的 v-bind 冲突了。我们需要把这两个 v-bind 合并。

而对于 JS,可以使用 {...$attrs, ...attrs} 的语法合并这两个 Object 为一个。于是,我们将 template 部分代码改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- /src/components/ui/base/floating-action-button.vue -->
<template>
<!-- 鼠标放置按钮之上可以看到提示 -->
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<v-btn
fab
large
buttom
absolute
right
v-bind="{...$attrs, ...attrs}"
v-on="on"
>
<v-icon>{{ icon }}</v-icon>
</v-btn>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>
</template>

就可以使用了。

1
2
3
4
5
6
<FloatingActionButton
icon="mdi-pencil"
color="primary"
tooltip="编辑"
@click="gotoEditUserDetail"
/>

让子组件 click 事件能触发父组件的 v-on:click 事件

还是接着上面的 tooltips + fab。

其实非常简单,只需要在 template 中添加一行 @click="$emit('click')",让子组件 <v-btn>v-on:click 事件设置为触发 click 事件,让其向上传递即可,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- /src/components/ui/base/floating-action-button.vue -->
<template>
<!-- 鼠标放置按钮之上可以看到提示 -->
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<v-btn
fab
large
buttom
absolute
right
v-bind="{...attrs, ...$attrs}"
v-on="on"
@click="$emit('click')"
>
<v-icon>{{ icon }}</v-icon>
</v-btn>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>
</template>

<script> 部分则完全不需要修改。

嵌套 slot

这里的场景是想要把某个自定义组件封装成一个组件(下称为中间层),以供(外层)复用。该自定义组件中用到的 Vuetify 组件(内层)用到了 slot 语法,想把这个内层的 slot 暴露给外层。

外层的写法和 Vuetify 的写法一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
<PasswordEditDialog
:user="userProfile"
>
<template v-slot:activator="{on, attrs}">
<v-btn
color="warning"
v-bind="attrs"
v-on="on"
>
修改密码
</v-btn>
</template>
</PasswordEditDialog>

中间层需要注意的是, v-bind 的东西是整个插槽 prop

1
2
3
4
5
6
7
8
<v-dialog
v-model="dialog"
transition="dialog-bottom-transition"
>
<template v-slot:activator="defaultProps">
<slot name="activator" v-bind="defaultProps"/>
</template>
</v-dialog>

Vuetify 网格居中

对于我来说,居中、向右对齐一直是个头疼的问题。各种组件的对齐方法都不同,况且对齐还分为了上下对齐和左右对齐。网上似乎也没有什么总结性的帖子,就只好记录一下自己编码的时候遇到的坑了。

但是最后发现遇到的大部分问题都可以用网格系统来解决。

网格系统中的居中理论

网格系统中,如果想要上下对齐,可以使用 <v-row>alignalign-content 属性;想要左右对齐,可以使用 <v-row>justify 属性。 v-row 文档

对于 alignalign-content 的区别,请看:知乎 - 弹性盒子 align-items 与 align-content 的区别。简单来说,align 或 CSS 中的 align-items 控制当前行中的列的行为,而 align-content 控制的是所有行的行为。

illustration of align-items
illustration of align-items

上图是 align,下图是 align-content

illustration of align-content
illustration of align-content

<v-col> 也提供了一个 align-self,不过不知道有什么用。v-col 文档

利用 css text-align 居中

但是以上都没有解决我的需求。我的需求是让下面的 button 能在网格居中,而不是像下图一样靠左。

错误示范
错误示范

在 Google “按钮居中 css” – 跳转到 csdn 找到了一种方法:在按钮的上一层(在这里就是 <v-col>)加一个 css: text-align: center

1
<v-col style="text-align: center">
添加 css
添加 css

但是这个方法并不能使右边的 v-switch 居中(可能只能居中文字?)。


顺便一提,Vuetify 中,style="text-align: center" 可以用 class="text-center" 简写。

利用 v-row justify center

真正的解决方法在 GitHub issue 上找到了:在 <v-col> 内 再添加一层 <v-row justify="center">

正确示范
正确示范
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
<v-container>
<v-row align="center">
<v-col cols="4">
<v-row justify="center">
<v-fade-transition>
<v-btn
color="warning"
@click="showChangePasswordForm=!showChangePasswordForm"
>
{{ showChangePasswordForm ? '不想改了' : '修改密码' }}
</v-btn>
</v-fade-transition>
</v-row>
</v-col>

<v-col cols="4">
<v-row justify="center">
<v-switch
label="订阅邮件推送"
v-model="userProfile.subscribe_email"
:disabled="submitting"
/>
</v-row>
</v-col>

<v-col cols="4">
<v-row justify="center">
<v-btn
:disabled="!formValid"
:loading="submitting"
:color="submitColor"
@click="submit"
>
<v-icon v-if="success">
mdi-check
</v-icon>
<template v-else>
更新信息
</template>
</v-btn>
</v-row>
</v-col>
</v-row>
<v-container>

利用 v-row fill-height 上下居中

在阅读 利用 <v-img> 构建一个 Gallery 的代码中注意到,示例代码能让 <v-progress-circular> 上下和左右居中。

使用 v-row 就可以完成左右居中,而必须同时加上 v-row class="fill-height ma-0" 才可以上下居中。

Vuetify 显示头像

Vuetify 用一个 v-card 来展示头像、用 v-responsive 控制 card 为正方形、再配上 v-skeleton-loader 作为占位符会比较舒服。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<v-container>
<v-row align="end">
<v-col cols="4">
<v-row justify="start">
<v-card>
<v-responsive aspect-ratio="1" width="200px">
<v-img
:src="userProfile.avatar_url"
aspect-ratio="1"
>
<template v-slot:placeholder>
<v-responsive>
<v-skeleton-loader type="image@3"/>
<!-- 一个 image 固定 200px,要想 height 变大,就得多几个 image -->
</v-responsive>
</template>
</v-img>
</v-responsive>
</v-card>
</v-row>
</v-col>
</v-row>
</v-container>
效果图
效果图

额 就是编码 gif 的时候颜色有点失真。

编辑页面弹窗阻止用户退出

直接 Google 没搜到结果,过几天换了一个关键词搜,居然就搜到了。

文档

文档说的很清楚了。放在 Vue 里面,如果放弃老浏览器的兼容,直接在 activated 函数加一行即可。另外,退出页面时也记得取消这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default {
//...
activated() {
window.onbeforeunload = () => '系统可能不会保存您所做的更改。';

// 其他 activated 触发的事件
},

deactivated() {
window.onbeforeunload = null;
},
}
</script>

顺便一提,由于安全问题,弹窗显示的内容在较新的浏览器中都不允许自定义。

to 和 href

对于超链接和路由,可以使用 Vue Router 提供的 to 属性,也可以使用 html 提供的 href 属性。to 属性经过编译后也会变成 href 属性。

二者的区别在于:

  1. to 只能针对 Vue 内的页面,href 对 Vue 内外的页面都可以使用;
  2. to 跳转 Vue 内页面不会刷新、不会丢失 Store 数据,而 href 会;

二者对不同 url 的表现如下:

属性-值 to href
/ 开头的页内链接,如 /foo/bar 跳转 跳转
完整 url,如 https://google.com/ 强行解释为站内链接,错误错误 跳转
Vue Router 语法,如 {name: 'foo'} 跳转 不跳转

所以,结论就是:对站内的链接用 to,对站外的链接用 href

如果一个 Array(比如侧边栏)包含跳转到站内的 item,也包含跳转到站外的 item 呢?

答案是:混用 tohref

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
<!-- 部分 html -->
<template v-for="item in items">
<v-list-item
:to="item.to"
:href="item.href"
ripple
active-class="grey lighten-3"
>
<v-list-item-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>

<script>
export default {
data() {
return {
items: [
{
title: '活动',
icon: 'mdi-compass',
to: '/activity',
},
{
title: '用户',
icon: 'mdi-account-multiple',
to: '/user',
},
{
title: '相册',
icon: 'mdi-image-multiple',
to: '/gallery',
},
{
title: '云盘',
icon: 'mdi-cloud',
href: 'https://drive.google.com/',
}
]
}
}
}

编译出来后,每个标签都包含且包含一个 href 属性。