前端框架系列之(vue-property-decorator)

簡介:

這節我們繼續解析一個叫vue-property-decorator的第三方庫,首先可以看它官網的一個介紹:

This library fully depends on [vue-class-component](https://github.com/vuejs/vue-class-component), so please read its README before using this library.

也就是說它是基於vue-class-component庫的,在上一篇文章中我們介紹瞭如何在vue中利用裝飾器使用類組件,我們寫了一篇叫vue-class-component的文章,大家有興趣可以去看一下。

實現:

創建工程:

我們直接copy一份上一節代碼的demo,然後讓它支持一下typescript

vue-property-decorator-demo

vue-property-decorator-demo
	demo
  	index.html //頁面入口文件
	lib
  	main.js //webpack打包過後的文件
	src
  	view
    	component.d.ts //類組件ts聲明文件
			component.js //類組件文件
    	demo-class.vue //demo組件
		main.ts //應用入口文件
	babel.config.js //babel配置文件
	tsconfig.json //ts配置文件
	package.json //項目清單文件
	webpack.config.js //webpack配置文件

index.html:

我們直接引用打包過後的文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app"></div>
    <script src="http://127.0.0.1:8081/main.js"></script>
</body>
</html>

demo-class.vue:

<template>
    <div @click="say()">{{msg}}</div>
</template>
<script lang="ts">
import Vue from "vue";
import Component from "./component";

@Component
class DemoComponent extends Vue{
    msg = 'hello world';
    say(){
       alert(this.msg);
    }
}
export default DemoComponent;
</script>

main.ts:

加載demo.vue組件,掛在到“#app”元素上

import Vue from "vue";
import Demo from "./view/demo-class.vue";
new Vue({
    render(h){
        return h(Demo);
    }
}).$mount("#app");

component.d.ts:

export declare const $internalHooks: string[];
export default function componentFactory(Component: any, options?: any): any;

component.js:

import Vue from "vue";
export const $internalHooks = [
    'data',
    'beforeCreate',
    'created',
    'beforeMount',
    'mounted',
    'beforeDestroy',
    'destroyed',
    'beforeUpdate',
    'updated',
    'activated',
    'deactivated',
    'render',
    'errorCaptured', // 2.5
    'serverPrefetch' // 2.6
];
function collectDataFromConstructor(vm,Component) {
    //創建一個組件實例
    const data = new Component();
    const plainData = {};
    //遍歷當前對象的屬性值
    Object.keys(data).forEach(key => {
        if (data[key] !== void 0) {
            plainData[key] = data[key];
        }
    });
    //返回屬性值
    return plainData
}
/**
 * 組件工程函數
 * @param Component //當前類組件
 * @param options //參數
 */
function componentFactory(Component, options = {}) {
    options.name = options.name || Component.name; //如果options沒有name屬性的話就直接使用類名
    //獲取類的原型
    const proto = Component.prototype;
    //遍歷原型上面的屬性
    Object.getOwnPropertyNames(proto).forEach((key) => {
        // 過濾構造方法
        if (key === 'constructor') {
            return
        }
        // 賦值vue自帶的一些方法
        if ($internalHooks.indexOf(key) > -1) {
            options[key] = proto[key];
            return
        }
        //獲取屬性描述器
        const descriptor = Object.getOwnPropertyDescriptor(proto, key);
        if (descriptor.value !== void 0) {
            //如果是方法的話直接賦值給methods屬性
            if (typeof descriptor.value === 'function') {
                (options.methods || (options.methods = {}))[key] = descriptor.value;
            } else {
                //不是方法屬性的話就通過mixins方式直接賦值給data
                (options.mixins || (options.mixins = [])).push({
                    data() {
                        return {[key]: descriptor.value}
                    }
                });
            }
        }
    });
    //通過類實例獲取類屬性值通過mixins給data
    (options.mixins || (options.mixins = [])).push({
        data(){
            return collectDataFromConstructor(this, Component)
        }
    });

    //獲取當前類的父類
    const superProto = Object.getPrototypeOf(Component.prototype);
    //獲取Vue
    const Super = superProto instanceof Vue
        ? superProto.constructor
        : Vue;
    //使用Vue.extend方法創建一個vue組件
    const Extended = Super.extend(options);
    //直接返回一個Vue組件
    return Extended
}

/**
 * 組件裝飾器
 * @param options 參數
 * @returns {Function} 返回一個vue組件
 */
export default function Component(options) {
    //判斷有沒有參數
    if (typeof options === 'function') {
        return componentFactory(options)
    }
    return function (Component) {
        return componentFactory(Component, options)
    }
}

babel.config.js:

babel的配置跟上一節的是一樣的,大家感興趣可以去看一下前端框架系列之(裝飾器Decorator

module.exports = {
    "presets": [
        ["@babel/env", {"modules": false}]
    ],
    "plugins": [
        ["@babel/plugin-proposal-decorators", {"legacy": true}],
        ["@babel/proposal-class-properties", {"loose": true}]
    ]
};

package.json:

因爲要編譯vue文件所以我們加入了webpack跟vue、vue-loader等依賴

{
  "name": "decorator-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.10.1",
    "@babel/core": "^7.10.2",
    "@babel/plugin-proposal-class-properties": "^7.10.1",
    "@babel/plugin-proposal-decorators": "^7.10.1",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "ts-loader": "^7.0.5",
    "vue-loader": "^15.9.2",
    "vue-template-compiler": "^2.6.11",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0"
  },
  "dependencies": {
    "typescript": "^3.9.5",
    "vue": "^2.6.11"
  }
}

webpack.config.js:

const VueLoaderPlugin = require('vue-loader/lib/plugin');
const path = require('path');
module.exports = {
    mode: 'development',
    context: __dirname,
    entry: './src/main.ts',
    output: {
        path: path.join(__dirname, 'lib'),
        filename: 'main.js'
    },
    resolve: {
        alias: {
            vue$: 'vue/dist/vue.esm.js'
        },
        extensions: ['.ts', '.tsx', '.js']
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                exclude: /node_modules/,
                use: [
                    'babel-loader',
                    {
                        loader: 'ts-loader',
                        options: {
                            appendTsSuffixTo: [/\.vue$/],
                            appendTsxSuffixTo: [/\.vue$/]
                        }
                    }
                ]
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    'babel-loader',
                ]
            },
            {
                test: /\.vue$/,
                use: ['vue-loader']
            }
        ]
    },
    devtool: 'source-map',
    plugins: [
        new VueLoaderPlugin(),
        new (require('webpack/lib/HotModuleReplacementPlugin'))()
    ]
};

