1. 说明

先上代码片断分享链接: https://developers.weixin.qq.com/s/vl3ws9mA72GG

  • [x] 使用 painter 画图
  • [x] 按钮传递定制化信息

效果如下: 按钮 1

按钮 2

2. 关键代码说明

文件列表如下: 文件列表

1
2
3
4
5
{
"usingComponents": {
"painter": "/components/painter/painter"
}
}
  • painter 为组件,在页面 json 文件中引入;

页面使用 painter 组件

  • 组件使用 palette 传入画图参数,参数中使用 json 格式,内容基本是把 css 转换下写法,可以此在线绘制生成你需要的 json 内容: https://painterjs.github.io/
  • button 通过 data-xxx 传值,js 中通过 e.target.dataset.xxx 获取数据;
  • bindtap 绑定个空的函数,防止默认事件,比如跳转等;

画图成功与失败调用的函数

  • 可拿到生成的图片地址,图片地址格式类似为 http://tmp/xxxxxxxxxxxxxxx,经实践用户转发发送其它人是可以看得到的,这省去了上传后台的麻烦;

设置 palette 数据

  • painter 组件自动监听 palette 数据,一旦有新数据传入,它即自动开始绘制,时间较快;

分享触发函数

  • 函数中通过 e.from 判断是 buttonmenu
  • 加上 showLoading 提示体验好点,后面记得 hideLoading 取消提示;
  • 当前官方版本支持 3 秒的 promise 异步时间,因此绘制图片时间不超过 3 秒才行,现在手机性能较好,我这里 1 秒的定时器来等待图片生成再去拿图片;
  • 为了避免相同图片重复生成,可以增加一定的缓存,我这里使用页面级别的缓存;
  • 最后注意返回这个 promise,因为转发图主要是 imageUrl 来设置;

3. 总结

小程序开发还是有坑和体验不好的地方,但相比前端来讲又稍微简单了点,不过小程序体积大了以后,一个页面上千行代码也是经常的事,所以提前使用上 typescript scss ,多使用 class 封装等对于后续维护有更好的体验。

参考资料: [1] https://github.com/Kujiale-Mobile/Painter [2] https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html#onShareAppMessage-Object-object

1. GPG 安装

Linux 包管理器安装即可,Windows 可在如下链接下载。 下载链接: www.gnupg.org
Windows 下推荐:https://gpg4win.org/download.html

2. GPG 使用

1
2
3
4
# gpg --full-generate-key
gpg --gen-key # 生成密钥(公钥和私钥),按照流程提示进行
gpg --list-keys # 列出当前所有的密钥,检查刚才的密钥是否生成成功
# 列表的一个长字符串即<USER_ID_HASH>

可以通过--output来导出密钥到文件,--armor选项以ASCII码的格式导出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 导出
gpg --output <YOUR_KEY_FILE_NAME>-pub.key --armor --export <USER_ID_HASH>
gpg --output <YOUR_KEY_FILE_NAME>-sec.key --armor --export-secret-key <USER_ID_HASH>

# 导入
gpg --import <YOUR_KEY_FILE_NAME>-pub.key
gpg --allow-secret-key-import --import <YOUR_KEY_FILE_NAME>-sec.key

# 加密
gpg --encrypt -armor -r key-id filename

# 解密
gpg --output 新文件名 --decrypt 加密文件名

# 修改密钥
gpg --edit-key 标识名

# 删除密钥
gpg --delete-secret-keys 标识名

# 删除公钥
gpg --delete-keys 标识名

3. Git 启用 GPG

1
2
git config --global user.signingkey <USER_ID_HASH>
git config --global commit.gpgsign true

bash 环境将 export GPG_TTY=$(tty) 加入到 ~/.bash_profile

Windows 可能还要配置 GPG 程序路径:

1
git config --global gpg.program "D:\Program Files (x86)\GnuPG\bin\gpg.exe"

设置 gpg-agent.conf 延长 gpg 密钥缓存时间,内容设置为:

1
2
3
4
default-cache-ttl 86400
max-cache-ttl 604800
# wsl2 下生效,wsl 中请注释以下行
pinentry-program "/mnt/d/Program Files (x86)/Gpg4win/bin/pinentry.exe"

设置后可能需要通过运行以下命令重新启动 GPG 代理: gpg-connect-agent reloadagent /bye

其中Linux / macOS 路径为:~/.gnupg/gpg-agent.conf,Windows (使用 gpg --version 确认使用的路径),一般为:

1
2
%AppData%\gnupg\gpg-agent.conf
%userprofile%\.gnupg\gpg-agent.conf

Wsl 的 Linux 下可用方案为 ~/.bashrc 下弄 2 个函数,用于缓存密钥密码:

1
2
3
4
5
6
7
8
9
10
gpg-login() {
# 对 "test" 这个字符串进行 gpg 签名,这时候需要输密码。
# 然后密码就会被缓存,下次就不用输密码了。
# 重定向输出到 null,就不会显示到终端中。
echo "test" | gpg --clearsign > /dev/null 2>&1
}

gpg-logout() {
echo RELOADAGENT | gpg-connect-agent
}

VSCode 配置加上:

