65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使用装饰器创建简单的 UI 库

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.67/5 (3投票s)

2021年11月28日

CPOL

8分钟阅读

viewsIcon

9906

downloadIcon

40

在本文中,我想分享一些我在使用TypeScript装饰器构建UI库过程中获得的经验。

概念

以下代码揭示了我脑海中的概念。它代表了一个简单的Web组件,该组件根据输入显示不同大小的不同表情符号。

@element('my-smiley, `<span></span>`)
class SmileyElement extends BaseElement {
 
    @input()    
    type: string = 'happy';
 
    @input()    
    size = 'small' | 'medium' | 'large' = 'medium';
 
    @query('span')
    spanEl;
 
    onChanges(changes) {
        // TODO: perform DOM operations
    }
}

首先,我爱装饰器!我希望你也一样。我想尽可能多地利用它们来实现可重用的业务逻辑。您可以在上面的片段中看到,我们应用了几个装饰器,如 element 、 input 和 query 。

element 装饰器将类转换为Web组件。input 装饰器顾名思义,用于将属性标记为输入。query 装饰器用于在访问应用属性时自动查询并返回子元素。我们可以用装饰器做更多有趣的事情,比如用一个自动将事件绑定到函数的装饰器?是的,我们可以!暂时保持简单。请查看tiny github存储库以参考更多装饰器。

另一个需要注意的重要事项是, SmileyElement 继承自基类 BaseElement 。为了使装饰器正常工作,我们需要做一些配置工作,而且我们还有其他工作要做……例如渲染传入的模板、用于操作DOM的辅助方法等。最重要的是,要将一个类注册为Web组件,它应该继承自 HTMLElement 或内置元素之一,而 BaseElement 继承自 HTMLElement 。

基类还提供了一些生命周期钩子,供组件拦截和执行操作。正如您所看到的,有一个 onChanges 方法,每次输入发生变化时都会调用它,您需要在该方法中执行DOM操作。由于我们没有那些酷炫的数据绑定,所以我们需要手动执行DOM更新。别担心,基类提供了许多辅助方法,使该过程更简单、更有效,并具有绝对的控制权。

好了,让我们设置项目,看看我们如何首先构建这些装饰器,然后是基类。

设置项目

选择您喜欢的编辑器(我喜欢WebStorm),然后创建一个名为“base-element”的新项目。我们需要TypeScript和Webpack来启动开发。首先,在终端中运行以下命令来初始化“package.json”。

npm init

系统会问您一系列问题,如项目名称、许可证等。随意输入详细信息,创建package.json文件后,运行以下命令来安装开发依赖项。

npm i typescript ts-loader webpack webpack-cli webpack-dev-server --save-dev

要配置TypeScript,您需要创建一个“tsconfig.json”文件。创建它并将以下内容粘贴到其中。需要注意的重要一点是 experimentalDecorators 标志,它应该设置为true才能使装饰器工作。此外,我们应该在 lib 属性中包含“es2018”、“dom”和“dom.iterable”包。

{
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "dist",
    "skipLibCheck": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "forceConsistentCasingInFileNames": true,
    "downlevelIteration": true,
    "module": "commonjs",
    "noEmitOnError": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noUnusedLocals": false,
    "experimentalDecorators": true,
    "noImplicitOverride": false,
    "noImplicitAny": false,
    "pretty": true,
    "sourceMap": true,
    "strict": true,
    "strictNullChecks": false,
    "target": "es2020",
    "incremental": true,
    "newLine": "LF",
    "lib": ["es2018", "dom", "dom.iterable"]
  },
  "files": ["dev.ts"],
  "include": [
    "lib/**/*.ts"
  ]
}

为了测试我们的 SmileyElement ,我们需要一个“index.html”文件,当然还需要一个Web服务器来启动它。创建“index.html”文件并用以下内容填充。不要忘记对“app.js”的脚本引用,这很重要。

<html lang="en">
<head>
 <title>Simplifying Creating Web Components Using TypeScript Decorators</title>
 <meta charset="utf-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 <link href="https://fonts.googleapis.com/css2?family=Pacifico&display=swap" rel="stylesheet">
 <link href="https://fonts.googleapis.com/css2?family=Comfortaa:wght@300&family=Pacifico&display=swap" rel="stylesheet">
 <style>
   body {
       margin: 0 auto;
       display: flex;
       text-align: center;
       justify-content: center;
       flex-direction: column;
       background-color: #f9f9f9;
       font-family: 'Comfortaa', cursive;
   }
 
   h1 {
       font-family: 'Pacifico', cursive;
   }
 
   pre {
       font-family: 'courier';
       color: gray;
       margin: 2rem 0;
   }
 
   footer {
       color: gray;
       font-size: 0.6rem;
   }
 </style>
