vue3最新篇

249 阅读10分钟

参考文献 cn.vuejs.org/guide/intro…

创建项目

  • node@16(可用 nvm 管理 node 版本,方便 vue2 vue3 切换)
  • npm init vue@latest
  • 包版本信息
  "dependencies": {
    "axios": "^1.4.0",
    "dayjs": "^1.11.7",
    "element-plus": "^2.3.3",
    "pinia": "^2.0.34",
    "pinia-plugin-persistedstate": "^3.1.0",
    "vue": "^3.2.47",
    "vue-router": "^4.1.6"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.1.0",
    "core-js": "^3.30.1",
    "eslint": "^8.38.0",
    "eslint-config-prettier": "^8.8.0",
    "eslint-plugin-prettier": "^4.2.1",
    "eslint-plugin-vue": "^9.10.0",
    "prettier": "^2.8.7",
    "sass": "^1.62.0",
    "sass-loader": "^13.2.2",
    "unplugin-auto-import": "^0.15.3",
    "unplugin-element-plus": "^0.7.1",
    "unplugin-vue-components": "^0.24.1",
    "vite": "^4.1.4",
    "vite-plugin-mock": "^3.0.0"
  }

全局 API 应用实例

  • main.js
import { createApp } from 'vue'
const app = createApp({})
  • Vue.prototype 替换为 app.config.globalProperties
// src/main.js
const app = createApp(App);
app.config.globalProperties.msg = 'hello'


// template中拿取
<template>
  <h3>App</h3>
  <p>msg:{{ msg }}</p>
</template>

// js中拿取
<script setup>
const { msg } = getCurrentInstance().appContext.config.globalProperties;
// hello
console.log('globalProperties---msg', msg);
</script>
  • app.component 、app.directive、 app.use
app.component('button-counter', {
  data: () => ({
    count: 0,
  }),
  template: '<button @click="count++">Clicked {{ count }} times.</button>',
});

app.directive('focus', {
  mounted: (el) => el.focus(),
});

基础

模板指令

  • v-model
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />

<!-- 是以下的简写: -->
<ChildComponent
  :title="pageTitle"
  @update:title="pageTitle = $event"
  :content="pageContent"
  @update:content="pageContent = $event"
/>
  • key
<!-- Vue 2.x -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="a">No</div>

<!-- Vue 3.x (推荐方案:移除 key) -->
<div v-if="condition">Yes</div>
<div v-else>No</div>

<!-- Vue 2.x -->
<template v-for="item in list">
  <div v-if="item.isVisible" :key="item.id">...</div>
  <span v-else :key="item.id">...</span>
</template>

<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
  <div v-if="item.isVisible">...</div>
  <span v-else>...</span>
</template>

  • v-if 与 v-for 的优先级对比
2.x 版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用。

3.x 版本中 v-if 总是优先于 v-for 生效。

setup

  • 顶层的绑定会被暴露给模板

<template>
  <button @click="log">{{ msg }}</button>
  <MyComponent />
</template>
<script setup>
// import 导入的内容也会以同样的方式暴露
import { capitalize } from './helpers'
import MyComponent from './MyComponent.vue'

// 变量
const msg = 'Hello!'
// 函数
function log() {
  console.log(msg)
}
</script>

实现数据响应式的API ref + reactive

ref 和 reactive 是 Vue3 中用来实现数据响应式的 API ref 定义基本数据类型或引用数据类型,reactive 只能定义引用数据类型

reactive

  • 其底层是通过 ES6 的 Proxy 来实现数据响应式,相对于 Vue2 的 Object.defineProperty,具有能监听普通对象的增删操作,数组的下标修改、数组的 length 修改、数组的增删改
/**
 * 模拟vue3 Proxy代理 实现对于对象的拦截
 * target: 被代理的对象 data
 * key: 操作的属性名
 * value:  值
 */
const p = new Proxy(data, {
  //读取属性会执行的回调
  get(target, key, receiver) {
    console.log('get', target, key);
    return Reflect.get(target, key, receiver);
  },
  //修改或添加属性会执行的回调
  set(target, key, value, receiver) {
    console.log('set', target, key, value);
    return Reflect.set(target, key, value, receiver);
  },
  //删除属性会执行的回调
  deleteProperty(target, key) {
    console.log('deleteProperty', target, key);
    return delete target[key];
  },
});
  • reactive 的示例
