博客 / 詳情

返回

下一代的模板引擎:lit-html

前面的文章介紹了 Web Components 的基本用法,今天來看看基於這個原生技術,Google 二次封存的框架 lit-html。

其實早在 Google 提出 Web Components 的時候,就在此基礎上發佈了 Polymer 框架。只是這個框架一直雷聲大雨點小,內部似乎也對這個項目不太滿意,然後他們團隊又開發了兩個更加現代化的框架(或者説是庫?): lit-html、lit-element,今天的文章會重點介紹 lit-html 的用法以及優勢。

發展歷程

在講到 lit-html 之前,我們先看看前端通過 JavaScript 操作頁面,經歷過的幾個階段:

發展階段

原生 DOM API

最早通過 DOM API 操作頁面元素,操作步驟較為繁瑣,而且 JS 引擎與瀏覽器 DOM 對象的通信相對耗時,頻繁的 DOM 操作對瀏覽器性能影響較大。

var $box = document.getElementById('box')
var $head = document.createElement('h1')
var $content = document.createElement('div')
$head.innerText = '關注我的公眾號'
$content.innerText = '打開微信搜索:『自然醒的筆記本』'
$box.append($head)
$box.append($content)

jQuery 操作 DOM

jQuery 的出現,讓 DOM 操作更加便捷,內部還做了很多跨瀏覽器的兼容性處理,極大的提升了開發體驗,並且還擁有豐富的插件體系和詳細的文檔。

var $box = $('#box')

var $head = $('<h1/>', { text: '關注我的公眾號' })
var $content = $('<div/>', { text: '打開微信搜索:『自然醒的筆記本』' })

$box.append($head, $content)

雖然提供了便捷的操作,由於其內部有很多兼容性代碼,在性能上就大打折扣了。而且它的鏈式調用,讓開發者寫出的麪條式代碼也經常讓人詬病(PS. 個人認為這也不能算缺點,只是有些人看不慣罷了)。

模板操作

『模板引擎』最早是後端 MVC 框架的 View 層,用來拼接生成 HTML 代碼用的。比如,mustache 是一個可以用於多個語言的一套模板引擎。

mustache

後來前端框架也開始搗鼓 MVC 模式,漸漸的前端也開始引入了模板的概念,讓操作頁面元素變得更加順手。下面的案例,是 angluar.js 中通過指令來使用模板:

var app = angular.module("box", []);

app.directive("myMessage", function (){
  return {
    template : '' +
    '<h1>關注我的公眾號</h1>' +
    '<div>打開微信搜索:『自然醒的筆記本』</div>'
  }
})

後來的 Vue 更是將模板與虛擬 DOM 進行了結合,更進一步的提升了 Vue 中模板的性能,但是模板也有其缺陷存在。

  • 不管是什麼模板引擎,在啓動時,解析模板是需要花時間,這是沒有辦法避免的;
  • 連接模板與 JavaScript 的數據比較麻煩,而且在數據更新時還需進行模板的更新;
  • 各式各樣的模板創造了自己的語法結構,使用不同的模板引擎,就需要重新學習一遍其語法糖,這對開發體驗不是很友好;

JSX

GitHub - OpenJSX/logo: Logo of JSX-IR

React 在官方文檔中這樣介紹 JSX:

JSX,是一個 JavaScript 的語法擴展。我們建議在 React 中配合使用 JSX,JSX 可以很好地描述 UI 應該呈現出它應有交互的本質形式。JSX 可能會使人聯想到模板語言,但它具有 JavaScript 的全部功能。
var title = '關注我的公眾號'
var content = '打開微信搜索:『自然醒的筆記本』'

const element = <div>
  <h1>{title}</h1>
  <div>{content}</div>
</div>;

ReactDOM.render(
  element,
  document.getElementById('root')
)

JSX 的出現,給前端的開發模式帶來更大的想象空間,更是引入了函數式編程的思想。

UI = fn(state)

但是這也帶來了一個問題,JSX 語法必須經過轉義,將其處理成 React.createElement 的形式,這也提高了 React 的上手難度,很多新手望而卻步。

lit-html 介紹

lit-html 的出現就儘可能的規避了之前模板引擎的問題,通過現代瀏覽器原生的能力來構建模板。

  • ES6 提供的模板字面量;
  • Web Components 提供的 <template> 標籤;
// Import lit-html
import {html, render} from 'lit-html';