</head>
<body>
<h1>My life of emotions</h1>
<main>
 <div class="app-container"></div>
<pre>
<my-smiley type="happy"></my-smiley>
</pre>
   <footer>
     Demo of building UI components using native browser technologies leveraging typescript decorators.<br> Please look into <a target="_blank" href="https://github.com/vjai/tiny">tiny</a> library for real-world development of UI components.
   </footer>
</main>
<script src="app.js"></script>
</body>
</html>

创建webpack文件(webpack.config.js)以运行开发服务器,这样我们就可以在 SmileyComponent 准备好后对其进行测试。

const path = require('path');
 
module.exports = {
 mode: 'development',
 entry: { app: './dev.ts' },
 module: {
   rules: [
     {
       test: /\.js[x]?$/,
       exclude: /(node_modules)/,
       use: {
         loader: 'babel-loader'
       }
     },
     {
       test: /\.ts$/,
       exclude: /(node_modules)/,
       use: {
         loader: 'ts-loader'
       }
     }
   ]
 },
 resolve: {
   modules: [path.resolve(__dirname), 'node_modules'],
   extensions: ['.ts', '.js', '.jsx', '.json']
 },
 devServer: {
   static: {
     directory: path.resolve(__dirname)
   }
 },
 devtool: 'source-map'
};

最后,在“package.json”的“scripts”部分添加以下命令来运行和构建项目。

"scripts": {
 "start": "webpack-dev-server --config webpack.config.js",
 "build": "tsc --build tsconfig.json"
}

呼……我们的项目设置完成了。让我们去创建装饰器。

创建装饰器

创建一个名为“lib”的新文件夹,用于存放我们所有的装饰器、基类和其他组件构建相关的东西。

装饰器有助于为类、方法或属性添加自定义元数据。这就是一个典型的类装饰器的样子,

export function decoratorName(...options): ClassDecorator {
  return (target: any) => {
  };
}

本质上它是一个返回函数的函数,是的,有点像闭包!我们将在这里使用装饰器来指定有关组件的元数据信息,如选择器名称、模板、输入、查询访问器等。

好了,首先让我们创建一个名为 ElementMetadata 的类来存储组件信息,如选择器名称、模板、输入和其他内容。在“lib”文件夹下创建一个名为“element.metadata.ts”的新文件,并将以下类放入其中。

export class ElementMetadata {
 name: string = null;
 tpl: string = null;
 accessors = new Map<string, { selector: string; }>();
 inputs = new Set<{property: string; attribute: boolean; dataType: AttributeValueDataType; }>();
}

accessors 属性存储用于查询的属性的详细信息,inputs 用于存储输入属性的详细信息。

在“lib”文件夹下创建一个名为“decorators.ts”的文件。让我们开始创建 element 装饰器。

element 装饰器接受选择器名称和一个可选的HTML模板,并将其存储在元数据中。此外,它还通过调用原生的 customElements.define 方法将其注册为Web组件。

/**
 * Registers a class into web component.
 * @param name selector name.
 * @param [tpl] html template string.
 */
export function element(
 name: string,
 tpl?: string
): ClassDecorator {
 return (target: any) => {
   if (window.customElements.get(name)) {
     throw new Error(`Already an element is registered with the name ${name}`);
   }
 
   window.customElements.define(name, target);
   setMeta(target, Object.assign(getMeta(target), { name, tpl }));
 };
}
 
function getMeta(target: Function) {
 return target[ELEMENT_META_KEY] || new ElementMetadata();
}
 
function setMeta(target: Function, meta: ElementMetadata) {
 target[ELEMENT_META_KEY] = meta;
}

元数据存储在组件类的静态属性中。我们也可以使用 reflect-metadata 库来存储元数据,但我们在这里不这样做是为了避免生产依赖,也是为了保持我们的包大小最小。 ELEMENT_META_KEY 是一个常量,您可以创建一个单独的常量文件来存放它,或者直接将其放在“element.metadata.ts”文件中。

export const ELEMENT_META_KEY = '__ELEMENT_INFO__'

让我们看看其他两个装饰器。它们很简单,它们所做的只是接受传入的参数并将其存储在元数据中。

