[聊一聊系列]聊一聊前端模板與渲染那些事兒

歡迎你們收看聊一聊系列,這一套系列文章,能夠幫助前端工程師們瞭解前端的方方面面(不只僅是代碼):
https://segmentfault.com/blog...php

做爲現代應用,ajax的大量使用,使得前端工程師們平常的開發少不了拼裝模板,渲染模板。咱們今天就來聊聊,拼裝與渲染模板的那些事兒。html

若是喜歡本文請點擊右側的推薦哦,你的推薦會變爲我繼續更文的動力。前端

1 頁面級的渲染

在剛有web的時候,前端與後端的交互,很是直白,瀏覽器端發出URL,後端返回一張拼好了的HTML串。瀏覽器對其進行渲染。html中可能會混有一些php(或者php中混有一些html)。在服務端將數據與模板進行拼裝,生成要返回瀏覽器端的html串。react

這與咱們如今作一個普通網頁沒什麼區別。只不過如今,咱們更常使用模板技術來解決先後端耦合的問題。git

前端使用模板引擎,在html中寫一些標籤,與數據與邏輯基本無關。後端在渲染的時候,解析這些標籤,生成HTML串,如smarty。其實前端與後端的交互在服務端就已經有一次了。github

模板:web

front.tpl

<div>
    {%$a%}
</div>

後端:

// 設置變量
$smarty->assign('a', 'give data');

// 展現模板
$smarty->display("front.tpl");

到前端時是渲染好的html串:

<div>
    give data
</div>

這種方式的特色是展現數據快,直接後端拼裝好數據與模板,展示到用戶面前。ajax

2 異步的請求與新增模板

新的時代,由ajax引領。(Asynchronous Javascript And XML),這種技術的歷史,我就再也不贅述。ajax的用法也有多種。算法

ajax接受各類類型的返回。包括XML/JSON/String等。前端發起ajax請求,後端直接將數據返回。json

可是,讀者們有沒有想過,ajax回來的數據是幹嗎用的呢?相信大部分人使用ajax拿回的數據是用來展現的。前端得把ajax拿回來的數據與模板進行拼裝。這就面臨了一個問題,當你的模板很是「華麗」的時候(也就是模板代碼比較多的時候)。咱們在前端寫的拼字符串的邏輯,會很是的複雜。

也有的人圖省事,直接就在ajax的返回值中,傳輸拼裝好的html字符串。這樣能夠直接把ajax拿到的html字符串,填充到頁面上。

下面實例說明一下兩種方式:

2.1 ajax獲取字符串直接渲染方式

如圖2.1.1所示:

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <h1>下面是待填充區域:</h1>
        <div class="blankPlace"></div>
        <script>
            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
                    document.querySelector('.blankPlace').innerHTML = xhr.responseText;
                }   
            };  
            xhr.open('GET', './a.html');
            xhr.send(null);
        </script>
    </body>
</html>

========================================================================
a.html

<h2>我是模板</h2>
<div>這是請求回來的數據</div>

    
171834_IGuj_1177792.png
圖2.1.1

2.2 ajax獲取數據,前端進行拼裝的方式

效果如圖2.2.1所示:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <h1>下面是待填充區域:</h1>
        <div class="blankPlace"></div>
        <script>
            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
                    var res = JSON.parse(xhr.responseText);
                    document.querySelector('.blankPlace').innerHTML = ''
                        +'<h2>'+
                           '我是模板'+
                        '</h2>'+
                        '<div>'+
                           res.data+
                        '</div>';
                }   
            };  
            xhr.open('GET', './b.json');
            xhr.send(null);
        </script>
    </body>
</html>

================================================

b.json

{"data": "這是請求回來的數據"}

172537_JrgD_1177792.png
圖 2.2.1

2.3 兩種方式的權衡 

那麼,如何權衡兩種方式呢?

筆者單從本身的思惟考慮,得出如下結論。若是這種模板的拼裝會發生屢次。是一個很是頻繁的行爲,且模板基本一致,只是數據變更的話,最好是一開始採用客戶端拼裝的方法。由於,一樣的模板沒有必要被傳到客戶端好幾回。這樣,咱們能夠剩下傳輸一樣模板的流量,請求更快。

