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

目录
  • what happen
  • 探究
  • 解决
  • 总结

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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解vue-cli@2.x项目迁移日志

    vue-cli@2.x项目迁移日志 虽然 vue-cli@3 早就已经巨普及了,新项目应该已经很少有人还有使用 vue-cli@2.x . 但是对于一些稍微早些时候的 vue 项目,如果当时没有做一些优化.配置,随着 webpack vue 等包的升级,有一些配置已经不一样了,并且关于 vue-cli@2.x 项目的文档.博客也越来越少,如果遇到问题也许也会有麻烦,因此就想着把当前的 vue-cli@2.x 项目原地升级配置. 项目结构 vue-cli@2.x 项目结构,其中红色圈出的部分是 2

  • Vue如何从1.0迁移到2.0

    使用vue-route路由到新的页面是,会刷新当前页面的dom,el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子. 1.0 是使用ready钩子函数 2.0使用mounted钩子函数代替 总结 以上所述是小编给大家介绍的Vue如何从1.0迁移到2.0,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的.在此也非常感谢大家对我们网站的支持!

  • 浅谈将three项目迁移至vue项目遇到的问题

    目录 通过npm下载的three依赖无法正常使用 导入模型的路径出现了问题 3D场景渲染后没有进行销毁 由于我的3D场景起初是自己为了测试搭建的,所以使用的是html + three,后来将代码迁移到vue项目的过程中出现了下面的几个问题: 通过npm下载three依赖无法正常使用 导入模型的路径出现了问题,导致模型无法正常渲染 3D场景渲染后没有进行销毁 通过npm下载的three依赖无法正常使用 在原项目中使用的是three相关的js文件,而迁移项目的时候本来准备直接通过npm下载相关依赖进

  • 详解Vue2.5+迁移至Typescript指南

    为什么要迁移至Typescript Javascript本身是动态弱类型的语言,这样的特点导致了Javascript代码中充斥着很多Uncaught TypeError的报错,给开发调试和线上代码稳定都带来了不小的负面影响. 而Typescript提供了静态类型检查,使很多类型错误在编写时就已经发现,不会带到测试阶段. 同时,Javascript不定义model就可以使用一个对象,有人喜欢这样的灵活性,的确这样的语法在model不复杂的时候可以快速的开发出需要的功能,但一旦model庞大,找一个

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

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

  • 详解多页应用 Webpack4 配置优化与踩坑记录

    前言 最近新起了一个多页项目,之前都未使用 webpack4,于是准备上手实践一下.这篇文章主要就是一些配置介绍,对于正准备使用 webpack4 的同学,可以做一些参考. webpack4 相比之前的 2 与 3,改变很大.最主要的一点是很多配置已经内置,使得 webpack 能"开箱即用".当然这个开箱即用不可能满足所有情况,但是很多以往的配置,其实可以不用了.比如在之前,压缩混淆代码,需要增加uglify插件,作用域提升(scope hosting)需要增加ModuleConca

  • 详解vue的数据劫持以及操作数组的坑

    TL;DR 给data添加新属性的时候vm.$set(vm.info,'newKey','newValue') data上面属性值是数组的时候,需要用数组的方法操作数组,而不能通过index或者length属性去操作数组,因为监听不到属性操作的动作. 安装和初使用vue vue是构建用户界面的渐进式框架.所谓的渐进式:vue + components + vue-router + vuex + vue-cli可以根据需要选择相应的功能. 来串命令mkdir vue-apply;cd vue-ap

  • 详解配置Django的Celery异步之路踩坑

    人生苦短,我用python. 看到这句话的时候,感觉可能确实是很深得人心,不过每每想学学,就又止步,年纪大了,感觉学什么东西都很慢,很难,精神啊注意力啊思维啊都跟不上.今天奶牛来分享自己今天踩的一个坑. 先说说配置过程吧,初学Django,啥都不懂,当然,python也很水,啥东西都得现查现用.Django安装还是很简单的. apt-get install python3 pip3 install django 嗯,就是两条命令的事儿. 再说celery的安装: pip3 install cel

  • vue中使用微信公众号js-sdk踩坑记录

    最近又在vue中捣鼓了下微信公众号api的接入,不得不说这里边水是真的深啊,上次分享了微信授权登录和js-sdk签名的部分,其中很多朋友私信我表示了疑惑,今天我就再次尝试理顺一下这里边的坑吧: 微信JS-SDK是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包. 通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照.选图.语音.位置等手机系统的能力,同时可以直接使用微信分享.扫一扫.卡券.支付等微信特有的能力,为微信用户提供更优质的网页体验. 分享页面到朋友圈 上文是从官方文档

  • vue css 相对路径导入问题级踩坑记录

    目录 前提 先附上成功的配置 1.build/utils.js 2.build/webpack.base.conf.js 遇到的坑 1.错误写法 2.正确的写法 3.如果想在“style”里面用上~@表示的相对路径 前提 npm install style-loader --save-dev npm install css-loader --save-dev npm install file-loader --save-dev npm install vue-style-loader - sav

  • 详解Vue3.0 + TypeScript + Vite初体验

    项目创建 npm: $ npm init vite-app <project-name> $ cd <project-name> $ npm install $ npm run dev or yarn: $ yarn create vite-app <project-name> $ cd <project-name> $ yarn $ yarn dev 项目结构 main.js 在个人想法上,我觉得createApp()是vue应用的实例,createApp

  • 详解vue 模版组件的三种用法

    本文介绍了详解vue 模版组件的三种用法,分享给大家,具体如下: 第一种 //首先,别忘了引入vue.js <div id="user_name_01"></div> <script src="../node_modules/vue/dist/vue.js"></script> <script> var User_01 = Vue.extend({// 创建可复用的构造器 template: '<p&

  • 详解 vue.js用法和特性

    前  言 最近用Vue.js做了一个数据查询平台,还做了一个拼图游戏,突然深深的感到了vue的强大. Vue.js是一套构建用户界面(user interface)的渐进式框架.与其他重量级框架不同的是,Vue 从根本上采用最小成本.渐进增量(incrementally adoptable)的设计.Vue 的核心库只专注于视图层,并且很容易与其他第三方库或现有项目集成.另一方面,当与单文件组件和 Vue 生态系统支持的库结合使用时,Vue 也完全能够为复杂的单页应用程序提供有力驱动. Vue.j

  • 详解vue的数据binding绑定原理

    自从angular火了以后,各种mvc框架喷涌而出,angular虽然比较火,但是他的坑还是蛮多的,还有许多性能问题被人们吐槽.比如坑爹的脏检查机制,数据binding是受人喜爱的,脏检查就有点-性能低下了.有时候改了一个地方,脏循环要循环多次来保证数据是不是真的变了和是否停止变化了.这样性能就很低了.于是人们开始钻研新的双向数据binding的方法.尤大的vue binding就是本人蛮喜欢的一种实现方式,本文跟随尤大的一个例子来详解vue的数据binding的原理. 数据binding,一般

随机推荐