解决 nodejs 17: digital envelope routines::unsupported

  1. "scripts": {
  2. "serve": "set NODE_OPTIONS=--openssl-legacy-provider & vue-cli-service serve",
  3. "build": "set NODE_OPTIONS=--openssl-legacy-provider & vue-cli-service build",
  4. "lint": "set NODE_OPTIONS=--openssl-legacy-provider & vue-cli-service lint"
  5. },

解决的问题

为什么选择 CompositionAPI

Vue2 的局限性

  • 组件逻辑膨胀导致的可读性变差
  • 无法跨组件重用代码
  • Vue2 对 TS 的支持有限

Vue3 的好处:

  • 更好的Typescript支持
  • 在复杂功能组件中可以实现根据特性组织代码 - 代码内聚性👍 比如: 排序和搜索逻辑内聚
  • 组件间代码复用

重构后的响应式机制带来了哪些改变?

对数组的全面监听

Vue 2.x 对数组只实现了 push,pop,shift,unshift,splice,sort,reverse 这七个方法的监听,以前通过数组下标改变值的时候,是不能触发视图更新的。这不是因为 Object.defineProperty 造成的,而是为了减少性能损耗导致的。

  1. const arr = ["2019","云","栖","音","乐","节"];
  2. arr.forEach((val,index)=>{
  3. Object.defineProperty(arr,index,{
  4. set(newVal){
  5. console.log("赋值");
  6. },
  7. get(){
  8. console.log("取值");
  9. return val;
  10. }
  11. })
  12. })
  13. let index = arr[1]; //取值
  14. arr[0] = "2050"; //赋值

Vue 2.x 中对数组的修改,框架会使用Object.defineProperty 加上 gettersetter 。由于数组的骚操作比较多,比如:arr.length=0,可以瞬间清空一个数组;arr[100]=1 又可以瞬间将一个数组的长度变为 100(其他位置用空元素填充),等等骚操作。我们要穷举每一种数组变化的可能,这样势必就会带来性能开销问题。

  1. const arr = ["2019","云","栖","音","乐","节"];
  2. let ProxyArray = newProxy(arr,{
  3. get:function(target, name, value, receiver) {
  4. console.log("取值")
  5. returnReflect.get(target,name);
  6. },
  7. set: function(target, name, value, receiver) {
  8. console.log("赋值")
  9. Reflect.set(target,name, value, receiver);;
  10. }
  11. })
  12. const index = ProxyArray[0]; //取值
  13. ProxyArray[0]="2050" //赋值

惰性监听

Vue3.0 把创建响应式对象从组件实例初始化中抽离了出来,通过暴露 API 的方式将响应式对象创建的权利交给开发者,开发者可以自由的决定何时何地创建响应式对象。

  • 提高了组件实例初始化速度:Vue3.0 以前组件实例在初始化的时候会将 data 整个对象变为可观察对象,通过递归的方式给每个 Key 使用 Object.defineProperty 加上 getter 和 settter,如果是数组就重写代理数组对象的七个方法。而在 Vue3.0 中,将可响应式对象创建的权利交给了开发者,开发者可以通过暴露的 reactive, compted, effect 方法自定义自己需要响应式能力的数据,实例在初始化时不需要再去递归 data 对象了,从而降低了组件实例化的时间。
  • 降低了运行内存的使用:Vue3.0 以前生成响应式对象会对对象进行深度遍历,同时为每个 Key 生成一个 def 对象用来保存 Key 的所有依赖项,当 Key 对应的 Value 变化的时候通知依赖项进行 update。但如果这些依赖项在页面整个生命周期内不需要更新的时候,这时 def 对象收集的依赖项不仅没用而且还会占用内存,如果可以在初始化 data 的时候忽略掉这些不会变化的值就好了。Vue3.0 通过暴露的 reactive 方法,开发者可以选择性的创建可观察对象,达到减少依赖项的保存,降低了运行内存的使用。