相似於新聞流這種網站比較適合這種方式,現在日頭條,如圖2.3.1所示:

173508_Jb0C_1177792.png
圖2.3.1

173613_MQUA_1177792.png
圖2.3.2

筆者在DOM上面打了斷點後,找到了其拼裝模板,確是在客戶端所作。

不過,這種作法也有問題,就是用戶同步刷新的時候,須要等頁面渲染完,再發一個請求,去請求第一屏的數據,才能開始渲染。這個過程至關於發了兩次請求,等待的時候仍是有所感知的,如圖2.3.3所示。

174016_s2Du_1177792.png
圖2.3.3

因此這種方式也是有些不盡人意的地方的。通過查看,網易新聞的web版,今日頭條的web版,每天快報的web版均是採用這種方式。

第二種方式,同步的時候,就將一段渲染好的HTML,直接輸出到頁面,而在異步的時候,請求的也是這段HTML,直接將請求回的HTML往頁面上一塞就完成了。這樣就能夠達到同步頁面的時候,直接輸出,用戶就不會看到等待中的小菊花了。

百度首頁就採起了這種方式。新聞直出,無需等待如圖2.3.4

174928_0Skn_1177792.png
圖2.3.4

可是每次請求新聞的時候,也會去請求HTML片斷,如圖2.3.5所示

175152_j366_1177792.png
圖2.3.5

這種方式雖然首屏較快,可是,仍是有優化空間的。

2.4 混合方式

看過了上述兩種方式,聰明的你確定會想:若是前端的js裏寫一份模板,後端的html(jsp/asp/smarty)中也寫一份模板呢?這樣,同步的時候,直接用後端HTML(jsp/asp/smarty)中的模板。異步拉取數據的時候,每次使用js中的模板進行拼裝 。同步也能保證首屏的速度,異步也能保證傳輸量的限制與速度。但是這樣,也會面臨問題,那就是,你的模板須要維護兩份。若是那天產品和你說,我要改一下頁面的結構。你不得不改動HTML的時候。js中與jsp/asp/smarty中的模板都須要一樣的更改兩次。

2.5 前端的模板引擎

若是說,後端能夠將html的拼裝轉變爲使用引擎的話,前端爲何不能夠呢?這裏我先給你們寫一個很是簡單的模板解析函數,效果如圖2.5.1

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <div id="content1"></div>
        <div id="content2"></div>
        <script>
            // 這是咱們的模板,怎麼樣,比直接寫html字符串拼裝看起來清爽多了吧?抱歉,markdown不能並列吧+號寫在前面,因此我就寫在了後面
            var template = ''
            +'<div>'+
               '{%=a%}'+
               '{%if (a===1){%}'+
                   '<span>'+
                       'a是1'+
                   '</span>'+
               '{%}%}'+
            '</div>';
            // 能解析輸出與if條件語句的函數
            function TEMPLATEparser(template, variables) {
                // 語法替換
                var funcStr = template
                .replace(/\{\%\=(\w+)\%\}/, function (code, variable) {
                    return '"; str += "' + variable + '"; str += "';
                })
                .replace(/\{\%(if.*?)\%\}(.*?)\{\%(\})\%\}/, function (code, judge, content, end) {
                    return '";' + judge + 'str+="' + content + '";' + end + 'str += "';
                });
                // 返回拼裝函數
                return new Function(variables, 'var str = ""; str += "' + funcStr + '";return str;');
            }

            // 實驗使用模板引擎去解析並傳入變量生成模板
            var outHTML = TEMPLATEparser(template, ['a'])(1);
            document.getElementById('content1').innerHTML = outHTML;
            outHTML = TEMPLATEparser(template, ['a'])(2);
            document.getElementById('content2').innerHTML = outHTML;
        </script>
    </body>
</html>

184530_Qv1G_1177792.png
圖2.5.1

這樣就製做了一個簡單的前端模板,有興趣的讀着能夠看看我寫的smartyMonkey前端模板引擎:

https://github.com/houyu01/sm...

2.6 先後端同構

剛剛說過了前端模板,後端模板,前端與後端都須要模板引擎。好比,咱們的在後端的模板是這樣寫的:

// 接下來是僞代碼
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        // 前端須要模板去渲染
        <textarea id="temp">include('./template.html')</textarea>
        <div id="content1">
            // 後端渲染模板
            include('./template.html');
        </div>
        <div id="content2"></div>
        <script>
            // 這是咱們的模板,怎麼樣,比直接寫html字符串拼裝看起來清爽多了吧?
            var template = document.getElementById('temp').value;
            // 能解析輸出與if條件語句的函數
            function TEMPLATEparser(template, variables) {
                // 語法替換
                var funcStr = template
                .replace(/\{\%\=(\w+)\%\}/, function (code, variable) {
                    return '"; str += "' + variable + '"; str += "';
                })
                .replace(/\{\%(if.*?)\%\}(.*?)\{\%(\})\%\}/, function (code, judge, content, end) {
                    return '";' + judge + 'str+="' + content + '";' + end + 'str += "';
                });
                // 返回拼裝函數
                return new Function(variables, 'var str = ""; str += "' + funcStr + '";return str;');
            }

            // 實驗使用模板引擎去解析並傳入變量生成模板
            var outHTML = TEMPLATEparser(template, ['a'])(1);
            document.getElementById('content1').innerHTML = outHTML;
            outHTML = TEMPLATEparser(template, ['a'])(2);
            document.getElementById('content2').innerHTML = outHTML;
        </script>
    </body>
</html>

============================
template.html

<div>
  {%=a%}
  {%if (a===1){%}
      <span>
          a是1
      </span>
  {%}%} 
</div>

前端解析模板的引擎的語法,與後端j解析模板引擎語法一致。這樣就達到了一份HTML先後端一塊兒使用的效果。一改俱改,一板兩用。其實這樣也不算極致的完美,由於聰明的讀者會發現,在頁面加載的時候,咱們多傳了一份模板給到前端,若是用戶不觸發從新渲染的話,可能咱們傳到前端的模板就算白傳了,形成了浪費。聰明的讀者們能夠考慮一下,如何把這份也給省下去。

3 模板的更新

有的時候,咱們須要整片DOM進行更新,好比:

<div class="我須要被更新" data-att="我須要被更新">
    <span>我須要被更新</span>
    <div class="我須要被更新"></div>
</div>

這些html中的節點,須要在某次行爲以後,一塊兒被更新。那麼咱們的js可能會變成這樣:

<script>
    // 數據更新
    $.ajax().done(function (data) {
        $('#wrapper').class(data.xxx);
        $('#wrapper').attr('data-attr', data.xxx);
        $('#wrapper span').html(data.xxx);
        $('#wrapper div').class(data.xxx);
    });
</script>

這樣的維護,成本極大,還不如直接把整個html從新刷新一遍。這就遇到了咱們的js拼裝模板了:

<script>
    // 模板
    var template = ''
    +'<div class="{%=newclass%}" data-attr="{%=newattr%}">'+
        '<span>{%=newcontent%}</span>'+
        '<div class={%=newinnerclass%}></div>'+
    '</div>';
    

    // 數據更新
    $.ajax().done(function (data) {
        // 每次數據更新,直接把模板全刷一遍
        $('#wrapper')[0].outerHTMl = TEMPLATEparser(template)(data);
    });
</script>

可是,直接刷HTML的成本過高。這樣瀏覽器不得不整顆html子樹所有從新構建一下,這種方法的性能又不如上一種方法好。

好在react給了咱們一種新的思路,它用最少的開銷幫咱們處理模板的更新,卻又不用咱們維護更新時繁瑣的步驟。有興趣的讀者能夠了解一下react-web的diff算法及其應用。

https://segmentfault.com/a/11...

4 課後思考

好了,關於前端常見的模板的拼裝與更新,咱們就講到這裏,同窗們有沒有考慮過,本身的項目中,若是有異步請求並渲染的邏輯的時候,採用前端拿數據拼裝、前端拿拼裝好的模板、混合使用哪一種更好呢?

文中說起到的例子,均在github上能夠找到:https://github.com/houyu01/te...

若是有想一塊兒開發smartyMonkey的同窗,請私信我,一塊兒開發力量更大~

接下來的一篇文章,我將會和讀者們一塊兒聊聊前端存儲那些事兒,不要走開,請關注我.....
聊一聊前端存儲那些事兒

相關文章
相關標籤/搜索