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

2020-06-22  本文已影响0人  vv_小虫虫

简介:

说到函数式组件跟类组件在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一份源码。

上一篇下一篇

猜你喜欢

热点阅读