/**
 * Marks the applied property as an input.
 * @param [attribute] True to bind the property with the attribute.
 * @param [dataType] The data type of the attribute.
 */
export function input(attribute = false, dataType = AttributeValueDataType.STRING): PropertyDecorator {
 return (target: object, property: string | symbol) => {
   const metadata = getMeta(target.constructor),
     { inputs } = metadata;
 
   if (inputs.has(property)) {
     throw new Error(
       `Input decorator is already applied for the property ${
         property as string
       }`
     );
   }
 
   inputs.add({ property, attribute, dataType });
   setMeta(target.constructor, metadata);
 };
}
 
/**
 * Marks the applied property as a CSS selector.
 * @param selector CSS selector.
 */
export function query(selector: string): PropertyDecorator {
 return (target: object, property: string | symbol) => {
   const metadata = getMeta(target.constructor),
     { accessors } = metadata;
 
   if (accessors.has(property)) {
     throw new Error(
       `Already a CSS selector is assigned for the property ${
         property as string
       }`
     );
   }
 
   accessors.set(property, { selector });
   setMeta(target.constructor, metadata);
 };
}

input 装饰器接受几个属性: attribute 和 dataType 。attribute 属性表示是否应初始从DOM元素属性读取输入属性值并保持同步。dataType 告诉我们属性的类型,当从属性读取值时,我们需要正确地解析它。

query 装饰器接受一个参数,该参数就是选择器。我们的装饰器已准备就绪,现在是时候编写基类了。

基类

要将类转换为Web组件,我们不仅必须通过调用 customElements.define 方法来注册它,而且它还必须继承自原生的 HTMLElement 或任何其他内置的HTML元素。我们将把基类继承自 HTMLElement 。

创建一个名为“base-element.ts”的文件,并将以下类放入其中。

class BaseElement extends HTMLElement {
} 

我们在这里需要做很多工作。首先,我们需要读取模板并渲染组件。其次,我们需要覆盖应用了装饰器的那些属性的getter和setter,以便组件能够检测到何时输入发生任何变化以刷新UI,或者在访问任何被query装饰器装饰的属性时查询并返回子元素。

总而言之,我们需要做的主要事情如下。

  1. 从元数据中读取模板并渲染它。
  2. 覆盖装饰器属性的getter和setter。
  3. 每次输入发生变化时,将该变化加入队列,并触发一个定时器,在下一个tick调用onChanges方法。
  4. 创建辅助方法来执行DOM操作,如添加/删除CSS类、添加/删除样式等。

好了,让我们先看看如何从元数据中读取模板并进行渲染。

/**
 * Base class for all custom web components.
 */
class BaseElement extends HTMLElement {
 
  /**
   * The component metadata.
   */
  private readonly _metadata: ElementMetadata = null;
 
  /**
   * True when the component is rendered.
   */
  private _rendered: boolean = false;
 
  protected constructor() {
    super();
    // Read the metadata from the constructor.
    this._metadata = this.constructor[ELEMENT_META_KEY];
  }
 
  /**
   * Native life-cycle hook.
   */
  protected connectedCallback() {
    // Call the render if the component is not rendered.
    if (!this._rendered) {
      this.render();
      this._rendered = true;
    }
  }
 
  /**
   * Reads the template from metadata and renders the template.
   */
  protected render() {
    if (!this._metadata.tpl) {
      return;
    }
 
    const template = document.createElement('template');
    template.innerHTML = this._metadata.tpl;
    this.appendChild(template.content.cloneNode(true));
  }
}

需要注意的重要一点是,我们已经挂载到了Web组件的原生 connectedCallback 生命周期处理程序上,以渲染模板。我们还创建了一个标志 _rendered 来确保只渲染一次。

接下来,我们需要覆盖装饰器属性的getter和setter。让我们先看看如何实现 query 装饰器。query 装饰器将应用属性转换为CSS选择器,这意味着每次访问该属性时,它都会自动查询并返回匹配的DOM元素。为了实现这一点,我们需要覆盖属性的getter,以查询并返回与装饰器中CSS选择器匹配的DOM元素。

要在任何对象中覆盖属性的getter/setter,可以使用 Object.defineProperty 方法。

Object.defineProperty(obj, propName, {
  get() {
    // Override
  },
  set(value) {
    // Override
  },
});

这是我们更新后的代码。

export type UIElement = string | BaseElement | HTMLElement;
 
class BaseElement extends HTMLElement {
 
  ...
 
