詳解vue-class遷移vite的一次踩坑記錄
what happen
最進(jìn)項(xiàng)目從 vue-cli 遷移到了 vite,因?yàn)槭?vue2 的項(xiàng)目,使用了 vue-class-component 類組件做 ts 支持。
當(dāng)然遷移過程并沒有那么一帆風(fēng)順,瀏覽器控制臺報(bào)了一堆錯(cuò),大致意思是某某方法為 undefined,無法調(diào)用。打印了下當(dāng)前 this,為 undefined 的方法都來自于 vuex-class 裝飾器下的方法。這就是一件很神奇的事,為什么只有 vuex-class 裝飾器下的方法才會為 undefined ?
探究
在網(wǎng)上搜了下并沒有類似的問題,只能自己在 node_modules 中一步一步打斷點(diǎn)看是哪里出了問題。最先覺得有問題的是 vuex-class ,調(diào)試了下 /node_modules/vuex-class/lib/bindings.js 下的代碼,發(fā)現(xiàn) vuex-class 只是做了一層方法替換,通過 createDecorator 方法存到 vue-class-component 下的 __decorators__ 數(shù)組中。
import { createDecorator } from "vue-class-component"; function createBindingHelper(bindTo, mapFn) { ? function makeDecorator(map, namespace) { ? ? // 存入到 vue-class-component 的 __decorators__ 數(shù)組中 ? ? 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 裝飾器會返回一個(gè) vue對象的構(gòu)造函數(shù)。
// 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 , 在其內(nèi)部將 name 生命周期 methods computed 等注冊到 options 中,然后傳入 Vue.extend, 返回一個(gè) vue對象的構(gòu)造函數(shù) 。
export function componentFactory( ? Component: VueClass<Vue>, ? options: ComponentOptions<Vue> = {} ): VueClass<Vue> { ? // 。。。無關(guān)代碼 ? 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); ? // 。。。無關(guān)代碼 ? return Extended; }
至此基本沒有什么問題,那么壓力就來到 vue 這里。返回的 Extended 是 Vue.extend 生成的 vue對象構(gòu)造函數(shù)。
Vue.extend = function (extendOptions) { ? // 。。。無關(guān)代碼 ? var Sub = function VueComponent(options) { ? ? this._init(options); ? }; ? // 。。。無關(guān)代碼 ? return Sub; };
在 new Extended 的時(shí)候會調(diào)用 _init 初始化 vm 對象。
Vue.prototype._init = function (options) { ? // 。。。無關(guān)代碼 ? 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"); ? // 。。。無關(guān)代碼 };
接下來就是無聊的打斷點(diǎn)調(diào)試了,最終找到在執(zhí)行完 initState 方法后 vm 內(nèi)的有些方法變?yōu)榱?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); ? } }
再打斷點(diǎn)找到 initData 方法后產(chǎn)生的問題,initData 方法的作用是將 data 對象注冊到 vm 上,如果 data 是一個(gè)函數(shù),則會調(diào)用該函數(shù),那么問題就出現(xiàn)在 getData 中的 data.call(vm, vm) 這一句了。
function initData(vm) { ? var data = vm.$options.data; ? data = vm._data = typeof data === "function" ? getData(data, vm) : data || {}; ? // 。。。無關(guān)代碼 } 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(); ? } }
調(diào)用的 data.call(vm, vm) 是 vue-class-component 注冊的方法。好吧,又回到了 vue-class-component,我們來看看 vue-class-component 的代碼。
export function componentFactory( ? Component: VueClass<Vue>, ? options: ComponentOptions<Vue> = {} ): VueClass<Vue> { ? // 。。。無關(guān)代碼 ? (options.mixins || (options.mixins = [])).push({ ? ? data(this: Vue) { ? ? ? return collectDataFromConstructor(this, Component); ? ? }, ? }); ? // 。。。無關(guān)代碼 }
在上面的 componentFactory 方法中,data 返回一個(gè) collectDataFromConstructor 方法。在 collectDataFromConstructor 我們應(yīng)該就可以解開謎題了。
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) ? // 。。。無關(guān)代碼 ? return data; }
function Vue(options) { ? this._init(options); }
傳下來的 Component 參數(shù)即 export default class HelloWorld extends Vue { ... }, new Component() 會獲取到 HelloWorld 內(nèi)的所有參數(shù)。 Component 繼承于 Vue ,因此在 new Component() 時(shí),會像 Vue 一樣先調(diào)用一遍 _init 方法,collectDataFromConstructor 置換了 Component 的 _init。
在置換的 _init 方法中,會遍歷 vm 上的所有屬性,并且將這些屬性通過 Object.defineProperty 再指回 vm 上。原因在于 initData 前會先 initProps initMethods 意味著,那么在 new Component() 時(shí),探測到屬于 props methods 的值時(shí)就會指向 vm,而剩下的就是 data 值。
整個(gè)流程跑下來好像沒什么問題。不過既然使用了 Object.defineProperty 做 get set ,那會不會和 set 方法有關(guān)系呢?在 set 方法里打了一層斷點(diǎn),果然觸發(fā)了,觸發(fā)的條件有些奇特。
@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; ? } }
上面是一個(gè)很基礎(chǔ)的類組件,increment setCount 的 set 觸發(fā),一個(gè)被傳入了 undefined 一個(gè)被傳入 () => { this.count = this.count + 1 },兩個(gè)都屬于 methods 但都是不是以 fn(){} 的方式賦予初始值,所以 incrementEvent 的 set 沒有觸發(fā),increment 被傳入了 undefined,setCount 被傳入了一個(gè)函數(shù)
class A { ? increment; ? setCount = () => {}; ? incrementEvent() {} }
increment 和 setCount 為一個(gè)變量,而 incrementEvent 會被看做一個(gè)方法
奇怪的是在 vue-cli 中沒什么問題,set 方法不會觸發(fā),為什么切換到 vite 之后 會觸發(fā) 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 的編譯結(jié)果并不一致,vite 比 vue-cli 多出了 count increment 兩個(gè)默認(rèn)值,這兩個(gè)值默認(rèn)值是 undefined,在 vue-cli 并沒有編譯進(jìn)去。下面只能去翻 vite 文檔了,一個(gè)屬性吸引了我。
查了下這個(gè) useDefineForClassFields 屬性,簡單來講,useDefineForClassFields 為 false 的情況下 ts 會 跳過為 undefined 的變量,為 true 就會將默認(rèn)值為 undefined 的變量屬性依然編譯進(jìn)去。正常情況下不會有什么問題,但是 vue-class-component 會對 props methods 的屬性做一層劫持,那 new 初始化 的時(shí)候探測到這些值就會觸發(fā) set,如果沒有默認(rèn)值就會被賦值為 undefined。
解決
想要解決很簡單,只要在 tsconfig 中加入 useDefineForClassFields 屬性,并設(shè)置為 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"] }
總結(jié)
在轉(zhuǎn)到 vite 的過程中,還是有許多坑要踩的,有時(shí)候并不是 vite 的問題,而是來自多方的問題,useDefineForClassFields 帶來的變化也不僅僅是會編譯為 undefined 的屬性,可以多了解一下,也可以拓寬一些知識。
到此這篇關(guān)于詳解vue-class遷移vite的一次踩坑記錄的文章就介紹到這了,更多相關(guān)vue-class遷移vite內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue 2.0學(xué)習(xí)筆記之Vue中的computed屬性
本篇文章主要介紹了Vue 2.0學(xué)習(xí)筆記之Vue中的computed屬性,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10詳解從新建vue項(xiàng)目到引入組件Element的方法
本篇文章主要介紹了詳解從新建vue項(xiàng)目到引入組件Element的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08vue實(shí)現(xiàn)同時(shí)設(shè)置多個(gè)倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)同時(shí)設(shè)置多個(gè)倒計(jì)時(shí),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05web前端Vue報(bào)錯(cuò):Uncaught?(in?promise)?TypeError:Cannot?read?
這篇文章主要給大家介紹了關(guān)于web前端Vue報(bào)錯(cuò):Uncaught?(in?promise)?TypeError:Cannot?read?properties?of?nu的解決方法,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01