1
"git.enableCommitSigning": true,

4. Git 服务器添加公钥

将上面生成的公钥<YOUR_KEY_FILE_NAME>-pub.key内容添加至 Git 服务器。

参考资料: [1] https://blog.miniasp.com/post/2020/09/21/Keep-GnuPG-credentials-cached-for-entire-day [2] https://stackoverflow.com/questions/63440623/no-gpg-passphrase-prompt-in-visual-studio-code-on-windows-10-for-signed-git-comm

1. 需求

相信大家都有这样的场景,含敏感信息的配置文件等想加密上传至 git,而且不同分支的配置文件并不相同,合并时不想他们合并。

2. git 删除敏感信息文件

针对可能已经上传了敏感信息的 git 文件,想在记录中彻底删除他们。

1
2
3
4
5
6
cd /path/to/project
git filter-branch --force --index-filter "git rm --cached --ignore-unmatch PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA" --prune-empty --tag-name-filter cat -- --all

# 以下命令要生效到远程仓库,请谨慎
git push origin --force --all # 注意分支保护和权限
git push origin --force --tags # 注意分支保护和权限

3. git 加密方案 git-crypt

安装 git-crypt,支持 yum, apt, brew 等。加密方式使用共享密钥和 GPG 密钥方式二选一即可。

3.1 初始化

1
2
cd /path/to/project
git-crypt init # 类似于 git init,安装 git-crypt 到项目中

3.2 GPG 密钥方式

加密工具 gpg 生成公私钥(生成后注意备份):

1
2
3
4
# gpg --full-generate-key
gpg --gen-key # 生成密钥(公钥和私钥),按照流程提示进行
gpg --list-keys # 列出当前所有的密钥,检查刚才的密钥是否生成成功
git-crypt add-gpg-user youname # 添加密钥用户

3.2 配置 .gitattributes

文件格式和 .gitignore 一样,如:

