vue3项目开发

756 阅读9分钟

Vue3 项目开发

示例代码

stackblitz.com/edit/vue-ty…

import { createApp } from 'vue';

import App from './App.vue';

export default () => {
  const app = createApp(App);
  
  app.config.warnHandler = () => { }; // 关闭提示

  app.mount('#app');
};

<template>
  <div>{{ hello }}</div>
  <div>{{ addStr }}</div>
  <div>{{ name }}</div>
  <div @click="clickSize">
    {{ size }}
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
  ref,
  computed,
  reactive,
  toRefs,
  onMounted,
  nextTick,
  watch,
  getCurrentInstance,
  inject,
} from 'vue';
// import Test from './Test';

export default defineComponent({
  name: 'HelloWorld',
  components: {
    // Test
  },
  props: {
    test: {
      type: String,
      default: 'test1',
    },
  },
  emits: ['close'],
  setup(props, context) {
    const instance = getCurrentInstance();
    const hello = ref('hello');
    const obj = reactive({
      name: 'aa',
      size: 'bb',
    });

    const other = inject('other');

    const { test } = toRefs(props);

    const addStr = computed(() => {
      return props.test + hello.value;
    });

    const clickSize = () => {
      obj.size = test.value;
      context.emit('close');
    };

    watch(
      () => obj.name,
      () => {
        hello.value = 'hello1';
      }
    );

    // 生命周期
    onMounted(() => {
      nextTick(() => {
        setTimeout(() => {
          obj.name = 'cc';
        }, 5000);
      });
    });

    return {
      hello,
      addStr,
      other,
      ...toRefs(obj),
      clickSize,
    };
  },
});
</script>

<style lang="less" scoped>

</style>

vue3 重要变更

Vue3.0设计模式与2.0不同,采用的是多模块架构思想使得项目中引用Vue框架时耦合降低,不必要强依赖于Vue。比如在项目中使用Vue3.0时可以按需引用使用的功能模块不再整个引用Vue整个的框架,项目打包时只会打包相应的功能模块应用体积会大大降低。若需要进行其他平台的适配、基于Vue3.0进行个性化的难度也大大降低,只需要重写相应的更新模块即可。

  • 新增 Composition API 可以更好的逻辑复用和代码组织,同一功能的代码不至于像以前一样太分散,虽然 Vue2 中可以用 minxin 来实现复用代码,但也存在问题,比如方法或属性名会冲突,代码来源也不清楚等

  • Proxy 代替 Object.defineProperty 重构了响应式系统,可以监听到数组下标变化,及对象新增属性,因为监听的不是对象属性,而是对象本身,还可拦截 apply、has 等13种方法

  • 重构了虚拟 DOM,在编译时会将事件缓存、将 slot 编译为 lazy 函数、保存静态节点直接复用(静态提升)、以及添加静态标记、Diff 算法使用 最长递增子序列 优化了对比流程,使得虚拟 DOM 生成速度提升 200%

  • Vue3 是用 TypeScript 写的,所以对 TypeScript 的支持度更好

  • 删除了两个生命周期 beforeCreate、created,直接用 setup 代替了

  • 支持 Tree-Shaking,会在打包时去除一些无用代码,没有用到的模块,使得代码打包体积更小

  • 新增了三个组件:Fragment 支持多个根节点、Suspense 可以在组件渲染之前的等待时间显示指定内容、Teleport 可以让子组件能够在视觉上跳出父组件(如父组件overflow:hidden)

  • 新增了开发环境的两个钩子函数,在组件更新时 onRenderTracked 会跟踪组件里所有变量和方法的变化、每次触发渲染时 onRenderTriggered 会返回发生变化的新旧值,可以让我们进行有针对性调试

  • Vue3 不兼容 IE11

  • 从 Vue 3.0 开始,filters过滤器已移除,且不再支持。

  • $on$off$once 实例方法已被移除,组件实例不再实现事件触发接口。事件总线模式可以被替换为使用外部的、实现了事件触发器接口的库,例如 mitt 或 tiny-emitter

  • 指令的钩子函数已经被重命名,以更好地与组件的生命周期保持一致。在 Vue 3 中,为自定义指令创建了一个更具凝聚力的 API

  • Vue3.2开始正式上线script setup 语法糖 <script setup></script>

  • 新增指令 v-memo,可以缓存 html 模板,比如 v-for 列表不会变化的就缓存,简单说就是用内存换时间

  • 支持在 <style></style> 里使用 v-bind,给 CSS 绑定 JS 变量(color: v-bind(str))

  • 此更改统一了 3.x 中的普通插槽和作用域插槽。

  • v-model 升级, 在 Vue 3 中,双向数据绑定的 API 已经标准化,以减少开发者在使用 v-model 指令时的混淆,并且更加灵活

  • Vue3 中 使用 defineAsyncComponent 定义异步组件,配置选项 component 替换为 loader ,Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise

Vue3 性能是如何提升的?

响应式系统提升

Vue2.0中响应式系统的核心defineProperty 在初始化时会遍历data中的所有成员,通过defineProperty将对象的属性转换为getter和setter,如果对象中的成员又是对象的话需要递归处理里面的每一个属性,因为是在初始化时进行的defineProperty,所以没有使用的属性也进行了响应化处理

Vue3.0中使用Proxy对象重写响应式系统 使用的Es6后新增的Proxy对象,本身性能就比defineProperty要好。另外代理对象可以拦截属性的访问、赋值、删除等操作,不需要初始化时遍历所有的属性。另外多层嵌套属性只有在访问时才会递归处理下一层属性

可以监听动态新增的属性,vue2.0中需要调用vue.set()进行处理 可以监听删除的属性,vue2.0监听不到属性的删除 可以监听数组的索引和length属性,vue2.0监听不到这两个属性的操作

为什么要用 Proxy 代替 defineProperty ?好在哪里?

  • Object.defineProperty 是 Es5 的方法,Proxy 是 Es6 的方法
  • defineProperty 不能监听到数组下标变化和对象新增属性,Proxy 可以
  • defineProperty 是劫持对象属性,Proxy 是代理整个对象
  • defineProperty 局限性大,只能针对单属性监听,所以在一开始就要全部递归监听。Proxy 对象嵌套属性运行时递归,用到才代理,也不需要维护特别多的依赖关系,性能提升很大,且首次渲染更快
  • defineProperty 会污染原对象,修改时是修改原对象,Proxy 是对原对象进行代理并会返回一个新的代理对象,修改的是代理对象
  • defineProperty 不兼容 IE8,Proxy 不兼容 IE11

编译时对虚拟Dom的性能优化

vue2.x中的虚拟dom是进行全量的对比。而vue3.0新增了静态标记。在与上次虚拟节点进行对比的时候,只对比带有patch flag的节点,并且可以通过flag的信息得知当前节点要对比的具体内容。

vue3.0的diff算法在创建虚拟dom的时候,会根据dom中的内容是否发生变化,添加静态标记。只对比带有patch flag的节点。

Vue3 Diff算法和 Vue2 的区别

我们知道在数据变更触发页面重新渲染,会生成虚拟 DOM 并进行 patch 过程,这一过程在 Vue3 中的优化有如下

编译阶段的优化:

  • 事件缓存:将事件缓存(如: @click),可以理解为变成静态的了
  • 静态提升:第一次创建静态节点时保存,后续直接复用
  • 添加静态标记:给节点添加静态标记,以优化 Diff 过程

由于编译阶段的优化,除了能更快的生成虚拟 DOM 以外,还使得 Diff 时可以跳过"永远不会变化的节点",Diff 优化如下

  • Vue2 是全量 Diff,Vue3 是静态标记 + 非全量 Diff
  • 使用最长递增子序列优化了对比流程

Vue3 update 性能提升了 1.3~2 倍

hoistStatic 静态节点提升

vue2.x中无论元素是否参与更新,每次都会重新创建,然后再渲染。vue3.0中对于不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用即可。 当使用hoistStatic时,所有 静态的节点都被提升到render方法之外。这意味着,他们只会在应用启动的时候被创建一次,而后随着每次的渲染被不停的复用。 在大型应用中对于内存有很大的优化。

Composition API

ref() vs reactive()

这两个方法都是Vue3.0中暴露出的进行响应式绑定的函数。

但是两者不同点在于: ref()一般处理是基础数据类型生成的响应式对象获取值或者更改值需要使用value才能获取到对应的值,不仅如此Vue3.0取消了this.$refs进行组件的绑定,而是通过ref()绑定组件实例进行相应操作。

如果将对象分配为 ref 值,则它将被 reactive 函数处理为深层的响应式对象。

