组件化是前端发展的一个重要方向,它一方面提高开发效率,另一方面降低维护成本。主流的 Vue.js、React 及其延伸的 Ant Design、uniapp、Taro 等都是组件框架。

Web Components 是一组 Web 原生 API 的总称,允许我们创建可重用的自定义组件,并在我们 Web 应用中像使用原生 HTML 标签一样使用。目前已经很多前端框架/库支持 Web Components

本文将带大家回顾 Web Components 核心 API,并从 0 到 1 实现一个基于 Web Components API 开发的业务组件库。

最终效果:https://blog.pingan8787.com/exe-components/demo.html 仓库地址:https://github.com/pingan8787/Learn-Web-Components

一、回顾 Web Components

在前端发展历史中,从刚开始重复业务到处复制相同代码,到 Web Components 的出现,我们使用原生 HTML 标签的自定义组件,复用组件代码,提高开发效率。通过 Web Components 创建的组件,几乎可以使用在任何前端框架中。

1. 核心 API 回顾

Web Components 由 3 个核心 API 组成:

  • Custom elements(自定义元素):用来让我们定义自定义元素及其行为,对外提供组件的标签;
  • Shadow DOM(影子 DOM):用来封装组件内部的结构,避免与外部冲突;
  • HTML templates(HTML 模版):包括 <template><slot> 元素,让我们可以定义各种组件的 HTML 模版,然后被复用到其他地方,使用过 Vue/React 等框架的同学应该会很熟悉。

    另外,还有 HTML imports,但目前已废弃,所以不具体介绍,其作用是用来控制组件的依赖加载。

image.png

2. 入门示例