1
2
3
secretfile filter=git-crypt diff=git-crypt
*.key filter=git-crypt diff=git-crypt
secretdir/** filter=git-crypt diff=git-crypt

上传到 git

1
2
3
4
git rm -r --cached config/  # 清理 config 的 git 缓存
git add .
git commit -m 'chore: git-crypt'
git push

3.3 共享密钥方式

导出密钥,文件 git-crypt-key

1
git-crypt export-key ../git-crypt-key

导出了密钥以后,就可以分发给有需要的团队内部人员。

当团队其他成员获取了代码以后,需要修改配置文件,需要先解密,解密动作只需要做一次,往后就不需要再进行解密了。

解密

1
2
cd /path/to/project
git-crypt unlock /path/to/git-crypt-key

4. 合并忽略指定文件

创建自定义 merge driver:

1
git config --global merge.ours.driver true

在被合并的分支下配置 .gitattributes,如 dev 合并到 master,则在 dev 分支中配置,合并后 .gitattributes 也将合并到 master 分支下。结合上文中的 git-crypt,内容如:

1
2
3
secretfile filter=git-crypt diff=git-crypt merge=ours
*.key filter=git-crypt diff=git-crypt merge=ours
secretdir/** filter=git-crypt diff=git-crypt merge=ours

提交并进行合并即可。

重要

  • 如果 dev 的某个文件修改不需要合并到 master ,则 master 的文件修改时间必须在 dev 的修改时间之后。
  • 上文 dev 向 master 合并,需要先修改 dev 分支的文件并提交,再修改 master 的文件并提交,然后合并。

Git-secret 工具也不错,它具有更多的命令行功能,[6] 中有人总结了他们之间的差异。

参考资料: [1] https://github.com/AGWA/git-crypt
[2] https://einverne.github.io/post/2019/11/git-crypt-usage.html
[3] https://buddy.works/guides/git-crypt
[4] https://embeddedartistry.com/blog/2018/03/15/safely-storing-secrets-in-git/
[5] https://blog.csdn.net/fkaking/article/details/44955663
[6] https://github.com/sobolevn/git-secret/issues/101

1. 环境

系统:CentOS Linux release 7.7.1908 (Core) Kubernetes: 1.25.4 Cri-o: 1.25

2. 安装 crio

根据官方文档:

1
2
3
curl -L -o /etc/yum.repos.d/devel:kubic:libcontainers:stable.repo https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/CentOS_7/devel:kubic:libcontainers:stable.repo
curl -L -o /etc/yum.repos.d/devel:kubic:libcontainers:stable:cri-o:1.25.repo https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/1.25/CentOS_7/devel:kubic:libcontainers:stable:cri-o:1.25.repo
yum -y install cri-o

其它注意修改 /etc/crio/crio.conf 中的 pause_image,可使用 "ygqygq2/pause:3.8"

3. 安装 Kubernetes

此步略:(可使用或参考一键安装k8s脚本),脚本已解决 4 中的问题。

4. 无法运行容器

如果没有更新 runc,可能报如下类似错误:

1
Nov 28 18:48:36 master1 kubelet: E1128 18:48:36.921976    5567 pod_workers.go:965] "Error syncing pod, skipping" err="failed to \"StartContainer\" for \"kube-scheduler\" with CreateContainerError: \"container create failed: time=\\\"2022-11-28T18:48:36+08:00\\\" level=error msg=\\\"container_linux.go:349: starting container process caused \\\\\\\"error adding seccomp filter rule for syscall bdflush: requested action matches default action of filter\\\\\\\"\\\"\\ncontainer_linux.go:349: starting container process caused \\\"error adding seccomp filter rule for syscall bdflush: requested action matches default action of filter\\\"\\n\"" pod="kube-system/kube-scheduler-master1" podUID=ab975606d36a082ab6e36e8ea38bb29d

使用二进制文件 runc 更新下 /usr/bin/runc ,更新前注意备份,然后执行如下命令:

1
2
3
4
5
6
7
    mkdir -p /etc/crio/crio.conf.d
cat >/etc/crio/crio.conf.d/cri-o-runc <<EOF
[crio.runtime.runtimes.runc]
runtime_path = ""
runtime_type = "oci"
runtime_root = "/run/runc"
EOF

重启 crio 后,再次 kubeadm init 即可正常初始化。 cri-o 调试不如 containerd,有时候报错都没有,这个 runc 是因为文档描述了安装 ubuntu 上需要更新,才尝试在 CentOS7 这样处理。

Haproxy 使用 sni 配置 https 多域名 proxy 时,出现 NSS error -12263 (SSL_ERROR_RX_RECORD_TOO_LONG) SSL received a record that exceeded the maximum permissible length. 报错,解决办法是在 haproxy 配置中,将 defaults 下面的 mode 修改成 tcp 即可。 即:

1
2
defaults
mode http

改成

1
2
defaults
mode tcp

1. Plop 简单介绍

Plop是一个小型生成器框架,比如你要创建路由、控制器、组件等代码时,它就用得上了,而且你可以高度定制化它。

2. 安装

使用自己喜欢的 node 包管理工具安装即可,也可全局安装它。如:

1
pnpm install plop -D

package.jsonscripts 中添加命令 "plop": "plop"

3. vue3 项目中使用 plop 示例

3.1 项目根下创建plopfile.cjs

1
2
3
4
module.exports = function (plop) {
plop.setWelcomeMessage("请选择需要创建的模式:");
plop.setGenerator("component", require("./plop-templates/component/prompt.cjs"));
};

3.2 目录/plop-templates/下创建模板目录及模板文件

plop-templates/component/index.hbs 内容:

1
2
3
4
5
6
7
8
9
10
11
<script setup{{#if isGlobal}} name="{{ properCase name }}"{{/if}}>
</script>

<template>
<div>
</div>
</template>

<style lang="scss" scoped>
// scss
</style>

plop-templates/component/prompt.cjs 内容:

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
57
58
const fs = require("fs");

function getFolder(path) {
const components = [];
const files = fs.readdirSync(path);
files.forEach((item) => {
const stat = fs.lstatSync(`${path}/${item}`);
if (stat.isDirectory() === true && item !== "components") {
components.push(`${path}/${item}`);
components.push(...getFolder(`${path}/${item}`));
}
});
return components;
}

module.exports = {
description: "创建组件",
prompts: [
{
type: "confirm",
name: "isGlobal",
message: "是否为全局组件",
default: false,
},
{
type: "list",
name: "path",
message: "请选择组件创建目录",
choices: getFolder("src/views"),
when: (answers) => {
return !answers.isGlobal;
},
},
{
type: "input",
name: "name",
message: "请输入组件名称",
validate: (v) => {
if (!v || v.trim === "") return "组件名称不能为空";
else return true;
},
},
],
actions: (data) => {
let path = "";
if (data.isGlobal) path = "src/components/{{properCase name}}/index.vue";
else path = `${data.path}/components/{{properCase name}}/index.vue`;

const actions = [
{
type: "add",
path,
templateFile: "plop-templates/component/index.hbs",
},
];
return actions;
},
};

4. 命令使用

在项目目录下,使用包管理命令,执行 plop 命令,如:

1
pnpm plop

随后会以交互方式根据 component 模板文件生成 component 文件至相应目录。 是不是非常简单呢?当然,模板文件很复杂,你还需要学会使用 handlebars 语法,一般模板文件需求也不会很复杂,使用的时候查 plop 和 handlebars 文档,功能够用就行,学习成本较低。

参考资料: [1] https://plopjs.com/documentation/ [2] https://handlebarsjs.com/zh/guide/

1. 目的

不管是 k8s 官方 image 还是其它 google docker imag 等,从国内直接拉取不了,此方案使用 github action 作为中转,将 image 上传至 docker hub。

2. 过程

打开我的仓库 https://github.com/ygqygq2/docker-image-mirror,当然也欢迎 fork,修改成自己想要的样子。

新建 issue 创建issue

点击开始 点击开始

输入你要同步的 image 地址,前面[PORTER]加不加不影响。 输入 image 地址

等待 CI 完成 等待完成

转换后的 image 即可直接 docker pullissue详情

Vue 3 父组件调用子组件方法,可以在生命周期函数中直接调用:

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
57
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>父组件调用子组件方法</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
template: `
<div>
父页面
<child ref="sonRef"/>
<button @click="handleClick">test</button>
</div>
`,
setup() {
const { ref, onMounted } = Vue;
const sonRef = ref();

const handleClick = () => {
sonRef.value.song();
};

onMounted(() => {
console.log("这里执行子组件方法", sonRef.value.song());
});

return { sonRef, handleClick };
},
});

app.component("child", {
template: `
<div>
子页面
</div>
`,
setup() {
const song = () => alert("hello world");

return {
song,
};
},
});
app.mount("#root");
</script>
</html>

1. 混入 Mixin

基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const myMixin = {
data() {
return { number: 2 };
},
};

const app = Vue.createApp({
data() {
return { number: 1 };
},
mixins: [myMixin],
template: `
<div>
<div>{{ number }}</div>
</div>
`,
});

const vm = app.mount("#root");
  • 组件 data, methods 优先级高于 mixin data, methods 优先级
  • 生命周期函数先执行 mixin 里的,再执行组件里的
  • 组件中的自定义属性优先级高于 mixin 中的属性优先级
  • 默认 mixin 是局部的,需要声明注入,全局 mixin 直接定义在 app 中,不需要明确注入
  • app.config.optionMergesrategies.number 定义 mixin 属性优先级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const myMixin = {
number: 1,
};

const app = Vue.createApp({
mixins: [myMixin],
number: 2,
template: `
<div>
<div>{{ this.$options.number }}</div>
</div>
`,
});

app.config.optionMergeStrategies.number = (mixinVal, appValue) => {
return mixinVal || appValue;
};

const vm = app.mount("#root");

2. 自定义指令 directives

其支持各种生命周期函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const directives = {
focus: {
mounted(el) {
el.focus();
},
},
};

const app = Vue.createApp({
directives: directives,
template: `
<div>
<input v-focus>
</div>
`,
});

const vm = app.mount("#root");

使用数据控制 directive

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>directives</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
<style>
.header {
position: absolute;
}
</style>
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
data() {
return {
distance: 100,
};
},
template: `
<div>
<div v-pos:top="distance" class="header">
<input />
</div>
</div>
`,
});

// app.directive('pos', (el, binding) => {
// el.style[binding.arg] = (binding.value + 'px');
// });

app.directive("pos", {
mounted(el, binding) {
el.style[binding.arg] = binding.value + "px";
},
updated(el, binding) {
el.style[binding.arg] = binding.value + "px";
},
});

const vm = app.mount("#root");
</script>
</html>

3. 传送门 teleport

默认 html dom 是层层嵌套,teleport 可以把组件里的 dom 元素直接传送到其它位置进行展示。

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
57
58
59
60
61
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>传送门</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
<style>
.area {
position: absolute;
left: 50%;
top: 50%;
width: 200px;
height: 300px;
transform: translate(-50%, -50%);
background-color: green;
}

.mask {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: #000;
opacity: 0.5;
}
</style>
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
data() {
return {
show: false,
};
},
methods: {
handleBtnClick() {
this.show = !this.show;
},
},
template: `
<div class="area">
<button @click="handleBtnClick">按钮</button>
<teleport to="body">
<div class="mask" v-show="show"></div>
</teleport>
</div>
`,
});

const vm = app.mount("#root");
</script>
</html>

4. render 函数

template -> render -> h -> 虚拟 DOM(JS 对象)-> 真实 DOM -> 展示页面上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const app = Vue.createApp({
template: `
<my-title :level="2">
hello world
</my-title>
`,
});

app.component("my-title", {
props: ["level"],
render() {
const { h } = Vue;
return h("h" + this.level, {}, this.$slots.default());
},
});

const vm = app.mount("#root");

5. 插件 plugin

把通用性的功能封装起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const myPlugin = {
install(app, options) {
app.provide("name", "Hello World");
},
};
const app = Vue.createApp({
template: `
<my-title />
`,
});

app.component("my-title", {
inject: ["name"],
template: `<div>{{name}}</div>`,
});

app.use(myPlugin, { name: "test" });

const vm = app.mount("#root");

6. setup 函数

为了增加代码的可维护性,setup 可以将相关代码段聚集在一起。 setup 函数执行在 created 之前,即实例被完全初始化之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const app = Vue.createApp({
template: `
<div @click="handleClick">{{name}}</div>
`,
methods: {
test() {
console.log(this.$options.setup());
},
},
mounted() {
this.test();
},
setup(props, context) {
return {
name: "hello",
handleClick: () => {
alert("click");
},
};
},
});

const vm = app.mount("#root");

7. ref、reactive 响应式引用

原理:通过 proxy 对数据进行封装,当数据变化时, 触发模板等内容的更新

ref 只适合处理基本类型的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const app = Vue.createApp({
template: `
<div>{{name}}</div>
`,
setup(props, context) {
const { ref } = Vue;
// proxy , 'hello' 变成 proxy({value: 'hello'}) 这样的一个响应式引用
let name = ref("hello");
setTimeout(() => {
name.value = "world";
}, 2000);
return {
name,
};
},
});

const vm = app.mount("#root");

reactive 处理非基础类型的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const app = Vue.createApp({
template: `
<div>{{nameObj[0]}}</div>
`,
setup(props, context) {
const { reactive } = Vue;
const nameObj = reactive([123]);
setTimeout(() => {
nameObj[0] = 456;
}, 2000);
return {
nameObj,
};
},
});

const vm = app.mount("#root");

readonly 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const app = Vue.createApp({
template: `
<div>{{nameObj[0]}}</div>
`,
setup(props, context) {
const { reactive, readonly } = Vue;
const nameObj = reactive([123]);
const copyNameObj = readonly(nameObj);
setTimeout(() => {
nameObj[0] = 456;
copyNameObj[0] = 456;
}, 2000);
return {
nameObj,
copyNameObj,
};
},
});

const vm = app.mount("#root");

toRefs 会把 proxy({name: 'hello'}) 转换成 {name: proxy({value: 'hello'})}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const app = Vue.createApp({
template: `
<div>{{name}}</div>
`,
setup(props, context) {
const { reactive, readonly, toRefs } = Vue;
const nameObj = reactive({ name: "hello" });
setTimeout(() => {
nameObj.name = "world";
}, 2000);
const { name } = toRefs(nameObj);
return {
name,
};
},
});

const vm = app.mount("#root");

8. toRef 以及 context

toRef 将属性转换成响应式引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const app = Vue.createApp({
template: `
<div>name: {{name}}, age: {{age}}</div>
`,
setup(props, context) {
const { reactive, toRef } = Vue;
const data = reactive({
name: "hello",
age: 0,
});
const name = toRef(data, "name");
const age = toRef(data, "age");
setTimeout(() => {
age.value = "20";
}, 2000);
return { name, age };
},
});

const vm = app.mount("#root");

contextslots

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const app = Vue.createApp({
template: `
<child>parent</child>
`,
});

app.component("child", {
setup(props, context) {
const { h } = Vue;
const { attrs, slots, emit } = context;
// console.log(attrs.app); // None-Props 属性
return () => h("div", {}, slots.default());
},
});

const vm = app.mount("#root");

contextemit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const app = Vue.createApp({
methods: {
handleChange() {
alert("change");
},
},
template: `
<child @change="handleChange">parent</child>
`,
});

app.component("child", {
template: '<div @click="handleClick">123</div>',
setup(props, context) {
const { h } = Vue;
const { attrs, slots, emit } = context;
function handleClick() {
emit("change");
}
return { handleClick };
},
});

const vm = app.mount("#root");

9. 计算属性 computed

使用 composition api 配合 computed

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
const app = Vue.createApp({
setup() {
const { ref, computed } = Vue;
const count = ref(0);
const handleClick = () => {
count.value += 1;
};
// const countAddFive = computed(() => {
// return count.value + 5;
// });
let countAddFive = computed({
get: () => {
return count.value + 5;
},
set: (param) => {
count.value = param - 5;
},
});
setTimeout(() => {
countAddFive.value = 100;
}, 2000);
return {
count,
handleClick,
countAddFive,
};
},
template: `
<div>
<span @click="handleClick">{{count}}</span> -- {{countAddFive}}
</div>
`,
});

const vm = app.mount("#root");

10. 侦听器 watch 和 watchEffect

watch 具备一定的惰性,参数可以拿到当前值和之前值

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
const app = Vue.createApp({
setup() {
const { reactive, watch, toRefs } = Vue;
const nameObj = reactive({ name: "hello" });
watch(
() => nameObj.name,
(currentValue, preValue) => {
console.log(currentValue, preValue);
}
);
const { name } = toRefs(nameObj);
return { name };
},
template: `
<div>
<div>
Name: <input v-model="name"/>
</div>
<div>
Name is {{ name }}
</div>
</div>
`,
});

const vm = app.mount("#root");

一个侦听多个数据的变化

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
const app = Vue.createApp({
setup() {
const { reactive, watch, toRefs } = Vue;
const nameObj = reactive({ name: "hello", englishName: "world" });
watch([() => nameObj.name, () => nameObj.englishName], ([curName, curEng], [preName, preEng]) => {
console.log(curName, preName, "---", curEng, preEng);
});
const { name, englishName } = toRefs(nameObj);
return { name, englishName };
},
template: `
<div>
<div>
Name: <input v-model="name"/>
</div>
<div>
Name is {{ name }}
</div>
<div>
English Name: <input v-model="englishName"/>
</div>
<div>
English Name is {{ englishName }}
</div>
</div>
`,
});

const vm = app.mount("#root");

watch 和 watchEffect 的区别:

  • watchEffect 没有惰性,立即执行,即代码一加载就执行;
  • 不需要传递要侦听的内容,自动会感知代码依赖,不需要传递很多参数,只要传递一个回调函数
  • 不能获取之前数据的值
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
const app = Vue.createApp({
setup() {
const { reactive, watch, watchEffect, toRefs } = Vue;
const nameObj = reactive({ name: "hello", englishName: "world" });
// watch([() => nameObj.name, () => nameObj.englishName], ([curName, curEng], [preName, preEng]) => {
// console.log(curName, preName, '---', curEng, preEng);
// }, { immediate: true });
const stop = watchEffect(() => {
console.log("nameObj.name", nameObj.name);
setTimeout(() => {
stop();
}, 5000);
});
const { name, englishName } = toRefs(nameObj);
return { name, englishName };
},
template: `
<div>
<div>
Name: <input v-model="name"/>
</div>
<div>
Name is {{ name }}
</div>
<div>
English Name: <input v-model="englishName"/>
</div>
<div>
English Name is {{ englishName }}
</div>
</div>
`,
});

const vm = app.mount("#root");

11. 生命周期函数

mounted => onMounted beforeUpdate => onBeforeUpdate

setup 执行在 beforeCreatecreated 之间,所以没有这 2 个函数对应的 composition api 生命周期函数。

onRenderTracked 渲染后收集响应式的依赖。 onRenderTriggered 每次重新渲染被触发的时候。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const app = Vue.createApp({
template: `
<div>Hello world</div>
`,
setup() {
const {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onRenderTracked,
onRenderTriggered,
} = Vue;
onBeforeMount(() => {
console.log("onBeforeMount");
});
},
});

const vm = app.mount("#root");

12. provide inject ref 用法

使用 readonly 处理单向数据流

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
const app = Vue.createApp({
template: `
<div>
<child />
</div>
`,
setup() {
const { provide, ref, readonly } = Vue;
const name = ref("hello");
provide("name", readonly(name));
provide("changeName", (value) => {
name.value = value;
});
return {};
},
});

app.component("child", {
setup() {
const { inject } = Vue;
const name = inject("name");
const changeName = inject("changeName");
const handleClick = () => {
changeName("world");
};
return { name, handleClick };
},
template: `<div @click="handleClick">{{name}}</div>`,
});

const vm = app.mount("#root");

获取真实的 DOM 元素节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const app = Vue.createApp({
template: `
<div>
<div ref="hello">hello world</div>
</div>
`,
setup() {
const { ref, onMounted } = Vue;
const hello = ref(null);
onMounted(() => {
console.log(hello.value);
});
return { hello };
},
});

const vm = app.mount("#root");

1. 组件间传值及校验

父组件调用子组件的标签,通过标签上的属性,向子组件传值,子组件通过 props 来接收对应属性的内容,后子组件才能使用传递过来的值。

传值校验: required: true 必须传参数 default 默认值 validator 对参数深度校验

2. 单向数据流

父级 prop 的更新会向下流动到子组件中,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。

3. Non-Props 属性

子组件会使用 props:[''] 的方式接收父组件传递的参数,如果子组件不使用 props:[''] 接收参数,那这个参数就是一个 Non-Props 属性。一般在标签中定义 styleclass 时使用。

如果不想把 Non-Props 属性渲染到子组件最外层标签,子组件使用 inheritAttrs:false 即可。

子组件有多个最外层标签时, Vue 不知道该给哪个标签渲染,干脆就不渲染了,如果想指定哪个最外层标签使用 Non-Props 属性,在这个标签中使用 v-bind="$attrs" 即可。

4. 父子组件通过事件通信

子组件不能直接修改父组件的传值,但可以使用 $emit 来告诉父组件进行修改。

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
const app = Vue.createApp({
data() {
return { count: 1 };
},
methods: {
handleAddOne() {
this.count += 1;
},
},
template: `
<div><counter :count="count" @add-one="handleAddOne"/></div>
`,
});

app.component("counter", {
props: ["count"],
methods: {
handleClick() {
this.$emit("addOne");
},
},
template: `
<div @click="handleClick">{{ count }}</div>
`,
});

app.mount("#root");

使用 v-model 改写,其中如果不想用 modelValue 变量名,比如用 app 代替,可以写成 v-model:app,而且可以支持传递多个参数。

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
const app = Vue.createApp({
data() {
return { count: 1 };
},
methods: {
handleAddOne() {
this.count += 1;
},
},
template: `
<counter v-model="count" />
`,
// <counter v-model:app="count" v-model:app1="count"/>
});

app.component("counter", {
props: ["modelValue"],
methods: {
handleClick() {
this.$emit("update:modelValue", this.modelValue + 1);
},
},
template: `
<div @click="handleClick">{{ modelValue }}</div>
`,
});

app.mount("#root");

v-model 传递修饰符:

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
const app = Vue.createApp({
data() {
return {
count: "a",
};
},
template: `
<counter v-model.uppercase="count" />
`,
});

app.component("counter", {
props: {
modelValue: String,
modelModifiers: {
default: () => ({}),
},
},
methods: {
handleClick() {
let newValue = this.modelValue + "b";
if (this.modelModifiers.uppercase) {
newValue = newValue.toUpperCase();
}
this.$emit("update:modelValue", newValue);
},
},
template: `
<div @click="handleClick">
{{modelValue}}
</div>
`,
});

app.mount("#root");

5. slot 插槽

slot 不能绑定事件,外面可以包一层标签绑定事件。 slot 中使用的数据,作用域问题:

  • 父横板里调用的数据属性,使用的都是父模板里的数据
  • 子模板里调用的数据属性,使用的都是子模板里的数据

具名插槽:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const app = Vue.createApp({
template: `
<layout>
<template v-slot:header>
<div>header</div>
</template>
<template v-slot:footer>
<div>footer</div>
</template>
</layout>
`,
});

app.component("layout", {
template: `
<div>
<slot name="header"></slot>
<div>content</div>
<slot name="footer"></slot>
</div>
`,
});

app.mount("#root");

作用域插槽:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const app = Vue.createApp({
template: `
<list v-slot="{item}">
<div>{{ item }}</div>
</list>
`,
});

app.component("list", {
data() {
return { list: [1, 2, 3] };
},
template: `
<div>
<slot v-for="item in list" :item="item"/>
</div>
`,
});

const vm = app.mount("#root");

6. 动态组件和异步组件

动态组件:根据数据的变化,结合 component 这个标签来随时动态切换组件的显示

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
const app = Vue.createApp({
data() {
return { currentItem: "input-item" };
},
methods: {
handleClick() {
this.currentItem === "input-item" ? (this.currentItem = "common-item") : (this.currentItem = "input-item");
},
},
// template: `
// <input-item v-show="currentItem ==='input-item'"/>
// <common-item v-show="currentItem ==='common-item'"/>
// <button @click="handleClick">切换</button>
// `
template: `
<keep-alive>
<component :is="currentItem"/>
</keep-alive>
<button @click="handleClick">切换</button>
`,
});

app.component("input-item", {
template: `
<input />
`,
});

app.component("common-item", {
template: `
<div>hello word</div>
`,
});

const vm = app.mount("#root");

异步组件:是异步执行某些组件的逻辑

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
const app = Vue.createApp({
template: `
<common-item/>
<sync-common-item/>
`,
});

app.component("common-item", {
template: `
<div>hello word</div>
`,
});

app.component(
"sync-common-item",
Vue.defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
template: `
<div>this is an async component</div>
`,
});
}, 4000);
});
})
);

const vm = app.mount("#root");

7. 其它基础知识

v-once 让某个元素标签只渲染一次 ref 实际上是获取 dom 节点/组件 引用的一个语法 provide 传递给孙组件,inject 孙组件接收,其中 provide 要传递 data 内值时,应该写成类似如下:

1
2
3
4
5
provide() {
return {
count: this.count
}
}

8. Vue 中的动画

通过标签增减类名实现动画开关

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>动画效果</title>
<style>
/* 动画 */
@keyframes letToright {
0% {
transform: translateX(-100px);
}

50% {
transform: translateX(-50px);
}

100% {
transform: translateX(0px);
}
}