const paginationConfig = reactive({
  pageNum: 1,
  pageSize: 10,
});

const onChange = () => {
  paginationConfig.pageNum = 2;
  paginationConfig.pageSize = 20;
};

<a-pagination v-model:current="paginationConfig.pageNum"></a-pagination>;
  • reactive 不能定义基本数据类型
// 源码
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
  if (!shared.isObject(target)) {
    {
      console.warn(`value cannot be made reactive: ${String(target)}`);
    }
    return target;
  }
  // some code
}
  • reactive 实现深层嵌套的响应式
const obj = {
  a: {
    count: 1,
  },
};

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      console.log('这里是get');
      // 判断如果是个对象在包装一次,实现深层嵌套的响应式
      if (typeof target[key] === 'object') {
        return reactive(target[key]);
      }
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      console.log('这里是set');
      return Reflect.set(target, key, value, receiver);
    },
  });
}
const proxy = reactive(obj);
  • 以下是 reactive 定义的变量响应式失去的几个情况

1、解构 props 对象,丢失地址会导致失去响应式


<template>
  <h2>App</h2>
  <p>state.userInfo:{{state.userInfo}}</p>
  <button @click="changeUserInfo">change userInfo</button>
</template>

<script setup>
import { reactive, ref, isReadonly } from 'vue'
const state = reactive({ id: "112", userInfo: { name: "bwf", list: [{ id: "11" }, { id: "22" }] } })

let { id, userInfo } = state

const changeUserInfo = (data) => {
  // 以下操作不会响应式
  // id = 999
  // userInfo = { name: "bbbb" }
  // userInfo = reactive({ name: "bbbb" })

  // 这种操作会影响式, 没有丢失 userInfo 地址
  userInfo.name = 'xxxx'
}
</script>

2、 给 reactive 响应式对象直接赋值,会导致该对象失去响应式(我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失)

let userInfo = reactive({ name: 'bwf', list: [{ id: '11' }, { id: '22' }] });
const changeVue = () => {
  // 强制地替换 会失去响应式
  //  userInfo = { name: "xxx", list: [{ id: "11" }] }
  // 这样也会失去响应式
  //  userInfo = reactive({ name: "xxx", list: [{ id: "11" }] })
};

3、 解决 reactive 定义的变量,解构丢失属性地址 或 直接赋值 导致响应式丢失的方法

    1. 把响应式数据统统挂载到 一个名叫 state 的对象上,类似 vue2 中的 data 选项的做法
<template>
  <h2>App</h2>
  <p>state.userInfo:{{state.userInfo}}</p>
  <button @click="changeUserInfo({ name: 'xxx', list: [{ id: '33' }] })">change userInfo</button>
</template>

<script setup>
  import { reactive } from 'vue'
  const state = reactive({ userInfo: { name: "bwf", list: [{ id: "11" }, { id: "22" }] } })
  const changeUserInfo = (data) => {
    // 可以响应式
    state.userInfo = data
  }
</script>
    1. 将此对象改成用 ref 定义
<template>
  <h2>App</h2>
  <p>userInfo:{{userInfo}}</p>
  <button @click="changeUserInfo({ name: 'xxx', list: [{ id: '33' }] })">change userInfo</button>
</template

<script setup>
import { reactive, ref, isReadonly } from 'vue'
const userInfo = ref({ name: "bwf", list: [{ id: "11" }, { id: "22" }] })

const changeUserInfo = (data) => {
  // 可以响应式
  userInfo.value = data
}
</script>
    1. 不要直接赋值,对于普通对象,可以直接改变它的属性 或 采用 Object.assign 可以响应式;对于数组类型的,可以 push 等方法响应式
<template>
  <h2>App</h2>
  <p>userInfo:{{userInfo}}</p>
  <button @click="changeUserInfo">change userInfo</button>
</template>

<script setup>
import { reactive, ref, isReadonly } from 'vue'
const userInfo = reactive({ name: "bwf", list: [{ id: "11" }, { id: "22" }] })