接下来通过下面简单示例快速了解一下如何创建一个简单 Web Components 组件

  • 使用组件

    1. <!DOCTYPE html>
    2. <html lang="en">
    3. <head>
    4. <script src="./index.js" defer></script>
    5. </head>
    6. <body>
    7. <h1>custom-element-start</h1>
    8. <custom-element-start></custom-element-start>
    9. </body>
    10. </html>
  • 定义组件 ```javascript /**

    • 使用 CustomElementRegistry.define() 方法用来注册一个 custom element
    • 参数如下:
      • 元素名称,符合 DOMString 规范,名称不能是单个单词,且必须用短横线隔开
      • 元素行为,必须是一个类
      • 继承元素,可选配置,一个包含 extends 属性的配置对象,指定创建的元素继承自哪个内置元素,可以继承任何内置元素。 */

class CustomElementStart extends HTMLElement { constructor(){ super(); this.render(); } render(){ const shadow = this.attachShadow({mode: ‘open’}); const text = document.createElement(“span”); text.textContent = ‘Hi Custom Element!’; text.style = ‘color: red’; shadow.append(text); } }

customElements.define(‘custom-element-start’, CustomElementStart)

  1. 上面代码主要做 3 件事:
  2. 1. 实现组件类
  3. 通过实现 `CustomElementStart` 类来定义组件。
  4. 2. 定义组件
  5. 将组件的标签和组件类作为参数,通过 `customElements.define` 方法定义组件。
  6. 3. 使用组件
  7. 导入组件后,跟使用普通 HTML 标签一样直接使用自定义组件 `<custom-element-start></custom-element-start>`
  8. 随后浏览器访问 `index.html` 可以看到下面内容:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/186051/1640184692263-0c67f2f5-8289-4793-bf4f-62996c0ca015.png#clientId=ud1fa7408-3cf2-4&from=paste&height=317&id=u80381831&margin=%5Bobject%20Object%5D&name=image.png&originHeight=634&originWidth=1330&originalType=binary&ratio=1&size=78049&status=done&style=none&taskId=u9b432022-fe68-42a4-b8d1-fcdcbb7ffe2&width=665)
  9. <a name="gbqMv"></a>
  10. ### 3. 兼容性介绍
  11. [MDN | Web Components](https://developer.mozilla.org/zh-CN/docs/Web/Web_Components) 章节中介绍了其兼容性情况:
  12. > - Firefox(版本63)、ChromeOpera都默认支持Web组件。
  13. > - Safari支持许多web组件特性,但比上述浏览器少。
  14. > - Edge正在开发一个实现。
  15. 关于兼容性,可以看下图:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/186051/1638197844816-0f3f853a-c5ff-489f-8409-b3861cbd2ffe.png#clientId=ub454e772-d418-4&from=paste&height=362&id=ub9cacba7&margin=%5Bobject%20Object%5D&name=image.png&originHeight=517&originWidth=1091&originalType=binary&ratio=1&size=41722&status=done&style=none&taskId=u7f82dc72-f7fc-457a-8274-42941d23bd2&width=764.5)<br />图片来源:[https://www.webcomponents.org/](https://www.webcomponents.org/)
  16. 这个网站里面,有很多关于 Web Components 的优秀项目可以学习。
  17. <a name="poeCR"></a>
  18. ### 4. 小结
  19. 这节主要通过一个简单示例,简单回顾基础知识,详细可以阅读文档:
  20. - [使用 custom elements](https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_custom_elements)
  21. - [使用 shadow DOM](https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM)
  22. - [使用 templates and slots](https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_templates_and_slots)
  23. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/186051/1640185044772-6d0e6b1d-0dff-4934-ac8d-11297972b952.png#clientId=ud1fa7408-3cf2-4&from=paste&height=360&id=u75e5777f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=720&originWidth=1280&originalType=binary&ratio=1&size=189753&status=done&style=none&taskId=u40ebc710-88e3-490e-bb9b-e23ec920154&width=640)
  24. <a name="IXW0l"></a>
  25. ## 二、EXE-Components 组件库分析设计
  26. <a name="rEBT9"></a>
  27. ### 1. 背景介绍
  28. 假设我们需要实现一个 EXE-Components 组件库,该组件库的组件分 2 大类:
  29. 1. components 类型
  30. 以**通用简单组件**为主,如`exe-avatar`头像组件、 `exe-button`按钮组件等;
  31. 2. modules 类型
  32. 以**复杂、组合组件**为主,如`exe-user-avatar`用户头像组件(含用户信息)、`exe-attachement-list`附件列表组件等等。
  33. 详细可以看下图:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/186051/1640185209275-60f971c4-23ea-49d5-814e-00907597e053.png#clientId=ud1fa7408-3cf2-4&from=paste&height=693&id=uec938792&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1386&originWidth=1532&originalType=binary&ratio=1&size=460290&status=done&style=none&taskId=ud69a24ac-1750-42e3-80a2-ca30cff9e70&width=766)
  34. 接下来我们会基于上图进行 EXE-Components 组件库设计和开发。
  35. <a name="C7vab"></a>
  36. ### 2. 组件库设计
  37. 在设计组件库的时候,主要需要考虑以下几点:
  38. 1. 组件命名、参数命名等规范,方便组件后续维护;
  39. 1. 组件参数定义;
  40. 1. 组件样式隔离;
  41. 当然,这几个是最基础需要考虑的点,随着实际业务的复杂,还需要考虑更多,比如:工程化相关、组件解耦、组件主题等等。
  42. 针对前面提到这 3 点,这边约定几个命名规范:
  43. 1. 组件名称以 `exe-功能名称` 进行命名,如 `exe-avatar`表示头像组件;
  44. 1. 属性参数名称以 `e-参数名称` 进行命名,如 `e-src` 表示 `src` 地址属性;
  45. 1. 事件参数名称以 `on-事件类型` 进行命名,如 `on-click`表示点击事件;
  46. <a name="CKmVh"></a>
  47. ### 3. 组件库组件设计
  48. 这边我们主要设计 `exe-avatar` `exe-button` `exe-user-avatar`三个组件,前两个为简单组件,后一个为复杂组件,其内部使用了前两个组件进行组合。这边先定义这三个组件支持的属性:<br />![EXE-Components 组件库.png](https://cdn.nlark.com/yuque/0/2021/png/186051/1640186627647-00af73e1-834e-46d4-ab5f-88d00601cbeb.png#clientId=ud1fa7408-3cf2-4&from=ui&id=u5201764b&margin=%5Bobject%20Object%5D&name=EXE-Components%20%E7%BB%84%E4%BB%B6%E5%BA%93.png&originHeight=1200&originWidth=2370&originalType=binary&ratio=1&size=286253&status=done&style=none&taskId=u82a4d538-0648-49d0-be41-a8a3514098a)
  49. > 这边属性命名看着会比较复杂,大家可以按照自己和团队的习惯进行命名。
  50. 这样我们思路就清晰很多,实现对应组件即可。
  51. <a name="MGnjz"></a>
  52. ## 三、EXE-Components 组件库准备工作
  53. 本文示例最终将对实现的组件进行**组合使用**,实现下面「**用户列表**」效果:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/186051/1640186045088-0debaa0d-c0df-4689-8c58-1b5a81cdffa1.png#clientId=ud1fa7408-3cf2-4&from=paste&height=440&id=yMoVz&margin=%5Bobject%20Object%5D&name=image.png&originHeight=880&originWidth=1056&originalType=binary&ratio=1&size=187157&status=done&style=none&taskId=u4fbda1aa-ea93-4303-a995-87618816486&width=528)<br />体验地址:[https://blog.pingan8787.com/exe-components/demo.html](https://blog.pingan8787.com/exe-components/demo.html)
  54. <a name="xkvQk"></a>
  55. ### 1. 统一开发规范
  56. 首先我们先统一开发规范,包括:
  57. 1. 目录规范
  58. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/186051/1640187041774-85d4dd08-bfb9-483d-bd9b-5b9f399e55f6.png#clientId=ud1fa7408-3cf2-4&from=paste&height=638&id=u04111bed&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1276&originWidth=1546&originalType=binary&ratio=1&size=242345&status=done&style=none&taskId=u59cd4101-b60f-4fcd-aef1-43d59fce3ac&width=773)
  59. 2. 定义组件规范
  60. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/186051/1640187273799-cf5c88bf-4757-4653-b13e-5257725208f1.png#clientId=ud1fa7408-3cf2-4&from=paste&height=218&id=ud1c5352c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=436&originWidth=1488&originalType=binary&ratio=1&size=61801&status=done&style=none&taskId=uff44f719-f674-4560-b39d-17c1119c92d&width=744)
  61. 3. 组件开发模版
  62. 组件开发模版分 `index.js`**组件入口文件**和 `template.js` **组件 HTML 模版文件**:
  63. ```javascript
  64. // index.js 模版
  65. const defaultConfig = {
  66. // 组件默认配置
  67. }
  68. const Selector = "exe-avatar"; // 组件标签名
  69. export default class EXEAvatar extends HTMLElement {
  70. shadowRoot = null;
  71. config = defaultConfig;
  72. constructor(){
  73. super();
  74. this.render(); // 统一处理组件初始化逻辑
  75. }
  76. render() {
  77. this.shadowRoot = this.attachShadow({mode: 'closed'});
  78. this.shadowRoot.innerHTML = renderTemplate(this.config);
  79. }
  80. }
  81. // 定义组件
  82. if (!customElements.get(Selector)) {
  83. customElements.define(Selector, EXEAvatar)
  84. }
  1. // template.js 模版
  2. export default config => {
  3. // 统一读取配置
  4. const { avatarWidth, avatarRadius, avatarSrc } = config;
  5. return `
  6. <style>
  7. /* CSS 内容 */
  8. </style>
  9. <div class="exe-avatar">
  10. /* HTML 内容 */
  11. </div>
  12. `
  13. }

