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