了解基于它的元素和Web组件

有一次,我不得不紧急熟悉Web组件,并找到一种使用它们方便地进行开发的方法。 我打算写一系列的文章
以某种方式组织有关Web组件,元素的知识,并为他人简要介绍此技术。 我不是这项技术的专家,很乐意接受任何反馈。


lit-element是本机Web组件的包装(基本模板)。 它实现了规范中未包含的许多便捷方法。 由于它接近本机实现,因此与其他方法相比,lit-element在各种基准中显示出非常好的结果(截至02/06/2019)。


通过使用lit-element作为Web组件的基类,我看到了一些好处:


  1. 该技术已经实现了第二版,并且“患上了儿童疾病”,这是刚刚出现的仪器所特有的。
  2. 组装既可以用聚合物进行,也可以用webpack,打字稿,汇总等进行,这使您可以将照明元件嵌入任何现代项目中而没有任何问题。
  3. 照明元素在键入,初始化和转换值方面具有非常方便的属性处理系统。
  4. 点亮元素实现与react几乎相同的逻辑,即 它提供了最低限度的功能-用于构建组件及其呈现的单个模板,并且不会限制开发人员选择生态系统和其他库。

在lit-element上创建一个简单的Web组件。 我们来看一下文档。 我们需要以下内容:


  1. 在组件中添加带亮元素的npm软件包

    npm install --save lit-element 
  2. 创建我们的组件。

例如,我们需要创建一个在my-component标签中初始化的Web组件。 为此,请创建js文件my-component.js并定义其基本模板:


 //       lit-element import { } from ''; //      class MyComponent { } //      customElements.define(); 

首先,我们导入基本模板:


 import { LitElement, html } from 'lit-element'; // LitElement -    ()   - // html -  lit-html,     ,  //    html    

其次,使用LitElement创建Web组件本身


 //   ,    //  LitElement    HTMLElement class MyComponent extends LitElement { //    LitElement   //      constructor  connectedCallback //           //    ,       // shadowDOM   {mode: 'open'} render() { return html`<p>Hello World!</p>` } } 

最后一件事是在浏览器中注册Web组件


 customElements.define('my-component', MyComponent); 

结果,我们得到以下信息:


 import { LitElement, html } from 'lit-element'; class MyComponent extends LitElement { render() { return html`<p>Hello World!</p>` } } customElements.define('my-component', MyComponent); 

如果您不需要将my-component.js连接到html,那么就是这样。 最简单的组件已准备就绪。


我建议不要重新发明轮子,而要完成组装轻型组件的组装。 请按照指示进行:


 git clone https://github.com/PolymerLabs/lit-element-build-rollup.git cd lit-element-build-rollup npm install npm run build npm run start 

完成所有命令后,我们转到浏览器中的页面http:// localhost:5000 /


如果我们看一下html,我们会看到webcomponents-loader.js位于结束标记的前面。 这是一组用于网状部件的填料,并且对于网状部件的跨浏览器操作,希望存在该填料。 让我们看一下实现了与Web组件一起使用的所有标准的浏览器列表 ,它表示EDGE仍然没有完全实现这些标准(我对IE11保持沉默,仍然需要支持)。



为此Polyfill实现了2个选项:


  1. webcomponents-bundle.js-此版本包含polyfill的所有可能选项,它们都已启动,但是每个polyfill仅在检测到的迹象的基础上才能工作。
  2. webcomponents-loader.js是一个最小的引导程序,它根据检测到的症状来加载必要的polyfill

我还请您注意另一个polyfill-custom-elements-es5-adapter.js 。 根据规范,只能将ES6类添加到本机customElements.define中。 为了获得最佳性能,应仅将ES6代码传递给支持它的浏览器,并将ES5传递给其他人。 并非总是可以做到这一点,因此,为了更好地跨浏览器兼容,建议将所有ES6代码都转换为ES5。 但是在这种情况下,ES5上的Web组件将无法在浏览器中工作。 为了解决这个问题,有custom-elements-es5-adapter.js。