// Define a template
const template = (title, content) => html`
  <h1>${title}</h1>
  <div>${content}</div>
`;

// Render the template to the document
render(
  template('關注我的公眾號', '打開微信搜索:『自然醒的筆記本』'),
  document.body
);

模板語法

由於使用了原生的模板字符,可以無需轉義,直接進行使用,而且和 JSX 一樣也能使用 JavaScript 語法進行遍歷和邏輯控制。

const skillTpl = (title, skills) => html`
  <h2>${title || '技能列表' }</h2>
  <ul>
    ${skills.map(i => html`<li>${i}</li>`)}
  </ul>
`;

render(
  skillTpl('我的技能', ['Vue', 'React', 'Angluar']),
  document.body
);

除了這種寫法上的便利,lit-html 內部也提供了Vue 類似的事件綁定方式。

const Input = (defaultValue) => html`
  name: <input value=${defaultValue} @input=${(evt) => {
    console.log(evt.target.value)
  }} />
`;

render(
  Input('input your name'),
  document.body
);

樣式的綁定

除了使用原生模板字符串編寫模板外,lit-html 天生自帶的 CSS-in-JS 的能力。

import {html, render} from 'lit-html';
import {styleMap} from 'lit-html/directives/style-map.js';

const skillTpl = (title, skills, highlight) => {
 const styles = {
   backgroundColor: highlight ? 'yellow' : '',
 };
 return html`
   <h2>${title || '技能列表' }</h2>
   <ul style=${styleMap(styles)}>
     ${skills.map(i => html`<li>${i}</li>`)}
   </ul>
 `
};

render(
 skillTpl('我的技能', ['Vue', 'React', 'Angluar'], true),
 document.body
);

渲染流程

做為一個模板引擎,lit-html 的主要作用就是將模板渲染到頁面上,相比起 React、Vue 等框架,它更加專注於渲染,下面我們看看 lit-html 的基本工作流程。

// Import lit-html
import { html, render } from 'lit-html';

// Define a template
const myTemplate = (name) => html`<p>Hello ${name}</p>`;

// Render the template to the document
render(myTemplate('World'), document.body);

通過前面的案例也能看出,lit-html 對外常用的兩個 api 是 html 和 render。

構造模板

html 是一個標籤函數,屬於 ES6 新增語法,如果不記得標籤函數的用法,可以打開 Mozilla 的文檔(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals)複習下。

export const html = (strings, ...values) => {
  ……
};

html 標籤函數會接受多個參數,第一個參數為靜態字符串組成的數組,後面的參數為動態傳入的表達式。我們可以寫一個案例,看看傳入的 html 標籤函數的參數到底長什麼樣:

const foo = '吳彥祖';
const bar = '梁朝偉';

html`<p>Hello ${foo}, I'm ${bar}</p>`;

整個字符串會被動態的表達式進行切割成三部分,這個三個部分會組成一個數組,做為第一個參數傳入 html 標籤函數,而動態的表達式經過計算後得到的值會做為後面的參數一次傳入,我們可以將 strings 和 values 打印出來看看:

log

lit-html 會將這兩個參數傳入 TemplateResult 中,進行實例化操作。

export const html = (strings, ...values) => {
  return new TemplateResult(strings, values);
};
// 生成一個隨機字符
const marker = `{{lit-${String(Math.random()).slice(2)}}}`;
const nodeMarker = `<!--${marker}-->`;

export class TemplateResult {
    constructor(strings, values) {
        this.strings = strings;
        this.values = values;
    }
    getHTML() {
        const l = this.strings.length - 1;
        let html = '';
        let isCommentBinding = false;
        for (let i = 0; i < l; i++) {
            const s = this.strings[i];
            html += s + nodeMarker;
        }
        html += this.strings[l];
        return html;
    }
    getTemplateElement() {
        const template = document.createElement('template');
        let value = this.getHTML();
        template.innerHTML = value;
        return template;
    }
}

實例化的 TemplateResult 會提供一個 getTemplateElement 方法,該方法會創建一個 template 標籤,然後會將 getHTML 的值傳入 template 標籤的 innerHTML 中。而 getHTML 方法的作用,就是在之前傳入的靜態字符串中間插入 HTML 註釋。前面的案例中,如果調用 getHTML 得到的結果如下。

渲染到頁面

render 方法會接受兩個參數,第一個參數為 html 標籤函數返回的 TemplateResult,第二個參數為一個真實的 DOM 節點。