Map、Set、WeakSet、WeakMap 的监听

  1. let map = newMap([['name','wangyangyang']])
  2. let mapProxy = newProxy(map, {
  3. get(target, key, receiver) {
  4. var value = Reflect.get(...arguments)
  5. console.log("取值:",...arguments)
  6. returntypeof value == 'function' ? value.bind(target) : value
  7. }
  8. })
  9. mapProxy.get("name")

Vue 新语法

组合式 API

  1. <template>
  2. <h1>{{ msg }}</h1>
  3. <p>Hello vite</p>
  4. <p>{{ counter }}</p>
  5. <p>{{ doubleCounter }}</p>
  6. <p>{{ msg1 }}</p>
  7. <p ref="desc"></p>
  8. </template>
  9. <script>
  10. import {
  11. computed,
  12. reactive,
  13. onMounted,
  14. onUnmounted,
  15. ref,
  16. toRefs,
  17. watch,
  18. } from "vue";
  19. export default {
  20. name: "HelloWorld",
  21. props: {
  22. msg: String,
  23. },
  24. setup() {
  25. const { counter, doubleCounter } = useCounter();
  26. const msg1 = ref("some message");
  27. const desc = ref(null);
  28. watch(counter, (val, oldVal) => {
  29. const p = desc.value;
  30. p.textContent = `${oldVal} to ${val}`;
  31. });
  32. return { counter, doubleCounter, msg1, desc };
  33. },
  34. };
  35. function useCounter() {
  36. const data = reactive({
  37. counter: 1,
  38. doubleCounter: computed(() => data.counter * 2),
  39. });
  40. let timer;
  41. onMounted(() => {
  42. timer = setInterval(() => {
  43. data.counter++;
  44. }, 1000);
  45. });
  46. onUnmounted(() => {
  47. clearInterval(timer);
  48. });
  49. return toRefs(data);
  50. }
  51. </script>

teleport

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

  1. <template>
  2. <button @click="modalOpen = true">
  3. 弹出一个全屏模态窗口</button>
  4. <teleport to="body">
  5. <div v-if="modalOpen" class="modal">
  6. <div>
  7. 这是一个模态窗口!
  8. 我的父元素是"body"!
  9. <button @click="modalOpen = false">Close</button>
  10. </div>
  11. </div>
  12. </teleport>
  13. </template>
  14. <script>
  15. export default {
  16. data() {
  17. return {
  18. modalOpen: true
  19. }
  20. },
  21. };
  22. </script>
  23. <style scoped>
  24. .modal {
  25. position: absolute;
  26. top: 0; right: 0; bottom: 0; left: 0;
  27. background-color: rgba(0,0,0,.5);
  28. display: flex;
  29. flex-direction: column;
  30. align-items: center;
  31. justify-content: center;
  32. }
  33. .modal div {
  34. display: flex;
  35. flex-direction: column;
  36. align-items: center;
  37. justify-content: center;
  38. background-color: white;
  39. width: 300px;
  40. height: 300px;
  41. padding: 5px;
  42. }
  43. </style>

Fragments

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

  1. <template>
  2. <header>...</header>
  3. <main v-bind="$attrs">...</main>
  4. <footer>...</footer>
  5. </template>

