美文网首页
前端框架系列之(vue-class-component)

前端框架系列之(vue-class-component)

作者: vv_小虫虫 | 来源:发表于2020-06-22 21:33 被阅读0次

    简介:

    说到函数式组件跟类组件在react官方就有提供,具体差异的话大家可以自行查阅react开发文档,下面我们看一下在react中怎么使用这两种方式定义组件:

    函数式组件:

    function Welcome (props) {
      return <h1>Welcome {props.name}</h1>
    }
    

    类组件:

    class Welcome extends React.Component {
      render() {
        return (
          <h1>Welcome { this.props.name }</h1>
        );
      }
    }
    

    在vue中注册组件想必大家应该也很容易实现,比如:

    welcome.js:

    export default {
        name: "welcome",
        render(h){
            return h('div','hello world!');
        }
    }
    

    那如果我们也需要在vue中使用类组件的话,比如:

    export default class Welcome extends Vue{
      name="welcome";
        render(h){
            return h('div','hello world!');
        }
    }
    

    该怎么做呢? 接下来我们就一步一步实现一下。

    实现:

    创建工程:

    我们就直接使用vue做demo了,所以我们第一步就是搭建一个简单的vue项目vue-class-component-demo:

    vue-class-component-demo
        demo
        index.html //页面入口文件
        lib
        main.js //webpack打包过后的文件
        src
        view
            demo.vue //demo组件
            main.js //应用入口文件
        babel.config.js //babel配置文件
        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.vue:

    <template>
        <div>hello world</div>
    </template>
    <script>
    export default {
        name: "demo"
    }
    </script>
    
    

    main.js:

    加载demo.vue组件,挂在到“#app”元素上

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

    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",
        "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": {
        "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.js',
      output: {
        path: path.join(__dirname,'lib'),
        filename: 'main.js'
      },
      resolve: {
        alias: {
          vue$: 'vue/dist/vue.esm.js'
        }
      },
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              'babel-loader',
            ]
          },
          {
            test: /\.vue$/,
            use: ['vue-loader']
          }
        ]
      },
      devtool: 'source-map',
      plugins: [
        new VueLoaderPlugin(),
        new (require('webpack/lib/HotModuleReplacementPlugin'))()
      ]
    };
    
    

    运行工程:

    npm  run dev
    

    浏览器打开,http://127.0.0.1:8081/demo/index.html

    我们可以看到:

    在这里插入图片描述

    好啦,一个简单的vue工程就创建完毕了。

    类组件创建思路:

    可以看到我们现在的demo.vue文件:

    <template>
        <div>{{msg}}</div>
    </template>
    <script>
    export default {
        name: "demo",
        data(){
          return {
            msg: 'hello world'
          }
        }  
    }
    </script>
    
    

    我们要实现的目标文件是这样的demo-class.vue:

    <template>
        <div>{{msg}}</div>
    </template>
    <script>
    import Vue from "vue";
    
    export default class DemoComponent extends Vue {
        msg = 'hello world';
    }
    </script>
    
    

    小伙伴是不是已经有想法了呢?对的,其实就是把demo-class.vue通过装饰器的方式转换成:

    export default {
        name: "demo",
        data(){
          return {
            msg: 'hello world'
          }
        }  
    }
    

    就ok了~~

    创建装饰器:

    我们创建一个叫component的装饰器

    component.js:

    import Vue from "vue";
    
    /**
     * 组件工程函数
     * @param Component //当前类组件
     * @param options //参数
     */
    function componentFactory(Component, options={}) {
        options.name = options.name || Component.name; //如果options没有name属性的话就直接使用类名
        //TODO 简单测试
        options.data=function () {
            return {
                msg: "hello world11"
            }
        };
        //获取当前类的父类
        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)
        }
    }
    

    可以看到,我们简单的做了一个测试,在代码的todo模块:

    //TODO 简单测试
        options.data=function () {
            return {
                msg: "hello world11"
            }
        };
    

    我们直接在装饰器中给了一个data函数,然后返回了一个msg属性“hello world”

    使用装饰器:

    demo-class.vue:

    <template>
        <div>{{msg}}</div>
    </template>
    <script>
    import Vue from "vue";
    //获取装饰器
    import Component from "./component";
    
    @Component //使用装饰器
    class DemoComponent extends Vue{
        msg = 'hello world';
    }
    export default DemoComponent;
    </script>
    
    

    我们修改一下main.js中的组件:

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

    然后运行代码我们可以看到界面:

    在这里插入图片描述

    转换类组件:

    我们现在是直接定义了一个data属性,接下来我们动态的获取参数,然后转换成data函数。

    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)
        }
    }
    

    上一节我们已经知道了怎么定义一个类的装饰器了,所以我们直接拿到当前类的原型对象,然后获取原型对象上面的属性值,赋给options:

    const proto = Component.prototype;
        //遍历原型上面的属性
        Object.getOwnPropertyNames(proto).forEach((key) => {
          ...
        }
    

    当然,我们不是把所有的属性都给到options对象,所以我们会筛选出来我们需要定义的一些属性和方法,比如vue原生中自带的一些属性:

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

    但是小伙伴有没有注意,我们demo中定义的是一个类的属性msg,是需要创建实例后才能访问的:

    <template>
        <div>{{msg}}</div>
    </template>
    <script>
    import Vue from "vue";
    import Component from "./component";
    
    @Component
    class DemoComponent extends Vue{
        msg = 'hello world';
    }
    export default DemoComponent;
    </script>
    
    

    所以我们定一个叫collectDataFromConstructor的方法,然后创建一个组件实例,最后通过mixins的方式给到vue组件:

    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
    }
    

    我们定义一个叫say的方法,然后给个点击事件

    demo-class.vue:

    <template>
        <div @click="say()">{{msg}}</div>
    </template>
    <script>
    import Vue from "vue";
    import Component from "./component";
    
    @Component
    class DemoComponent extends Vue{
        msg = 'hello world';
        say(){
           alert(this.msg);
        }
    }
    export default DemoComponent;
    </script>
    
    

    小伙伴可以自己运行一下看效果哦~~

    好啦!vue-class-component就研究到这里了,不过vue-class-component里面的代码可不止我这么一点点了,感兴趣的小伙伴自己去clone一份源码。

    相关文章

      网友评论

          本文标题:前端框架系列之(vue-class-component)

          本文链接:https://www.haomeiwen.com/subject/ctoffktx.html