vue3和react,拿下vue3你要做好这些准备

vue3已经RC版了,但是目前国内最流行的UI库element-ui还没有动静,我们团队决定自己将ElementUI升级为3.0版本全面支持vue3.0

最近很多小伙伴都跃跃欲试开始参与到element3开源项目中来,但是经常有一些小问题卡着进行不下去,究其原因还是太着急,缺乏对vue3新特性和变化的足够了解,故有此文。基本上大家看完再去实践一下,vue3也就拿下了。

喜欢看视频学习的小伙伴请移步这里,我也准备了视频教程

快速起始

cdn

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.0.0-rc.7/vue.global.js"></script>
复制代码

vue-cli

升级vue-cli v4.5

npm i -g @vue/cli@next
<span class="copy-code-btn">复制代码</span>

新建项目会有vue3选项 vue-cli 4.5

vite

使用vite体验更快速

$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
复制代码

从vue2迁移

新特性

破坏性变化

移除

composition api

composition api为vue应用提供更好的逻辑复用和代码组织。

<template>
  <div>
    <p>counter: {{counter}}</p>
    <p>doubleCounter: {{doubleCounter}}</p>
    <p ref="desc"></p>
  </div>
</template>

<script>
import {
  reactive,
  computed,
  watch,
  ref,
  toRefs,
  onMounted,
  onUnmounted,
} from "vue";

export default {
  setup() {
    const data = reactive({
      counter: 1,
      doubleCounter: computed(() => data.counter * 2),
    });

    let timer

    onMounted(() => {
      timer = setInterval(() => {
        data.counter++
      }, 1000);
    })

    onUnmounted(() => {
      clearInterval(timer)
    })

    const desc = ref(null)

    watch(()=>data.counter, (val,oldVal)=>{
      // console.log(`counter change from ${oldVal} to ${val}`);
      desc.value.textContent = `counter change from ${oldVal} to ${val}`
    })
    
    return {...toRefs(data), desc};
  },
};
</script>
复制代码

Teleport

传送门组件提供一种简洁的方式可以指定它里面内容的父元素。

<template>
  <button @click="modalOpen = true">
    弹出一个全屏模态窗口</button>

  <teleport to="body">
    <div v-if="modalOpen" class="modal">
      <div>
        这是一个模态窗口!
        我的父元素是"body"!
        <button @click="modalOpen = false">Close</button>
      </div>
    </div>
  </teleport>
</template>

<script>
export default {
  data() {
    return {
      modalOpen: true
    }
  },
};
</script>