reactive()一般处理的是复杂数据类型,生成的响应式对象获取值则可以直接使用和修改

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1
<template>
  {{name}} // test
<template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';
export default defineComponent({
  setup() {
    let state = reactive({
      name: 'test'
    });
    return state
  }
});
</script>

computed 与 watch 、 watchEffect

computed 接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。或者,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。

watch API 与选项式 API this.$watch (以及相应的 watch 选项) 完全等效。watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。

watchEffect 立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

  • computed
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
  setup(props, context) {
    const age = ref(18)

    // 根据 age 的值,创建一个响应式的计算属性 readOnlyAge,它会根据依赖的 ref 自动计算并返回一个新的 ref
    const readOnlyAge = computed(() => age.value++) // 19

    return {
      age,
      readOnlyAge
    }
  }
});
</script>

  • watch
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

// 侦听器还可以使用数组以同时侦听多个源:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})
  • watchEffect
const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

provide/inject

通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。

对于这种情况,我们可以使用一对 provide 和 inject。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。

import { InjectionKey, provide, inject } from 'vue'

const key: InjectionKey<string> = Symbol()

provide(key, 'foo') // 若提供非字符串值将出错

const foo = inject(key) // foo 的类型: string | undefined

Vue 提供了一个 InjectionKey 接口,该接口是扩展了 Symbol 的泛型类型。它可用于在生产者和消费者之间同步 inject 值的类型

toRaw & toRefs

toRaw 返回 reactive 或 readonly 代理的原始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改。不建议保留对原始对象的持久引用。请谨慎使用。

const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true

toRefs 将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:

{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// ref 和原始 property 已经“链接”起来了
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

其他常用API

readonly() 接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 用于响应性追踪
  console.log(copy.count)
})

// 变更 original 会触发依赖于副本的侦听器
original.count++

// 变更副本将失败并导致警告
copy.count++ // 警告!

isRef() 用来判断某个值是否为 ref 创建出来的对象

isReactive() 检查对象是否是由 reactive 创建的响应式代理。

isReadonly() 检查对象是否是由 readonly 创建的只读代理。

isProxy() 检查对象是否是由 reactive 或 readonly 创建的 proxy

shallowRef 创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的。

const foo = shallowRef({})
// 改变 ref 的值是响应式的
foo.value = {}
// 但是这个值不会被转换。
isReactive(foo.value) // false

shallowReactive 创建一个响应式代理,它跟踪其自身属性的响应性shallowReactive生成非递归响应数据,只监听第一层数据的变化,但不执行嵌套对象的深层响应式转换 (暴露原始值)。

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式

shallowReadonly 作用只处理对象最外层属性的响应式(浅响应式)的只读,但不执行嵌套对象的深度只读转换 (暴露原始值)

markRaw 标记一个对象,使其永远不会转换为 proxy。返回对象本身。

customRef 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象。

toRef 可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。

triggerRef手动执行与 shallowRef 关联的任何作用 (effect)。

script setup 语法糖

组件自动注册

在 script setup 中,引入的组件可以直接使用,无需再通过components进行注册,并且无法指定当前组件的名字,它会自动以文件名为主,也就是不用再写name属性了。示例:

<template>
  <Test />
</template>

<script setup lang="ts">
import Test from './Test.vue'
</script>

通过defineProps指定当前 props 类型,获得上下文的props对象。

使用defineEmit定义当前组件含有的事件,并通过返回的上下文去执行 emit

可以通过useAttrs和useSlots, 从上下文中获取 slots 和 attrs

<script setup lang="ts">
  import { defineProps, defineEmits, useAttrs, useSlots } from 'vue'

  const props = defineProps({
    test: String,
  });
  
  const emit = defineEmits(['close', 'open']);
  
  const attrs = useAttrs();
  const slots = useSlots();
  
</script>

属性和方法无需返回,直接使用! 这可能是带来的较大便利之一,在以往的写法中,定义数据和方法,都需要在结尾 return 出去,才能在模板中使用。在 script setup 中,定义的属性和方法无需返回,可以直接使用!

<template>
  {{ welcome }} I have a counter on my website!
  Counter: {{ counter }}
</template>

<script setup lang="ts">
  import { ref } from 'vue';
  
  const welcome = ref('Hello Tailiang') as string | undefined;
  const count = ref(0) as number;
  const increaseCount = () => {
    count.value++;
  }
  increaseCount()
</script>

