Vue property decorator

Posted by jodyanne on Mon, 15 Jun 2020 07:00:54 +0200

Introduction:

In this section, we will continue to analyze a vue-property-decorator First of all, you can see an introduction of its official website:

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

That is to say, it is based on the vue class component library. In the previous article, we introduced how to use class components with decorators in vue. We wrote an article called vue-class-component If you are interested, you can go and have a look.

realization:

Create project:

Let's copy the demo of the previous code directly, and then let it support typescript

vue-property-decorator-demo

vue-property-decorator-demo
	demo
  	index.html //Page entry file
	lib
  	main.js //Web pack packed files
	src
  	view
    	component.d.ts //Class component ts declaration file
			component.js //Class component file
    	demo-class.vue //demo component
		main.ts //Application Portal file
	babel.config.js //babel profile
	tsconfig.json //ts profile
	package.json //Project list document
	webpack.config.js //webpack profile

index.html:

We directly refer to the packed file

<!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:

load demo.vue Component, hanging on the "app" element

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) {
    //Create a component instance
    const data = new Component();
    const plainData = {};
    //Traverses the property values of the current object
    Object.keys(data).forEach(key => {
        if (data[key] !== void 0) {
            plainData[key] = data[key];
        }
    });
    //Return property value
    return plainData
}
/**
 * Component engineering function
 * @param Component //Current class component
 * @param options //parameter
 */
function componentFactory(Component, options = {}) {
    options.name = options.name || Component.name; //Use the class name directly if options does not have the name attribute
    //Get the prototype of the class
    const proto = Component.prototype;
    //Traverse the properties on the prototype
    Object.getOwnPropertyNames(proto).forEach((key) => {
        // Filter construction method
        if (key === 'constructor') {
            return
        }
        // Some methods of valuing vue
        if ($internalHooks.indexOf(key) > -1) {
            options[key] = proto[key];
            return
        }
        //Get property descriptor
        const descriptor = Object.getOwnPropertyDescriptor(proto, key);
        if (descriptor.value !== void 0) {
            //If it's a method, assign it directly to the methods property
            if (typeof descriptor.value === 'function') {
                (options.methods || (options.methods = {}))[key] = descriptor.value;
            } else {
                //If it is not a method property, it is assigned to data directly through mixins
                (options.mixins || (options.mixins = [])).push({
                    data() {
                        return {[key]: descriptor.value}
                    }
                });
            }
        }
    });
    //Get the class attribute value through the class instance and give data through mixins
    (options.mixins || (options.mixins = [])).push({
        data(){
            return collectDataFromConstructor(this, Component)
        }
    });

    //Get the parent of the current class
    const superProto = Object.getPrototypeOf(Component.prototype);
    //Get Vue
    const Super = superProto instanceof Vue
        ? superProto.constructor
        : Vue;
    //use Vue.extend Method to create a vue component
    const Extended = Super.extend(options);
    //Directly return to a Vue component
    return Extended
}

/**
 * Component decorator
 * @param options parameter
 * @returns {Function} Return a vue component
 */
export default function Component(options) {
    //Judge whether there are parameters
    if (typeof options === 'function') {
        return componentFactory(options)
    }
    return function (Component) {
        return componentFactory(Component, options)
    }
}

babel.config.js:

The configuration of babel is the same as that in the last section. If you are interested, please take a look Front frame series (Decorator

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

package.json:

Because we need to compile vue files, we add webpack, vue, vue loader and other dependencies

{
  "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
}

Operation engineering:

npm  run dev

Browser open, http://127.0.0.1:8081/demo/index.html

We can see:


Well, our project is finished.

Effect:

main.ts:

import Vue from "vue";
import Demo from "./view/demo-class.vue";
new Vue({
    render(h){
        return h(Demo,{
            props:{
                msg: "I am a custom property 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>

OK, let's implement the code in the final form.

Code implementation:

Let's first revise our component.js File:

/**
 * Component engineering function
 * @param Component //Current class component
 * @param options //parameter
 */
function componentFactory(Component, options = {}) {
    options.name = options.name || Component.name; //Use the class name directly if options does not have the name attribute
    //Get the prototype of the class
    const proto = Component.prototype;
    //Traverse the properties on the prototype
    Object.getOwnPropertyNames(proto).forEach((key) => {
        // Filter construction method
        if (key === 'constructor') {
            return
        }
        // Some methods of valuing vue
        if ($internalHooks.indexOf(key) > -1) {
            options[key] = proto[key];
            return
        }
        //Get property descriptor
        const descriptor = Object.getOwnPropertyDescriptor(proto, key);
        if (descriptor.value !== void 0) {
            //If it's a method, assign it directly to the methods property
            if (typeof descriptor.value === 'function') {
                (options.methods || (options.methods = {}))[key] = descriptor.value;
            } else {
                //If it is not a method property, it is assigned to data directly through mixins
                (options.mixins || (options.mixins = [])).push({
                    data() {
                        return {[key]: descriptor.value}
                    }
                });
            }
        }
    });
    //Get the class attribute value through the class instance and give data through mixins
    (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__
    }

    //Get the parent of the current class
    const superProto = Object.getPrototypeOf(Component.prototype);
    //Get Vue
    const Super = superProto instanceof Vue
        ? superProto.constructor
        : Vue;
    //use Vue.extend Method to create a vue component
    const Extended = Super.extend(options);
    //Directly return to a Vue component
    return Extended
}

As you can see, we add a piece of code:

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

Is to bind a__ decorators__ Attribute is used in other places. In fact, what is the place? Yes, our view property- decorator.ts It is very simple to expose the options objects of our class components through the__ decorators__ Properties are provided elsewhere.

Then the__ decorators__ How can attributes be used elsewhere?

We continue in our component.js A createDecorator method is provided in

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) {
    //Create a component instance
    const data = new Component();
    const plainData = {};
    //Traverses the property values of the current object
    Object.keys(data).forEach(key => {
        if (data[key] !== void 0) {
            plainData[key] = data[key];
        }
    });
    //Return property value
    return plainData
}

/**
 * Component engineering function
 * @param Component //Current class component
 * @param options //parameter
 */
function componentFactory(Component, options = {}) {
    options.name = options.name || Component.name; //Use the class name directly if options does not have the name attribute
    //Get the prototype of the class
    const proto = Component.prototype;
    //Traverse the properties on the prototype
    Object.getOwnPropertyNames(proto).forEach((key) => {
        // Filter construction method
        if (key === 'constructor') {
            return
        }
        // Some methods of valuing vue
        if ($internalHooks.indexOf(key) > -1) {
            options[key] = proto[key];
            return
        }
        //Get property descriptor
        const descriptor = Object.getOwnPropertyDescriptor(proto, key);
        if (descriptor.value !== void 0) {
            //If it's a method, assign it directly to the methods property
            if (typeof descriptor.value === 'function') {
                (options.methods || (options.methods = {}))[key] = descriptor.value;
            } else {
                //If it is not a method property, it is assigned to data directly through mixins
                (options.mixins || (options.mixins = [])).push({
                    data() {
                        return {[key]: descriptor.value}
                    }
                });
            }
        }
    });
    //Get the class attribute value through the class instance and give data through mixins
    (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__
    }

    //Get the parent of the current class
    const superProto = Object.getPrototypeOf(Component.prototype);
    //Get Vue
    const Super = superProto instanceof Vue
        ? superProto.constructor
        : Vue;
    //use Vue.extend Method to create a vue component
    const Extended = Super.extend(options);
    //Directly return to a Vue component
    return Extended
}

/**
 * Component decorator
 * @param options parameter
 * @returns {Function} Return a vue component
 */
export default function Component(options) {
    //Judge whether there are parameters
    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))
    }
}

Next, we create a view property- decorator.ts File:

import {createDecorator} from "./component";

/**
 *  Attribute decorator
 * @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) {
    //Get the options property of the class component, and assign the options of the current property to the props property of the class component options
    createDecorator(function (componentOptions: any, k: string) {
      (componentOptions.props || (componentOptions.props = {}))[k] = options;
    })(target, key);
  };
}

The code is very simple. For children's shoes that are not familiar with decorator decoration properties, please take a look at an article I wrote earlier Front frame series (Decorator)

Final effect:

There are other functions in Vue property decorator:

We just implemented @ Prop. Interested partners can go to clone for a source code

If you implement other functions, you will find that there are different gains

Summary:

I've written three articles about decorators. I think it's really cool for class components, especially for other languages, such as java children's shoes!! , but just like the annotation in java, we use decorators to implement class components step by step. In terms of performance, we can't compare with function components, because each class component takes up a lot of memory compared with creating an instance object in memory. This is its disadvantage, but I won't say much about the benefits. It's closer to object-oriented language Speech design, especially the combination of typescript, plays a great role in some multi-user cooperation projects. It is acceptable to add a little memory.

All right! This is the end of this section. In the next section, I will use class components to demonstrate mvc, mvp, and mvvp architecture patterns in combination with project requirements. Please look forward!!

Topics: Vue Webpack Attribute JSON