基於zepto的組件系統搭建

前言

隨着前端開發複雜度的日益提高,組件化開發應運而生,對於一個相對簡單的活動頁面開發如何進行組件化是本文的主要內容。javascript

概述

下面咱們看一下在zepto的基礎上如何構建組件系統,首先,咱們要解決第一個問題,如何引用一個組件,咱們能夠經過設置一個屬性data-component來引用自定義的組件:css

<div data-component="my-component"></div>

那麼如何向組件中傳入數據呢,咱們一樣也能夠經過設置屬性來向組件傳遞數據,好比傳入一個id值:html

<div data-component="my-component" data-id="1"></div>

那麼組件之間如何進行通訊呢,咱們能夠採用觀察者模式來實現。前端

寫一個組件

咱們先來看看咱們如何來寫一個組件java

//a.js
defineComponent('a', function (component) {
    var el = '<p class="a">input-editor</p>';
    var id = component.getProp('id');//獲取參數id
    $(this).append(el);//視圖渲染
    component.setStyle('.a{color:green}');//定義樣式
    $(this).find('p').on('click', function () {
        component.emit('test', id, '2');//觸發test
    });
});

咱們先看看這個組件是怎麼定義的,首先調用defineComponent(先無論這個函數在哪定義的)定義一個組件a,後面那個函數是組件a的組要邏輯,這個函數傳入了一個component(先無論這個是哪來的,先看它能幹啥),在前面咱們說過如何向組件傳遞數據,在組件裏咱們經過component.getProp('id')來獲取,樣式咱們經過component.setStyle('.a{color:green}')來定義,組件以前的通訊咱們經過component.emit()來觸發(在別的組件裏經過component.on()來註冊),看上去咱們基本解決了前面關於組件的一些問題,那麼這個是怎麼實現的呢?node

組件實現原理

咱們先來看看上面那個組件咱們應該如何來實現,從上面定義一個組件來看有兩個地方是比較關鍵的,一個是defineComponent是怎麼實現的,一個就是component是什麼。

咱們先來看看defineComponent是怎麼實現的,很顯然defineComponent必須定義爲全局的(要否則a.js就沒法使用了,並且必須在加載a.js以前定義defineComponent),咱們來看看defineComponent的代碼git

//component.js
  var component = new Component();
  window.defineComponent = function (name, fn) {
        component.components[name] = {
            init: function () {
                //設置currentComponent爲當前組件
                currentComponent = this;
                fn.call(this, component);
                component.init(this);
            }
        };
    }

這裏咱們能夠看到定義了一個類Componentcomponent是它的一個實例,defineComponent就是在component.components註冊一個組件,這裏的關鍵是Component類,咱們來看看Component是怎麼定義的github