defineExpose API

传统的写法,我们可以在父组件中,通过 ref 实例的方式去访问子组件的内容,但在 script setup 中,该方法就不能用了,setup 相当于是一个闭包,除了内部的 template模板,谁都不能访问内部的数据和方法。 如果需要对外暴露 setup 中的数据和方法,需要使用 defineExpose API。示例:

<script setup>
import { defineExpose } from 'vue'
const a = 1
const b = 2
defineExpose({
  a
})
</script>

生命周期函数

你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。

下表包含如何在 setup () 内部调用生命周期钩子:

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

<script lang="ts">
import { defineComponent, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onErrorCaptured, onMounted, onUnmounted, onUpdated } from 'vue';
export default defineComponent({
  setup(props, context) {
    onBeforeMount(()=> {
      console.log('beformounted!')
    })
    onMounted(() => {
      console.log('mounted!')
    })

    onBeforeUpdate(()=> {
      console.log('beforupdated!')
    })
    onUpdated(() => {
      console.log('updated!')
    })

    onBeforeUnmount(()=> {
      console.log('beforunmounted!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })

    onErrorCaptured(()=> {
      console.log('errorCaptured!')
    })
  }
});
</script>

vue3 前端工程搭建

vuex + vue-router 运用

Vuex 4.0 提供 Vue 3 支持,其 API 与 3.x 大致相同。唯一的重大变化是插件的安装方式

Vue Router 4.0 提供了 Vue 3 支持,并且有许多自己的重大更改。查看其迁移指南以获取完整详细信息。

  • store/index.ts
import { createStore, createLogger } from 'vuex';

import common from './modules/common';

const debug = process.env.NODE_ENV !== 'production';

const store = createStore({
  modules: {
    common,
  },
  strict: debug,
  // plugins: debug ? [createLogger()] : []
});

export default store;

  • store/modules/common.ts
import { Module, GetterTree, ActionTree, MutationTree } from 'vuex';
import { RootState } from '../types.d';

interface State {
  demoName: string
}

const state = (): State => ({
  demoName: ''
});

const getters: GetterTree<State, RootState> = {};

const actions: ActionTree<State, RootState> = {
  getTestName({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('setTestName', 'Hello World!');
        resolve('');
      }, 500);
    });
  }
};

const mutations: MutationTree<State> = {
  setTestName(state, name: string) {
    state.demoName = name;
  }
};

const address: Module<State, RootState> = {
  namespaced: true,
  state,
  actions,
  mutations
};

export default address;

  • router.ts
import { createRouter, createWebHistory } from 'vue-router';

import Home from './pages/Home.vue';

const history = createWebHistory();
const router = createRouter({
  history,
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
    },
  ],
});

export default router;

  • App.vue
<template>
  <router-view v-slot="{ Component }">
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'App',
});
</script>

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

  • main.ts
import { createApp } from 'vue';
import store from './store';
import router from './router';

import App from './App.vue';

const app = createApp(App);
// app.config.warnHandler = () => { }; // 关闭提示

app.use(store);

// 加载router插件
app.use(router);
// 等待路由载入
router.isReady().then(() => {
  app.mount('#app')
})
  • pages/Home.vue
<template>
  <div class="div">
    {{ demoName }}
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue';
import { useStore } from 'vuex';
import { useRoute, useRouter } from 'vue-router';

export default defineComponent({
  name: 'HomePage',
  setup() {
    const store = useStore();
    const route = useRoute();
    const router = useRouter();

    const demoName = computed(() => store.state.common.demoName);

    // console.log(route);
    // router.push('/')

    return {
      demoName,
    };
  },
});
</script>

<style lang="less" scoped>
.div {
  color: #f00;
  text-align: center;
  font-size: 50px;
  padding: 30px;
}
</style>


vite

vite.config.ts

import { defineConfig } from 'vite';
import path from 'path';
import vuePlugin from '@vitejs/plugin-vue';
import { EsLinter, linterPlugin } from 'vite-plugin-linter';

export default defineConfig(configEnv => ({
  publicDir: 'public',
  plugins: [
    vuePlugin(),
    linterPlugin({
      include: ['src/**/*.ts', 'src/**/*.js', 'src/**/*.vue'],
      linters: [
        new EsLinter({
          configEnv,
          serveOptions: {
            fix: true
          }
        })
      ]
    })
  ]
}));

webpack

SSR 的支持