tsconfig.json:

{
  "compilerOptions": {
    "target": "esnext",
    "lib": [
      "dom",
      "esnext"
    ],
    "module": "es2015",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "jsx": "preserve",
    "jsxFactory": "h"
  },
  "include": [
    "./**/*.ts"
  ],
  "compileOnSave": false
}

運行工程:

npm  run dev

瀏覽器打開,http://127.0.0.1:8081/demo/index.html

我們可以看到:

在這裏插入圖片描述
好啦,我們的工程就創建完畢了。

實現效果:

main.ts:

import Vue from "vue";
import Demo from "./view/demo-class.vue";
new Vue({
    render(h){
        return h(Demo,{
            props:{
                msg: "我是自定義屬性msg"
            }
        });
    }
}).$mount("#app");

demo-class.vue:

<template>
    <div @click="say()">{{msg}}</div>
</template>
<script lang="ts">
import Vue from "vue";
import Component from "./component";
import {Prop} from "./view-property-decorator";

@Component
class DemoComponent extends Vue{
    @Prop({type: String,default: 'hello world'})msg!: string;
    say(){
       alert(this.msg);
    }
}
export default DemoComponent;
</script>

好了,我們照着最終的樣子實現一下代碼。

代碼實現:

我們首先修改一下我們的component.js文件:

/**
 * 組件工程函數
 * @param Component //當前類組件
 * @param options //參數
 */
function componentFactory(Component, options = {}) {
    options.name = options.name || Component.name; //如果options沒有name屬性的話就直接使用類名
    //獲取類的原型
    const proto = Component.prototype;
    //遍歷原型上面的屬性
    Object.getOwnPropertyNames(proto).forEach((key) => {
        // 過濾構造方法
        if (key === 'constructor') {
            return
        }
        // 賦值vue自帶的一些方法
        if ($internalHooks.indexOf(key) > -1) {
            options[key] = proto[key];
            return
        }
        //獲取屬性描述器
        const descriptor = Object.getOwnPropertyDescriptor(proto, key);
        if (descriptor.value !== void 0) {
            //如果是方法的話直接賦值給methods屬性
            if (typeof descriptor.value === 'function') {
                (options.methods || (options.methods = {}))[key] = descriptor.value;
            } else {
                //不是方法屬性的話就通過mixins方式直接賦值給data
                (options.mixins || (options.mixins = [])).push({
                    data() {
                        return {[key]: descriptor.value}
                    }
                });
            }
        }
    });
    //通過類實例獲取類屬性值通過mixins給data
    (options.mixins || (options.mixins = [])).push({
        data() {
            return collectDataFromConstructor(this, Component)
        }
    });
    // decorate options
    const decorators = Component.__decorators__;
    if (decorators) {
        decorators.forEach(fn => fn(options));
        delete Component.__decorators__
    }

    //獲取當前類的父類
    const superProto = Object.getPrototypeOf(Component.prototype);
    //獲取Vue
    const Super = superProto instanceof Vue
        ? superProto.constructor
        : Vue;
    //使用Vue.extend方法創建一個vue組件
    const Extended = Super.extend(options);
    //直接返回一個Vue組件
    return Extended
}

可以看到,我們加一了一段代碼:

// decorate options
    const decorators = Component.__decorators__;
    if (decorators) {
        decorators.forEach(fn => fn(options));
        delete Component.__decorators__
    }

就是在Component類上綁定了一個__decorators__屬性供給其它地方使用,其實地方是哪呢?對的,就是我們的view-property-decorator.ts,做法很簡單,就是把我們類組件的options對象暴露出去通過__decorators__屬性提供給其它地方。

那麼類組件中的__decorators__屬性怎麼給其它地方用呢?

我們繼續在我們的component.js中提供一個createDecorator方法

export function createDecorator(factory, key, index) {
    return (target, key, index) => {
        const Ctor = typeof target === 'function'
            ? target
            : target.constructor;
        if (!Ctor.__decorators__) {
            Ctor.__decorators__ = []
        }
        if (typeof index !== 'number') {
            index = undefined
        }
        Ctor.__decorators__.push(options => factory(options, key, index))
    }
}

component.js:

import Vue from "vue";

export const $internalHooks = [
    'data',
    'beforeCreate',
    'created',
    'beforeMount',
    'mounted',
    'beforeDestroy',
    'destroyed',
    'beforeUpdate',
    'updated',
    'activated',
    'deactivated',
    'render',
    'errorCaptured', // 2.5
    'serverPrefetch' // 2.6
];

function collectDataFromConstructor(vm, Component) {
    //創建一個組件實例
    const data = new Component();
    const plainData = {};
    //遍歷當前對象的屬性值
    Object.keys(data).forEach(key => {
        if (data[key] !== void 0) {
            plainData[key] = data[key];
        }
    });
    //返回屬性值
    return plainData
}

/**
 * 組件工程函數
 * @param Component //當前類組件
 * @param options //參數
 */