2. 开发环境搭建和工程化处理

为了方便使用 EXE-Components 组件库,更接近实际组件库的使用,我们需要将组件库打包成一个 UMD 类型的 js 文件。这边我们使用 rollup 进行构建,最终打包成 exe-components.js 的文件,使用方式如下:

  1. <script src="./exe-components.js"></script>

接下来通过 npm init -y生成 package.json文件,然后全局安装 rollup 和 http-server(用来启动本地服务器,方便调试):

  1. npm init -y
  2. npm install --global rollup http-server

然后在 package.jsonscript 下添加 "dev""build"脚本:

  1. {
  2. // ...
  3. "scripts": {
  4. "dev": "http-server -c-1 -p 1400",
  5. "build": "rollup index.js --file exe-components.js --format iife"
  6. },
  7. }

其中:

  • "dev" 命令:通过 http-server 启动静态服务器,作为开发环境使用。添加 -c-1 参数用来禁用缓存,避免刷新页面还会有缓存,详细可以看 http-server 文档
  • "build"命令:将 index.js 作为 rollup 打包的入口文件,输出 exe-components.js 文件,并且是 iife 类型的文件。

这样就完成简单的本地开发和组件库构建的工程化配置,接下来就可以进行开发了。

四、EXE-Components 组件库开发

1. 组件库入口文件配置