//component.js
  /**
     * Component類
     * @constructor
     */
    function Component() {
        this.components = {};//全部的組件
        this.events = {};//註冊的事件
        this.loadStyle = {};
        this.init('body');//初始化
    }

    var currentComponent = null;//當前的組件
    /**
     * 類的初始化函數
     * @param container 初始化的範圍,默認狀況下是body
     */
    Component.prototype.init = function (container) {
        var self = this;
        container = container || 'body';
        $(container).find('[data-component]').each(function () {
            self.initComponent(this);
        });

    };
    /**
     *  初始化單個組件
     * @param context 當前組件
     */
    Component.prototype.initComponent = function (context) {

        var self = this;
        var componentName = $(context).attr('data-component');
        if (this.components[componentName]) {
            this.components[componentName].init.call(context);
        } else {
            _loadScript('http://' + document.domain + ':5000/dist/components/' + componentName + '.js', function () {
                self.components[componentName].init.call(context);
                //設置樣式,同一個組件只設置一次
                if (!self.loadStyle[componentName] && self.components[componentName].style) {
                    $('head').append('<style>' + self.components[componentName].style + '</style>');
                    self.loadStyle[componentName] = true;
                }
            });
        }

    };
    /**
     * 設置樣式
     * @param style 樣式
     */
    Component.prototype.setStyle = function (style) {
        //獲取當前組件的名稱,currentComponent就是當前組件
        var currentComponentName = $(currentComponent).attr('data-component');
        var component = this.components[currentComponentName];
        if (component && !component.style) {
            component.style = style;
        }
    };
    /**
     * 獲取組件參數
     * @param prop 參數名
     * @returns {*|jQuery}
     */
    Component.prototype.getProp = function (prop) {
        var currentComponentNme = $(currentComponent).attr('data-component');
        if ($(currentComponent).attr('data-' + prop)) {
            return $(currentComponent).attr('data-' + prop)
        } else {
            //屬性不存在時報錯
            throw Error('the attribute data-' + prop + ' of ' + currentComponentNme + ' is undefined or empty')
        }

    };
    /**
     * 註冊事件
     * @param name 事件名
     * @param fn 事件函數
     */
    Component.prototype.on = function (name, fn) {
        this.events[name] = this.events[name] ? this.events[name] : [];
        this.events[name].push(fn);
    };
    /**
     * 觸發事件
     */
    Component.prototype.emit = function () {
        var args = [].slice.apply(arguments);
        var eventName = args[0];
        var params = args.slice(1);
        if(this.events[eventName]){
            this.events[eventName].map(function (fn) {
                fn.apply(null, params);
            });
        }else{
            //事件不存在時報錯
            throw Error('the event ' + eventName + ' is undefined')
        }

    };
    /**
     * 動態加載組價
     * @param url 組件路徑
     * @param callback 回調函數
     * @private
     */
    function _loadScript(url, callback) {
        var script = document.createElement("script");
        script.type = "text/javascript";
        if (typeof(callback) != "undefined") {
            if (script.readyState) {
                script.onreadystatechange = function () {
                    if (script.readyState == "loaded" || script.readyState == "complete") {
                        script.onreadystatechange = null;
                        callback();
                        $(script).remove();
                    }
                };
            } else {
                script.onload = function () {
                    callback();
                    $(script).remove();
                };
            }
        }
        script.src = url;
        $('body').append(script);
    }

咱們先了解一下大概的流程


大體的流程就是上面這張流程圖了,咱們全部的組件都是註冊在component.components裏,事件都是在component.events裏面。

咱們回頭看一下組件components裏頭的init方法gulp

//component.js
  var component = new Component();
  window.defineComponent = function (name, fn) {
        component.components[name] = {
            init: function () {
                //設置currentComponent爲當前組件
                currentComponent = this;
                fn.call(this, component);
                component.init(this);
            }
        };
    }

首先,將this賦給currentComponent,這個在哪裏會用到呢?在個getProp和setStyle這兩個方法裏都用到了app

//component.js
        /**
     * 設置樣式
     * @param style 樣式
     */
    Component.prototype.setStyle = function (style) {
        console.log(currentComponent);
        //獲取當前組件的名稱,currentComponent就是當前組件
        var currentComponentName = $(currentComponent).attr('data-component');
        var component = this.components[currentComponentName];
        if (component && !component.style) {
            component.style = style;
        }
    };
    /**
     * 獲取組件參數
     * @param prop 參數名
     * @returns {*|jQuery}
     */
    Component.prototype.getProp = function (prop) {
        return $(currentComponent).attr('data-' + prop)
    };

到這裏你們可能會對this比較疑惑,這個this究竟是什麼,咱們能夠先看在那個地方調用了組件的init方法

//component.js
        /**
     *  初始化單個組件
     * @param componentName 組件名
     * @param context 當前組件
     */
    Component.prototype.initComponent = function (componentName, context) {

        var self = this;
        if (this.components[componentName]) {
            this.components[componentName].init.call(context);
        } else {
            _loadScript('http://' + document.domain + ':5000/components/' + componentName + '.js', function () {
                self.components[componentName].init.call(context);
                //設置樣式,同一個組件只設置一次
                if (!self.loadStyle[componentName] && self.components[componentName].style) {
                    $('head').append('<style>' + self.components[componentName].style + '</style>');
                    self.loadStyle[componentName] = true;
                }
            });
        }

    };

就是在單個組件初始化的調用了init方法,這裏有call改變了init的this,使得this=context,那麼這個context又是啥呢

//component.js
       /**
     * 類的初始化函數
     * @param container 初始化的範圍,默認狀況下是body
     */
    Component.prototype.init = function (container) {
        var self = this;
        container = container || 'body';
        $(container).find('[data-component]').each(function () {
            var componentName = $(this).attr('data-component');
            console.log(this);
            self.initComponent(componentName, this);
        });

    };

