详解vue-class迁移vite的一次踩坑记录

 更新时间:2022年02月09日 15:47:23   作者:clench  
本文主要介绍了vue-class迁移vite的一次踩坑记录,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

what happen

最进项目从 vue-cli 迁移到了 vite,因为是 vue2 的项目,使用了 vue-class-component 类组件做 ts 支持。
当然迁移过程并没有那么一帆风顺,浏览器控制台报了一堆错,大致意思是某某方法为 undefined,无法调用。打印了下当前 this,为 undefined 的方法都来自于 vuex-class 装饰器下的方法。这就是一件很神奇的事,为什么只有 vuex-class 装饰器下的方法才会为 undefined ?

探究

在网上搜了下并没有类似的问题,只能自己在 node_modules 中一步一步打断点看是哪里出了问题。最先觉得有问题的是 vuex-class ,调试了下 /node_modules/vuex-class/lib/bindings.js 下的代码,发现 vuex-class 只是做了一层方法替换,通过 createDecorator 方法存到 vue-class-component 下的 __decorators__ 数组中。

import { createDecorator } from "vue-class-component";

function createBindingHelper(bindTo, mapFn) {
  function makeDecorator(map, namespace) {
    // 存入到 vue-class-component 的 __decorators__ 数组中
    return createDecorator(function (componentOptions, key) {
      if (!componentOptions[bindTo]) {
        componentOptions[bindTo] = {};
      }
      var mapObject = ((_a = {}), (_a[key] = map), _a);
      componentOptions[bindTo][key] =
        namespace !== undefined
          ? mapFn(namespace, mapObject)[key]
          : mapFn(mapObject)[key];
      var _a;
    });
  }
  function helper(a, b) {
    if (typeof b === "string") {
      var key = b;
      var proto = a;
      return makeDecorator(key, undefined)(proto, key);
    }
    var namespace = extractNamespace(b);
    var type = a;
    return makeDecorator(type, namespace);
  }
  return helper;
}

那就只能来看看 vue-class-component 了。

vue-class-component 的 @Component 装饰器会返回一个 vue对象的构造函数。

// vue-class-component/lib/component.js
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
  if (typeof options === 'function') {
    return componentFactory(options)
  }
  return function (Component: VueClass<Vue>) {
    return componentFactory(Component, options)
  }
}

// 类组件
@Component
export default class HelloWorld extends Vue { ... }

Component 方法会把 class HelloWorld 传入 componentFactory , 在其内部将 name 生命周期 methods computed 等注册到 options 中,然后传入 Vue.extend, 返回一个 vue对象的构造函数 。

export function componentFactory(
  Component: VueClass<Vue>,
  options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
  // 。。。无关代码

  options.name =
    options.name || (Component as any)._componentTag || (Component as any).name;

  const proto = Component.prototype;

  (options.methods || (options.methods = {}))[key] = descriptor.value;

  // typescript decorated data
  (options.mixins || (options.mixins = [])).push({
    data(this: Vue) {
      return { [key]: descriptor.value };
    },
  });

  // computed properties
  (options.computed || (options.computed = {}))[key] = {
    get: descriptor.get,
    set: descriptor.set,
  };

  // add data hook to collect class properties as Vue instance's data
  (options.mixins || (options.mixins = [])).push({
    data(this: Vue) {
      return collectDataFromConstructor(this, Component);
    },
  });

  // vuex-class 包装的方法会在此处注入
  const decorators = (Component as DecoratedClass).__decorators__;
  if (decorators) {
    decorators.forEach((fn) => fn(options));
    delete (Component as DecoratedClass).__decorators__;
  }

  const Super =
    superProto instanceof Vue ? (superProto.constructor as VueClass<Vue>) : Vue;
  const Extended = Super.extend(options);

  // 。。。无关代码

  return Extended;
}

至此基本没有什么问题,那么压力就来到 vue 这里。返回的 Extended 是 Vue.extend 生成的 vue对象构造函数。

Vue.extend = function (extendOptions) {
  // 。。。无关代码

  var Sub = function VueComponent(options) {
    this._init(options);
  };

  // 。。。无关代码
  return Sub;
};

在 new Extended 的时候会调用 _init 初始化 vm 对象。

Vue.prototype._init = function (options) {
  // 。。。无关代码

  initLifecycle(vm);
  initEvents(vm);
  initRender(vm);
  callHook(vm, "beforeCreate");
  initInjections(vm); // resolve injections before data/props
  initState(vm);
  initProvide(vm); // resolve provide after data/props
  callHook(vm, "created");

  // 。。。无关代码
};

