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

簡介:

說到函數式組件跟類組件在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一份源碼。

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