const changeUserInfo = (data) => {
  // 以下方式都可以响应式
  userInfo.name = "lalala"
  userInfo.list = [{ id: "33" }]
  Object.assign(userInfo, { name: "lalala" })
}
</script>

ref

  • ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象, ref 的 .value 属性也是响应式的
// 源码
class RefImpl {
  constructor(value, __v_isShallow) {
    this.__v_isShallow = __v_isShallow;
    this.dep = undefined;
    this.__v_isRef = true;
    this._rawValue = __v_isShallow ? value : toRaw(value);
    this._value = __v_isShallow ? value : toReactive(value);
  }
  get value() {
    // 取值的时候依赖收集
    trackRefValue(this);
    return this._value;
  }
  set value(newVal) {
    const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
    newVal = useDirectValue ? newVal : toRaw(newVal);
    if (shared.hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = useDirectValue ? newVal : toReactive(newVal);
      triggerRefValue(this, newVal);
    }
  }
}

const toReactive = (value) => (shared.isObject(value) ? reactive(value) : value);
  • ref 定义对象类型时,底层走的还是 reactive()的逻辑
  • ref 定义基本类型时,走的还是 getter setter
  • 使用 ref 定义基本数据类型时,在脚本里使用时,需要加.value 后缀,然而在模板里不需要
  • ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:
  • ref 使用示例
const user = ref({ name: "bwf" })
let isShow = ref(false)

const onChange = () => {
  // 以下两种更新都可以响应式
  user.value = { name: "xxxx", id: 999 }
  isShow.value = true
}

<p>user.name:{{user.name}}</p>
<a-modal v-model:visible="isShow"></a-modal>

ref 和 reactive 定义数组对比

//  ref定义
const tableData = ref([]);
const getTableData = async () => {
  const { data } = await getTableDataApi(); // 模拟接口获取表格数据
  tableData.value = data; // 修改
};

// reactive定义
const tableData = reactive([]);
const getTableData = async () => {
  const { data } = await getTableDataApi(); // 模拟接口获取表格数据
  // tableData = data // 修改,错误示例,这样赋值会使tableData失去响应式
  tableData.push(...data); // 先使用...将data解构,再使用push方法
  // tableData = reactive(data) // 赋值前再包一层reactive 也可以实现 tableData 的响应式
};

ref vs reactive

1.ref 用于定义基本类型和引用类型,reactive 仅用于定义引用类型 2.reactive 只能用于定义引用数据类型的原因在于内部是通过 ES6 的 Proxy 实现响应式的,而 Proxy 不适用于基本数据类型 3.ref 定义对象时,底层会通过 reactive 转换成具有深层次的响应式对象,所以 ref 本质上是 reactive 的再封装 4.在脚本里使用 ref 定义的数据时,记得加.value 后缀 5.在定义数组时,建议使用 ref,从而可避免 reactive 定义时值修改导致的响应式丢失问题 6.把响应式数据统统挂载到 一个名叫 state 的对象上,类似 vue2 中的 data 选项的做法,这样的话直接 reactive 定义变量就行 7. 不论是 ref 还是 reactive 定义的变量,解构赋值后都不可以改变引用地址,否则会失去响应式

<template>
  <h2>App</h2>
  <p>obj.foo:{{obj.foo}}</p>
  <p>obj:{{obj}}</p>
  <button @click="changeUserInfo">change userInfo</button>
</template>

<script setup>
import { reactive, ref, isReadonly } from 'vue'

const obj = ref({ foo: 12, info: { name: "bwf" } })
let { foo, info } = obj.value

const changeUserInfo = (data) => {
  // 以下 不能响应式
  // info = { name: "xxxxxxxx" }

  // 以下 可以响应式
  // obj.value.foo = 999999999
  // obj.value.info = { name: "xxx" }
  info.name = "lll"
}
</script>

computed watch watchEffect

  • computed computed() 方法默认接收一个 getter 函数,返回值为一个计算属性 ref(ComputedRefImpl )。可以通过 publishedBooksMessage.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value。
<template>
  <h2>App</h2>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
  <br>
  <button @click="changeBooks">change books</button>
</template>

