在前端開發中,常常須要動態添加一些元素到頁面上。那麼如何經過一些技巧,優化動態建立頁面元素的方式,使得代碼更加優雅,而且更易於維護呢?接下來咱們經過研究一些實例,一步步地找出最優方案。html
這篇文章儘可能寫得思路清晰且通俗易懂,由淺入深爲剛入門前端的新手們帶來一些思路和啓發。前端
老手們也能夠順着看下去,當作複習一次。亦或者直接跳到後半部分,去看稍微深刻一點的模板數據替換示例,一塊兒交流交流哦。jquery
因爲DOM和HTML會存在必定的歧義,因此爲了區別開來,文章中這兩個術語的意思分別是:git
DOM :專指文檔對象,是在JS上以對象的形式存在的。github
HTML:專指HTML文本,是一連串字符的集合。ajax
話很少說,咱們先來思考一下最基本的問題,如何用JS動態添加元素到頁面中去呢?瀏覽器
假設在點擊「添加一個乘客」按鈕的時候,須要JS動態建立出一個新的輸入框來填寫姓名:app
<h2>乘客列表:</h2> <form class="form"> <div class="form-group"> 乘客姓名:<input type="text" class="form-control" name="member[]"> </div> <button class="create-passenger" type="button">添加一個乘客</button> <button type="submit">保存</button> </form>
從上面能夠看出,要實現這個功能,咱們須要處理的HTML片斷是:框架
<div class="form-group"> 乘客姓名:<input type="text" class="form-control" name="member[]"> </div>
那麼咱們先來看看傳統的作法是這樣子的:dom
先直接手動複製粘貼HTML拼接成JS字符串,而後再插入到表單中。
$('.create-passenger').on('click', function() { // 先直接手動複製粘貼HTML拼接成JS字符串 var html = '<div class="form-group">' + ' 乘客姓名:<input type="text" class="form-control" name="member[]">' + '</div>'; // 而後再插入到表單中 $('.form').append(html); });
這是種偷懶的實現方式,在部分中小型網站、教科書上,最多見到它的身影。
在開發時的時候,某些狀況下使用這種方案,的確可能會比較快速,直接複製粘貼HTML拼成JS字符串就能夠了。
但知足這樣的條件必須是:
要拼接的HTML字符串很短;
頁面結構已經很穩定,能保證之後不會須要做出修改;
頁面HMTL和JS的代碼量都很少,或者已經直接把JS寫在頁面上了,因此即便設計不合理也能比較容易查看和維護。
沒有作好HTML和JS的分離,腳本強烈耦合了HTML,不妥不妥。
要是後期頁面上的HTML有了改動,必須同時記得去找出相關的腳本文件,在JS代碼中搜索並修改裏面寫死的HTML字符串才行。
換個角度再想想,若是插入的HTML很複雜,有幾百行的話。要在JS腳本中手動拼接龐大的字符串,是件很是的麻煩事情,還十分容易出差錯。
模板分離原則:將定義模板的那一部分,與JS的代碼邏輯分離開來,讓代碼更加優雅且利於維護。
經過分析頁面咱們能夠知道,表單初始的時候是至少會存在一個乘客輸入項的。
因此咱們能夠複製表單上第一個乘客的DOM來做爲模板:
$('.create-passenger').on('click', function() { // 複製第一個乘客的DOM做爲模板 var template = $('.form .form-group:first-child').clone(); // 將DOM模板插入到表單中,造成新的一行 $('.form').append(template); });
實例中用了jquery的clone()
方法,能夠複製整個jquery包裝過的DOM對象(不包括對象綁定的事件,若是要連事件也一塊兒複製的話,能夠加個參數clone(true)
哦)。
有時候複製過來的DOM對象有可能不是最原始的狀態,因此記得要初始化一下。例若有像input
這樣的輸入項,要記得把value
的值先初始化哦template.find('input').val('')
。
若是頁面原本就沒有相關的DOM,這時候能夠手動新建一個隱藏的<div>,而後在裏面定義咱們的模板:
<div id="passenger-template" style="display: none;"> <div class="form-group"> 乘客姓名:<input type="text" class="form-control" name="member[]"> </div> </div>
接下來用JS去取這個元素的內容做爲模板:
var template = $('#passenger-template > div').clone();
用一個標籤來包裹模板的理由,
一是取模板的時候能夠很方便,直接clone()
或者html()
就能夠了;
二是爲了更好地分類和規範。例如定義模板時,要求你們都用同一種標籤和CSS類:<div class="template">
固然不必定去用<div>,也可使用別的標籤,或者自定義一個<template>
標籤專門放模板,不過這時候要注意IE8下面自定義標籤會有些許問題哦。
若是想更加清晰地區分開模板和正常的頁面元素的話,還能夠用<script>標籤:
<script id="passenger-template" type="text/html"> <!- 注意標籤內的type屬性 -!> <div class="form-group"> 乘客姓名:<input type="text" class="form-control" name="member[]"> </div> </script>
<script>
標籤內的type="text/html"
,它能告訴瀏覽器這個標籤裏面的內容不是JS腳本,能夠直接忽略不用去解析。這樣瀏覽器就不會報錯了。
還有一點是這時候就不能直接使用clone()
了哦,由於<script>
標籤裏面的內容不是DOM對象,而是字符串類型的HTML片斷。
因此記得要經過html()
方法獲取咱們字符串形式的模板:
var template = $('#passenger-template').html(); // 獲取的是字符串,不是DOM對象
JS和HTML作到了徹底的解耦,十分利於後期的修改和維護。
腳本上沒有了多餘的代碼,咱們在開發的時候,只需關注業務邏輯了。
不用再去手動複製粘貼HTML來拼接JS字符串,寫HTML比拼JS字符串要來的輕鬆,並且不容易出錯。因此是一個明智之舉,也算是有技巧地偷懶。
若是複製頁面現有的DOM做爲模板的話,能夠徹底脫離後期須要維護模板的限制。之後即便頁面有修改了,JS這個「添加一個乘客」的功能,也同樣能正常工做,適應性極強。
咱們繼續之前面的主題展開研究。不過此次的重點,是探究幾種添加數據的實現方式,一步步找出最佳的方案。
新增的需求是這樣的:若是想把特定的乘客信息,添加到新增的頁面元素中,那樣該怎麼辦呢?
$.ajax({ url: '/getPassengers', // 後臺獲取全部乘客的信息 success: function(passengers) { var html = ''; // 儲存要插入到頁面的HTML片斷 var len = passengers.length; for (var i = 0; i < len; i++) { // 獲取帶有該乘客信息的HTML片斷 html += get_passenger_html(passengers[i]); // 後面將詳細講這個函數的實現方式 } $('.form').append(html); } });
下面將集中講一下,改如何生成帶有指定乘客信息的HTML片斷,也就是這個get_passenger_html()
的內部實現方式。
function get_passenger_html(passenger) { var html = ''; html += '<div class="form-group">'; html += ' 乘客姓名:<input type="text" class="form-control" name="member[]" '; html += ' value="' + passenger.name + '">'; // 將乘客姓名拼接到HTML字符串中 html += '</div>'; return html; }
這個也是最傳統的數據跟HTML字符串拼接的的方式,沒有用到模板,腳本上會存在冗長的HTML字符串拼接代碼。
這種作法沒辦法使用以前提到的模板技術,後期維護難是一個重大問題。
數據多一點或者html複雜一點,手動拼接字符串耗費精力、容易出錯的弊端就會愈來愈顯現。
能不能先定義好模板,而後再作數據插入的操做呢?這樣就能夠將模板定義和數據操做分離開來了,跟JS的字符串拼接Say good bye啦。
下面展現兩種分離數據操做和模板定義的實現方式:
若是要插入的數據恰好是在某個標籤或屬性內,可使用操做DOM對象的方式來插入數據:
function get_passenger_html(passenger) { var html = $('#passenger-template').html(); // 獲取HTML字符串模板 var dom = $(html); // 先即將HTML字符轉成DOM對象 dom.find('.name').html(passenger.name); // 找到存放乘客姓名的DOM節點並插入數據 dom.find('.tel').html(passenger.tel); // 找到存放乘客電話的DOM節點並插入數據 // 把處理完畢的DOM轉回HTML字符串並返回 return dom.prop("outerHTML"); }
若是模板不是clone()
得來的,要先用$(html)
將HTML字符串轉成DOM對象,而後才能用find()
去找到對應的DOM節點來操做哦。
html()
方法只能獲取子元素的HTML字符串,要獲取包括本身的HTML字符串的話,要去讀取outerHTML
屬性,這是個DOM對象原生的屬性,因此要用prop()
才能獲取獲得哦。
第一步先安照前面講到的模板分離原則,定義了一個模板。在定義這個模板的時候,順帶添加一些帶有特殊含義的佔位符:{name}
和{tel}
。
<script id="passenger-template" type="text/html"> <ul class="passenger-list"> <li> 乘客姓名: <span class="name">{name}</span> </li> <li> 乘客電話: <span class="tel">{tel}</span> </li> </ul> </div>
第二步就是利用String.replace()
逐個替換掉這些自定義的佔位符:
function get_passenger_html(passenger) { var html = $('#passenger-template').html(); // 獲取HTML字符串模板 // 用乘客姓名替換掉咱們自定義的佔位符 html = html.replace(passenger.name, '{name}'); // 替換姓名佔位符 html = html.replace(passenger.tel, '{tel}'); // 替換電話佔位符 return html; }
佔位符的邊界要特殊一點,例如用{
和}
,這樣子就能避免在替換的時候,把其餘有類似的字符被抹掉了。
介紹通用方案前,假設咱們獲取到的模板是下面這一段字符串:
var template = '乘客姓名:{name},他的電話是:{tel},哈哈哈哈哈。';
想要替換掉佔位符的JSON數據是:
var data = { name: '小神遊', tel: 12312423423 };
按以前介紹的方法,要一個個寫死:
template.replace('{name}', data.name); template.replace('{tel}', data.tel);
太麻煩了,原本已經模板上定了一次佔位符。可是到了對應的JS上也要再手寫一次,而且數據屬性名也要手寫,纔可以保證能夠替換成功。這樣子代碼寫得一點都不優雅。
懶惰的咱們,從不喜歡重複勞動。這時候新建了個通用方法,能將特定模板和對應數據智能地匹配。
使用方法是這樣的:
// 直接傳入模板和數據便可 var html = template_replace(template, data); console.log(html); // 輸出替換了數據的模板字符:乘客姓名:小神遊,他的電話是:12312423423,哈哈哈哈哈。
哈哈哈,直接搞定!可以智能匹配模板和數據,並且還能複用在別的地方,之後能夠偷懶了!
那麼怎樣寫這個方法,把模板和數據智能地匹配呢?
以替換佔位符{name}
爲例,大致思路是:
找出模板佔位符的左右邊界,也就是{
和}
獲取邊界內的字符串,獲得數據屬性名,也就是name
把整個佔位符用屬性值替換掉,也就是{name}
替換成data['name']
// 將模板和數據結合起來 var template_replace = function(template, data) { // 內部方法:獲取第一個匹配到的佔位符位置,也就是"{"和"}"的索引 function get_next_placeholder_index_range() { ... } // 內部方法:將索引範圍內的字符串,替換成data中具體的屬性值 function set_replacement(indexRange) { ... } // 內部方法:替換全部佔位符成爲對應數據 function begin_replace() { ... } // 開始執行替換 begin_replace(); return template; // 返回替換完畢的模板字符串 };
這個內部方法get_next_placeholder_index_range()
,
用於獲取第一個匹配到的佔位符位置,也就是"{"和"}"的索引
{
的索引值var leftIndex = template.indexOf('{', 0);
}
的索引值var rightIndex = template.indexOf('}', leftIndex);
if (leftIndex === -1 || rightIndex === -1) { // 沒有搜素到匹配的佔位符 return false; } else { // 存在佔位符,返回開始和結束的索引 return {left: leftIndex, right: rightIndex}; }
注意點:若是沒有匹配的項,indexOf()
會返回-1
。
這個內部方法set_replacement()
,用於將索引範圍內的字符串,替換成data中具體的屬性值。
{
和}
var key = template.slice(indexRange.left + 1, indexRange.right);
注意點:slice()
的第一個參數表示從哪一個index開始截取(包括這個index的字符),因此若是要忽略{
的話,要從indexRange.left + 1
開始截取。
注意點:slice()
的第二個參數表示獲取這個index值以前的字符串,因此恰好能夠直接寫indexRange.right
來忽略}
了。
{
和}
template = template.replace('{' + key + '}', data[key]);
注意點:示例沒有作二維三維數據的轉換,有須要的話能夠擴展下代碼:
var key = template.slice(indexRange.left + 1, indexRange.right); var keys = key.split('.'); // 根據點語法獲取各級的屬性名 var value = ''; // 屬性值 switch (keys.length) { case 1: // 一維,如{name} value = data[keys[0]]; break; case 2: // 二維,如{name.firstName} value = data[keys[0]][keys[1]]; break; case 3: // 三維,如{name.firstName.firstWord} value = data[keys[0]][keys[1]][keys[2]]; break; default:; } template = template.replace('{' + key + '}', value);
不過擴展時要注意適度的權衡。當咱們擴展的代碼愈來愈多的時候,就證實這個自定義的函數已經開始知足不了需求了,這時候建議轉向使用第三方解決方案,後面會有介紹一個最佳的模板框架。
注意點:這個簡單示例沒有作容錯機制,目的是展現數據替換的方法。因此前提是假設模板的佔位符都已經和數據是對應的哦。
begin_replace(); // 繼續遞歸替換
利用begin_replace
方法,檢查模板中還有沒有下一個佔位符,若是存在下一個佔位符的話,begin_replace
會繼續遞歸調用get_replacement
來替換下一個,這兩個函數的互相調用會一直輪迴,直到模板全部佔位符替換結束爲止。
這個方法begin_replace()
將會調用前面的定義兩個內部函數,目的是爲了統籌遞歸替換數據的操做。
var indexRange = get_next_placeholder_index_range();
若是有佔位符,能夠開始進行替換了:
if (indexRange) { // set_replacement(indexRange); }
var template = '乘客姓名:{name},他的電話是:{tel},哈哈哈哈哈。'; var data = { name: '小神遊', tel: 12312423423 }; // 直接傳入模板和數據便可 var html = template_replace(template, data); console.log(html); // 乘客姓名:小神遊,他的電話是:12312423423,哈哈哈哈哈。
var template_replace = function(template, data) { function get_next_placeholder_index_range() { var leftIndex = template.indexOf('{', 0); var rightIndex = template.indexOf('}', leftIndex); if (leftIndex === -1 || rightIndex === -1) { return false; } else { return {left: leftIndex, right: rightIndex}; } } function set_replacement(indexRange) { var key = template.slice(indexRange.left + 1, indexRange.right); template = template.replace('{' + key + '}', data[key]); begin_replace(); } function begin_replace() { var indexRange = get_next_placeholder_index_range(); if (indexRange) { set_replacement(indexRange); } } begin_replace(); return template; };
代碼最後大概20行左右,今後就能夠大大提升生產力,也讓之後寫的代碼都更加優雅。
當你須要更Power的模板功能的時候,不必定要本身寫,更理智的作法是使用成熟的模板引擎。
這裏給出我多年一直在使用的、認爲是最好的模板引擎:Artemplate.js
Github地址是:https://github.com/aui/artTem...
ArtTemplate是騰訊出的模板引擎,支持不少高級的模板操做,例如循環遍歷、條件分支等等;而且它的解析速度是衆多模板引擎中最快的。
哈哈,在咱們嘗試寫過簡單的模板解析,理解了應該怎樣善用模板和處理模板,讓代碼更加優雅且利於維護以後。用起第三方的模板引擎的時候會更加的感動:個人天,這東西怎麼會這麼方便。
經過對比「勉強的方案」,和介紹各類「更好的方案」,其實總結起來都離不開一句話:讓代碼更加優雅且利於維護。
記住這一點,在「夠用」和「更好」之間,咱們總要逆流而上,敢於在實踐中尋找「更好」。
其實不止是這篇文章中提到的一些小技巧,咱們在開發中還須要去處理各類類型的問題,對應的解決方案也確定不止一個,並且正在使用的方案也不必定是最優。因此要時刻有不將就的精神,多花點時間去優化。你能夠的!
文章中若有錯誤但願你們多多指正和多多包涵,我會當即改正的哈。還有要多多評論,多多交流,多多點贊哦~
謝謝你看到了最後,你們一塊兒加油!