前面 package.json 文件中配置的 "build" 命令,会使用根目录下 index.js 作为入口文件,并且为了方便 components 通用基础组件和 modules 通用复杂组件的引入,我们创建 3 个 index.js,创建后目录结构如下:
image.png
三个入口文件内容分别如下:

  1. // EXE-Components/index.js
  2. import './components/index.js';
  3. import './modules/index.js';
  4. // EXE-Components/components/index.js
  5. import './exe-avatar/index.js';
  6. import './exe-button/index.js';
  7. // EXE-Components/modules/index.js
  8. import './exe-attachment-list/index.js.js';
  9. import './exe-comment-footer/index.js.js';
  10. import './exe-post-list/index.js.js';
  11. import './exe-user-avatar/index.js';

2. 开发 exe-avatar 组件 index.js 文件

通过前面的分析,我们可以知道 exe-avatar组件需要支持参数:

  • e-avatar-src:头像图片地址,例如:./testAssets/images/avatar-1.png
  • e-avatar-width:头像宽度,默认和高度一致,例如:52px
  • e-button-radius:头像圆角,例如:22px,默认:50%
  • on-avatar-click:头像点击事件,默认无

接着按照之前的模版,开发入口文件 index.js

  1. // EXE-Components/components/exe-avatar/index.js
  2. import renderTemplate from './template.js';
  3. import { Shared, Utils } from '../../utils/index.js';
  4. const { getAttributes } = Shared;
  5. const { isStr, runFun } = Utils;
  6. const defaultConfig = {
  7. avatarWidth: "40px",
  8. avatarRadius: "50%",
  9. avatarSrc: "./assets/images/default_avatar.png",
  10. onAvatarClick: null,
  11. }
  12. const Selector = "exe-avatar";
  13. export default class EXEAvatar extends HTMLElement {
  14. shadowRoot = null;
  15. config = defaultConfig;
  16. constructor(){
  17. super();
  18. this.render();
  19. }
  20. render() {
  21. this.shadowRoot = this.attachShadow({mode: 'closed'});
  22. this.shadowRoot.innerHTML = renderTemplate(this.config);// 生成 HTML 模版内容
  23. }
  24. // 生命周期:当 custom element首次被插入文档DOM时,被调用。
  25. connectedCallback() {
  26. this.updateStyle();
  27. this.initEventListen();
  28. }
  29. updateStyle() {
  30. this.config = {...defaultConfig, ...getAttributes(this)};
  31. this.shadowRoot.innerHTML = renderTemplate(this.config); // 生成 HTML 模版内容
  32. }
  33. initEventListen() {
  34. const { onAvatarClick } = this.config;
  35. if(isStr(onAvatarClick)){ // 判断是否为字符串
  36. this.addEventListener('click', e => runFun(e, onAvatarClick));
  37. }
  38. }
  39. }
  40. if (!customElements.get(Selector)) {
  41. customElements.define(Selector, EXEAvatar)
  42. }

其中有几个方法是抽取出来的公用方法,大概介绍下其作用,具体可以看源码:

  • renderTemplate 方法

来自 template.js 暴露的方法,传入配置 config,来生成 HTML 模版。

  • getAttributes 方法

传入一个 HTMLElement 元素,返回该元素上所有属性键值对,其中会对 e-on- 开头的属性,分别处理成普通属性和事件属性,示例如下:

  1. // input
  2. <exe-avatar
  3. e-avatar-src="./testAssets/images/avatar-1.png"
  4. e-avatar-width="52px"
  5. e-avatar-radius="22px"
  6. on-avatar-click="avatarClick()"
  7. ></exe-avatar>
  8. // output
  9. {
  10. avatarSrc: "./testAssets/images/avatar-1.png",
  11. avatarWidth: "52px",
  12. avatarRadius: "22px",
  13. avatarClick: "avatarClick()"
  14. }
  • runFun方法

由于通过属性传递进来的方法,是个字符串,所以进行封装,传入 event 和事件名称作为参数,调用该方法,示例和上一步一样,会执行 avatarClick() 方法。

另外,Web Components 生命周期可以详细看文档:使用生命周期回调函数

3. 开发 exe-avatar 组件 template.js 文件

该文件暴露一个方法,返回组件 HTML 模版:

  1. // EXE-Components/components/exe-avatar/template.js
  2. export default config => {
  3. const { avatarWidth, avatarRadius, avatarSrc } = config;
  4. return `
  5. <style>
  6. .exe-avatar {
  7. width: ${avatarWidth};
  8. height: ${avatarWidth};
  9. display: inline-block;
  10. cursor: pointer;
  11. }
  12. .exe-avatar .img {
  13. width: 100%;
  14. height: 100%;
  15. border-radius: ${avatarRadius};
  16. border: 1px solid #efe7e7;
  17. }
  18. </style>
  19. <div class="exe-avatar">
  20. <img class="img" src="${avatarSrc}" />
  21. </div>
  22. `
  23. }

