摘要:前言隨著前端開發(fā)復(fù)雜度的日益提升,組件化開發(fā)應(yīng)運(yùn)而生,對(duì)于一個(gè)相對(duì)簡(jiǎn)單的活動(dòng)頁面開發(fā)如何進(jìn)行組件化是本文的主要內(nèi)容。
前言
隨著前端開發(fā)復(fù)雜度的日益提升,組件化開發(fā)應(yīng)運(yùn)而生,對(duì)于一個(gè)相對(duì)簡(jiǎn)單的活動(dòng)頁面開發(fā)如何進(jìn)行組件化是本文的主要內(nèi)容。
概述下面我們看一下在zepto的基礎(chǔ)上如何構(gòu)建組件系統(tǒng),首先,我們要解決第一個(gè)問題,如何引用一個(gè)組件,我們可以通過設(shè)置一個(gè)屬性data-component來引用自定義的組件:
那么如何向組件中傳入數(shù)據(jù)呢,我們同樣也可以通過設(shè)置屬性來向組件傳遞數(shù)據(jù),比如傳入一個(gè)id值:
那么組件之間如何進(jìn)行通信呢,我們可以采用觀察者模式來實(shí)現(xiàn)。
寫一個(gè)組件我們先來看看我們?nèi)绾蝸韺懸粋€(gè)組件
//a.js defineComponent("a", function (component) { var el = "input-editor
"; var id = component.getProp("id");//獲取參數(shù)id $(this).append(el);//視圖渲染 component.setStyle(".a{color:green}");//定義樣式 $(this).find("p").on("click", function () { component.emit("test", id, "2");//觸發(fā)test }); });
我們先看看這個(gè)組件是怎么定義的,首先調(diào)用defineComponent(先不管這個(gè)函數(shù)在哪定義的)定義一個(gè)組件a,后面那個(gè)函數(shù)是組件a的組要邏輯,這個(gè)函數(shù)傳入了一個(gè)component(先不管這個(gè)是哪來的,先看它能干啥),在前面我們說過如何向組件傳遞數(shù)據(jù),在組件里我們通過component.getProp("id")來獲取,樣式我們通過component.setStyle(".a{color:green}")來定義,組件之前的通信我們通過component.emit()來觸發(fā)(在別的組件里通過component.on()來注冊(cè)),看上去我們基本解決了前面關(guān)于組件的一些問題,那么這個(gè)是怎么實(shí)現(xiàn)的呢?
組件實(shí)現(xiàn)原理我們先來看看上面那個(gè)組件我們應(yīng)該如何來實(shí)現(xiàn),從上面定義一個(gè)組件來看有兩個(gè)地方是比較關(guān)鍵的,一個(gè)是defineComponent是怎么實(shí)現(xiàn)的,一個(gè)就是component是什么。
我們先來看看defineComponent是怎么實(shí)現(xiàn)的,很顯然defineComponent必須定義為全局的(要不然a.js就無法使用了,而且必須在加載a.js之前定義defineComponent),我們來看看defineComponent的代碼
//component.js var component = new Component(); window.defineComponent = function (name, fn) { component.components[name] = { init: function () { //設(shè)置currentComponent為當(dāng)前組件 currentComponent = this; fn.call(this, component); component.init(this); } }; }
這里我們可以看到定義了一個(gè)類Component,component是它的一個(gè)實(shí)例,defineComponent就是在component.components注冊(cè)一個(gè)組件,這里的關(guān)鍵是Component類,我們來看看Component是怎么定義的
//component.js /** * Component類 * @constructor */ function Component() { this.components = {};//所有的組件 this.events = {};//注冊(cè)的事件 this.loadStyle = {}; this.init("body");//初始化 } var currentComponent = null;//當(dāng)前的組件 /** * 類的初始化函數(shù) * @param container 初始化的范圍,默認(rèn)情況下是body */ Component.prototype.init = function (container) { var self = this; container = container || "body"; $(container).find("[data-component]").each(function () { self.initComponent(this); }); }; /** * 初始化單個(gè)組件 * @param context 當(dāng)前組件 */ 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); //設(shè)置樣式,同一個(gè)組件只設(shè)置一次 if (!self.loadStyle[componentName] && self.components[componentName].style) { $("head").append(""); self.loadStyle[componentName] = true; } }); } }; /** * 設(shè)置樣式 * @param style 樣式 */ Component.prototype.setStyle = function (style) { //獲取當(dāng)前組件的名稱,currentComponent就是當(dāng)前組件 var currentComponentName = $(currentComponent).attr("data-component"); var component = this.components[currentComponentName]; if (component && !component.style) { component.style = style; } }; /** * 獲取組件參數(shù) * @param prop 參數(shù)名 * @returns {*|jQuery} */ Component.prototype.getProp = function (prop) { var currentComponentNme = $(currentComponent).attr("data-component"); if ($(currentComponent).attr("data-" + prop)) { return $(currentComponent).attr("data-" + prop) } else { //屬性不存在時(shí)報(bào)錯(cuò) throw Error("the attribute data-" + prop + " of " + currentComponentNme + " is undefined or empty") } }; /** * 注冊(cè)事件 * @param name 事件名 * @param fn 事件函數(shù) */ Component.prototype.on = function (name, fn) { this.events[name] = this.events[name] ? this.events[name] : []; this.events[name].push(fn); }; /** * 觸發(fā)事件 */ 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{ //事件不存在時(shí)報(bào)錯(cuò) throw Error("the event " + eventName + " is undefined") } }; /** * 動(dòng)態(tài)加載組價(jià) * @param url 組件路徑 * @param callback 回調(diào)函數(shù) * @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); }
我們先了解一下大概的流程
大致的流程就是上面這張流程圖了,我們所有的組件都是注冊(cè)在component.components里,事件都是在component.events里面。
我們回頭看一下組件components里頭的init方法
//component.js var component = new Component(); window.defineComponent = function (name, fn) { component.components[name] = { init: function () { //設(shè)置currentComponent為當(dāng)前組件 currentComponent = this; fn.call(this, component); component.init(this); } }; }
首先,將this賦給currentComponent,這個(gè)在哪里會(huì)用到呢?在個(gè)getProp和setStyle這兩個(gè)方法里都用到了
//component.js /** * 設(shè)置樣式 * @param style 樣式 */ Component.prototype.setStyle = function (style) { console.log(currentComponent); //獲取當(dāng)前組件的名稱,currentComponent就是當(dāng)前組件 var currentComponentName = $(currentComponent).attr("data-component"); var component = this.components[currentComponentName]; if (component && !component.style) { component.style = style; } }; /** * 獲取組件參數(shù) * @param prop 參數(shù)名 * @returns {*|jQuery} */ Component.prototype.getProp = function (prop) { return $(currentComponent).attr("data-" + prop) };
到這里大家可能會(huì)對(duì)this比較疑惑,這個(gè)this到底是什么,我們可以先看在那個(gè)地方調(diào)用了組件的init方法
//component.js /** * 初始化單個(gè)組件 * @param componentName 組件名 * @param context 當(dāng)前組件 */ 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); //設(shè)置樣式,同一個(gè)組件只設(shè)置一次 if (!self.loadStyle[componentName] && self.components[componentName].style) { $("head").append(""); self.loadStyle[componentName] = true; } }); } };
就是在單個(gè)組件初始化的調(diào)用了init方法,這里有call改變了init的this,使得this=context,那么這個(gè)context又是啥呢
//component.js /** * 類的初始化函數(shù) * @param container 初始化的范圍,默認(rèn)情況下是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其實(shí)就是遍歷的每一個(gè)組件,到這里我們回過頭來看看我們是怎么定義一個(gè)組件
//b.js defineComponent("b", function (component) { var el = "text-editor
我們知道this就是組件本身也就是下面這個(gè)
這個(gè)組件通過component.on注冊(cè)了一個(gè)test事件,在前面我們知道test事件是在a組件觸發(fā)的,到這里我們就把整個(gè)組件系統(tǒng)框架開發(fā)完成了,下面就是一個(gè)個(gè)去增加組件就好了,整個(gè)的代碼如下:
//component.js (function () { /** * Component類 * @constructor */ function Component() { this.components = {};//所有的組件 this.events = {};//注冊(cè)的事件 this.loadStyle = {}; this.init("body");//初始化 } var currentComponent = null;//當(dāng)前的組件 /** * 類的初始化函數(shù) * @param container 初始化的范圍,默認(rèn)情況下是body */ Component.prototype.init = function (container) { var self = this; container = container || "body"; $(container).find("[data-component]").each(function () { self.initComponent(this); }); }; /** * 初始化單個(gè)組件 * @param context 當(dāng)前組件 */ 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); //設(shè)置樣式,同一個(gè)組件只設(shè)置一次 if (!self.loadStyle[componentName] && self.components[componentName].style) { $("head").append(""); self.loadStyle[componentName] = true; } }); } }; /** * 設(shè)置樣式 * @param style 樣式 */ Component.prototype.setStyle = function (style) { //獲取當(dāng)前組件的名稱,currentComponent就是當(dāng)前組件 var currentComponentName = $(currentComponent).attr("data-component"); var component = this.components[currentComponentName]; if (component && !component.style) { component.style = style; } }; /** * 獲取組件參數(shù) * @param prop 參數(shù)名 * @returns {*|jQuery} */ Component.prototype.getProp = function (prop) { var currentComponentNme = $(currentComponent).attr("data-component"); if ($(currentComponent).attr("data-" + prop)) { return $(currentComponent).attr("data-" + prop) } else { //屬性不存在時(shí)報(bào)錯(cuò) throw Error("the attribute data-" + prop + " of " + currentComponentNme + " is undefined or empty") } }; /** * 注冊(cè)事件 * @param name 事件名 * @param fn 事件函數(shù) */ Component.prototype.on = function (name, fn) { this.events[name] = this.events[name] ? this.events[name] : []; this.events[name].push(fn); }; /** * 觸發(fā)事件 */ 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{ //事件不存在時(shí)報(bào)錯(cuò) throw Error("the event " + eventName + " is undefined") } }; /** * 動(dòng)態(tài)加載組價(jià) * @param url 組件路徑 * @param callback 回調(diào)函數(shù) * @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 () { //設(shè)置currentComponent為當(dāng)前組件 currentComponent = this; fn.call(this, component); component.init(this); } }; } })();工程化
上面搭建的組件系統(tǒng)有個(gè)不好的地方,就是我們定義的html和style都是字符串,對(duì)于一些大的組件來說,html和style都是非常長的,這樣的話調(diào)試就會(huì)很困難,因此,我們需要對(duì)組件系統(tǒng)進(jìn)行工程化,最終目標(biāo)是html,js和css可以分開開發(fā),現(xiàn)有的工程化工具比較多,你可以用gulp或者node自己寫一個(gè)工具,這里介紹一下如何使用node來實(shí)現(xiàn)組件系統(tǒng)的工程化。
我們先來看看目錄結(jié)構(gòu)
我們首先要獲取到編譯前組件的路徑
//get-path.js var glob = require("glob"); exports.getEntries = function (globPath) { var entries = {}; /** * 讀取src目錄,并進(jìn)行路徑裁剪 */ glob.sync(globPath).forEach(function (entry) { var tmp = entry.split("/"); tmp.shift(); tmp.pop(); var pathname = tmp.join("/"); // 獲取前兩個(gè)元素 entries[pathname] = entry; }); return entries; };
然后根據(jù)路徑分別讀取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("