乐趣区

关于前端:下一代的模板引擎lithtml

后面的文章介绍了 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 是一个能够用于多个语言的一套模板引擎。

起初前端框架也开始捣鼓 MVC 模式,慢慢的前端也开始引入了模板的概念,让操作页面元素变得更加棘手。上面的案例,是 angluar.js 中通过指令来应用模板:

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

app.directive("myMessage", function (){
  return {template : ''+'<h1> 关注我的公众号 </h1>'+'<div> 关上微信搜寻:『天然醒的笔记本』</div>'}
})

起初的 Vue 更是将模板与虚构 DOM 进行了联合,更进一步的晋升了 Vue 中模板的性能,然而模板也有其缺点存在。

  • 不论是什么模板引擎,在启动时,解析模板是须要花工夫,这是没有方法防止的;
  • 连贯模板与 JavaScript 的数据比拟麻烦,而且在数据更新时还需进行模板的更新;
  • 各式各样的模板发明了本人的语法结构,应用不同的模板引擎,就须要重新学习一遍其语法糖,这对开发体验不是很敌对;

JSX

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 打印进去看看:

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 行代码,感兴趣能够看看。

退出移动版