<style scoped>
.modal {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background-color: rgba(0,0,0,.5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.modal div {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: white;
  width: 300px;
  height: 300px;
  padding: 5px;
}
</style>
复制代码

Fragments

vue3中组件可以拥有多个根。

<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>
复制代码

Emits Component Option

vue3中组件发送的自定义事件需要定义在emits选项中:

  • 原生事件会触发两次,比如click
  • 更好的指示组件工作方式
  • 对象形式事件校验
<template>
  <div @click="$emit('click')">
    <h3>自定义事件</h3>
  </div>
</template>

<script>
export default {
  emits: ['click']
}
</script>
复制代码

自定义渲染器 custom renderer

Vue3.0中支持 自定义渲染器 (Renderer):这个 API 可以用来自定义渲染逻辑。比如下面的案例我们可以把数据渲染到canvas上。

首先创建一个组件描述要渲染的数据,我们想要渲染一个叫做piechart的组件,我们不需要单独声明该组件,因为我们只是想把它携带的数据绘制到canvas上。创建CanvasApp.vue

<template>
  <piechart @click="handleClick" :data="state.data" :x="200" :y="200" :r="200"></piechart>
</template>
<script>
import { reactive, ref } from "vue";
export default {
  setup() {
    const state = reactive({
      data: [
        { name: "大专", count: 200, color: "brown" },
        { name: "本科", count: 300, color: "yellow" },
        { name: "硕士", count: 100, color: "pink" },
        { name: "博士", count: 50, color: "skyblue" }
      ]
    });
    function handleClick() {
      state.data.push({ name: "其他", count: 30, color: "orange" });
    }
    return {
      state,
      handleClick
    };
  }
};
</script>
复制代码

下面我们创建自定义渲染器,main.js

import { createApp, createRenderer } from 'vue'
import CanvasApp from './CanvasApp.vue'

const nodeOps = {
  insert: (child, parent, anchor) => {
    // 我们重写了insert逻辑,因为在我们canvasApp中不存在实际dom插入操作
    // 这里面只需要将元素之间的父子关系保存一下即可
    child.parent = parent;

    if (!parent.childs) { 
      parent.childs = [child]
    } else {
      parent.childs.push(child);
    }

    // 只有canvas有nodeType,这里就是开始绘制内容到canvas
    if (parent.nodeType == 1) {
      draw(child); 
      // 如果子元素上附加了事件,我们给canvas添加监听器
      if (child.onClick) {
        ctx.canvas.addEventListener('click', () => {
          child.onClick();
          setTimeout(() => {
            draw(child)
          }, 0);
        })
      }
    }
  },
  remove: child => {},
  createElement: (tag, isSVG, is) => {
    // 创建元素时由于没有需要创建的dom元素,只需返回当前元素数据对象
    return {tag}
  },
  createText: text => {},
  createComment: text => {},
  setText: (node, text) => {},
  setElementText: (el, text) => {},
  parentNode: node => {},
  nextSibling: node => {},
  querySelector: selector => {},
  setScopeId(el, id) {},
  cloneNode(el) {},
  insertStaticContent(content, parent, anchor, isSVG) {},
  patchProp(el, key, prevValue, nextValue) {
    el[key] = nextValue;
  },
};

// 创建一个渲染器
let renderer = createRenderer(nodeOps);

// 保存画布和其上下文
let ctx;
let canvas;

// 扩展mount,首先创建一个画布元素
function createCanvasApp(App) {
  const app = renderer.createApp(App);
  const mount = app.mount
  app.mount = function (selector) {
    canvas = document.createElement('canvas');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    document.querySelector(selector).appendChild(canvas);
    ctx = canvas.getContext('2d');
    mount(canvas);
  }
  return app
}

createCanvasApp(CanvasApp).mount('#demo')
复制代码

index.html里面添加一个div#demo

编写绘制逻辑

const draw = (el,noClear) => {
  if (!noClear) {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
  }
  if (el.tag == 'piechart') {
    let { data, r, x, y } = el;
    let total = data.reduce((memo, current) => memo + current.count, 0);
    let start = 0,
        end = 0;
    data.forEach(item => {
      end += item.count / total * 360;
      drawPieChart(start, end, item.color, x, y, r);
      drawPieChartText(item.name, (start + end) / 2, x, y, r);
      start = end;
    });
  }
  el.childs && el.childs.forEach(child => draw(child,true));
}

const d2a = (n) => {
  return n * Math.PI / 180;
}
const drawPieChart = (start, end, color, cx, cy, r) => {
  let x = cx + Math.cos(d2a(start)) * r;
  let y = cy + Math.sin(d2a(start)) * r;
  ctx.beginPath();
  ctx.moveTo(cx, cy);
  ctx.lineTo(x, y);
  ctx.arc(cx, cy, r, d2a(start), d2a(end), false);
  ctx.fillStyle = color;
  ctx.fill();
  ctx.stroke();
  ctx.closePath();
}
const drawPieChartText = (val, position, cx, cy, r) => {
  ctx.beginPath();
  let x = cx + Math.cos(d2a(position)) * r/1.25 - 20;
  let y = cy + Math.sin(d2a(position)) * r/1.25;
  ctx.fillStyle = '#000';
  ctx.font = '20px 微软雅黑';
  ctx.fillText(val,x,y);
  ctx.closePath();
}
复制代码

Global API 改为应用程序实例调用

vue2中有很多全局api可以改变vue的行为,比如Vue.component等。这导致一些问题:

  • vue2没有app概念,new Vue()得到的根实例被作为app,这样的话所有创建的根实例是共享相同的全局配置,这在测试时会污染其他测试用例,导致测试变得困难。
  • 全局配置也导致没有办法在单页面创建不同全局配置的多个app实例。

vue3中使用createApp返回app实例,由它暴露一系列全局api

import { createApp } from 'vue'
const app = createApp({})
    .component('comp', { render: () => h('div', 'i am comp') })
  .mount('#app')
复制代码

列举如下:

2.x Global API 3.x Instance API (app)
Vue.config app.config
Vue.config.productionTip removed (see below)
Vue.config.ignoredElements app.config.isCustomElement (see below)
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use (see below)
Vue.filter removed

Global and internal APIs重构为可做摇树优化

vue2中不少global-api是作为静态函数直接挂在构造函数上的,例如Vue.nextTick(),如果我们从未在代码中用过它们,就会形成所谓的dead code,这类global-api造成的dead code无法使用webpack的tree-shaking排除掉。

import Vue from 'vue'

Vue.nextTick(() => {
  // something something DOM-related
})
复制代码

vue3中做了相应的变化,将它们抽取成为独立函数,这样打包工具的摇树优化可以将这些dead code排除掉。

import { nextTick } from 'vue'

nextTick(() => {
  // something something DOM-related
})
复制代码

受影响api:

  • Vue.nextTick
  • Vue.observable (replaced by Vue.reactive)
  • Vue.version
  • Vue.compile (only in full builds)
  • Vue.set (only in compat builds)
  • Vue.delete (only in compat builds)

model选项和v-bindsync 修饰符被移除,统一为v-model参数形式

vue2中.sync和v-model功能有重叠,容易混淆,vue3做了统一。

<div id="app">
  <h3>{{data}}</h3>    
  <comp v-model="data"></comp>
</div>
复制代码
app.component('comp', {
  template: `
    <div @click="$emit('update:modelValue', 'new value')">
        i am comp, {{modelValue}}
    </div>
    `,
  props: ['modelValue'],
})
复制代码

渲染函数API修改

渲染函数变得更简单好用了,修改主要有以下几点:

不再传入h函数,需要我们手动导入;拍平的props结构。scopedSlots删掉了,统一到slots

import {h} from 'vue'

render() {
  const emit = this.$emit
  const onclick = this.onclick
  return h('div', [
    h('div', {
      onClick() {
          emit('update:modelValue', 'new value')
        }}, 
      `i am comp, ${this.modelValue}`
    ),
    h('button', {
      onClick(){
          onclick()
        }}, 
      'buty it!'
    )
  ])
},
复制代码

函数式组件仅能通过简单函数方式创建,functional选项废弃

函数式组件变化较大,主要有以下几点:

  • 性能提升在vue3中可忽略不计,所以vue3中推荐使用状态组件
  • 函数时组件仅能通过纯函数形式声明,接收propscontext两个参数
  • SFC中<template>不能添加functional特性声明函数是组件
  • { functional: true }组件选项移除

声明一个函数式组件,Functional.js

import { h } from 'vue'

const Heading = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots)
}

