SoFunction
Updated on 2025-04-05

Detailed explanation of the pitfall record of vue-class migration vite

what happen

The most advanced project was moved from vue-cli to vite because it is a vue2 project, and it is used.vue-class-component Class components are supported by ts.
Of course, the migration process was not so smooth. The browser console reported a lot of errors, which roughly means that a certain method is undefined and cannot be called. The current method of this is undefined is printed from the method under the vuex-class decorator. This is a very magical thing, why is the only method under the vuex-class decorator undefined?

Explore

I searched online and there was no similar problem. I could only break the point step by step in node_modules to see what went wrong. The first thing I thought was that there was a problem was vuex-class. I debugged the code under /node_modules/vuex-class/lib/ and found that vuex-class only made a layer of method replacement and saved it into the __decorators__ array under vue-class-component through the createDecorator method.

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

function createBindingHelper(bindTo, mapFn) {
  function makeDecorator(map, namespace) {
    // Save it into the __decorators__ array of vue-class-component    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;
}

Then we can only look at vue-class-component.

The @Component decorator of vue-class-component returns a constructor for a vue object.

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

// Class components@Component
export default class HelloWorld extends Vue { ... }

The Component method will pass class HelloWorld in componentFactory , register the name lifecycle methods computed and other in options, and then pass in, returning a constructor of a vue object.

export function componentFactory(
  Component: VueClass<Vue>,
  options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
  // .  .  .  No code
   =
     || (Component as any)._componentTag || (Component as any).name;

  const proto = ;

  ( || ( = {}))[key] = ;

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

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

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

  // The vuex-class wrapper method will be injected here  const decorators = (Component as DecoratedClass).__decorators__;
  if (decorators) {
    ((fn) => fn(options));
    delete (Component as DecoratedClass).__decorators__;
  }

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

  // .  .  .  No code
  return Extended;
}

There is basically no problem at this point, so the pressure comes to vue. The returned Extended is the generated vue object constructor.

 = function (extendOptions) {
  // .  .  .  No code
  var Sub = function VueComponent(options) {
    this._init(options);
  };

  // .  .  .  No code  return Sub;
};

When new Extended, _init will be called to initialize the vm object.

._init = function (options) {
  // .  .  .  No code
  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");

  // .  .  .  No code};

Next is the boring breakpoint debugging. Finally, after executing the initState method, some methods in vm become undefined. The function of initState is to register data methods, etc. to vm.

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

The problem arises after breaking the point and finding the initData method. The function of the initData method is to register the data object to vm. If data is a function, the function will be called, and the problem occurs in the sentence (vm, vm) in getData.

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

  // .  .  .  No code}

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

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

The (vm, vm) called is the method registered by vue-class-component. Well, back to vue-class-component, let's take a look at the code of vue-class-component.

export function componentFactory(
  Component: VueClass<Vue>,
  options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
  // .  .  .  No code  ( || ( = [])).push({
    data(this: Vue) {
      return collectDataFromConstructor(this, Component);
    },
  });
  // .  .  .  No code}

In the componentFactory method above, data returns a collectDataFromConstructor method. In collectDataFromConstructor we should be able to solve the puzzle.

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

    if (vm.$) {
      for (var key in vm.$) {
        if (!(key)) {
          (key);
        }
      }
    }

    (function (key) {
      (_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)

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

The passed Component parameter is export default class HelloWorld extends Vue { ... }, new Component() will get all parameters in HelloWorld. Component inherits from Vue, so when new Component(), the _init method will be called first like Vue, and collectDataFromConstructor replaces Component's _init.

In the permuted _init method, all properties on vm are traversed and these properties are pointed back to vm through. The reason is that initProps initMethods is meant that when new Component() is detected, it will point to vm, and the remaining data value is the data value.

There seems to be no problem with the whole process. But since you use get set, will it have anything to do with the set method? A breakpoint was hit in the set method, and it was triggered, and the triggering conditions were a bit strange.

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

  // data
  msg: string = "Hello Vue 3 + TypeScript + Vite";
  //   methods
  incrementEvent() {
    (this);
    ();
     =  + " + " + ;
  }
  //   life cycle  beforeCreate() {}
  created() {
    (this);
     =  + " + " + ;
  }
}

The above is a very basic class component. The set of increment setCount triggers, one is passed in undefined and the other is passed in () => { = + 1 }. Both belong to methods, but both are not assigned the initial value in the form of fn(){}, so the set of incrementEvent is not triggered, increment is passed in undefined, and setCount is passed in a function.

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

increment and setCount are a variable, and incrementEvent will be regarded as a method

Strangely, there is no problem in vue-cli, the set method will not trigger, why does it trigger after switching to vite set reset the initial value of some variables. I wondered if it was a problem with the compilation of both. I compared the compiled files of the two, and sure enough.

vue-cli

export default class HelloWorld {
  constructor() {
     = () => {
       =  + 1;
    };
    // data
     = "Hello Vue 3 + TypeScript + Vite";
  }
  //   methods
  incrementEvent() {
    (this);
    ();
     =  + " + " + ;
  }
  //   life cycle  beforeCreate() {}
  created() {
    (this);
     =  + " + " + ;
  }
}

vite

export default class HelloWorld {
  // vuex
  count;
  increment;
  setCount = () => {
     =  + 1;
  };
  // data
  msg = "Hello Vue 3 + TypeScript + Vite";
  //   methods
  incrementEvent() {
    (this);
    ();
     =  + " + " + ;
  }
  //   life cycle  beforeCreate() {}
  created() {
    (this);
     =  + " + " + ;
  }
}

You can see that the compilation results of vue-cli vite are not consistent. vite has two more default values ​​than vue-cli, and the default values ​​of these two values ​​are undefined, and they are not compiled in vue-cli. I can only search for the vite document below, and one attribute attracted me.

Checked thisuseDefineForClassFields In short, if useDefineForClassFields is false, ts willSkip the variable as undefined, if true, the default value is undefined variable attributeStill compiled. Under normal circumstances, there will be no problem, but vue-class-component will hijack the props methods attributes. When new initialization, these values ​​will be triggered. If there is no default value, it will be assigned to undefined.

solve

It is very simple to solve the problem. Just add the useDefineForClassFields property to tsconfig and set it to 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"]
}

Summarize

In the process of transferring to vite, there are still many pitfalls to be stepped on. Sometimes it is not a problem with vite, but a problem from multiple parties.useDefineForClassFields The changes brought about are not just the properties that will be compiled into undefined. You can learn more about them and broaden your knowledge.

This is the end of this article about the detailed explanation of the pitfall record of vue-class migration vite. For more related content on vue-class migration vite, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!