.animation {
animation: letToright 3s;
}

/* 过渡 */
.transition {
transition: 3s background-color ease;
}

.blue {
background-color: blue;
}

.green {
background-color: green;
}
</style>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
data() {
return {
animate: {
transition: true,
blue: true,
green: false,
animation: false,
},
};
},
methods: {
handleClick() {
this.animate.animation = !this.animate.animation;
this.animate.blue = !this.animate.blue;
this.animate.green = !this.animate.green;
},
},
template: `
<div>
<div :class="animate"> hello world</div>
<button @click="handleClick">切换</button>
</div>
`,
});

const vm = app.mount("#root");
</script>
</html>

通过 style 来控制过渡

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>动画效果</title>
<style>
/* 过渡 */
.transition {
transition: 3s background-color ease;
}
</style>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
data() {
return {
styleObj: {
background: "blue",
},
};
},
methods: {
handleClick() {
if (this.styleObj.background === "blue") {
this.styleObj.background = "green";
} else {
this.styleObj.background = "blue";
}
},
},
template: `
<div>
<div class="transition" :style="styleObj"> hello world</div>
<button @click="handleClick">切换</button>
</div>
`,
});

const vm = app.mount("#root");
</script>
</html>

单元素,单组件的入场出场动画

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>动画效果</title>
<style>
@keyframes shake {
0% {
transform: translate(-100px, 0);
}

50% {
transform: translate(-50px, 0);
}

100% {
transform: translate(50px, 0);
}
}