Heading.props = ['level']

export default Heading
复制代码
<Functional level="3">这是一个h3</Functional>
复制代码

移除functional选项,这里以element中divider为例说明

异步组件要求使用defineAsyncComponent 方法创建

由于vue3中函数式组件必须定义为纯函数,异步组件定义时有如下变化:

  • 必须明确使用defineAsyncComponent包裹

  • component 选项重命名为 loader

  • Loader 函数不在接收 resolve and reject 且必须返回一个Promise

定义一个异步组件

import { defineAsyncComponent } from 'vue'

// 不带配置的异步组件
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))
复制代码

带配置的异步组件,loader选项是以前的component

import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

// 待配置的异步组件
const asyncPageWithOptions = defineAsyncComponent({
  loader: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})
复制代码

组件data选项应该总是声明为函数

vue3中data选项统一为函数形式,返回响应式数据。

createApp({
  data() {
    return {
      apiKey: 'a1b2c3'
    }
  }
}).mount('#app')
复制代码

自定义组件白名单

vue3中自定义元素检测发生在模板编译时,如果要添加一些vue之外的自定义元素,需要在编译器选项中设置isCustomElement选项。

使用构建工具时,模板都会用vue-loader预编译,设置它提供的compilerOptions即可:

rules: [
  {
    test: /\.vue$/,
    use: 'vue-loader',
    options: {
      compilerOptions: {
        isCustomElement: tag => tag === 'plastic-button'
      }
    }
  }
  // ...
]
复制代码

我们演示项目使用vite,在vite.config.js中配置vueCompilerOptions即可:

module.exports = {
  vueCompilerOptions: {
    isCustomElement: tag => tag === 'piechart'
  }
}
复制代码

如果是采用的运行时编译版本的vue,可通过全局配置isCustomElement

const app = Vue.createApp({})
app.config.isCustomElement = tag => tag === 'plastic-button'
复制代码

is属性仅限于用在component标签上

vue3中设置动态组件时,is属性仅能用于component标签上

<component is="comp"></component>
复制代码

dom内模板解析使用v-is代替

<table>
  <tr v-is="'blog-post-row'"></tr>
</table>
复制代码

仅限in-dom模板,因此我们测试放到独立页面测试,index2.html

