GOGOGO
接下来的工作就是开发组件相关的内容,毕业也4年了,总不能一事无成,就准备再度捡起web-component写一个组件库,梦想总是要有的,虽然不知道能坚持写几个。
至于为什么不再用stencil,纯纯地只是厌倦了vdom。今天,必须开历史的倒车。搜了搜juejin,发现和lit相关的文章也比较少,毕竟,也没有多少人会去研究web-component。
拒绝工程化,拒绝虚拟dom,还html一个朗朗晴天,今天就从我做起。
有兴趣的同学可以看看这篇
START
开发框架选好了,UI框架就选vant了,因为之后也要开发的,写的时候,也是一边看着vant的源码和文档,一边在这里写组件。直接就选了BUTTON,毕竟每个框架里,BUTTON永远都在第一个位置,只是单纯的我,没有想到,BUTTON还包含了loading,icon,然后 icon 里又包含了badge。再加上,每天磨磨蹭蹭,写到今天才写完,先上图。
因为都是照着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。
问题
问题确实挺多的,主要是因为我水平有限。
现有问题
- 页面闪烁问题,在遇到有slot的情况下,组件没编译完成时,slot中的内容先会出现在页面中。这个问题,我自己想的办法就是先隐藏组件,在组件挂载完成后再显示出来。但总觉得有些不对劲。
- 宿主问题。在vue中,我们给组件添加class或者style后,他会同时添加在我们组件内部的宿主元素中,但是在web-component中,因为多了一层自定义标签,我们无法把class加到内部,如果有这种需求,需要再套一层attribute
- shadowDom在使用了它的样式隔离之后,肯定也要接受他的缺点。由于shdowDom能够继承的属性有限,在需要自定义样式的时候,就显得尤为困难。可以参照vant中configure组件,或者直接不使用shadowDom。
以后会遇到的问题
- 事件系统。就像之前说的,因为现在的组件比较简单,没有遇到大量的事件交互,后面的组件肯定会遇到事件传递的问题。
- 项目工程化。
- 文档编写。
- 单元测试。
- 服务端渲染。
- 兼容性问题。
虽然不知道要做到什么时候,但是至少有了目标,可以去解决一个个问题。对lit或者web-component有兴趣的同学可以一起交流一起进步。
最后,有赞前端招人哇,hc多,双休,每周2天6点下班~