vue

Vue3

一个轻量、高效且易于扩展的前端框架,引入了 Composition API、性能提升和更灵活的组件系统,适合构建现代化的用户界面。

Posted by page on June 30, 2022

Vue3

创建项目

vue CLI

@vue.cli 4.5 提供了内置选项创建Vue3项目

vue create temp-vue3
// 选择 vue3 预设

vite项目

npm create vue@latest

全局API

createApp

createApp创建应用而非new实例,不再支持vue2中对Vue类的全局配置方式,仅对应用实例配置,大部分配置项保留:

import { createApp } from 'vue';
// 构造vue应用实例
const app = createApp({ ... });
app.config.globalProperties.$http = () => {}

app实例

Vue/app对应表

Vue app
Vue.config.productionTip 移除
Vue.config.ignoredElements app.config.compilerOptions.isCustomElement
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties

全局API 具名导出

为了全局API支持被模块打包工具的TreeShaking,全局API采用具名导出访问

import { nextTick, inject } from 'vue'

nextTick(() => {...});
await nextTick();

组合式(composable)

组合式API

将数据通过 ref 响应化,声明数据逻辑(watch、生命周期、计算属性,计算值)

// 全局导入选项
import { ref, watch, computed, onMounted } from 'vue';

export default {
    ... 兼容options
    setup (props, { slots, expose }) {
      const list = ref([]);
      const getListData = async () => {
        list.value = await fetchUserRepositories(props.user)
      };

      watch(list, (newValue, oldValue) => {
          console.log('The new value is: ' + list.value)
      });

      computed(userName, () => props.user.name);

      onMounted(getListData);

      return {
        list,        // 被视为 options.data/computed/props
        getListData  // 被视为 options.methods
      }
    }
}

组合式:将各部分数据的逻辑以函数的形式独立为块,最终在setup中混合输出;

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { type: String, required: true }
  },
  setup(props) {
    const { user } = toRefs(props);
    // list列表功能
    const { repositories, getUserRepositories } = useUserRepositories(user);
    // 筛选/搜索功能
    const {
      searchQuery,
      repositoriesMatchingSearchQuery
    } = useRepositoryNameSearch(repositories);
    const {
      filters,
      updateFilters,
      filteredRepositories
    } = useRepositoryFilters(repositoriesMatchingSearchQuery);
    return {
      repositories: filteredRepositories,
      getUserRepositories,
      searchQuery,
      filters,
      updateFilters
    }
  }
} 

setup

单文件组件(SFC)下组合式API语法糖

支持将普通的的setup Function以 **

<template>
    <div></div>
    <Comp v-directive-bind />
</template>

<script setup>
    // 顶层变量暴露于模板
    import { ref } from 'vue';
    import Comp from './Comp.vue';
    let msg = ref('tip message');
    let list = ref([]);
    // 允许函数作为自定义指令
    const vDirectiveBind = {
      beforeMount(){ ... }, 
      ...
    };
    // vue选项声明
    const props = defineProps({
      foo: String
    });
    const emit = defineEmits(['change', 'delete']);
    // <script setup> 内访问 $slot $attrs
    const slots = useSlots();
    const attrs = useAttrs();
    // 仅通过defineExpose暴露的属性,可被外部调用$parent/children访问
    defineExpose({ list, foo });
    // <script setup> 内支持顶层async/await
    list.value = await this.$http.getList(params);
</script>

hook

组合式函数,利用 Vue 的组合式 API 来封装和复用 有状态逻辑 的函数;

生命周期

destroyed 更名为onUnmounted

beforeDestroy 更名为 onBeforeUnmount

生命周期事件

// @hook更改为@vnode
<template>
  <child-component @vnode-updated="onUpdated">
</template>

v-model

移除了 v-bind/:attr.sync 写法,将其集成至 v-model

v-model 的默认 prop 由 value 变为 modelValue,且支持添加自定义修饰符;

<user-form v-model:title="formTitle" v-model="formData" />

// 等价于

<user-form
  :title="formTitle" 
  @update:title="v => formTitle = v" 
  :modelValue="formData"
  @update:modelValue="v => formData= v" 
/>

注:

emit("update:modelValue", [...list, item]) 更新引用类型数组项,需 reactive(item) 使其立即作为可响应值

emit 发射后,监听处理将在下次 nextTick 时机才会统一执行

自定义事件

  • 任何情况必须声明事件选项emit
  • 任何情况必须声明props,即需要访问的传递值

指令

生命周期

// 指令生命周期与组件生命周期一致
const vMyDirective = {
  created(el, binding, vnode, prevVnode) {
      // binding.instance访问组件实例,vnode访问组件虚拟节点
      const vm = binding.instance;
  }, // 新增
  beforeMount() {},
  mounted() {},
  beforeUpdate() {}, // 新增
  updated() {},
  beforeUnmount() {}, // 新增
  unmounted() {}
}

// 模板语法直接使用 v-my-directive

template

多根节点支持

片段即支持多根节点的组件,但需要求开发者显式定义 attribute 分布节点;

<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

teleport

vue3新增内置组件,teleport实现了UI结构不再作为Vue逻辑组件结构体现

允许指定Vue组件的UI部分/全部内容在页面的其它位置展现;

<teleport :to="'#el'/'body'">
    <child-component name="John" />
</teleport>

template支持绑定key标识

且会为v-if v-else-if v-else 自动生成key,不再需要手动添加 key 以不被重用;

v-if/v-for

v-if 优先级高于 v-for

组件

引用

<script setup>
  const comp1 = ref(null) // 对comp引用
</script>

<template>
  <comp ref="comp1"/>
</template>

异步组件

