Vue 程序设计
2024年10月31日大约 6 分钟
参考:
- 官网 —— https://cn.vuejs.org/guide/introduction.html
4 小时快速入门前端 Vue3+Vite+Pinia —— https://www.bilibili.com/video/BV1aa1NYxECK/(vue2 语法)
项目初始化
$ npm init vite@latest project-name
Need to install the following packages:
create-vite@6.1.1
Ok to proceed? (y) y
> npx
> create-vite .
√ Select a framework: » Vue
√ Select a variant: » TypeScript
Scaffolding project in xxx\blog\code\demo-js-antv\demo-01-g6...
Done. Now run:
npm install
npm run dev基本使用
模板语法
渲染变量(
{{ val }},v-html="val"),变量计算({{ val1 + val2 }})<template> <!-- 渲染变量(不解析标签) --> <p>{{message}}</p> <!-- 渲染变量(渲染标签) --> <p v-html="message"></p> <!-- 双向绑定 --> <p> <input v-model="message" placeholder="edit me!"/> <br> <textarea v-model="message" placeholder="add multiple lines"></textarea> </p> <!-- 变量计算 --> <p> {{ number + 1 }}, {{ ok ? 'yes' : 'no' }}, {{ message.split(' ').reverse().join(' ') }} </p> </template> <script> export default { data() { return { ok: true, message: "hello <span style='color:red'>vue</span>!!", number: 14, } } } </script>属性变量(
v-bind:attr="val/:attr="val)
动态属性变量(v-bind:[val1,val2,...]="valn"/:[val1,val2,...]="valn")<template> <p> <!-- 属性变量 --> <a v-bind:href="href01">超链接</a> - <a :href="href01">超链接(简写)</a> - <a :href="href01 + '/calc'" :class="{ cssasfsfasfs01: true }" :style="{fontSize: number + 'px'}">超链接(计算)</a> - <!-- 动态属性变量 --> <a v-bind:[attributename]="href01">超链接(动态属性)</a> - </p> </template> <script> export default { data() { return { number: 14, href01: "http://example.org/", attributename: "href", // 不能有大写(attributeName),否则解析失败(且没有报错日志 “坑”) } } } </script> <style> .cssasfsfasfs01 { color: red; } </style>事件绑定(
v-on:click="count++"/@click="func1"),按键修饰符(keyup/keydown/...),系统修饰符(.ctrl/.alt/...),精确修饰符(.exact),鼠标按键修饰符(.left/.right/.middle)<template> <!-- 事件触发 --> 按键修饰符: <a href="https://vueframework.com/docs/v3/cn/guide/migration/keycode-modifiers.html">link</a><br> <ul style='font-size: 12px;'> <li> 系统修饰键: <ul> <li>.ctrl</li> <!-- @click.ctrl = Ctrl + Click --> <li>.alt</li> <!-- @keyup.alt.enter = Alt + Enter --> <li>.shift</li> <li>.meta —— 精确修饰组合:允许你精确的控制组合触发事件</li> <li>.exact</li> <!-- @click.ctrl = 即使 Alt 或 Shift 被同时按下也会触发 --> <!-- @click.ctrl.exact = 只有 Ctrl 被按下才会触发 --> <!-- @click.exact = 没有任何系统修饰符被按下时才会触发 --> </ul> </li> <li> 鼠标按钮修饰符: <ul> <li>.left</li> <li>.right</li> <li>.middle</li> </ul> </li> </ul> <hr> 鼠标/点击事件:<br> <!-- 注意:零参数默认传event事件,否则需要$event注入事件 --> <button v-on:click="count++">点击</button> / <button v-on:click="addOne(2,$event)">点击+2</button> / <button @click="addOne(3,$event),console.log('touch')">点击+3</button> : {{ count }} <hr /> 键盘/输入事件: (keyup = 按下后的释放事件)<br> <input style='width:100%;' placeholder='输入回车/下一页按钮,然后观察控制台' v-model='msg' @keyup.enter="console.log('enter')" @keyup.page-down="console.log('page-down')" /> <br> <span v-html='msg'></span> <br> </template> <script> export default { data() { return { count: 1, msg: '', addOne: (val, event) => { console.log(event); this.count += val ? val : 1; }, } }, methods: { // addOne } } </script>条件渲染(
v-if/v-else-if/v-else,v-show)<template> <p><button @click="awesome++">变</button> {{awesome}}:</p> <p> <!-- 是否渲染 --> <!-- 注意:template标签是vue自创的,作用就是不会被vue渲染 --> <template v-if="awesome%3==1">Vue is awesome!</template> <template v-else-if="awesome%3===2">Oh no 😨 xxx</template> <template v-else>Oh no 😨 ???</template> </p> <p> <!-- 是否显示 --> <!-- 注意: 1. v-show 不支持 <template> 2. v-show 不支持 v-else-show / v-else --> <span v-show="awesome%2==1">Vue is awesome!</span> <span v-show="awesome%2===0">Oh no 😨 xxx</span> </p> </template> <script> export default { data() { return { awesome: 1, } }, } </script>循环渲染(
v-for="(item,index) in items" :key="item.id"/v-for="(value,key,index) in obj" :key="key",v-for="(value,key) of obj" :key="key")<template> <p><span v-for="n in 10" :key="n">{{ n }}</span></p> <p> <!-- 数组循环 --> <!-- 注意:key的使用 --> <span v-for="(item,index) in items" :key="item.id" >{{ index }} - {{ item }}, </span ><br /> <!-- 两参数,数组 --> <span v-for="(value,key) in obj" :key="key" >{{ key }} - {{ value }}, </span ><br /> <!-- 两参数,对象 --> <span v-for="(value,key,index) in obj" :key="key" >{{ index }} - {{ key }} - {{ value }}, </span ><br /> <!-- 三参数,对象 --> </p> <p> <!-- 对象循环 --> <!-- 注意:key的使用 --> <span v-for="(value,key) of items" :key="key" >{{ key }}: {{ value }}</span >, </p> </template> <script> export default { data() { return { items: [ { id: 1, message: "Foo" }, { id: 2, message: "Bar" }, ], obj: { f1: "v1", f2: "v2", }, } }, } </script>模板标签(
<template>)<template> <!-- v-for 和 v-if 同时使用 --> <!-- 注意:不要写在同一个标签上,否则可能有意外现象 --> <template v-for="item in items" :key="item.id"> <li v-if="!(item.id==1)">{{item.id}} - {{item.message}}</li> </template> </template> <script> export default { data() { return { items: [ { id: 1, message: "Foo" }, { id: 2, message: "Bar" }, ], } }, } </script>
API
官网:
初始化:
- createApp —— 指定根组件
- createSSRApp
- app.mount —— 指定根组件被挂在到哪个dom内部
- app.unmount
- app.config
- app.config.errorHandler
- app.config.warnHandler
- app.config.throwUnhandledErrorInProduction —— 在生产模式下,错误只会被记录到控制台以尽量减少对最终用户的影响。然而,这可能会导致只在生产中发生的错误无法被错误监控服务捕获。通过该选项让生产环境的未处理错误被抛出。
- app.config.performance —— 性能监控,仅在开发环境生效,可通过浏览器开发工具查看
app.config.compilerOptions—— 一般没用,直接看vite的配置app.config.globalProperties —— 设置全局属性(在vue3中废弃)- app.config.optionMergeStrategies —— 配置合并设置
- app.config.idPrefix —— 为
useId()配置前缀 (只有在有多个 Vue App 的场景有意义)
组件:
- app.component —— 注册自定义组件(Components) https://cn.vuejs.org/guide/components/registration
- app.directive —— 注册自定义指令(Directives) https://cn.vuejs.org/guide/reusability/custom-directives
- app.use —— 安装插件 (Plugins) https://cn.vuejs.org/guide/reusability/plugins
app.mixin —— 注册“混入”(公共逻辑/配置的提取)(过时技术,用“组合式函数”替代)- app.provide —— 注册提供给子组件的变量/方法
- app.runWithContext —— 执行方法,并传递返回值(可以调用当前上下文设置,如通过inject获取provide值)
- app.version
- defineComponent
- defineAsyncComponent
生命周期:
- nextTick —— 等待下一次 DOM 更新刷新的工具方法
- app.onUnmount
SSR
todo createSSRApp
对比:组件、指令、插件
todo 对比
对比:mixin、组合式函数
定义:
- mixin:公共逻辑/配置的提取(不包含html和css内容) —— 基于vue2的选项式API做的配置混入,本质上是“对象的合并”
- 组合式函数: 负责有状态逻辑的封装
例子:
mixin
<template>
mixin 对比 组合式函数:
<div ref='devRef'>{{ count }}</div>
<button @click='handleClick'>add</button>
</template>
<script>
const myMixin = {
data() {
return {
count: 2,
}
},
mounted() {
console.log('myMixin', this.$refs.devRef);
},
methods: {
handleClick() {
this.count += 2;
console.log('jia 2');
}
}
}
export default {
data() {
return {
count: 1,
}
},
mixins: [
myMixin
],
methods: {
handleClick() {
this.count++;
console.log("jia 1")
}
},
mounted() {
console.log('mounted')
}
}
</script>自定义组件
语法
vue2
script:
- 数据(
data(){return{val1:data1,...}}) - 计算数据(
computed:{data1(){return val1},...}) - 方法(
methods:{func1(){...},...}) - 生命周期(
beforeCreate/created/beforeMount/mounted/beforeUpdate/updated/beforeUnmount/unmounted) link - 变更监听(
watch: {val:{handler:(newVal,oldVal)=>{...},deep:true}}) - 组件(
components:{Component1,...}) - 上层变量(
props:{prop1:{type:String,default:""},...})
component:
- 变量传递(
<Component1 val1="data1">,props:{val1},{{val1}}) - 变量绑定(
<Component1 v-model="val1">,props:{modelValue},this.$emit('update:modelValue',value)) - 事件传递(
<Component1 @event1="func1">,this.$emit("event1")) - 匿名插槽(
<Component1><slot>) - 具名插槽(
<Component1><slot name="namedSlot" :val1="val1">)
例子
<template>
<div>
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<div>hello world!</div>
<!-- 计算属性 -->
<div>{{ dayInfo }}</div>
<div>{{ dayInfo }}</div>
<div>{{ count }}</div>
<div>{{ obj }}</div>
<button @click="clickMe">clickMe</button>
<hr>
<!-- 自定义组件 in vue2 -->
<button @click="showDialog=!showDialog">弹出对话框组件</button>
<MyDialog title="自定义提示" content="<span style='color:red'>测试</span>内容" v-model="weather"
v-if="showDialog" @close="showDialog=false">
<!-- 默认插槽 -->
<span style='color:red'>测试</span>内容 {{ count }} (“默认”插槽)
<!-- 具名插槽 -->
<template #namedSlot>
<span style='color:red'>测试</span>内容 {{ count }} (“具名”插槽)
</template>
<!-- 具名插槽,使用下层值 -->
<template #namedSlot2="slotName">
<span style='color:red'>测试</span>内容 {{ count }} (“具名”插槽,传值 "{{ slotName.val1 }}/{{ slotName.val2 }}")
</template>
</MyDialog>
<hr>
<!-- 自定义组件 in vue3 (组合式API) -->
<button @click="showDialog2=!showDialog2">刷出内容</button>
<MyDialog2 title="自定义标题"
v-if="showDialog2"></MyDialog2>
</template>
<script lang="ts">
import MyDialog from "./components/MyDialog.vue"; // vue2 组件写法
import MyDialog2 from "./components/MyDialog2.vue"; // vue3 组件写法
function toString(obj: any) {
return JSON.stringify(obj);
}
export default {
components: {MyDialog, MyDialog2},
data() { // 数据
return {
count: 0,
obj: {
name: "计数器",
count: 0
},
weather: "晴天",
x: 0,
showDialog: false,
showDialog2: false
}
},
methods: { // 方法
clickMe() {
this.count++;
this.obj.count++;
},
},
watch: { // 监听:值改变的回调
count(newVal, oldVal) { // 值监听
console.log(`count 更新: '${oldVal}' -> '${newVal}'`)
},
obj: { // 深度监听
handler: (newVal, oldVal) => {
console.log(`obj 更新: '${toString(oldVal)}' -> '${toString(newVal)}'`)
},
deep: true // 配置深度监听启动
}
},
computed: { // 计算属性
// 注意:不要在计算属性中更改 this 的属性值,否则结果会不合预期。
// 异步计算 https://my.minecraft.kim/tech/845/%E5%A6%82%E4%BD%95%E5%9C%A8-vue3-%E4%B8%AD%E5%BC%82%E6%AD%A5%E4%BD%BF%E7%94%A8-computed-%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7/
dayInfo() {
return ( // 语法解析时,括号没啥用
"今天的天气是:" + this.weather + ",时间是:" + new Date().toISOString() + ",Counter:" + this.x++ // 每次引用都会重新计算一遍(留意x的值)
);
}
},
// ==== 启动时必然触发 ====
beforeCreate() {
console.log("beforeCreate")
},
created() {
console.log("created")
},
beforeMount() {
console.log("beforeMound")
},
mounted() {
console.log("mounted")
},
// ==== 数据变化时触发 ====
beforeUpdate() {
console.log("beforeUpdate")
},
updated() {
console.log("update")
},
// ==== 组件卸载时触发 ====
beforeUnmount() {
console.log("beforeUnmount")
},
unmounted() {
console.log("unmounted")
},
}
</script>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>vue2
<!-- Vue2 语法 -->
<template>
<div class="dialog-bg">
<div class="dialog">
<!-- props 传值 -->
<div>{{ title }}</div>
<div v-html="content"></div>
<!-- slot 传值 -->
<div>
<!-- 默认插槽 -->
<slot>
插槽默认值
</slot>
</div>
<div>
<!-- 具名插槽 -->
<slot name="namedSlot"></slot>
</div>
<div>
<!-- 具名插槽,向上层传值 -->
<slot :val1="val1" val2="myVal2" name="namedSlot2"></slot>
</div>
<div>
<!-- 与上层v-model双向绑定 -->
<!-- 问题:https://www.mintimate.cn/2024/01/17/vModelVue/ -->
<input type="text" v-bind:value="modelValue"
v-on:input="updateValue($event.target.value)"
@keydown.enter="submit" @keydown.esc="cancel"></div>
<div class="btn-group">
<button @click="submit">确定</button>
<button @click="cancel">取消</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: { // 2. 上层传入的属性
title: {
type: String,
default: ""
},
content: {
type: String,
default: ""
},
modelValue: { // 接收上层的 v-model 传值,名字固定只能是 "modelValue"
type: String,
default: ""
}
},
data() {
return {
val1: "myVal1"
}
},
methods: {
// 1. 取消事件向上层传递,在上层决定如何处理事件
submit() {
// this.$emit('update:model-value', this.modelValue)
this.$emit("close")
},
cancel() {
this.$emit("close")
},
updateValue(value) {
this.$emit('update:modelValue', value)
}
},
beforeUnmount() { // 事件:取消挂载前
console.log("beforeUnmount");
},
unmounted() { // 事件:取消挂载
console.log("unmounted");
}
}
</script>
<style scoped>
.dialog-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
text-align: center;
}
.dialog {
width: 300px;
padding: 20px;
margin: 0 auto;
margin-top: 200px;
background-color: #e4e4e4;
}
.btn-group {
margin-top: 50px;
display: flex;
justify-content: space-around;
}
</style>vue3
组合式 API
<!-- 组合式API(vue3语法) -->
<template>
<div>
<p>{{ props.title }}</p>
<p>{{ a }}</p>
<button v-on:click="testClick">ClickMe!</button>
<ul>
<li v-for="(item,index) in b">
{{ index }} , {{ item.id }} , {{ item.name }}
</li>
</ul>
<button @click="testClick2">ClickMe!</button>
<p>{{ c }}</p>
<button @click="cancel">收缩内容</button>
</div>
</template>
<!-- 注意:有setup标签 -->
<script setup lang="ts">
import { ref,reactive } from "vue";
import { onMounted, watch, computed } from "vue";
let a = ref("张三") // 变量定义
function testClick() {
a.value = "翠花"; // 变量值改变
}
let b = reactive([{id:1,name:'翠花'},{id:2,name:'王五'}]); // 深度变量绑定
function testClick2() {
b[1].name = "张三"
}
// 变量监听
watch(b, (oldVal, newVal) => {
console.log("watch in vue", oldVal, newVal)
})
// 计算属性
const c = computed(() => {return a.value + " - " + b[0].name})
// 生命周期
onMounted(() => {
console.log("onMounted in vue3")
})
// 上层传入的变量
const props = defineProps({
title: {
type: String,
default: ""
}
})
// 发送消息(向上层传变量)
const emit = defineEmits({})
function cancel() {
emit("close");
}
</script>
<style scoped>
</style>路由: Vue Router
- 不同的历史模式 (link)
- createWebHashHistory ——
#请求未发送服务器 无法 SEO 捕获 - createWebHistory —— (常见)适合浏览器场景,路径看起来 “正常” (e.g.
https://example.com/user/id),但是路径无法被直接访问 - createMemoryHistory —— 不记录历史记录,适合 node 环境和 SSR,不适合浏览器场景
- createWebHashHistory ——
npm install vue-router@4路由配置
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import { createWebHistory, createRouter, RouteRecordRaw } from "vue-router";
import { createPinia } from 'pinia'
const routes: Readonly<RouteRecordRaw[]> = [
{
path: "/", component: () => import("./views/Home.vue"), children: [
{ path: "/page1", component: () => import("./views/page01.vue") },
{ path: "/page2", component: () => import("./views/page02.vue") },
]
},
{
path: "/about", component: () => import("./views/About.vue")
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
createApp(App)
.use(router)
.use(createPinia())
.mount("#app");路由显示
<template>
<RouterView></RouterView>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>二级路由
<template>
<div>
Home
<ul>
<li>
<!-- 路由触发 -->
<button @click="toAbout">/about</button>
</li>
<!-- 二级路由 -->
<li>
<button @click="toPage(1)">/page01</button>
</li>
<li>
<button @click="toPage(2)">/page02</button>
</li>
</ul>
<!-- 二级路由显示 -->
<RouterView></RouterView>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
const route = useRoute(); // 获取上层路由传递的参数
function toAbout() {
// 路由转跳
router.push({
path: '/about',
query: {
name: '张三',
id: 1
}
})
}
function toPage(num: number) {
router.push({
path: '/page' + num
})
}
</script>
<style scoped>
</style>状态管理
- bus
- vuex —— 【过时】 2022 年
- pinia —— 2019 年 ~ 2024 年
issue271
https://pinia.vuejs.org/
npm install pinia引入
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import { createWebHistory, createRouter, RouteRecordRaw } from "vue-router";
import { createPinia } from 'pinia'
const routes: Readonly<RouteRecordRaw[]> = [
{
path: "/", component: () => import("./views/Home.vue"), children: [
{ path: "/page1", component: () => import("./views/page01.vue") },
{ path: "/page2", component: () => import("./views/page02.vue") },
]
},
{
path: "/about", component: () => import("./views/About.vue")
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
createApp(App)
.use(router)
.use(createPinia())
.mount("#app");定义
<template>
<div>
page{{ props.num }} - {{ counter.count }} and {{ counter.testCount }} and {{ message }} <!-- 3. 使用 -->
-
<button @click="touchStore">add</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
num: {
type: String,
default: "-1"
}
})
// pinia
import { defineStore, storeToRefs } from "pinia";
import { testStore } from "../stores/TestStore"; // 1. 引入
const counter = testStore(); // 2. 实例化
const {message} = storeToRefs(counter) // 绑定
function touchStore() {
// counter.count++; // 直接改变
counter.increment(); // 通过方法改变
message.value = (counter.count & 1) == 0 ? '张三' : "李四" // 通过绑定对象改变值
}
</script>
<style lang="scss" scoped>
</style>使用
<template>
<div>
<TestA num="01"></TestA>
</div>
</template>
<script setup lang="ts">
import TestA from "../components/TestA.vue";
</script>
<style scoped>
</style>VSCode 插件
- Vue - Official —— 开发环境监测、高亮
- Vue VSCode Snippets —— 提示
VSCode 缩写
- vbase —— 模板
- imd —— import xxx 缩写
实战
todo 后台管理
todo 权限管理 https://www.bilibili.com/video/BV126DpYrES7/
todo ??? https://www.bilibili.com/video/BV1Xh411V7b5