.v-enter-from {
opacity: 0;
}

.v-enter-to {
opacity: 1;
}

.v-enter-active {
transition: opacity 3s ease-out;
animation: shake 3s;
}

.v-leave-from {
opacity: 1;
}

.v-leave-to {
opacity: 0;
}

.v-leave-active {
transition: opacity 3s ease-in;
animation: shake 3s;
}
</style>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
data() {
return {
show: false,
};
},
methods: {
handleClick() {
this.show = !this.show;
},
},
// 下面如果写成 <transition name="app">,上面 style中 .v-enter-from 这种得写成 .app-enter-from
template: `
<div>
<transition>
<div v-if="show"> hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>
`,
});

const vm = app.mount("#root");
</script>
</html>

自定义 css 样式,或者配合 Animate.css

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
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>动画效果</title>
<style>
@keyframes shake {
0% {
transform: translate(-100px, 0);
}

50% {
transform: translate(-50px, 0);
}

100% {
transform: translate(50px, 0);
}
}

.hello {
transition: opacity 3s ease-out;
animation: shake 3s;
}

.bye {
transition: opacity 3s ease-in;
animation: shake 3s;
}
</style>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
data() {
return {
show: false,
};
},
methods: {
handleClick() {
this.show = !this.show;
},
},
template: `
<div>
<transition enter-active-class="animate__animated animate__bounce" leave-active-class="bye">
<div v-if="show"> hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>
`,
});

