初探 web-component 框架 Lit

GOGOGO

接下来的工作就是开发组件相关的内容,毕业也4年了,总不能一事无成,就准备再度捡起web-component写一个组件库,梦想总是要有的,虽然不知道能坚持写几个。

至于为什么不再用stencil,纯纯地只是厌倦了vdom。今天,必须开历史的倒车。搜了搜juejin,发现和lit相关的文章也比较少,毕竟,也没有多少人会去研究web-component。

拒绝工程化,拒绝虚拟dom,还html一个朗朗晴天,今天就从我做起。

有兴趣的同学可以看看这篇

juejin.cn/post/697655…

START

开发框架选好了,UI框架就选vant了,因为之后也要开发的,写的时候,也是一边看着vant的源码和文档,一边在这里写组件。直接就选了BUTTON,毕竟每个框架里,BUTTON永远都在第一个位置,只是单纯的我,没有想到,BUTTON还包含了loading,icon,然后 icon 里又包含了badge。再加上,每天磨磨蹭蹭,写到今天才写完,先上图。

image.png

image.png

image.png

image.png

image.png

因为都是照着vant的源码写的,css肯定是直接偷了,demo的展示也和vant的官网上是保持一致的,包括传的attribute。

编码感受

在写这些组件的过程中,总体感觉还是挺好的。文档中基本都能找到使用方法。render方法中,也找到了之前用jq写模板的感觉,逝去的青春。因为写的组件比较简单,还没到后面几个复杂组件,所以遇到的问题不是很多,后面会一起写,先贴一贴button的代码感受一下。

import { LitElement, html, css } from 'lit';
import { html as sHtml, unsafeStatic } from 'lit/static-html.js';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import '../goo-icon/index.js';
import '../goo-loading/index.js'

import { styles } from './css.js';
import { base } from '../style/base';
import { createClassName, realizeClassName, BORDER_SURROUND } from '../utils';
import { styleMap } from 'lit/directives/style-map.js';

export type ButtonType = 'default' | 'primary' | 'success' | 'warning' | 'danger';

export type ButtonSize = 'large' | 'normal' | 'small' | 'mini';

export type PositionType = 'left' | 'right';

export type LoadingType = 'spinner' | 'circular';

const nameSpace = 'van-button'
const ccn = createClassName.bind(null, nameSpace);

@customElement('goo-button')
export class GooButton extends LitElement {
  // 按钮类型
  @property({type: String})
  type: ButtonType = 'default';

  // 按钮大小
  @property({type: String})
  size: ButtonSize = 'normal';

  // 按钮文字
  @property({type: String})
  text = '';

  // 按钮颜色
  @property({type: String})
  color = '';

  // 左侧图标名称或图片链接
  @property({type: String})
  icon = '';

  // 图标类名前缀,等同于icon组件的class-prefix属性
  @property({type: String})
  iconPrefix = 'van-icon';

  // 图标展示位置,可选值为right left
  @property({type: String})
  iconPosition: PositionType = 'left';

  // 按钮根节点的HTML标签
  @property({type: String})
  tag: keyof HTMLElementTagNameMap = 'button';

  // 原生button标签的type属性
  @property({type: String})
  nativeType = 'button';

  // 是否为块级元素
  @property({type: Boolean})
  block = false;

  // 是否为朴素按钮
  @property({type: Boolean})
  plain = false;

  // 是否为方形按钮
  @property({type: Boolean})
  square = false;

  // 是否为圆形按钮
  @property({type: Boolean})
  round = false;

  // 是否禁用按钮
  @property({type: Boolean})
  disabled = false;

  // 是否使用0.5px边框
  @property({type: Boolean})
  hairline = false;

  // 是否显示为加载状态
  @property({type: Boolean})
  loading = false;

  // 加载文案
  @property({type: String, attribute: 'loading-text'})
  loadingText = '';

  // 加载图标类型,可选值为 spinner circular
  @property({type: String, attribute: 'loading-type'})
  loadingType: LoadingType = 'spinner';

  // 加载图标字体大小
  @property({type: [String, Number], attribute: 'loading-size'})
  loadingSize = '20px';

  // 加载图标颜色
  @property({type: String, attribute: 'loading-color'})
  loadingColor = '#fff';

  static override styles = css`
    ${base}
    ${styles}
  `;

