摘要:前言隨著前端開發復雜度的日益提升,組件化開發應運而生,對于一個相對簡單的活動頁面開發如何進行組件化是本文的主要內容。
前言
隨著前端開發復雜度的日益提升,組件化開發應運而生,對于一個相對簡單的活動頁面開發如何進行組件化是本文的主要內容。
概述下面我們看一下在zepto的基礎上如何構建組件系統,首先,我們要解決第一個問題,如何引用一個組件,我們可以通過設置一個屬性data-component來引用自定義的組件:
那么如何向組件中傳入數據呢,我們同樣也可以通過設置屬性來向組件傳遞數據,比如傳入一個id值:
那么組件之間如何進行通信呢,我們可以采用觀察者模式來實現。
寫一個組件我們先來看看我們如何來寫一個組件
//a.js defineComponent("a", function (component) { var el = "input-editor
"; 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()來注冊),看上去我們基本解決了前面關于組件的一些問題,那么這個是怎么實現的呢?
組件實現原理我們先來看看上面那個組件我們應該如何來實現,從上面定義一個組件來看有兩個地方是比較關鍵的,一個是defineComponent是怎么實現的,一個就是component是什么。
我們先來看看defineComponent是怎么實現的,很顯然defineComponent必須定義為全局的(要不然a.js就無法使用了,而且必須在加載a.js之前定義defineComponent),我們來看看defineComponent的代碼
//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); } }; }
這里我們可以看到定義了一個類Component,component是它的一個實例,defineComponent就是在component.components注冊一個組件,這里的關鍵是Component類,我們來看看Component是怎么定義的
//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(""); 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方法
//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這兩個方法里都用到了
//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(""); 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 = "text-editor
我們知道this就是組件本身也就是下面這個
這個組件通過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(""); 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); } }; } })();工程化
上面搭建的組件系統有個不好的地方,就是我們定義的html和style都是字符串,對于一些大的組件來說,html和style都是非常長的,這樣的話調試就會很困難,因此,我們需要對組件系統進行工程化,最終目標是html,js和css可以分開開發,現有的工程化工具比較多,你可以用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("", fileStr); readFile.readFile(i + "/index.css", function (fileStr) { js = js.replace("