  /**
   * True when the component is initialized (applied the decorators and refreshed with the initial inputs state).
   */
  private _initialized: boolean = false;
 
  /**
   * Overrides the getter of the properties decorated with `query` decorator to return the dom elements
   * on accessing the properties.
   */
  private _applyAccessors() {
    [...this._metadata.accessors].forEach(
      ([prop, { selector }]) => {
        Object.defineProperty(this, prop, {
          get() {
            return this.$(selector);
          }
        });
      }
    );
  }
 
  private _element(el: UIElement): UIElement {
    if (arguments.length === 0 || el === 'self') {
      return this;
    }
 
    if (el instanceof HTMLElement) {
      return el;
    }
 
    return this.$(el as string);
  }
   
  protected connectedCallback() {
    if (!this._rendered) {
      this.render();
      this._rendered = true;
    }
 
    if (!this._initialized) {
      this._applyAccessors();
      this._initialized = true;
    }
  }
 
  /**
   * Returns the DOM element for the passed selector.
   * @param selector CSS selector.
   * @param [element] Optional parent element. If not passed the element is queried inside the current component.
   */
  $<T extends HTMLElement>(selector: string, element: UIElement = this): T {
    const el = this._element(element) as HTMLElement;
 
    if (!el) {
      return <any>this;
    }
 
    if (el === this) {
      return this.querySelector(selector);
    }
 
    if (el instanceof BaseElement) {
      return el.$(selector);
    }
 
    return el.querySelector(selector) as T;
  }
}

我们在 _applyAccessors 方法中所做的是遍历每个应用属性,并覆盖getter,以查询并返回与装饰器中传递的选择器匹配的子DOM元素。 $ 方法返回组件的子元素或从传递的父元素(element)匹配选择器。

让我们看看如何实现 input 装饰器。这有点复杂。每次输入属性更改时,我们都需要捕获更改,将其推送到内部队列,并触发一个定时器,在下一个tick使用 setTimeout 来更新UI。不仅如此,如果 attribute 标志为 true ,那么我们就必须从DOM属性中读取初始值,并使用 dataType 正确地解析它。

function isVoid(val) {
  return val === null || val === undefined;
}
 
export type ElementChanges = Map<string, { oldValue: any; newValue: any }>;
 
 
class BaseElement extends HTMLElement {
 
  ...
 
  /**
   * Changes of inputs.
   */
  private _changes = new Map<string, { oldValue: any; newValue: any }>();
 
  /**
   * The current state of properties.
   */
  private _props = new Map<string, any>();
 
  /**
   * Timer to refresh the UI from the changes map.
   */
  private _updateTimer: any = null;
 
  /**
   * Overrides the getter and setter of the properties decorated with `input` decorator.
   * The getter is overridden to return the current state from the `_props` property and the setter is
   * overridden to track the change and push to the `changes` map eventually triggering the update timer to
   * refresh the UI in the next tick.
   */
  private _applyInputs() {
    [...this._metadata.inputs].forEach(({ property, attribute, dataType }) => {
      let value;
 
      // If attribute is passed as `true` then read the initial value of the property from
      // DOM attribute parse it based on the data type and store it in the `_props`.
      if (attribute) {
        let attrValue: any = this.getAttr(property);
 
        if (attrValue !== null) {
          if (
            dataType === AttributeValueDataType.NUMBER &&
            !isNaN(parseFloat(attrValue))
          ) {
            attrValue = parseFloat(attrValue);
          } else if (dataType === AttributeValueDataType.BOOLEAN) {
            attrValue = attrValue === 'true' || attrValue === '';
          }
 
          value = attrValue;
        } else {
          value = this[property];
        }
 
        if (!isVoid(value) && value !== attrValue) {
          this.setAttr({ [property]: value });
        }
      } else {
        value = this[property];
      }
 
      this._pushChange(property, value);
      this._props.set(property, value);
 
      const target = this;
 
      // Override the getter and setter.
      // On setting a new value push the change and trigger the timer.
      Object.defineProperty(this, property, {
        get() {
          return target._props.get(property);
        },
        set(value) {
          if (attribute) {
            if (value) {
              target.setAttr({
                [property]: !isVoid(value) ? value.toString() : value
              });
            } else {
              target.removeAttr(property);
            }
          }
 
          target._pushChange(property, value);
          target._props.set(property, value);
          target._initialized && target._triggerUpdate();
        }
      });
    });
  }
 