  getClassMap() {
    const map = {
      type: this.type,
      size: this.size,
      block: this.block,
      round: this.round,
      plain: this.plain,
      square: this.square,
      loading: this.loading,
      disabled: this.disabled,
      hairline: this.hairline
    };

    return {
      [nameSpace]: true,
      [BORDER_SURROUND]: this.hairline,
      ...realizeClassName(map, nameSpace)
    }
  }

  getStyleMap() {
    const { color, plain, loadingColor, loading } = this;
    const style: any = {};
    if (loading && loadingColor) {
      style.color = loadingColor;
    } else if (color) {
      if (plain) {
        style.color = color;
      } else {
        style.color = 'white';
      }
    }
    if (!plain) {
      // Use background instead of backgroundColor to make linear-gradient work
      style.background = color;
    }

    // hide border when color is linear-gradient
    if (color.includes('gradient')) {
      style.border = 0;
    } else {
      style.borderColor = color;
    }

    return style;
  }

  renderIcon() {
    if (this.loading) {
      return this.renderLoadingIcon();
    }
    if (this.icon) {
      return html`
        <goo-icon
          name=${this.icon}
          classPrefix=${this.iconPrefix}
          button
        ></goo-icon>
      `
    }
    return null;
  }

  renderLoadingIcon() {
    return html`
      <goo-loading
        size=${this.loadingSize}
        type=${this.loadingType}
        color=${this.loadingColor}
      ></goo-loading>
    `;
  }

  renderText() {
    let txt;
    if (this.loading) {
      txt = this.loadingText || '';
    }
    const text = html`
      ${txt || this.text || html`<slot></slot>`}
    `;

    const style: any = {};
    const base = 'var(--van-padding-base)';
    if (this.icon || this.loading) {
      if ((this.icon && (this.iconPosition === 'left')) || this.loading) {
        style.marginLeft = base;
      } else if (this.icon && (this.iconPosition === 'right')) {
        style.marginRight = base
      }
    }

    return html`
      <span style=${styleMap(style)}>${text}</span>
    `
  }

  override render() {
    const tag = unsafeStatic(this.tag);
    return sHtml`
      <${tag}
       class=${classMap(this.getClassMap())} 
       style=${styleMap(this.getStyleMap())}
       @click=${this.clickHandle}
      >
        <div class="${ccn('content', false)}">
          ${this.iconPosition === 'left' ? this.renderIcon() : null}
          ${this.renderText()}
          ${this.iconPosition === 'right' ? this.renderIcon() : null}
        </div>
      </${tag}>
    `
  }

  clickHandle(e) {
    if (this.loading) {
      e.preventDefault();
      e.stopPropagation();
    }
  }
}
复制代码

中间发生了一个很搞笑的事情,我因为怎么进行click事件绑定,苦思冥想了好多个晚上。怎么传参,入参,怎么事件触发。今天,突然发现,这个是html原生标签,直接绑定个id,addEventListener就行了。我直接把这个归咎于是框架用久了的危害,每天@click,onClick,基本知识都记不清了。

想做这个的初衷就是,希望在写简单页面的时候,可以很方便地使用组件进行页面布局,不用引入框架,不用框架的语法,没有工程化,就是一个简简单单的页面,写一写js,html,css。

问题

问题确实挺多的,主要是因为我水平有限。

现有问题

  1. 页面闪烁问题,在遇到有slot的情况下,组件没编译完成时,slot中的内容先会出现在页面中。这个问题,我自己想的办法就是先隐藏组件,在组件挂载完成后再显示出来。但总觉得有些不对劲。
  2. 宿主问题。在vue中,我们给组件添加class或者style后,他会同时添加在我们组件内部的宿主元素中,但是在web-component中,因为多了一层自定义标签,我们无法把class加到内部,如果有这种需求,需要再套一层attribute
  3. shadowDom在使用了它的样式隔离之后,肯定也要接受他的缺点。由于shdowDom能够继承的属性有限,在需要自定义样式的时候,就显得尤为困难。可以参照vant中configure组件,或者直接不使用shadowDom。

以后会遇到的问题

  1. 事件系统。就像之前说的,因为现在的组件比较简单,没有遇到大量的事件交互,后面的组件肯定会遇到事件传递的问题。
  2. 项目工程化。
  3. 文档编写。
  4. 单元测试。
  5. 服务端渲染。
  6. 兼容性问题。

虽然不知道要做到什么时候,但是至少有了目标,可以去解决一个个问题。对lit或者web-component有兴趣的同学可以一起交流一起进步。

最后,有赞前端招人哇,hc多,双休,每周2天6点下班~

猜你喜欢

转载自juejin.im/post/7019223483376730148