<script setup>
import { reactive, computed, isRef } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
  return author.books.length > 2 ? 'Yes' : 'No'
})
// true true
console.log(isRef(publishedBooksMessage), isReadonly(publishedBooksMessage));
const changeBooks = ()=>{
  author.books.length = 0
}
</script>
  • watch watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组 注意,你不能直接侦听响应式对象的属性值,例如:
<template>
  <h3>app</h3>
  <p>x:{{x}}</p>
  <p>user:{{user}}</p>
  <button @click="++x">change x</button>
  <button @click="user.name='xxx'">change user.name</button>
  <button @click="changeUser">change user</button>
</template>

<script setup>
import { reactive, ref, watch } from 'vue'
const x = ref(0)
const user = reactive({ name: "bwf", list: [{ name: "xx" }, { name: "ll" }] })

const changeUser = () => {
  user.list.push({ name: "add" })
  // user.list = [{ name: "add" }]
}
// watch 一个 ref
watch(x, (newVal) => {
  console.log('x', newVal)
})

// 注意,你不能直接侦听响应式对象的属性值,例如:
watch(user.name, (newVal) => {
  // 以下不会执行
  console.log('user.name1', newVal)
})

// 只有改变 user.name 才会触发
watch(() => user.name, (newVal) => {
  console.log('user.name2', newVal)
})

// 在嵌套的属性变更时触发 (user里面的name 或者 list发生任何改变都会触发)
watch(user, (newVal) => {
  console.log('user', newVal)
})

watch(() => user.list, (newVal) => {
  // 在不配置deep:true的情况下
  // 会触发
  // user.list = [{ name: "add" }]
  // 不会触发
  // user.list.push({ name: "add" })

  // 配置deep:true(当用于大型数据结构时,开销很大)   以下都会触发
  // user.list = [{ name: "add" }]
  // user.list.push({ name: "add" })
  console.log('user.list', newVal)
}, { deep: true })
</script>

即时回调的侦听器

watch(
  source,
  (newValue, oldValue) => {
    // 立即执行,当 `source` 改变时会再次执行
  },
  { immediate: true }
);
  • watchEffect
<template>
  <h3>app</h3>
  <p>pid:{{pid}}</p>
  <button @click="changePid">change pid</button>
</template>

<script setup>
import { ref, watchEffect } from 'vue'