  /**
   * Checks if there is really a change if yes then push it to the `_changes` map.
   * @param prop
   * @param value
   */
  private _pushChange(prop: string, value: any) {
    if (!this._changes.has(prop)) {
      this._changes.set(prop, { oldValue: this[prop], newValue: value });
      return;
    }
 
    const { oldValue, newValue } = this._changes.get(prop);
    if (oldValue === newValue && this._initialized) {
      this._changes.delete(prop);
      return;
    }
 
    this._changes.set(prop, { oldValue, newValue: value });
  }
 
  /**
   * Kicks the UI update timer.
   */
  private _triggerUpdate() {
    if (this._updateTimer) {
      return;
    }
 
    this._updateTimer = setTimeout(() => this.refresh(), 0);
  }
 
  protected connectedCallback() {
    if (!this._rendered) {
      this.render();
      this._rendered = true;
    }
 
    if (!this._initialized) {
      this._applyAccessors();
      this._applyInputs();
      this._initialized = true;
    }
 
    this.refresh();
  }
   
  /**
   * Invoked whenever there is a change in inputs.
   * @param changes
   */
  protected onChanges(changes) {}
 
  protected refresh() {
    this.onChanges(this._changes);
    this._changes.clear();
    this._updateTimer && window.clearTimeout(this._updateTimer);
    this._updateTimer = null;
  }
}

_changes 属性用于存储输入更改的队列。_props 存储这些属性的最新值。这就是我们为了让装饰器工作所需要做的全部工作。

让我们添加一些帮助操作DOM的方法。

export interface KeyValue {
 [key: string]: any;
}
 
import { isVoid } from "./util";
import { KeyValue, UIElement } from "./base-element";
 
class BaseElement extends HTMLElement {
 
  ...
 
  /**
   * Adds single or multiple css classes.
   * @param classes
   * @param [element]
   */
  addClass(
    classes: string | Array<string>,
    element: UIElement = this
  ): BaseElement {
    const el = this._element(element) as HTMLElement;
 
    if (!el) {
      return this;
    }
 
    el.classList.add(...(Array.isArray(classes) ? classes : [classes]));
    return this;
  }
 
  /**
   * Removes single or multiple css classes.
   * @param classes
   * @param [element]
   */
  removeClass(
    classes: string | Array<string>,
    element: UIElement = this
  ): BaseElement {
    const el = this._element(element) as HTMLElement;
 
    if (!el) {
      return this;
    }
 
    el.classList.remove(...(Array.isArray(classes) ? classes : [classes]));
    return this;
  }
 
  /**
   * Applies passed styles.
   * @param styles
   * @param [element]
   */
  addStyle(styles: KeyValue, element: UIElement = this): BaseElement {
    const el = this._element(element) as HTMLElement;
 
    if (!el) {
      return this;
    }
 
    Object.entries(styles).forEach(([k, v]) => {
      if (k.startsWith('--')) {
        el.style.setProperty(k, v);
      } else if (v === null) {
        this.removeStyles(k, el);
      } else {
        el.style[k] = v;
      }
    });
    return this;
  }
 
  /**
   * Removes passed styles.
   * @param styles
   * @param [element]
   */
  removeStyles(
    styles: string | Array<string>,
    element: UIElement = this
  ): BaseElement {
    const el = this._element(element) as HTMLElement;
 
    if (!el) {
      return this;
    }
 
    (Array.isArray(styles) ? styles : [styles]).forEach(
      style => (el.style[style] = null)
    );
    return this;
  }
 
  /**
   * Returns passed attribute's value.
   * @param name
   * @param [element]
   */
  getAttr(name: string, element: UIElement = this): string {
    const el = this._element(element) as HTMLElement;
 
    if (!el) {
      return '';
    }
 
    return el.getAttribute(name);
  }
 
  /**
   * Sets the attributes.
   * @param obj
   * @param [element]
   */
  setAttr(obj: KeyValue, element: UIElement = this): BaseElement {
    const el = this._element(element) as HTMLElement;
 
    if (!el) {
      return this;
    }
 
    Object.entries(obj).forEach(([key, value]) =>
      isVoid(value) ? this.removeAttr(key) : el.setAttribute(key, value)
    );
    return this;
  }
 
  /**
   * Removes the passed attributes.
   * @param attrs
   * @param [element]
   */
  removeAttr(
    attrs: string | Array<string>,
    element: UIElement = this
  ): BaseElement {
    const el = this._element(element) as HTMLElement;
 
    if (!el) {
      return this;
    }
 
    (Array.isArray(attrs) ? attrs : [attrs]).forEach(attr =>
      el.removeAttribute(attr)
    );
 
    return this;
  }
 