最终实现效果如下:
image.png

开发完第一个组件,我们可以简单总结一下创建和使用组件的步骤:
image.png

4. 开发 exe-button 组件

按照前面 exe-avatar组件开发思路,可以很快实现 exe-button 组件。
需要支持下面参数:

  • e-button-radius:按钮圆角,例如:8px
  • e-button-type:按钮类型,例如:default, primary, text, dashed
  • e-button-text:按钮文本,默认:打开
  • on-button-click:按钮点击事件,默认无 ```javascript // EXE-Components/components/exe-button/index.js import renderTemplate from ‘./template.js’; import { Shared, Utils } from ‘../../utils/index.js’;

const { getAttributes } = Shared; const { isStr, runFun } = Utils; const defaultConfig = { buttonRadius: “6px”, buttonPrimary: “default”, buttonText: “打开”, disableButton: false, onButtonClick: null, }

const Selector = “exe-button”;

export default class EXEButton extends HTMLElement { // 指定观察到的属性变化,attributeChangedCallback 会起作用 static get observedAttributes() { return [‘e-button-type’,’e-button-text’, ‘buttonType’, ‘buttonText’] }

  1. shadowRoot = null;
  2. config = defaultConfig;
  3. constructor(){
  4. super();
  5. this.render();
  6. }
  7. render() {
  8. this.shadowRoot = this.attachShadow({mode: 'closed'});
  9. }
  10. connectedCallback() {
  11. this.updateStyle();
  12. this.initEventListen();
  13. }
  14. attributeChangedCallback (name, oldValue, newValue) {
  15. // console.log('属性变化', name)
  16. }
  17. updateStyle() {
  18. this.config = {...defaultConfig, ...getAttributes(this)};
  19. this.shadowRoot.innerHTML = renderTemplate(this.config);
  20. }
  21. initEventListen() {
  22. const { onButtonClick } = this.config;
  23. if(isStr(onButtonClick)){
  24. const canClick = !this.disabled && !this.loading
  25. this.addEventListener('click', e => canClick && runFun(e, onButtonClick));
  26. }
  27. }
  28. get disabled () {
  29. return this.getAttribute('disabled') !== null;
  30. }
  31. get type () {
  32. return this.getAttribute('type') !== null;
  33. }
  34. get loading () {
  35. return this.getAttribute('loading') !== null;
  36. }

}

if (!customElements.get(Selector)) { customElements.define(Selector, EXEButton) }

  1. 模版定义如下:
  2. ```javascript
  3. // EXE-Components/components/exe-button/tempalte.js
  4. // 按钮边框类型
  5. const borderStyle = { solid: 'solid', dashed: 'dashed' };
  6. // 按钮类型
  7. const buttonTypeMap = {
  8. default: { textColor: '#222', bgColor: '#FFF', borderColor: '#222'},
  9. primary: { textColor: '#FFF', bgColor: '#5FCE79', borderColor: '#5FCE79'},
  10. text: { textColor: '#222', bgColor: '#FFF', borderColor: '#FFF'},
  11. }
  12. export default config => {
  13. const { buttonRadius, buttonText, buttonType } = config;
  14. const borderStyleCSS = buttonType
  15. && borderStyle[buttonType]
  16. ? borderStyle[buttonType]
  17. : borderStyle['solid'];
  18. const backgroundCSS = buttonType
  19. && buttonTypeMap[buttonType]
  20. ? buttonTypeMap[buttonType]
  21. : buttonTypeMap['default'];
  22. return `
  23. <style>
  24. .exe-button {
  25. border: 1px ${borderStyleCSS} ${backgroundCSS.borderColor};
  26. color: ${backgroundCSS.textColor};
  27. background-color: ${backgroundCSS.bgColor};
  28. font-size: 12px;
  29. text-align: center;
  30. padding: 4px 10px;
  31. border-radius: ${buttonRadius};
  32. cursor: pointer;
  33. display: inline-block;
  34. height: 28px;
  35. }
  36. :host([disabled]) .exe-button{
  37. cursor: not-allowed;
  38. pointer-events: all;
  39. border: 1px solid #D6D6D6;
  40. color: #ABABAB;
  41. background-color: #EEE;
  42. }
  43. :host([loading]) .exe-button{
  44. cursor: not-allowed;
  45. pointer-events: all;
  46. border: 1px solid #D6D6D6;
  47. color: #ABABAB;
  48. background-color: #F9F9F9;
  49. }
  50. </style>
  51. <button class="exe-button">${buttonText}</button>
  52. `
  53. }