const id = ref('1')
const pid = ref('0000')
const data = ref(null)
// watchEffect 回调会立即执行
// pid id任何一个发生改变时都会触发
watchEffect(async () => {
  console.log('watchEffect---start');
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${pid.value}/${id.value}`)
  data.value = await response.json()
})

const changePid = () => {
   pid.value = "99"
}
</script>

如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项, 后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect()

要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:

const unwatch = watchEffect(() => {});

// ...当该侦听器不再需要时
unwatch();

类和样式绑定

  • 类绑定
<div :class="classObject"></div>

const classObject = reactive({
  'active': true,
  'text-danger': false
})

// 或者通过计算
const isActive = ref(true)
const error = ref(null)
const classObject = computed(() => ({
  active: isActive.value && !error.value,
  'text-danger': error.value && error.value.type === 'fatal'
}))

  • 在组件上使用
<MyComponent :class="{ active: isActive }" />

// MyComponent组件内容 组件有多个根元素 指定哪个根元素来接收这个 class
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
  • 样式绑定
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

const activeColor = ref('red')
const fontSize = ref(30)

生命周期钩子 onMounted 和 onUnmounted

<script setup>
  import {onMounted} from 'vue' onMounted(() => {console.log(`the component is now mounted.`)})
</script>

nextTick

<template>
  <button id="counter" @click="increment">{{ count }}</button>
</template>

<script setup>
import { ref, nextTick } from 'vue'

const count = ref(0)
async function increment() {
  count.value++
  // DOM 还未更新
  console.log(document.getElementById('counter').textContent) // 0
  await nextTick()
  // DOM 此时已经更新
  console.log(document.getElementById('counter').textContent) // 1
}
</script>

访问 DOM 元素 + 子组件上实例

<template>
  <input ref="input" />
  <Child ref="child" />
</template>

<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

// 声明一个 ref 来存放该元素的引用, 必须和模板里的 ref 同名
const input = ref(null)
const child = ref(null)

onMounted(() => {
  input.value.focus()
  // child.value 是 <Child /> 组件的实例
})
</script>

defineProps、defineEmits、defineExpose

  • 都是只能在 <script setup> 中使用的编译器宏,不需要导入
  • 单向流
  • 使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露
// 父组件
<template>
  <h3>app</h3>
  <p>count:{{count}}</p>
  <p>user:{{user}}</p>
  <button @click="getChild">获取子实例的属性或方法</button>
  <Child :count="count" :user="user" @changeCount="changeCount" @changeUser="changeUser" ref="childRef"></Child>
</template>

<script setup>
import { reactive, ref } from 'vue'
import Child from './components/Child.vue'

const count = ref(0)
const user = reactive({ name: "bwf", list: [{ id: "11" }] })
const childRef = ref(null)

const changeCount = (params) => {
  // 以下会响应式
  count.value = params
}

const changeUser = (params) => {
  // 这样会丢失响应式
  // user = params

  // 以下 会响应式
  Object.assign(user, params)
}

const getChild = ()=>{
  // childMethod
 childRef.value.childMethod()
}
</script>


// 子组件
<template>
  <h3>Child</h3>
  <p>count:{{count}}</p>
  <p>countNew:{{countNew}}</p>
  <p>user:{{user}}</p>
  <button @click="changeCount">change count</button>
  <button @click="changeUser">change user</button>
</template>

<script setup>
import { isReadonly, reactive, ref } from "vue";

const props = defineProps({
  count: Number,
  user: {
    type: Object,
  }
})
const emits = defineEmits(['changeCount', 'changeUser'])
// 只是将 props.count 作为初始值,这样做就使 count 和后续更新无关了
const countNew = ref(props.count)
const changeCount = () => {
  emits('changeCount', 3)
  // emits('changeCount', ++countNew.value)
}

const changeUser = () => {
  emits('changeUser', { name: "xxxx", id: "999" })
}

const childMethod = () => {
  console.log('childMethod');
}
defineExpose({childMethod})
</script>

深入组件

透传 Attributes

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和 id。

<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>

// 父组件使用了这个组件
<MyButton class="large" />

//最后渲染出的 DOM 结果是: 对 class 和 style 的合并
<button class="btn large">click me</button>

  • 禁用 Attributes 继承 就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs 选项为 false,你可以完全控制透传进来的 attribute 被如何使用。
<script>
// 使用普通的 <script> 来声明选项
export default {
  inheritAttrs: false
}
</script>

<script setup>
// ...setup 部分逻辑
</script>
  • $attrs

透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到

// 这个 $attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 class,style,v-on 监听器等等。
<span>Fallthrough attribute: {{ $attrs }}</span>


// v-bind="$attrs" 多根节点模板
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
  • 在 JavaScript 中访问透传 Attributes
<script setup>import {useAttrs} from 'vue' const attrs = useAttrs()</script>

插槽 Slots

  • 具名插槽
<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

// BaseLayout组件
<div class="container">
  <header>
    {/* 插槽的默认内容 */}
    <slot name="header">header default content</slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
  • 子组件传入插槽的 props 作为了 v-slot 指令的值,可以在插槽内的表达式中访问
<!-- <MyComponent> 的模板 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

// 使用MyComponent的父组件 会把子组件slot上的属性收集在1个对象中
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

// 也可以在 v-slot 中使用解构:
<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

依赖注入 provide 和 inject

任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

<script setup>
  import {provide} from 'vue'
  {/* 注入名,可以是一个字符串 或是 一个 Symbol */}
  {/* 提供的值,值可以是任意类型,包括响应式的状态 */}
  provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>;

// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
import { inject } from 'vue';
const value = inject('message', '这是默认值');
  • 使用 Symbol 作注入名 我们通常推荐在一个单独的文件中导出这些注入名 Symbol:
// keys.js
export const myInjectionKey = Symbol();

// 在供给方组件中
import { provide } from 'vue';
import { myInjectionKey } from './keys.js';
provide(myInjectionKey, {
  /*
  要提供的数据
*/
});

// 注入方组件
import { inject } from 'vue';
import { myInjectionKey } from './keys.js';

const injected = inject(myInjectionKey);

组合式函数

“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

  • 和 Mixin 的对比 ​
Vue 2 的用户可能会对 mixins 选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板:

1.不清晰的数据来源:当使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。

2.命名空间冲突:多个来自不同作者的 mixin 可能会注册相同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量进行重命名来避免相同的键名。

3.隐式的跨 mixin 交流:多个 mixin 需要依赖共享的属性名来进行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。

基于上述理由,我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户。
  • 应用示例
<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <p>Double Count: {{ counter.doubleCount }}</p>
    <button @click="counter.increment">Increment</button>
    <button @click="counter.decrement">Decrement</button>
  </div>
</template>

<script setup>
import { isRef } from 'vue';
import useCounter from './composables/useCounter'
const counter = useCounter(0)
// true
console.log('counter', isRef(counter.count));
</script>


// composables/useCounter
import { ref, computed } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export default function useCounter(initialValue) {
 // 用于初始化计数器的值
  const count = ref(initialValue)
  function increment() {
    count.value++
  }
  function decrement() {
    count.value--
  }
  const doubleCount = computed(() => count.value * 2)
  // 返回值 count doubleCount 是两个ref, ref 则可以维持这一响应性连接。
  return {
    count,
    increment,
    decrement,
    doubleCount
  }
}

插件 拥有 install() 方法的对象

插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码

// main.js
import { createApp } from 'vue';
const app = createApp({});
// 使用时 会传入应用实例 + 额外选项作为参数
app.use(myPlugin, {
  /* 可选的选项 */
});

const myPlugin = {
  install(app, options) {
    // 配置此应用
  },
};
  • 常见场景主要包括以下几种
  1. 通过 app.component() 和 app.directive() 注册一到多个全局组件或自定义指令。
  2. 通过 app.provide() 使一个资源可被注入进整个应用。
  3. 向 app.config.globalProperties 中添加一些全局实例属性或方法
  4. 一个可能上述三种都包含了的功能库 (例如 vue-router)。

vue 项目中使用 eslint + Prettier(vscode 插件) 规范代码格式

  • 1.安装项目插件
npm install eslint eslint-plugin-vue eslint-config-prettier eslint-plugin-prettier prettier --save-dev
  • 2.安装全局插件
npm install -g prettier
  • 3.项目根目录 创建名为 .eslintrc.js 的文件
module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: ['plugin:vue/vue3-essential', 'eslint:recommended', 'prettier'],
  parserOptions: {
    ecmaVersion: 2020,
  },
  plugins: ['vue', 'prettier'],
  // off(关闭规则) warn(开启规则并将问题视为警告) error(开启规则并将问题视为错误)。
  rules: {
    'no-unused-vars': 'off', //允许存在未使用的变量
    'vue/multi-word-component-names': 'off', //允许组件名称由1个单词组成
  },
};
  • 4.项目根目录 创建名为 .eslintignore 的文件
dist;
node_modules;
  • 5.项目根目录 创建名为 .prettierrc 的文件 (注意一定要严格遵循 json 格式,譬如不能有注释,不能使用单引号,否则该配置文件不生效)
{
  "printWidth": 100000, // 每行最多 100000 个字符
  "tabWidth": 2, // 缩进使用两个空格
  "useTabs": false,
  "semi": true, // 每个结束语句后必须添加分号
  "singleQuote": true, // js中一律使用单引号
  "trailingComma": "es5" // 对象、数组最后一个元素后面允许添加逗号
}
  • 6.设置 vscode 保存时按照 .prettierrc 文件 格式
// 6.1 代码文件 右键 选择 使用...格式化文件
// 6.2 配置默认的格式化程序
// 6.3 选择Prettier
// 6.4 点击vscode编辑器设置---勾选Format On Save
  • 7.package.json 文件中配置指令
"lint": "eslint . --ext .js,.vue"

使用 scss

  • 1.安装 sass 和 sass-loader
npm install sass sass-loader --save-dev
  • 2.在项目根目录 vite.config.js 文件,并添加以下代码:
  css: {  //配置处理 CSS 的选项,包括预处理器、样式优化等
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";` //添加全局 SCSS 变量
      }
    },
    postcss: {
      plugins: [
        // require('autoprefixer')
      ]
    }
  },
  • 3.创建 src/styles/variables.scss 文件