const vm = app.mount("#root");
</script>
</html>

type 设置过渡时间以 transition 还是 animation 为准。 duration 设置过渡时间,单位为毫秒。

1
2
<transition :duration="{enter: 1000, leave: 3000}">
</transition>

使用 js 做动画

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
57
58
59
60
61
62
63
64
65
66
67
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>动画效果</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
data() {
return {
show: false,
};
},
methods: {
handleClick() {
this.show = !this.show;
},
handleBeforeEnter(el) {
el.style.color = "red";
},
handleEnterActive(el, done) {
const animation = setInterval(() => {
const color = el.style.color;
if (color === "red") {
el.style.color = "green";
} else {
el.style.color = "red";
}
}, 1000);
setTimeout(() => {
clearInterval(animation);
done();
}, 3000);
done();
},
handleEnterEnd() {
console.log("enter end");
},
},
// 还有 before-leava leava leave-after
template: `
<div>
<transition
:css="false"
@before-enter="handleBeforeEnter"
@enter="handleEnterActive"
@after-enter="handleEnterEnd"
>
<div v-if="show"> hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>
`,
});

const vm = app.mount("#root");
</script>
</html>

单元素多标签切换动画

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
57
58
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>动画效果</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
<style>
.v-enter-from,
.v-leave-to {
opacity: 0;
}

