亲宝软件园·资讯

展开

vue-class迁移vite

clench 人气:0

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 的属性,可以多了解一下,也可以拓宽一些知识。

加载全部内容

相关教程
猜你喜欢
用户评论