后面的文章介绍了 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 行代码,感兴趣能够看看。