最终效果如下:
image.png

5. 开发 exe-user-avatar 组件

该组件是将前面 exe-avatar 组件和 exe-button 组件进行组合,不仅需要支持点击事件,还需要支持插槽 slot 功能

由于是做组合,所以开发起来比较简单~先看看入口文件:

  1. // EXE-Components/modules/exe-user-avatar/index.js
  2. import renderTemplate from './template.js';
  3. import { Shared, Utils } from '../../utils/index.js';
  4. const { getAttributes } = Shared;
  5. const { isStr, runFun } = Utils;
  6. const defaultConfig = {
  7. userName: "",
  8. subName: "",
  9. disableButton: false,
  10. onAvatarClick: null,
  11. onButtonClick: null,
  12. }
  13. export default class EXEUserAvatar extends HTMLElement {
  14. shadowRoot = null;
  15. config = defaultConfig;
  16. constructor() {
  17. super();
  18. this.render();
  19. }
  20. render() {
  21. this.shadowRoot = this.attachShadow({mode: 'open'});
  22. }
  23. connectedCallback() {
  24. this.updateStyle();
  25. this.initEventListen();
  26. }
  27. initEventListen() {
  28. const { onAvatarClick } = this.config;
  29. if(isStr(onAvatarClick)){
  30. this.addEventListener('click', e => runFun(e, onAvatarClick));
  31. }
  32. }
  33. updateStyle() {
  34. this.config = {...defaultConfig, ...getAttributes(this)};
  35. this.shadowRoot.innerHTML = renderTemplate(this.config);
  36. }
  37. }
  38. if (!customElements.get('exe-user-avatar')) {
  39. customElements.define('exe-user-avatar', EXEUserAvatar)
  40. }

主要内容在 template.js 中:

  1. // EXE-Components/modules/exe-user-avatar/template.js
  2. import { Shared } from '../../utils/index.js';
  3. const { renderAttrStr } = Shared;
  4. export default config => {
  5. const {
  6. userName, avatarWidth, avatarRadius, buttonRadius,
  7. avatarSrc, buttonType = 'primary', subName, buttonText, disableButton,
  8. onAvatarClick, onButtonClick
  9. } = config;
  10. return `
  11. <style>
  12. :host{
  13. color: "green";
  14. font-size: "30px";
  15. }
  16. .exe-user-avatar {
  17. display: flex;
  18. margin: 4px 0;
  19. }
  20. .exe-user-avatar-text {
  21. font-size: 14px;
  22. flex: 1;
  23. }
  24. .exe-user-avatar-text .text {
  25. color: #666;
  26. }
  27. .exe-user-avatar-text .text span {
  28. display: -webkit-box;
  29. -webkit-box-orient: vertical;
  30. -webkit-line-clamp: 1;
  31. overflow: hidden;
  32. }
  33. exe-avatar {
  34. margin-right: 12px;
  35. width: ${avatarWidth};
  36. }
  37. exe-button {
  38. width: 60px;
  39. display: flex;
  40. justify-content: end;
  41. }
  42. </style>
  43. <div class="exe-user-avatar">
  44. <exe-avatar
  45. ${renderAttrStr({
  46. 'e-avatar-width': avatarWidth,
  47. 'e-avatar-radius': avatarRadius,
  48. 'e-avatar-src': avatarSrc,
  49. })}
  50. ></exe-avatar>
  51. <div class="exe-user-avatar-text">
  52. <div class="name">
  53. <span class="name-text">${userName}</span>
  54. <span class="user-attach">
  55. <slot name="name-slot"></slot>
  56. </span>
  57. </div>
  58. <div class="text">
  59. <span class="name">${subName}<slot name="sub-name-slot"></slot></span>
  60. </div>
  61. </div>
  62. ${
  63. !disableButton &&
  64. `<exe-button
  65. ${renderAttrStr({
  66. 'e-button-radius' : buttonRadius,
  67. 'e-button-type' : buttonType,
  68. 'e-button-text' : buttonText,
  69. 'on-avatar-click' : onAvatarClick,
  70. 'on-button-click' : onButtonClick,
  71. })}
  72. ></exe-button>`
  73. }
  74. </div>
  75. `
  76. }