接下来就是无聊的打断点调试了,最终找到在执行完 initState 方法后 vm 内的有些方法变为了 undefined ,initState 的作用是将 data methods 等注册到 vm 上。

function initState(vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) {
    initProps(vm, opts.props);
  }
  if (opts.methods) {
    initMethods(vm, opts.methods);
  }
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) {
    initComputed(vm, opts.computed);
  }
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

再打断点找到 initData 方法后产生的问题,initData 方法的作用是将 data 对象注册到 vm 上,如果 data 是一个函数,则会调用该函数,那么问题就出现在 getData 中的 data.call(vm, vm) 这一句了。

function initData(vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};

  // 。。。无关代码
}

function getData(data, vm) {
  // #7573 disable dep collection when invoking data getters
  pushTarget();

  try {
    const a = data.call(vm, vm);
    return a;
  } catch (e) {
    handleError(e, vm, "data()");
    return {};
  } finally {
    popTarget();
  }
}

调用的 data.call(vm, vm) 是 vue-class-component 注册的方法。好吧,又回到了 vue-class-component,我们来看看 vue-class-component 的代码。

export function componentFactory(
  Component: VueClass<Vue>,
  options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
  // 。。。无关代码
  (options.mixins || (options.mixins = [])).push({
    data(this: Vue) {
      return collectDataFromConstructor(this, Component);
    },
  });
  // 。。。无关代码
}

在上面的 componentFactory 方法中,data 返回一个 collectDataFromConstructor 方法。在 collectDataFromConstructor 我们应该就可以解开谜题了。

function collectDataFromConstructor(vm, Component) {
  Component.prototype._init = function () {
    var _this = this;
    // proxy to actual vm
    var keys = Object.getOwnPropertyNames(vm); // 2.2.0 compat (props are no longer exposed as self properties)

    if (vm.$options.props) {
      for (var key in vm.$options.props) {
        if (!vm.hasOwnProperty(key)) {
          keys.push(key);
        }
      }
    }

    keys.forEach(function (key) {
      Object.defineProperty(_this, key, {
        get: function get() {
          return vm[key];
        },
        set: function set(value) {
          vm[key] = value;
        },
        configurable: true,
      });
    });
  }; // should be acquired class property values

  var data = new Component(); // restore original _init to avoid memory leak (#209)

  // 。。。无关代码

  return data;
}
function Vue(options) {
  this._init(options);
}

传下来的 Component 参数即 export default class HelloWorld extends Vue { ... }, new Component() 会获取到 HelloWorld 内的所有参数。 Component 继承于 Vue ,因此在 new Component() 时,会像 Vue 一样先调用一遍 _init 方法,collectDataFromConstructor 置换了 Component 的 _init。

在置换的 _init 方法中,会遍历 vm 上的所有属性,并且将这些属性通过 Object.defineProperty 再指回 vm 上。原因在于 initData 前会先 initProps initMethods 意味着,那么在 new Component() 时,探测到属于 props methods 的值时就会指向 vm,而剩下的就是 data 值。

整个流程跑下来好像没什么问题。不过既然使用了 Object.defineProperty 做 get set ,那会不会和 set 方法有关系呢?在 set 方法里打了一层断点,果然触发了,触发的条件有些奇特。

@Component
export default class HelloWorld extends Vue {
  // vuex
  @model.State
  count: number;
  @model.Mutation("increment")
  increment: () => void;
  @model.Mutation("setCount")
  setCount: () => void = () => {
    this.count = this.count + 1;
  };

  // data
  msg: string = "Hello Vue 3 + TypeScript + Vite";
  //   methods
  incrementEvent() {
    console.log(this);
    this.increment();
    this.msg = this.msg + " + " + this.count;
  }
  //   生命周期
  beforeCreate() {}
  created() {
    console.log(this);
    this.msg = this.msg + " + " + this.count;
  }
}

上面是一个很基础的类组件,increment setCount 的 set 触发,一个被传入了 undefined 一个被传入 () => { this.count = this.count + 1 },两个都属于 methods 但都是不是以 fn(){} 的方式赋予初始值,所以 incrementEvent 的 set 没有触发,increment 被传入了 undefined,setCount 被传入了一个函数

class A {
  increment;
  setCount = () => {};
  incrementEvent() {}
}

increment 和 setCount 为一个变量,而 incrementEvent 会被看做一个方法

奇怪的是在 vue-cli 中没什么问题,set 方法不会触发,为什么切换到 vite 之后 会触发 set 重置掉一些变量的初始值。我想到是不是二者的编译又问题。我对比了下二者编译后的文件,果然。

vue-cli