  /**
   * Updates the inner html.
   * @param html
   * @param [element]
   */
  updateHtml(html: string, element: UIElement = this): BaseElement {
    const el = this._element(element) as HTMLElement;
 
    if (!el) {
      return this;
    }
 
    el.innerHTML = !isVoid(html) ? html : '';
    return this;
  }
}

您可以看到有一个受保护的 onChanges 方法,它接受一个未实现的 changes 参数,并且派生类应该覆盖此方法,使用辅助方法执行DOM操作。

我们的基类几乎准备好了,作为最后的润色,我们暴露了两个额外的生命周期钩子,派生类可以在元素连接到DOM或从DOM断开连接时使用它们来添加自定义逻辑。

/**
* Native life-cycle hook.
*/
protected connectedCallback() {
 ...
 
 // Call our custom life-cycle hook method.
 this.onConnected();
 
 // Refresh the UI with the initial input property values.
 this.refresh();
}
 
/**
* Native life-cycle hook.
*/
protected disconnectedCallback() {
 this.onDisconnected();
}
 
/**
* Custom life-cycle hook meant to be overridden by derived class if needed.
*/
protected onConnected() {}
 
/**
* Custom life-cycle hook meant to be overridden by derived class if needed.
*/
protected onDisconnected() {}

完成Smiley Element

我们已经完成了创建自定义Web组件的所有工作。下面是我们之前看到的 SmileyElement 的完整代码,它根据传入的输入显示不同的表情符号。

import { BaseElement, element, ElementChanges, input, query } from './lib';
 
enum sizeRemMap {
 'small' = 1,
 'medium' = 2,
 'large' = 3,
}
 
enum smileyMap {
 'happy'= '😀',
 'lol' = '😂',
 'angel' = '😇',
 'hero' = '😎',
 'sad' = '😞',
 'cry' = '😢',
 'romantic' = '😍',
 'sleep' = '😴',
 'nerd' = '🤓'
}
 
@element('my-smiley', `<span></span>`)
class SmileyElement extends BaseElement {
 
 @input(true)
 type: string = 'happy';
 
 @input(true)
 size: 'small' | 'medium' | 'large' = 'medium';
 
 @query('span')
 spanEl;
 
 onChanges(changes: ElementChanges) {
   if (changes.has('type')) {
     this.updateHtml(smileyMap[this.type || 'happy'], this.spanEl);
   }
 
   if (changes.has('size')) {
     this.addStyle({ 'font-size': `${sizeRemMap[this.size]}rem`}, this.spanEl);
   }
 }
}

我们在 input 装饰器中将 attribute 参数设置为true,以便将这些值作为HTML属性传递。您可以看到 onChanges 方法,我们在其中检查类型或大小等输入参数是否已更改,并相应地更新DOM。

让我们创建一个小的App组件来渲染多个笑脸。

@element('my-app', `
 <my-smiley type="happy"></my-smiley>
 <my-smiley type="lol"></my-smiley>
 <my-smiley type="angel"></my-smiley>
 <my-smiley type="hero"></my-smiley>
 <my-smiley type="sad"></my-smiley>
 <my-smiley type="cry"></my-smiley>
 <my-smiley type="romantic"></my-smiley>
 <my-smiley type="sleep"></my-smiley>
 <my-smiley type="nerd"></my-smiley>
`)
class App extends BaseElement {
}

最后,为了渲染App组件,我们需要将一个处理程序连接到文档的 DOMContentLoaded 事件。

document.addEventListener('DOMContentLoaded', () => {
 const app = document.createElement('my-app');
 document.querySelector('.app-container').appendChild(app);
});

最后,我们完成了开发。让我们启动“index.html”文件,并通过运行以下命令来查看一切是否正常工作。

npm start

如果一切顺利,您应该会看到下面的屏幕,

接下来呢?

这是创建我们自己的UI库的一个小尝试,使用纯粹的本地概念和利用装饰器来构建组件。对于实际使用,请查看 tiny 项目,该项目提供了更多的装饰器和大量的DOM操作辅助方法。请给仓库点个赞,并随时fork。您还可以尝试一些新的东西,比如如何在模板中构建简单的数据绑定,这样我们就无需进行手动DOM操作。去试试吧!创建自己的东西并在实际应用中使用它会非常有趣。

源代码: https://github.com/vjai/base-element

Tiny: https://github.com/vjai/tiny

 

© . All rights reserved.