Emits Component Option

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

  • 原生事件会触发两次,比如click
  • 更好的指示组件工作方式
  • 对象形式事件校验
    1. <template>
    2. <div @click="$emit('click')">
    3. <h3>自定义事件</h3>
    4. </div>
    5. </template>
    6. <script>
    7. export default {
    8. emits: ['click']
    9. }
    10. </script>

    自定义渲染器 custom renderer

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

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

  1. <template>
  2. <piechart @click="handleClick" :data="state.data" :x="200" :y="200" :r="200"></piechart>
  3. </template>
  4. <script>
  5. import { reactive, ref } from "vue";
  6. export default {
  7. setup() {
  8. const state = reactive({
  9. data: [
  10. { name: "大专", count: 200, color: "brown" },
  11. { name: "本科", count: 300, color: "yellow" },
  12. { name: "硕士", count: 100, color: "pink" },
  13. { name: "博士", count: 50, color: "skyblue" }
  14. ]
  15. });
  16. function handleClick() {
  17. state.data.push({ name: "其他", count: 30, color: "orange" });
  18. }
  19. return {
  20. state,
  21. handleClick
  22. };
  23. }
  24. };
  25. </script>

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

  1. import { createApp, createRenderer } from 'vue'
  2. import CanvasApp from './CanvasApp.vue'
  3. const nodeOps = {
  4. insert: (child, parent, anchor) => {
  5. // 我们重写了insert逻辑,因为在我们canvasApp中不存在实际dom插入操作
  6. // 这里面只需要将元素之间的父子关系保存一下即可
  7. child.parent = parent;
  8. if (!parent.childs) {
  9. parent.childs = [child]
  10. } else {
  11. parent.childs.push(child);
  12. }
  13. // 只有canvas有nodeType,这里就是开始绘制内容到canvas
  14. if (parent.nodeType == 1) {
  15. draw(child);
  16. // 如果子元素上附加了事件,我们给canvas添加监听器
  17. if (child.onClick) {
  18. ctx.canvas.addEventListener('click', () => {
  19. child.onClick();
  20. setTimeout(() => {
  21. draw(child)
  22. }, 0);
  23. })
  24. }
  25. }
  26. },
  27. remove: child => {},
  28. createElement: (tag, isSVG, is) => {
  29. // 创建元素时由于没有需要创建的dom元素,只需返回当前元素数据对象
  30. return {tag}
  31. },
  32. createText: text => {},
  33. createComment: text => {},
  34. setText: (node, text) => {},
  35. setElementText: (el, text) => {},
  36. parentNode: node => {},
  37. nextSibling: node => {},
  38. querySelector: selector => {},
  39. setScopeId(el, id) {},
  40. cloneNode(el) {},
  41. insertStaticContent(content, parent, anchor, isSVG) {},
  42. patchProp(el, key, prevValue, nextValue) {
  43. el[key] = nextValue;
  44. },
  45. };
  46. // 创建一个渲染器
  47. let renderer = createRenderer(nodeOps);
  48. // 保存画布和其上下文
  49. let ctx;
  50. let canvas;
  51. // 扩展mount,首先创建一个画布元素
  52. function createCanvasApp(App) {
  53. const app = renderer.createApp(App);
  54. const mount = app.mount
  55. app.mount = mount(selector) {
  56. canvas = document.createElement('canvas');
  57. canvas.width = window.innerWidth;
  58. canvas.height = window.innerHeight;
  59. document.querySelector(selector).appendChild(canvas);
  60. ctx = canvas.getContext('2d');
  61. mount(canvas);
  62. }
  63. return app
  64. }
  65. createCanvasApp(CanvasApp).mount('#demo')

编写绘制逻辑

  1. const draw = (el,noClear) => {
  2. if (!noClear) {
  3. ctx.clearRect(0, 0, canvas.width, canvas.height)
  4. }
  5. if (el.tag == 'piechart') {
  6. let { data, r, x, y } = el;
  7. let total = data.reduce((memo, current) => memo + current.count, 0);
  8. let start = 0,
  9. end = 0;
  10. data.forEach(item => {
  11. end += item.count / total * 360;
  12. drawPieChart(start, end, item.color, x, y, r);
  13. drawPieChartText(item.name, (start + end) / 2, x, y, r);
  14. start = end;
  15. });
  16. }
  17. el.childs && el.childs.forEach(child => draw(child,true));
  18. }
  19. const d2a = (n) => {
  20. return n * Math.PI / 180;
  21. }
  22. const drawPieChart = (start, end, color, cx, cy, r) => {
  23. let x = cx + Math.cos(d2a(start)) * r;
  24. let y = cy + Math.sin(d2a(start)) * r;
  25. ctx.beginPath();
  26. ctx.moveTo(cx, cy);
  27. ctx.lineTo(x, y);
  28. ctx.arc(cx, cy, r, d2a(start), d2a(end), false);
  29. ctx.fillStyle = color;
  30. ctx.fill();
  31. ctx.stroke();
  32. ctx.closePath();
  33. }
  34. const drawPieChartText = (val, position, cx, cy, r) => {
  35. ctx.beginPath();
  36. let x = cx + Math.cos(d2a(position)) * r/1.25 - 20;
  37. let y = cy + Math.sin(d2a(position)) * r/1.25;
  38. ctx.fillStyle = '#000';
  39. ctx.font = '20px 微软雅黑';
  40. ctx.fillText(val,x,y);
  41. ctx.closePath();
  42. }

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

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

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

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

  1. import { createApp } from 'vue'
  2. const app = createApp({})
  3. .component('comp', { render: () => h('div', 'i am comp') })
  4. .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排除掉。