export default class HelloWorld {
  constructor() {
    this.setCount = () => {
      this.count = this.count + 1;
    };
    // data
    this.msg = "Hello Vue 3 + TypeScript + Vite";
  }
  //   methods
  incrementEvent() {
    console.log(this);
    this.increment();
    this.msg = this.msg + " + " + this.count;
  }
  //   生命周期
  beforeCreate() {}
  created() {
    console.log(this);
    this.msg = this.msg + " + " + this.count;
  }
}

vite

export default class HelloWorld {
  // vuex
  count;
  increment;
  setCount = () => {
    this.count = this.count + 1;
  };
  // data
  msg = "Hello Vue 3 + TypeScript + Vite";
  //   methods
  incrementEvent() {
    console.log(this);
    this.increment();
    this.msg = this.msg + " + " + this.count;
  }
  //   生命周期
  beforeCreate() {}
  created() {
    console.log(this);
    this.msg = this.msg + " + " + this.count;
  }
}

可以看到 vue-cli vite 的编译结果并不一致,vite 比 vue-cli 多出了 count increment 两个默认值,这两个值默认值是 undefined,在 vue-cli 并没有编译进去。下面只能去翻 vite 文档了,一个属性吸引了我。

查了下这个 useDefineForClassFields 属性,简单来讲,useDefineForClassFields 为 false 的情况下 ts 会 跳过为 undefined 的变量,为 true 就会将默认值为 undefined 的变量属性依然编译进去。正常情况下不会有什么问题,但是 vue-class-component 会对 props methods 的属性做一层劫持,那 new 初始化 的时候探测到这些值就会触发 set,如果没有默认值就会被赋值为 undefined。

解决

想要解决很简单,只要在 tsconfig 中加入 useDefineForClassFields 属性,并设置为 false 就可以了。

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": false,
    "module": "ESNext",
    "lib": ["ESNext", "DOM"],
    "moduleResolution": "Node",
    "strict": true,
    "sourceMap": false,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "noEmit": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true
  },
  "include": ["./src"]
}

总结

在转到 vite 的过程中,还是有许多坑要踩的,有时候并不是 vite 的问题,而是来自多方的问题,useDefineForClassFields 带来的变化也不仅仅是会编译为 undefined 的属性,可以多了解一下,也可以拓宽一些知识。

到此这篇关于详解vue-class迁移vite的一次踩坑记录的文章就介绍到这了,更多相关vue-class迁移vite内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Vue实现向PDF文件中添加二维码

    Vue实现向PDF文件中添加二维码

    这篇文章主要为大家详细介绍了如何利用Vue实现向PDF文件中添加二维码的功能,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
    2023-06-06
  • Vue 2.0学习笔记之Vue中的computed属性

    Vue 2.0学习笔记之Vue中的computed属性

    本篇文章主要介绍了Vue 2.0学习笔记之Vue中的computed属性,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • Vue中的组件详谈

    Vue中的组件详谈

    这篇文章主要介绍了Vue的组件,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-10-10
  • 详解从新建vue项目到引入组件Element的方法

    详解从新建vue项目到引入组件Element的方法

    本篇文章主要介绍了详解从新建vue项目到引入组件Element的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • Vue中用props给data赋初始值遇到的问题解决

    Vue中用props给data赋初始值遇到的问题解决

    这篇文章主要介绍了Vue中用props给data赋初始值遇到的问题解决,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-11-11
  • vue+antd实现折叠与展开组件

    vue+antd实现折叠与展开组件

    这篇文章主要为大家详细介绍了vue+antd实现折叠与展开组件,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-09-09
  • vue关于自定义指令与v-if冲突的问题

    vue关于自定义指令与v-if冲突的问题

    这篇文章主要介绍了vue关于自定义指令与v-if冲突的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • vue实现同时设置多个倒计时

    vue实现同时设置多个倒计时

    这篇文章主要为大家详细介绍了vue实现同时设置多个倒计时,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-05-05
  • Vue 3.x+axios跨域方案的踩坑指南

    Vue 3.x+axios跨域方案的踩坑指南

    这篇文章主要给大家介绍了关于Vue 3.x+axios跨域方案的踩坑指南,文中通过示例代码介绍的非常详细,对大家学习或者使用Vue 3.x具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-07-07
  • web前端Vue报错:Uncaught (in promise) TypeError:Cannot read properties of nu解决

    web前端Vue报错:Uncaught (in promise) TypeError:Cannot read 

    这篇文章主要给大家介绍了关于web前端Vue报错:Uncaught (in promise) TypeError:Cannot read properties of nu的解决方法,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-01-01

最新评论