$primary_color: #1890ff;
$font_size_18: 18px;
  • 4.组件中使用
<style lang="scss" scoped>
$bgc: yellow;

.ab {
  color: $primary_color;
  font-size: $font_size_18;
  background: $bgc;
}
</style>

项目中使用 环境变量

  • 1.在项目根目录创建 .env.development .env.test .env.production 文件, 定义的变量必须以 VITE_ 前缀开头
// .env.development
VITE_API_BASE_URL=https://apidev.example.com

// .env.test
VITE_API_BASE_URL=https://apitest.example.com

// 当然也可以在 package.json 直接设置环境变量
// 1. npm i cross-env -D
// 2. 添加指令如 "mock": "cross-env VITE_MOCK=true vite",
  • 2.package.json 读取对应文件
"dev": "vite",
"build:test": "vite build --mode test",
"build": "vite build",
  • 3.项目中读取变量
console.log('VITE_API_BASE_URL', import.meta.env.VITE_API_BASE_URL);
  • 4.vite.config.js 中读取环境变量 loadEnv
import { defineConfig, loadEnv } from 'vite';
export default ({ mode }) => {
  const env = loadEnv(mode, __dirname);
  const { VITE_MOCK } = env;
  return defineConfig({});
};

使用 Vue Router@4

  • 1.定义并配置路由
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // 定义路由
    // { path: '/foo', component: Foo },
    // { path: '/bar', component: Bar }
  ],
});