context其實就是遍歷的每個組件,到這裏咱們回過頭來看看咱們是怎麼定義一個組件

//b.js
defineComponent('b', function (component) {
    var el = '<p class="text-editor">text-editor</p></div><div data-component="a" data-id="1"></div>';
    $(this).append(el);
    component.on('test', function (a, b) {
        console.log(a + b);
    });
    var style = '.text-editor{color:red}';
    component.setStyle(style)
});

咱們知道this就是組件自己也就是下面這個

<div data-component="b"></div>

這個組件經過component.on註冊了一個test事件,在前面咱們知道test事件是在a組件觸發的,到這裏咱們就把整個組件系統框架開發完成了,下面就是一個個去增長組件就行了,整個的代碼以下:

//component.js
(function () {
    /**
     * Component類
     * @constructor
     */
    function Component() {
        this.components = {};//全部的組件
        this.events = {};//註冊的事件
        this.loadStyle = {};
        this.init('body');//初始化
    }

    var currentComponent = null;//當前的組件
    /**
     * 類的初始化函數
     * @param container 初始化的範圍,默認狀況下是body
     */
    Component.prototype.init = function (container) {
        var self = this;
        container = container || 'body';
        $(container).find('[data-component]').each(function () {
            self.initComponent(this);
        });

    };
    /**
     *  初始化單個組件
     * @param context 當前組件
     */
    Component.prototype.initComponent = function (context) {

        var self = this;
        var componentName = $(context).attr('data-component');
        if (this.components[componentName]) {
            this.components[componentName].init.call(context);
        } else {
            _loadScript('http://' + document.domain + ':5000/dist/components/' + componentName + '.js', function () {
                self.components[componentName].init.call(context);
                //設置樣式,同一個組件只設置一次
                if (!self.loadStyle[componentName] && self.components[componentName].style) {
                    $('head').append('<style>' + self.components[componentName].style + '</style>');
                    self.loadStyle[componentName] = true;
                }
            });
        }

    };
    /**
     * 設置樣式
     * @param style 樣式
     */
    Component.prototype.setStyle = function (style) {
        //獲取當前組件的名稱,currentComponent就是當前組件
        var currentComponentName = $(currentComponent).attr('data-component');
        var component = this.components[currentComponentName];
        if (component && !component.style) {
            component.style = style;
        }
    };
    /**
     * 獲取組件參數
     * @param prop 參數名
     * @returns {*|jQuery}
     */
    Component.prototype.getProp = function (prop) {
        var currentComponentNme = $(currentComponent).attr('data-component');
        if ($(currentComponent).attr('data-' + prop)) {
            return $(currentComponent).attr('data-' + prop)
        } else {
            //屬性不存在時報錯
            throw Error('the attribute data-' + prop + ' of ' + currentComponentNme + ' is undefined or empty')
        }

    };
    /**
     * 註冊事件
     * @param name 事件名
     * @param fn 事件函數
     */
    Component.prototype.on = function (name, fn) {
        this.events[name] = this.events[name] ? this.events[name] : [];
        this.events[name].push(fn);
    };
    /**
     * 觸發事件
     */
    Component.prototype.emit = function () {
        var args = [].slice.apply(arguments);
        var eventName = args[0];
        var params = args.slice(1);
        if(this.events[eventName]){
            this.events[eventName].map(function (fn) {
                fn.apply(null, params);
            });
        }else{
            //事件不存在時報錯
            throw Error('the event ' + eventName + ' is undefined')
        }

    };
    /**
     * 動態加載組價
     * @param url 組件路徑
     * @param callback 回調函數
     * @private
     */
    function _loadScript(url, callback) {
        var script = document.createElement("script");
        script.type = "text/javascript";
        if (typeof(callback) != "undefined") {
            if (script.readyState) {
                script.onreadystatechange = function () {
                    if (script.readyState == "loaded" || script.readyState == "complete") {
                        script.onreadystatechange = null;
                        callback();
                        $(script).remove();
                    }
                };
            } else {
                script.onload = function () {
                    callback();
                    $(script).remove();
                };
            }
        }
        script.src = url;
        $('body').append(script);
    }

    var component = new Component();

    window.defineComponent = function (name, fn) {
        component.components[name] = {
            init: function () {
                //設置currentComponent爲當前組件
                currentComponent = this;
                fn.call(this, component);
                component.init(this);
            }
        };
    }

})();