其中 renderAttrStr 方法接收一个属性对象,返回其键值对字符串:

  1. // input
  2. {
  3. 'e-avatar-width': 100,
  4. 'e-avatar-radius': 50,
  5. 'e-avatar-src': './testAssets/images/avatar-1.png',
  6. }
  7. // output
  8. "e-avatar-width='100' e-avatar-radius='50' e-avatar-src='./testAssets/images/avatar-1.png' "

最终效果如下:
image.png

6. 实现一个用户列表业务

接下来我们通过一个实际业务,来看看我们组件的效果:
image.png
其实实现也很简单,根据给定数据,然后循环使用组件即可,假设有以下用户数据:

  1. const users = [
  2. {"name":"前端早早聊","desc":"帮 5000 个前端先跑 @ 前端早早聊","level":6,"avatar":"qdzzl.jpg","home":"https://juejin.cn/user/712139234347565"}
  3. {"name":"来自拉夫德鲁的码农","desc":"谁都不救我,谁都救不了我,就像我救不了任何人一样","level":2,"avatar":"lzlfdldmn.jpg","home":"https://juejin.cn/user/994371074524862"}
  4. {"name":"黑色的枫","desc":"永远怀着一颗学徒的心。。。","level":3,"avatar":"hsdf.jpg","home":"https://juejin.cn/user/2365804756348103"}
  5. {"name":"captain_p","desc":"目的地很美好,路上的风景也很好。今天增长见识了吗","level":2,"avatar":"cap.jpg","home":"https://juejin.cn/user/2532902235026439"}
  6. {"name":"CUGGZ","desc":"文章联系微信授权转载。微信:CUG-GZ,添加好友一起学习~","level":5,"avatar":"cuggz.jpg","home":"https://juejin.cn/user/3544481220801815"}
  7. {"name":"政采云前端团队","desc":"政采云前端 ZooTeam 团队,不掺水的原创。 团队站点:https://zoo.team","level":6,"avatar":"zcy.jpg","home":"https://juejin.cn/user/3456520257288974"}
  8. ]

我们就可以通过简单 for 循环拼接 HTML 片段,然后添加到页面某个元素中:

  1. // 测试生成用户列表模版
  2. const usersTemp = () => {
  3. let temp = '', code = '';
  4. users.forEach(item => {
  5. const {name, desc, level, avatar, home} = item;
  6. temp +=
  7. `
  8. <exe-user-avatar
  9. e-user-name="${name}"
  10. e-sub-name="${desc}"
  11. e-avatar-src="./testAssets/images/users/${avatar}"
  12. e-avatar-width="36px"
  13. e-button-type="primary"
  14. e-button-text="关注"
  15. on-avatar-click="toUserHome('${home}')"
  16. on-button-click="toUserFollow('${name}')"
  17. >
  18. ${
  19. level >= 0 && `<span slot="name-slot">
  20. <span class="medal-item">(Lv${level})</span>
  21. </span>`}
  22. </exe-user-avatar>
  23. `
  24. })
  25. return temp;
  26. }
  27. document.querySelector('#app').innerHTML = usersTemp;

到这边我们就实现了一个用户列表的业务,当然实际业务可能会更加复杂,需要再优化。

五、总结

本文首先简单回顾 Web Components 核心 API,然后对组件库需求进行分析设计,再进行环境搭建和开发,内容比较多,可能没有每一点都讲到,还请大家看看我仓库的源码,有什么问题欢迎和我讨论。

写本文的几个核心目的:

  1. 当我们接到一个新任务的时候,需要从分析设计开始,再到开发,而不是盲目一上来就开始开发;
  2. 带大家一起看看如何用 Web Components 开发简单的业务组件库;
  3. 体验一下 Web Components 开发组件库有什么缺点(就是要写的东西太多了)。

最后看完本文,大家是否觉得用 Web Components 开发组件库,实在有点复杂?要写的太多了。
没关系,下一篇我将带大家一起使用 Stencil 框架开发 Web Components 标准的组件库,毕竟整个 ionic 已经是使用 Stencil 重构,Web Components 大势所趋~!

拓展阅读