const app = createApp(App);
app.use(router);
app.mount('#app');
  • 2.使用
<template>
  <div class="about">
    <h1>This is an about page</h1>
    <p>route:{{ route.path }}</p>
    <button @click="goHomeView">to home</button>
    <router-link to="/home">to home</router-link>
    <router-view></router-view>
  </div>
</template>

<script setup>
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();

const goHomeView = () => {
  router.push('/');
};
</script>

使用 pina

import { createPinia } from 'pinia';
// 持久化插件 https://prazdevs.github.io/pinia-plugin-persistedstate/guide/
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
const app = createApp(App);
app.use(pinia);
  • 3.新建 src/store/counter.js
// 写法1 组合式写法(推荐)
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useCounterStore = defineStore(
  'counter',
  () => {
    const count = ref(0);
    function increment() {
      count.value++;
    }
    function incrementAge() {
      age.value++;
    }

    return { count, increment };
  },
  // Your whole store will now be saved with the default persistence settings.
  {
    persist: true, // localStorage 存储改 counter 所有的 key
  }
);

// 写法2 选项式写法
import { defineStore } from 'pinia';

export const useStore = defineStore('store', {
  state: () => ({
    save: {
      me: 'saved',
      notMe: 'not-saved',
    },
    saveMeToo: 'saved',
  }),
  persist: {
    paths: ['save.me', 'saveMeToo'], //only save.me and saveMeToo values will be persisted.
    storage: sessionStorage, //default: localStorage
  },
});
  • 4.页面中使用
<template>
  <h3>App</h3>
  <p>count from store: {{ count }}</p>
  <button @click="increment">increment store count</button>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/store/counter'

const store = useCounterStore()
const { count } = storeToRefs(store)
const { increment } = store

</script>
  • 5.和组合式函数比较 定义和使用模式类似,但是 pinia 是全局化的状态管理,全局共享一份数据。组合式函数是为了实现特定功能。

自动按需引入 vue vue-router element-plus antd-vue

unplugin-auto-import + unplugin-vue-components

  • 1.npm i unplugin-auto-import unplugin-vue-components -D
  • 2.vite.config.js 中配置
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// 若是 antd-vue
// import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'

// https://blog.csdn.net/Delete_89x/article/details/126430049  解决element-plus自动引入后ElLoading、ElMessage、ElNotification、ElMessageBox样式丢失的问题
import ElementPlus from 'unplugin-element-plus/vite'


plugins: [
    vue(),
    AutoImport({
      imports: ['vue', 'vue-router'],
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
      // 若是antd-vue
      // resolvers: [AntDesignVueResolver()]
    }),
    ElementPlus({}),
],
  • 3.应用