// 不再支持
const comp = () => import('./comp.vue');
const asyncModal = {
  component: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
}

// 改为显示定义
import { defineAsyncComponent } from 'vue'
const comp = defineAsyncComponent(() => import('./comp.vue'));
const asyncModal = defineAsyncComponent({
  loader: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
})

Suspense

<Suspense /> 组件实际上是一个提升用户友好度的组件,当如果你在渲染时遇到异步依赖项 (异步组件或者具有 async setup组件),它将等到所有异步依赖项解析完成时再显示默认插槽。

$attr

classstyle 属性也会存在于 $attr 上,所以也会受到 inheritAttr 控制;$listeners成为了 $attrs 对象一部分

<comp @success="handle"></comp>
// 如comp内 $emits 选项未接收handle,则作为原生事件绑定给内层元素上
// $attrs.onSuccess

defineOptions

可用于声明组件选项

<script setup>
  defineOptions({ inheritAttrs: false, customOptions: { /* ... */ } })
</script>

拓展

响应式

markRow(value):标记一个值作为原数据存在,使响应式不作反应

shadowRow(value):创建一个浅层响应式值,值内部修改不作反应

渲染函数(render)

vue提供了一个 h() 函数用于创建 vnodes,默认接受3个参数:

// 完整参数签名
function h(
  type: string | Component,
  props?: object | null,
  children?: Children | Slot | Slots
): VNode

配合 setup 返回渲染函数使用

import { useViewable } from "./useViewable.js";
import { ref, reactive, h, defineComponent } from "vue";

export const UseViewable = defineComponent({
  name: "UseViewable",
  props: {
    containerElement: {
      type: Object,
      default: null,
      validator(value) {
        return value instanceof Element;
      },
    },
    initialSizePercentage: { type: Number, default: 1 },
    scaleStep: { type: Number, default: 0.02 },
    onDrag: { type: Function, default: () => {} },
    onScale: { type: Function, default: () => {} },
  },
  setup(props, { slots, expose }) {
    const target = ref(null);
    const data = reactive(useViewable(target, { ...props }));
    expose(data);
    return () =>
      h(
        "div",
        { ref: target, style: data.style },
        slots.default ? slots.default(data) : []
      );
  },
});

单文件样式

作用域样式 scoped

// 深度选择器 :deep() 
// 代替 ::v-deep
.comp :deep(.el-form){
  margin-bottom: 24px;
}
// 插槽选择器 :slotted
:slotted(.item) {
  width: 240px;
}
// 全局选择器 :global
:global(.piker-popover) {
  padding: 24px;
}

CSS Modules

<style module> 标签会被作为 CSS Modules,暴露的 $style 对象包含module声明的CSS类;

<template>
  <p :class="$style.collapse">
    There is some content...
  </p>
  <div class="box">
    <div class="$box.title">Title 1</div>
  </div>    
</template>

<!-- setup 中使用 -->
<script>
    import { useCssModule } from 'vue'
    export default {
      setup {
        const $style = useCssModule();
        const getTitleClassName = () => $style[`title${i}`];
        return { getTitleClassName };
      }
    }
</script>
<script setup>
    import { useCssModule } from 'vue'
    const $boxStyle = useCssModule('box');
    const getTitleClassName = () => $style[`title${i}`];
</script>

<style module>
    .collapse {
      height: 120px;
      overflow: hidden;
    }
</style>
<style module="box">
    .title {
      font-size: 24px;
      font-weight: bold;
    }
</style>

动态CSS

<script>
    const colors = ['red', 'green', 'blue']; 
    export default {
        ...
        computed: {
            color(){
                return colors[this.index];
            }
        }
    }
</script>

<script setup>
    const colors = ['red', 'green', 'blue']; 
    const color = () => colors[this.index];
</script>

<style scoped>
    .text {
      color: v-bind(color);
    }
    .test {
      color: v-bind(theme.color);
    }
</style> 

移除项

extend

// vue2
const Comp = Vue.extend(comp);
new Comp().$mount('#el');

// vue3
Vue.createApp(comp).mount('#el');

v-on.native

新增 emits 选项定义子组件真正会被触发的事件

否则作为 native 事件监听( inheirtAttr: false 情况外)

事件API

vm.$on|$off|$once 事件API移除,事件总线$eventBus 使用第三方库 mitt代替

// eventBus.js
import emitter from 'tiny-emitter/instance'

export default {
  $on: (...args) => emitter.on(...args),
  $once: (...args) => emitter.once(...args),
  $off: (...args) => emitter.off(...args),
  $emit: (...args) => emitter.emit(...args),
}

生态

Vue Router

createRouter

// import Router from 'vue-router'
import { createRouter } from 'vue-router'

const router = createRouter({
  // ...
})

history

import { createRouter, createWebHistory } from 'vue-router'
// 还有 createWebHashHistory 和 createMemoryHistory
createRouter({
  history: createWebHistory('/base-directory/'), // 替代mode,旧的base选项改为传入参数
  routes: [],
})

通配符路由

{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }

router.match 改为 router.resolve

带有空 path 的命名子路由不再添加斜线

Vuex

createStore

// store/index.js
import { createStore } from 'vuex'
export const store = createStore({
  state () {
    return { ... }
  }
})

// main.js
import { createApp } from 'vue'
import { store } from './store'
import App from './App.vue'
const app = createApp(App)
app.use(store)
app.mount('#app')

useStore

vuex 4 为组件的 setup 钩子函数提供 useStore

import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore();
    const componentData = computed(() => store.getters['component/componentData']);
    const ACTIVE_COMPONENT = (payload) =>
    store.commit('component/ACTIVE_COMPONENT', payload);
  }
}