工程化

上面搭建的組件系統有個很差的地方,就是咱們定義的htmlstyle都是字符串,對於一些大的組件來講,htmlstyle都是很是長的,這樣的話調試就會很困難,所以,咱們須要對組件系統進行工程化,最終目標是htmljscss能夠分開開發,現有的工程化工具比較多,你能夠用gulp或者node本身寫一個工具,這裏介紹一下如何使用node來實現組件系統的工程化。

咱們先來看看目錄結構


咱們首先要獲取到編譯前組件的路徑

//get-path.js
var glob = require('glob');
exports.getEntries = function (globPath) {
    var entries = {};
    /**
     * 讀取src目錄,並進行路徑裁剪
     */
    glob.sync(globPath).forEach(function (entry) {
        var tmp = entry.split('/');
        tmp.shift();
        tmp.pop();
        var pathname = tmp.join('/'); // 獲取前兩個元素

        entries[pathname] = entry;

    });

    return entries;
};

而後根據路徑分別讀取index.js,index.html,index.css

//read-file.js
var readline = require('readline');
var fs = require('fs');

exports.readFile = function (file, fn) {
    console.log(file);
    var fRead = fs.createReadStream(file);
    var objReadline = readline.createInterface({
        input: fRead
    });
    function trim(str) {
        return str.replace(/(^\s*)|(\s*$)|(\/\/(.*))|(\/\*(.*)\*\/)/g, "");
    }
    var fileStr = '';
    objReadline.on('line', function (line) {
        fileStr += trim(line);
    });
    objReadline.on('close', function () {
        fn(fileStr)
    });
};


//get-component.js
var fs = require('fs');
var os = require('os');

var getPaths = require('./get-path.js');
var routesPath = getPaths.getEntries('./src/components/**/index.js');

var readFile = require('./read-file');

for (var i in routesPath) {
    (function (i) {
        var outFile = i.replace('src', 'dist');
        readFile.readFile(i + '/index.js', function (fileStr) {
            var js = fileStr;
            readFile.readFile(i + '/index.html', function (fileStr) {
                js = js.replace('<html>', fileStr);
                readFile.readFile(i + '/index.css', function (fileStr) {
                    js = js.replace('<style>', fileStr);
                    var writeRoutes = fs.createWriteStream(outFile + '.js');
                    writeRoutes.write(js);
                });
            });

        });
    })(i)
}

index.htmlindex.css轉化成字符串插入到index.js中,咱們看看index.js

// a/index.js
defineComponent('a', function (component) {
    var el = '<html>';
    var id = component.getProp('id');//獲取參數id
    $(this).append(el);//視圖渲染
    var style = '<style>';
    component.setStyle(style);//定義樣式
    $(this).find('p').on('click', function () {
        component.emit('test', id, '2');//觸發test
    })
});

<html><style>替換成以前index.htmlindex.css轉化的字符串,最後對componets文件夾下面的文件進行監控

//component-watch.js
var exec = require('child_process').exec;
var chokidar = require('chokidar');

console.log('開始監聽組件...');

chokidar.watch('./src/components/**/**').on('change', function (path) {
    console.log(dateFormat(new Date(), 'yyyy-M-d h:m:s') + ':' + path + '變化了...');

    exec('node get-component.js', function (err, out, code) {
        console.log(dateFormat(new Date(), 'yyyy-M-d h:m:s') + ':' + '編譯完成...');
    });

});

//時間格式化
function dateFormat(date, fmt) {
    var o = {
        "M+": date.getMonth() + 1, //月份
        "d+": date.getDate(), //日
        "h+": date.getHours(), //小時
        "m+": date.getMinutes(), //分
        "s+": date.getSeconds(), //秒
        "q+": Math.floor((date.getMonth() + 3) / 3), //季度
        "S": date.getMilliseconds() //毫秒
    };
    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
    for (var k in o)
        if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
    return fmt;
}

到這裏組件系統的工程化就完成了。
具體代碼在這裏

你們有興趣的話能夠關注一下個人博客

相關文章
相關標籤/搜索