<template>
  <h3>App</h3>
  <p>count:{{ count }}</p>

  <!-- element-plus -->
  <el-button @click="goAbout">to about</el-button>

  <!-- antd-vue -->
  <a-button type="primary" @click="add">click</a-button>

  <router-view></router-view>
</template>

<script setup>
  // 再也不用写以下 引入包 的代码了
  // import { ref } from 'vue'
  // import { useRouter, useRoute } from 'vue-router';

  const count = ref(1);
  const router = useRouter();
  const goAbout = () => {
    router.push('/about');
  };
</script>

vite.config.js 常用配置

import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';

// https://element-plus.gitee.io/zh-CN/guide/quickstart.html#%E6%8C%89%E9%9C%80%E5%AF%BC%E5%85%A5
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

// https://blog.csdn.net/Delete_89x/article/details/126430049  解决element-plus自动引入后ElLoading、ElMessage、ElNotification、ElMessageBox样式丢失的问题
import ElementPlus from 'unplugin-element-plus/vite';

import { viteMockServe } from 'vite-plugin-mock';

// https://vitejs.dev/config/
export default ({ mode }) => {
  const env = loadEnv(mode, __dirname);
  const { VITE_MOCK } = env;
  return defineConfig({
    base: '/', //配置项目的基础路径,默认为 /。
    build: {
      //build:配置项目构建选项,包括输出路径、是否开启压缩等。
      outDir: 'dist',
      assetsDir: 'assets',
      sourcemap: false,
    },
    css: {
      //配置处理 CSS 的选项,包括预处理器、样式优化等
      preprocessorOptions: {
        scss: {
          additionalData: `@import "@/styles/variables.scss";`, //添加全局 SCSS 变量
        },
      },
      postcss: {
        plugins: [],
      },
    },
    server: {
      // 配置开发服务器选项,包括端口号、代理设置等
      host: 'localhost',
      port: 5200,
      // open: true, // 自动打开浏览器
      proxy: {
        '/api': 'http://localhost:3000',
      },
    },
    plugins: [
      vue(),
      AutoImport({
        imports: ['vue', 'vue-router'],
        resolvers: [ElementPlusResolver()],
      }),
      Components({
        resolvers: [ElementPlusResolver()],
      }),
      ElementPlus({}),
      viteMockServe({
        mockPath: 'mock/', // 指定 mock ⽂件所在的⽬录
        enable: VITE_MOCK === 'true', // 开启或关闭 mock 服务
        watchFiles: false, // 不要 监视 mock/ ⽂件更改,需要每次重启服务,才能更新 mock 数据
      }),
    ],
    resolve: {
      // 配置模块解析选项,包括别名、模块查找路径等。
      alias: {
        '@': path.resolve(__dirname, 'src'),
        vue: 'vue/dist/vue.esm-bundler.js',
      },
    },
  });
};

vite-plugin-mock

  • 1.安装 npm i vite-plugin-mock -D
  • 2.在 根目录下创建 mock 文件夹,创建 mock/index.js 文件
  • 3.在 vite.config.js 中配置
// mock/index.js
import users from "./user";
export default [
  ...users
]

// mock/user/index.js
export default [
  {
    url: "/api/getUsers",
    method: "get",
    response: () => {
      return {
        code: 0,
        message: "ok",
        data: {
          name: "vben",
          age: 18,
        },
      }
    }
  },
  {
    url: "/api/saveUsers",
    method: "post",
    response: () => {
      return {
        code: 0,
        message: "save ok",
      }
    }
  }
]

// vite.config.js
import { viteMockServe } from 'vite-plugin-mock'

plugins: [
    vue(),
    viteMockServe({
      mockPath: 'mock/',
      enable: true,  // 开启或关闭 mock 服务
      watchFiles: false, // 不要 监视 mock/ ⽂件更改,需要每次重启服务,才能更新 mock 数据
    })
  ],

搭配的 ui 框架

  • Element Plus:适用于开发中后台系统的界面。
  • Ant Design Vue:提供了丰富的 UI 组件和模板。
  • Vant:适用于移动端的组件和模板。