现在让我们打开./src/my-element.js文件


 import {html, LitElement, property} from 'lit-element'; class MyElement extends LitElement { // @property - ,    babel  ts //         //  ,   @property({type: String}) myProp = 'stuff'; render() { return html` <p>Hello World</p> ${this.myProp} `; } } customElements.define('my-element', MyElement); 

lit-html模板引擎可以不同地处理字符串。 我会给你几个选择:


 //  : html`<div>Hi</div>` // : html`<div>${this.disabled ? 'Off' : 'On'}</div>` // : html`<x-foo .bar="${this.bar}"></x-foo>` // : html`<div class="${this.color} special"></div>` //   boolean,  checked === false, //        HTML: html`<input type="checkbox" ?checked=${checked}>` //  : html`<button @click="${this._clickHandler}"></button>` 

优化render()函数的提示:


  • 不得更改元素的状态,
  • 不应该有副作用,
  • 应该仅取决于元素的属性,
  • 传输相同值时应返回相同结果。

不要在render()函数之外更新DOM。


Lit-html负责渲染照明元素-这是描述应如何显示Web组件的声明性方式。 lit-html通过仅更改DOM中需要更改的那些部分来保证快速更新。


几乎所有这些代码都在一个简单的示例中,但是为myProp属性添加了myProp 装饰器 。 此装饰器指示我们在my-element一个名为myprop的属性。 如果未设置此类属性,则默认情况下将字符串值设置为stuff


 <!--  myProp  ,       -   'stuff' --> <my-element></my-element> <!--  myprop         lowerCamelCase .. myProp   -      'else' --> <my-element myprop="else"></my-element> 

lit-element提供了两种使用property


  1. 通过装饰器。
  2. 通过一个静态的getter properties

第一个选项可以分别指定每个属性:


 @property({type: String}) prop1 = ''; @property({type: Number}) prop2 = 0; @property({type: Boolean}) prop3 = false; @property({type: Array}) prop4 = []; @property({type: Object}) prop5 = {}; 

第二种是在一个地方指定所有内容,但是在这种情况下,如果属性具有默认值,则必须使用类构造函数方法编写该属性:


 static get properties() { return { prop1: {type: String}, prop2: {type: Number}, prop3: {type: Boolean}, prop4: {type: Array}, prop5: {type: Object} }; } constructor() { this.prop1 = ''; this.prop2 = 0; this.prop3 = false; this.prop4 = []; this.prop5 = {}; } 

用于处理lit-element属性的API相当广泛:


  • attribute属性是否可以成为可观察的属性。 如果为false ,则该属性将从观察中排除;不会为该属性创建任何吸气剂。 如果为true或缺少attribute ,则在getter中以LowerCamelCase格式指定的属性将与字符串格式的属性相对应。 如果指定了字符串,例如my-prop ,则它将在属性中与相同名称对应。
  • converter :包含有关如何将值从/转换为属性/属性的描述。 值可以是用于序列化和反序列化值的函数,也可以是具有fromAttributetoAttribute键的对象,这些键包含用于转换值的单独函数。 默认情况下,该属性包含对基本类型BooleanStringNumberObjectArray 。 转换规则在此处列出。
  • type :指示此属性将包含的基本类型之一。 它用作转换器有关该属性应包含的类型的“提示”。
  • 反射 :指示属性是否应该与属性关联( true )并根据typeconverter的规则进行更改。
  • hasChanged :每个属性都有它,包含一个函数,该函数确定旧值和新值之间是否存在变化,并分别返回Boolean 。 如果为true ,它将开始更新项目。
  • noAccessor :此属性接受Boolean ,默认为false 。 它禁止为从类中访问它们的每个属性生成获取器和设置器。 这不会取消转换。

让我们做一个假设的例子:我们将编写一个Web组件,其中包含一个参数,该参数包含一个字符串,该单词应在屏幕上绘制,其中每个字母都大于前一个字母。


 <!-- index.html --> <ladder-of-letters letters=""></ladder-of-letters> 

 //ladder-of-letters.js import {html, LitElement, property} from 'lit-element'; class LadderOfLetters extends LitElement { @property({ type: Array, converter: { fromAttribute: (val) => { // console.log('in fromAttribute', val); return val.split(''); } }, hasChanged: (value, oldValue) => { if(value === undefined || oldValue === undefined) { return false; } // console.log('in hasChanged', value, oldValue.join('')); return value !== oldValue; }, reflect: true }) letters = []; changeLetter() { this.letters = ['','','','','']; } render() { // console.log('in render', this.letters); //    ,    //        return html` <div>${this.letters.map((i, idx) => html`<span style="font-size: ${idx + 2}em">${i}</span>`)}</div> // @click     ,     //   'click'    <button @click=${this.changeLetter}>  ''</button> `; } } customElements.define('ladder-of-letters', LadderOfLetters); 

最后我们得到:



单击该按钮时,该属性已更改,这首先导致了检查,然后将其发送以进行重画。



并使用reflect我们还可以看到html的变化



如果使用此Web组件外部的代码更改此属性,我们还将导致Web组件重绘。


现在考虑组件的样式。 我们有2种样式样式元素:


  1. 通过将样式标签添加到render方法进行样式

     render() { return html` <style> p { color: green; } </style> <p>Hello World</p> `; } 


  2. 通过静态吸气剂styles

     import {html, LitElement, css} from 'lit-element'; class MyElement extends LitElement { static get styles() { return [ css` p { color: red; } ` ]; } render() { return html` <p>Hello World</p> `; } } customElements.define('my-element', MyElement); 


结果,我们发现没有创建具有样式的标签,而是根据规范将其写入( >= Chrome 73 )在元素的Shadow DOM 。 这可以提高大量元素的性能,因为 在注册新组件时,他已经知道他的样式决定了哪些属性;不需要每次都注册和重新计算它们。




此外,如果不支持此规范,则会在组件中创建常规style标签。




另外,请不要忘记,通过这种方式,我们还可以分隔将在页面上添加和计算的样式。 例如,要不是在CSS中而是在JS中使用媒体查询,并仅实现所需的样式,例如(这是百搭的,但必须如此):


 static get styles() { const mobileStyle = css`p { color: red; }`; const desktopStyle = css`p { color: green; }`; return [ window.matchMedia("(min-width: 400px)").matches ? desktopStyle : mobileStyle ]; } 

因此,如果用户登录到屏幕宽度大于400px的设备,我们将看到此信息。



这是用户访问宽度小于400像素的设备访问的站点。



我的看法:当用户在移动设备上工作时,突然遇到一台屏幕宽度为1920像素的成熟显示器时,几乎没有合适的案例。 延迟加载组件。 结果,我们通过快速的组件渲染获得了非常优化的前端。 唯一的问题是支持困难。


现在,我建议熟悉照明元件的生命周期方法:


  • render() :使用lit-html实现DOM元素的描述。 理想情况下, render函数是仅使用元素当前属性的纯函数。 render()方法由update()函数调用。
  • shouldUpdate(changedProperties) :在更改属性或调用requestUpdate()时,有必要控制更新和呈现时实现。 changedProperties函数的参数是一个Map其中包含更改后的属性的键。 默认情况下,此方法始终返回true ,但是可以更改方法的逻辑以控制组件的更新。
  • performUpdate() :用于控制更新时间,例如与调度程序集成。
  • update(changedProperties) :此方法调用render() 。 它还根据属性的值更新元素的属性。 在此方法内设置属性不会导致其他更新。
  • firstUpdated(changedProperties) :在第一次对DOM元素进行更新之后(在update updated()调用之前updated()调用。 此方法对于捕获指向您需要直接使用的可视化静态节点的链接很有用,例如,在updated()
  • Updated(changedProperties) :每当更新和显示项目的DOM时调用。 通过DOM API更新后执行任务的实现,例如,专注于元素。
  • requestUpdate(名称,oldValue) :调用项目的异步更新请求。 当需要根据不是由设置属性引起的某些状态更新项目时,应调用此方法。
  • createRenderRoot() :默认情况下为该项目创建一个影子根。 如果不需要使用Shadow DOM,则该方法应返回this

元素如何更新:


  • 该属性被赋予一个新值。
  • 如果hasChanged(value, oldValue)返回false ,则不会更新该项目。 否则,通过调用requestUpdate()计划更新。
  • requestUpdate() :在微任务之后(在事件循环结束时且在下一次重绘之前更新元素。
  • performUpdate() :更新正在进行中,并继续使用其余更新API。
  • shouldUpdate(changedProperties) :如果返回true更新将继续。
  • firstUpdated(changedProperties) :第一次更新项目时调用,紧接在调用update()之前。
  • update(changedProperties) :更新项目。 用此方法更改属性不会引起其他更新。
    • render() :返回一个lit-html模板,用于在DOM中渲染元素。 用此方法更改属性不会引起其他更新。

  • Updated(changedProperties) :每当更新项目时调用。

要了解组件生命周期的所有细微差别,建议您查阅文档


在工作中,我有一个关于Adobe Experience Manager(AEM)的项目,其创作过程是用户可以将组件拖放到页面上,并且根据AEM意识形态,该组件包含一个script标签,该script标签包含实现该组件的逻辑所需的一切。 但是实际上,这种方法引起了很多阻塞资源,并且在该系统的前端实施中遇到了困难。 为了实现前端,选择了Web组件作为不更改服务器端呈现的方法(他做得很好),并采用一种新的,渐进的,按位的方法丰富了旧的实现。 我认为,有几种方法可以实现为此系统的Web组件的加载:收集捆绑包(它可能会变得很大)或将其分解成块(很多小文件,需要动态加载),或者使用当前方法在每个脚本中嵌入脚本在服务器端呈现的组件(我真的不想返回到此)。 我认为,第一个和第三个选择不是一个选择。 第二,您需要一个动态的引导加载程序,如模板中所示。 但是对于“方框”中的发光元素,则未提供。 轻型元素开发人员曾尝试创建一个动态加载程序 ,但这是一个实验,不建议在生产中使用它。 同样来自精简元素开发人员的Web组件规范存储库中存在一个问题提议建议向规范添加基于页面上html标记为Web组件动态加载必要js的功能。 而且,我认为,这种本机工具是一个很好的主意,它使您可以创建Web组件的初始化点,并将其简单地添加到站点的所有页面中。


为了与PolymerLabs成员动态地动态加载带元素的Web组件,开发了可拆分元素 。 这是一个实验性的解决方案。 它的工作方式如下:


  • 要创建SplitElement,请在两个模块中编写两个元素定义。
  • 其中之一是存根,它定义了元素的加载部分:通常是名称和属性。 必须使用存根定义属性,以便lit-element可以及时生成可观察的属性以调用customElements.define()
  • 存根还必须具有返回实现类的静态异步加载方法。
  • 另一个类是“实现”,其中包含其他所有内容。
  • SplitElement构造函数加载实现类并运行upgrade()

存根示例:


 import {SplitElement, property} from '../split-element.js'; export class MyElement extends SplitElement { // MyElement    load   //      connectedCallback()   static async load() { //        //      MyElement return (await import('./my-element-impl.js')).MyElementImpl; } //      //   - @property() message: string; } customElements.define('my-element', MyElement); 

实施示例:


 import {MyElement} from './my-element.js'; import {html} from '../split-element.js'; // MyElementImpl  render    - export class MyElementImpl extends MyElement { render() { return html` <h1>I've been upgraded</h1> My message is ${this.message}. `; } } 

ES6上的SplitElement示例:


 import {LitElement, html} from 'lit-element'; export * from 'lit-element'; //    LitElement  SplitElement //       export class SplitElement extends LitElement { static load; static _resolveLoaded; static _rejectLoaded; static _loadedPromise; static implClass; static loaded() { if (!this.hasOwnProperty('_loadedPromise')) { this._loadedPromise = new Promise((resolve, reject) => { this._resolveLoaded = resolve; this._rejectLoaded = reject; }); } return this._loadedPromise; } //      - //      static _upgrade(element, klass) { SplitElement._upgradingElement = element; Object.setPrototypeOf(element, klass.prototype); new klass(); SplitElement._upgradingElement = undefined; element.requestUpdate(); if (element.isConnected) { element.connectedCallback(); } } static _upgradingElement; constructor() { if (SplitElement._upgradingElement !== undefined) { return SplitElement._upgradingElement; } super(); const ctor = this.constructor; if (ctor.hasOwnProperty('implClass')) { //   ,   ctor._upgrade(this, ctor.implClass); } else { //    if (typeof ctor.load !== 'function') { throw new Error('A SplitElement must have a static `load` method'); } (async () => { ctor.implClass = await ctor.load(); ctor._upgrade(this, ctor.implClass); })(); } } //       render() { return html``; } } 

如果仍在使用上面汇总建议的程序集,请确保将babel设置为能够处理动态导入


 npm install @babel/plugin-syntax-dynamic-import 

并在.babelrc设置中添加


 { "plugins": ["@babel/plugin-syntax-dynamic-import"] } 

在这里,我举了一个延迟加载Web组件的小例子: https : //github.com/malay76a/elbrus-split-litelement-web-components


我尝试应用动态加载Web组件的方法,得出以下结论:该工具非常有效,您需要将Web组件的所有定义收集在一个文件中,并通过大块单独连接组件本身的描述。 如果没有http2,此方法将不起作用,因为 形成了大量描述组件的小文件。 根据原子设计原理,必须在主体中确定原子的导入,但是主体必须已经作为独立的组件连接。 瓶颈之一是用户将在浏览器中收到许多用户元素的定义,这些定义将在浏览器中以一种或另一种方式初始化,并且将确定初始状态。 这样的解决方案是多余的。 组件加载程序的简单解决方案之一是以下算法:


  1. 加载所需的实用程序,
  2. 加载polyfill,
  3. 从轻型DOM组装自定义元素:
    1. 选择标记名称中包含连字符的所有DOM元素
    2. 列表被过滤,并且列表由前几个元素组成。
  4. :
    1. Intersection Observer,
    2. +- 100px import.
    1. 3 shadowDOM,
    2. , shadowDOM , , import JS.


- lit-element open-wc.org . webpack rollup, - storybook, IDE.


:


  1. Let's Build Web Components! Part 5: LitElement
  2. Web Component Essentials
  3. A night experimenting with Lit-HTML…
  4. LitElement To Do App
  5. LitElement app tutorial part 1: Getting started
  6. LitElement tutorial part 2: Templating, properties, and events
  7. LitElement tutorial part 3: State management with Redux
  8. LitElement tutorial part 4: Navigation and code splitting
  9. LitElement tutorial part 5: PWA and offline
  10. Lit-html workshop
  11. Awesome lit-html

Source: https://habr.com/ru/post/zh-CN445438/


All Articles