function componentFactory(Component, options = {}) {
    options.name = options.name || Component.name; //如果options沒有name屬性的話就直接使用類名
    //獲取類的原型
    const proto = Component.prototype;
    //遍歷原型上面的屬性
    Object.getOwnPropertyNames(proto).forEach((key) => {
        // 過濾構造方法
        if (key === 'constructor') {
            return
        }
        // 賦值vue自帶的一些方法
        if ($internalHooks.indexOf(key) > -1) {
            options[key] = proto[key];
            return
        }
        //獲取屬性描述器
        const descriptor = Object.getOwnPropertyDescriptor(proto, key);
        if (descriptor.value !== void 0) {
            //如果是方法的話直接賦值給methods屬性
            if (typeof descriptor.value === 'function') {
                (options.methods || (options.methods = {}))[key] = descriptor.value;
            } else {
                //不是方法屬性的話就通過mixins方式直接賦值給data
                (options.mixins || (options.mixins = [])).push({
                    data() {
                        return {[key]: descriptor.value}
                    }
                });
            }
        }
    });
    //通過類實例獲取類屬性值通過mixins給data
    (options.mixins || (options.mixins = [])).push({
        data() {
            return collectDataFromConstructor(this, Component)
        }
    });
    // decorate options
    const decorators = Component.__decorators__;
    if (decorators) {
        decorators.forEach(fn => fn(options));
        delete Component.__decorators__
    }

    //獲取當前類的父類
    const superProto = Object.getPrototypeOf(Component.prototype);
    //獲取Vue
    const Super = superProto instanceof Vue
        ? superProto.constructor
        : Vue;
    //使用Vue.extend方法創建一個vue組件
    const Extended = Super.extend(options);
    //直接返回一個Vue組件
    return Extended
}

/**
 * 組件裝飾器
 * @param options 參數
 * @returns {Function} 返回一個vue組件
 */
export default function Component(options) {
    //判斷有沒有參數
    if (typeof options === 'function') {
        return componentFactory(options)
    }
    return function (Component) {
        return componentFactory(Component, options)
    }
}

export function createDecorator(factory, key, index) {
    return (target, key, index) => {
        const Ctor = typeof target === 'function'
            ? target
            : target.constructor;
        if (!Ctor.__decorators__) {
            Ctor.__decorators__ = []
        }
        if (typeof index !== 'number') {
            index = undefined
        }
        Ctor.__decorators__.push(options => factory(options, key, index))
    }
}

接下來我們創建一個view-property-decorator.ts文件:

import {createDecorator} from "./component";

/**
 *  屬性裝飾器
 * @param options
 * @returns {(target: any, key: string) => any}
 * @constructor
 */
export function Prop(options: any) {
  if (options === void 0) {
    options = {};
  }
  return function (target: any, key: string) {
    //獲取類組件的options屬性,把當前屬性的options賦給類組件options的props屬性
    createDecorator(function (componentOptions: any, k: string) {
      (componentOptions.props || (componentOptions.props = {}))[k] = options;
    })(target, key);
  };
}

代碼很簡單,對裝飾器修飾屬性不熟悉的童鞋,可以看一下我前面寫的一篇文章前端框架系列之(裝飾器Decorator)

最終效果:

在這裏插入圖片描述

在vue-property-decorator中還有一些其它功能:

我們只是簡單的實現了一下@Prop,感興趣的小夥伴可以自己去clone一份源碼

把其它功能都實現一下,你會發現有不一樣的收穫的~~

總結:

寫了三篇關於裝飾器的文章了,我覺得就對類組件這一塊的話,特別是接觸過一些其它語言,比如java的童鞋來說,用起來是真的爽!!,但是就像java中的註解一樣,我們也是一步一步的用裝飾器實現了一下類組件,性能方面的話肯定是比不上函數組件的,因爲我們每一個類組件就相對於在內存中創建了一個實例對象,這是很佔內存的,這是它的弊端,但是好處我就不多說了吧,更貼近面嚮對象語言設計,特別是結合typescript,然後在一些多人合作的項目上還是發揮着很大的作用的,就增加一點點內存的話其實也還能接受。

好啦! 這節就到這裏了,下面一節我將會利用類組件結合項目需求做mvc、mvp、mvvp架構模式的演示,大家敬請期待!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章