.v-enter-active,
.v-leave-active {
transition: opacity 1s ease-in;
}

.v-enter-to,
.v-leave-from {
opacity: 1;
}
</style>
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
data() {
return {
show: false,
};
},
methods: {
handleClick() {
this.show = !this.show;
},
},
// mode 不加时默认同时展示动画,支持 in-out, out-in。appear 作用为刷新页面时展示动画
template: `
<div>
<transition mode="out-in" appear>
<div v-if="show">hello world</div>
<div v-else="show">bye world</div>
</transition>
<button @click="handleClick">切换</button>
</div>
`,
});

const vm = app.mount("#root");
</script>
</html>

多个单组件间的切换

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
57
58
59
60
61
62
63
64
65
66
67
68
69
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>动画效果</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
<style>
.v-enter-from,
.v-leave-to {
opacity: 0;
}

.v-enter-active,
.v-leave-active {
transition: opacity 1s ease-in;
}

.v-enter-to,
.v-leave-from {
opacity: 1;
}
</style>
</head>
<div id="root"></div>

<body></body>
<script>
const ComponentA = {
template: `<div>hello world</div>`,
};

const ComponentB = {
template: `<div>bye world</div>`,
};

const app = Vue.createApp({
data() {
return {
show: false,
};
},
methods: {
handleClick() {
this.show = !this.show;
},
},
components: {
"component-a": ComponentA,
"component-b": ComponentB,
},
// mode 不加时默认同时展示动画,支持 in-out, out-in。appear 作用为刷新页面时展示动画
template: `
<div>
<transition mode="out-in" appear>
<component :is="show ? 'component-a' : 'component-b'"></component>
</transition>
<button @click="handleClick">切换</button>
</div>
`,
});