export const parts = new WeakMap();
export const render = (result, container) => {
  // 先獲取DOM節點之前對應的緩存
  let part = parts.get(container);
  // 如果不存在緩存,則重新創建
  if (part === undefined) {
    part = new NodePart()
    parts.set(container, part);
    part.appendInto(container);
  }
  // 將 TemplateResult 設置到 part 中
  part.setValue(result);
  // 調用 commit 進行節點的創建或更新
  part.commit();
};

render 階段會先到 parts 裏面查找之前構造過的 part 緩存。可以將 part 理解為一個節點的構造器,用來將 template 的內容渲染到真實的 DOM 節點中。

如果 part 緩存不存在,會先構造一個,然後調用 appendInto 方法,該方法會在 DOM 節點的前後插入兩個註釋節點,用於後續插入模板。

const createMarker = () => document.createComment('');
export class NodePart {
  appendInto(container) {
    this.startNode = container.appendChild(createMarker());
    this.endNode = container.appendChild(createMarker());
  }
}

然後通過 commit 方法創建真實的節點,並插入到兩個註釋節點中。下面我們看看 commit 方法的具體操作:

export class NodePart {
  setValue(result) {
    // 將 templateResult 放入 __pendingValue 屬性中
    this.__pendingValue = result;
  }
  commit() {
    const value = this.__pendingValue;
    // 依據 value 的不同類型進行不同的操作
    if (value instanceof TemplateResult) {
      // 通過 html 標籤方法得到的 value
      // 肯定是 TemplateResult 類型的
      this.__commitTemplateResult(value);
    } else {
      this.__commitText(value);
    }
  }
  __commitTemplateResult(value) {
    // 調用 templateFactory 構造模板節點
    const template = templateFactory(value);
    // 如果之前已經構建過一次模板,則進行更新
    if (this.value.template === template) {
      // console.log('更新DOM', value)
      this.value.update(value.values);
    } else {
      // 通過模板節點構造模板實例
      const instance = new TemplateInstance(template);
      // 將 templateResult 中的 values 更新到模板實例中
            const fragment = instance._clone();
      instance.update(value.values);
      // 拷貝模板中的 DOM 節點,插入到頁面
      this.__commitNode(fragment);
      // 模板實例放入 value 屬性進行緩存,用於後續判斷是否是更新操作
      this.value = instance;
    }
  }
}

實例化之後的模板,首先會調用 instance._clone() 進行一次拷貝操作,然後通過 instance.update(value.values) 將計算後的動態表達式插入其中。

最後調用 __commitNode 將拷貝模板得到的節點插入真實的 DOM 中。

export class NodePart {
  __insert(node) {
    this.endNode.parentNode.insertBefore(node, this.endNode);
  }
  __commitNode(value) {
    this.__insert(value);
    this.value = value;
  }
}

可以看到 lit-html 並沒有類似 Vue、React 那種將模板或 JSX 構造成虛擬 DOM 的流程,只提供了一個輕量的 html 標籤方法,將模板字符轉化為 TemplateResult,然後用註釋節點去填充動態的位置。TemplateResult 最終也是通過創建 <template> 標籤,然後通過瀏覽器內置的 innerHTML 進行模板解析的,這個過程也是十分輕量,相當於能交給瀏覽器的部分全部交給瀏覽器來完成,包括模板創建完後的節點拷貝操作。

export class TemplateInstance {
  _clone() {
    const { element } = this.template;
    const fragment = document.importNode(element.content, true);
    // 省略部分操作……
    return fragment;
  }
}

其他

lit-html 只是一個高效的模板引擎,如果要用來編寫業務代碼還缺少了類似 Vue、React 提供的生命週期、數據綁定等能力。為了完成這部分的能力,Polymer 項目組還提供了另一個框架:lit-element,可以用來創建 WebComponents。

除了官方的 lit-element 框架,Vue 的作者還將 Vue 的響應式部分剝離,與 lit-html 進行了結合,創建了一個 vue-lit(https://github.com/yyx990803/vue-lit) 的框架,一共也就寫了 70 行代碼,感興趣可以看看。

image

user avatar 1023 頭像 laughingzhu 頭像 jidongdehai_co4lxh 頭像 ivyzhang 頭像 huishou 頭像 ziyeliufeng 頭像 codepencil 頭像 shaochuancs 頭像 susouth 頭像 yilezhiming 頭像 frontoldman 頭像 liyl1993 頭像
50 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.