例如,vue2中会这样写:

  1. import Vue from 'vue'
  2. Vue.nextTick(() => { // something something DOM-related})

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

  1. import { nextTick } from 'vue'
  2. nextTick(() => {
  3. // something something DOM-related
  4. })

受影响 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-bind****sync** 修饰符被移除,统一为v-model 参数形式

  1. <div id="app">
  2. <h3>{{data}}</h3>
  3. <comp v-model="data"></comp>
  4. </div>
  5. app.component('comp', {
  6. template: `
  7. <div @click="$emit('update:modelValue', 'new value')">
  8. i am comp, {{modelValue}}
  9. </div>
  10. `,
  11. props: ['modelValue'],
  12. })

渲染函数API修改

渲染函数变得更简单好用了,修改主要有以下几点:
不再传入h函数,需要我们手动导入;拍平的props结构。scopedSlots删掉了,统一到slots

  1. import {h} from 'vue'
  2. render() {
  3. const emit = this.$emit
  4. const onclick = this.onclick
  5. return h('div', [
  6. h('div', {
  7. onClick() {
  8. emit('update:modelValue', 'new value')
  9. }},
  10. `i am comp, ${this.modelValue}`
  11. ),
  12. h('button', {
  13. onClick(){
  14. onclick()
  15. }},
  16. 'buty it!'
  17. )
  18. ])
  19. },

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

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

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

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

  1. import { h } from 'vue'
  2. const Heading = (props, context) => {
  3. return h(`h${props.level}`, context.attrs, context.slots)
  4. }
  5. Heading.props = ['level']
  6. export default Heading

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

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

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

  • 必须明确使用defineAsyncComponent包裹
  • component 选项重命名为 loader
  • Loader 函数不在接收 resolve and reject 且必须返回一个Promise

定义一个异步组件

  1. import { defineAsyncComponent } from 'vue'
  2. // 不带配置的异步组件const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))

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

  1. import ErrorComponent from './components/ErrorComponent.vue'
  2. import LoadingComponent from './components/LoadingComponent.vue'
  3. // 待配置的异步组件
  4. const asyncPageWithOptions = defineAsyncComponent({
  5. loader: () => import('./NextPage.vue'),
  6. delay: 200,
  7. timeout: 3000,
  8. errorComponent: ErrorComponent,
  9. loadingComponent: LoadingComponent
  10. })

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

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

  1. createApp({
  2. data() {
  3. return {
  4. apiKey: 'a1b2c3'
  5. }
  6. }
  7. }).mount('#app')

自定义组件白名单

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

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

vue.config.js

  1. rules: [
  2. {
  3. test: /\.vue$/,
  4. use: 'vue-loader',
  5. options: {
  6. compilerOptions: {
  7. isCustomElement: tag => tag === 'plastic-button'
  8. }
  9. }
  10. }
  11. // ...
  12. ]

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

  1. module.exports = {
  2. vueCompilerOptions: {
  3. isCustomElement: tag => tag === 'piechart'
  4. }
  5. }

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

  1. const app = Vue.createApp({})
  2. app.config.isCustomElement = tag => tag === 'plastic-button'

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

  1. <component is="comp"></component>
  2. <table>
  3. <tr v-is="'blog-post-row'"></tr>
  4. </table>
  5. <div id="app">
  6. <table>
  7. <tr v-is="'row'" v-for="item in items" :data="item"></tr>
  8. </table>
  9. </div>
  10. <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.0.0-rc.9/vue.global.js"></script>
  11. <script>
  12. Vue.createApp({
  13. data() {
  14. return {
  15. items: ["aaa", "bbb"],
  16. };
  17. },
  18. })
  19. .component("row", {
  20. props: ["data"],
  21. template: "<tr><td>{{this.data}}</td></tr>",
  22. })
  23. .mount("#app");
  24. </script>

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

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

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

  1. <script>
  2. import {h} from 'vue'
  3. export default {
  4. props: {
  5. to: {
  6. type: String,
  7. required: true,
  8. },
  9. },
  10. render() {
  11. return h("a", { href: this.to }, this.$slots.default());
  12. },
  13. };
  14. </script>

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

特性强制策略变更

底层api变化,不影响多数开发者
https://v3.vuejs.org/guide/migration/attribute-coercion.html#overview

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

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

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

写一个指令实验一下

  1. const app = Vue.createApp({})
  2. app.directive('highlight', {
  3. beforeMount(el, binding, vnode) {
  4. el.style.background = binding.value
  5. }
  6. })
  7. <p v-highlight="yellow">Highlight this text bright yellow</p>

transition 类名变更

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

Vue2中过度流程图:图中两个起始类名发生变化
Vue.js 3 - 图1

试验一下,TransitionTest.vue

  1. <template>
  2. <div id="demo">
  3. <button @click="show = !show">Toggle</button>
  4. <transition name="fade">
  5. <p v-if="show">hello</p>
  6. </transition>
  7. </div>
  8. </template>
  9. <script>
  10. export default {
  11. data() {
  12. return {
  13. show: true,
  14. };
  15. },
  16. };
  17. </script>
  18. <style scoped>
  19. .fade-enter-active,
  20. .fade-leave-active {
  21. transition: opacity 0.5s ease;
  22. }
  23. .fade-enter-from,
  24. .fade-leave-to {
  25. opacity: 0;
  26. }
  27. </style>

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

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

  1. this.$watch(() => this.foo.bar, (v1, v2) => {
  2. console.log(this.foo.bar)
  3. })

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

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

  1. <!-- keyCode方式不再被支持 -->
  2. <input v-on:keyup.13="submit" />
  3. <!-- 只能使用alias方式 -->
  4. <input v-on:keyup.enter="submit" />

$on, $off and $once 移除

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

  1. <script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
  2. // 创建emitter
  3. const emitter = mitt()
  4. // 发送事件
  5. emitter.emit('foo', 'foooooooo')
  6. // 监听事件
  7. emitter.on('foo', msg => console.log(msg))

Filters移除

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

Inline templates attributes移除

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

  1. <my-component inline-template>
  2. <div>
  3. <p>These are compiled as the component's own template.</p>
  4. <p>Not parent's transclusion content.</p>
  5. </div>
  6. </my-component>
  7. <script type="text/html" id="my-comp-template">
  8. <div>{{ hello }}</div>
  9. </script>
  10. const MyComp = {
  11. template: '#my-comp-template'
  12. // ...
  13. }

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

style 标签

style 标签的特殊属性,可以通过 v-bind 函数来使用 JavaScript 中的变量去渲染样式,如果这个变量是响应式数据,就可以很方便地实现样式的切换。

参考

【1】Vue.js 3 文档
【2】重头来过的 Vue 3 带来了什么
【3】带你了解 vue-next(Vue 3.0)之 小试牛刀
【4】带你了解 vue-next(Vue 3.0)之 初入茅庐
【5】拿下vue3你要做好这些准备
【6】Vue3.0光速上手[视频]
【7】尝鲜Vue3之一:浏览器中如何断点调试Vue3 源码
【8】尝鲜Vue3之二:最重要的变化CompositionApi
【9】尝鲜Vue3之三:CompositionAPI小结和包装对象
【10】尝鲜Vue3之四:如何运行Jest单元测试
【11】尝鲜Vue3之五:源码结构
【12】尝鲜Vue3之六:响应式原理的革新 - Vue2、Vue3实现对比
【13】[Vue3官方教程]🎄万字笔记 | 同步导学视频
【14】[Vue官方教程笔记]- 尤雨溪手写mini-vue
【15】茶余饭后聊聊 Vue3.0 响应式数据那些事儿