<div id="app">
 <table>
   <tr v-is="'row'" v-for="item in items" :data="item"></tr>
 </table>
</div>

复制代码


$scopedSlots 属性被移除,都用$slots代替

vue3中统一普通插槽和作用域插槽到$slots,具体变化如下:

  • 插槽均以函数形式暴露
  • $scopedSlots移除

函数形式访问插槽内容,MyLink.vue

<script>
import {h} from 'vue'
export default {
  props: {
    to: {
      type: String,
      required: true,
    },
  },
  render() {
    return h("a", { href: this.to }, this.$slots.default());
  },
};
</script>
<span class="copy-code-btn">复制代码</span>

迁移时,注意修改$slots.xx$slots.xx(),这里以element中uploader为例说明

特性强制策略变更

底层api变化,不影响多数开发者

v3.vuejs.org/guide/migra…

自定义指令API和组件保持一致

vue3中指令api和组件保持一致,具体表现在:

  • bind → beforeMount
  • inserted → mounted
  • beforeUpdate: new! 元素自身更新前调用, 和组件生命周期钩子很像
  • update → removed! 和updated基本相同,因此被移除之,使用updated代替。
  • componentUpdated → updated
  • beforeUnmount new! 和组件生命周期钩子相似, 元素将要被移除之前调用。
  • unbind → unmounted

写一个指令实验一下

const app = Vue.createApp({})

app.directive('highlight', {
  beforeMount(el, binding, vnode) {
    el.style.background = binding.value
  }
})
复制代码
<p v-highlight="yellow">Highlight this text bright yellow</p>
复制代码

transition类名变更:

  • v-enterv-enter-from
  • v-leavev-leave-from

Vue2中过度流程图:图中两个起始类名发生变化

试验一下,TransitionTest.vue

<template>
  <div id="demo">
    <button @click="show = !show">Toggle</button>

    <transition name="fade">
      <p v-if="show">hello</p>
    </transition>
  </div>
</template>

<script>
export default {
  data() {
    return {
      show: true,
    };
  },
};
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
<span class="copy-code-btn">复制代码</span>

组件watch选项和实例方法$watch不再支持点分隔符字符串路径

.分割的表达式不再被watch和watch支持,可以使用计算函数作为watch支持,可以使用计算函数作为watch支持,可以使用计算函数作为watch参数实现。

this.$watch(() => this.foo.bar, (v1, v2) => {
  console.log(this.foo.bar)
})
复制代码

Vue 2.x中应用程序根容器的 outerHTML 会被根组件的模板替换 (或被编译为template),Vue 3.x现在使用根容器的innerHTML取代

keyCode 作为 v-on 修饰符被移除

vue2中可以使用keyCode指代某个按键,vue3不再支持。

<!-- keyCode方式不再被支持 -->
<input v-on:keyup.13="submit" />

<!-- 只能使用alias方式 -->
<input v-on:keyup.enter="submit" />
复制代码

on,on, on,off and $once 移除

上述3个方法被认为不应该由vue提供,因此被移除了,可以使用其他三方库实现。

<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
复制代码
// 创建emitter
const emitter = mitt()

// 发送事件
emitter.emit('foo', 'foooooooo')

// 监听事件
emitter.on('foo', msg => console.log(msg))
复制代码

Filters移除

vue3中移除了过滤器,请调用方法或者计算属性代替。

Inline templates attributes移除

vue2中提供inline-template特性可提供自定义组件内部内容作为其模板

<my-component inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-component>
<span class="copy-code-btn">复制代码</span>

vue3不再支持,可以使用script替代

<script type="text/html" id="my-comp-template">
  <div>{{ hello }}</div>
</script>
复制代码
const MyComp = {
  template: '#my-comp-template'
  // ...
}
复制代码

vue3和react,拿下vue3你要做好这些准备的相似文章

vue3 composition api文档,Vue3.2 setup语法糖、Composition API、状态库Pinia归纳总结分析【Vue3官方教程】🎄万字笔记 | 同步导学视频分析Vue3 全家桶 + TS+ Vite2 + element-plus 搭建简洁时尚的博客网站实战及踩坑记分析vue代理服务器proxy配置,Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?分析vite vue3 typescript,vite工程化实践分析compositionapi 详解,Composition-API实操还觉得短吗分析composition用法,Composition API原理深度剖析分析Vue3丨从 5 个维度来讲 Vue3 变化分析Vue3 的 script setup 语法糖是真的的香,学习使我进步分析