const vm = app.mount("#root");
</script>
</html>

列表动画

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
57
58
59
60
61
62
63
64
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>动画效果</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
<style>
.v-enter-from {
opacity: 0;
transform: translate(0, 30px);
}

.v-enter-acitve {
transition: all 0.5s ease-in;
}

.v-enter-to {
opacity: 1;
transform: translate(0, 0);
}

.v-move {
transition: all 0.5s ease-in;
}

.list-item {
display: inline-block;
margin-right: 10px;
}
</style>
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
data() {
return {
list: [1, 2, 3],
};
},
methods: {
handleClick() {
this.list.unshift(this.list.length + 1);
},
},
template: `
<div>
<transition-group>
<span class="list-item" v-for="item in list" v-bind:key="item">{{item}}</span>
</transition-group>
<button @click="handleClick">增加</button>
</div>
`,
});

const vm = app.mount("#root");
</script>
</html>

状态动画

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>动画效果</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>
<style>
.v-enter-from {
opacity: 0;
transform: translate(0, 30px);
}

.v-enter-acitve {
transition: all 0.5s ease-in;
}

.v-enter-to {
opacity: 1;
transform: translate(0, 0);
}

.v-move {
transition: all 0.5s ease-in;
}

.list-item {
display: inline-block;
margin-right: 10px;
}
</style>
</head>
<div id="root"></div>

<body></body>
<script>
const app = Vue.createApp({
data() {
return {
number: 1,
animateNumber: 1,
};
},
methods: {
handleClick() {
this.number = 10;
if (this.animateNumber < this.number) {
const animation = setInterval(() => {
this.animateNumber += 1;
if (this.animateNumber === 10) {
clearInterval(animation);
}
}, 100);
}
},
},
template: `
<div>
<div>{{animateNumber}}</div>
<button @click="handleClick">增加</button>
</div>
`,
});

const vm = app.mount("#root");
</script>
</html>
0%