mantou132 / gem

💎 Lightweight WebApp development library using custom elements
https://gemjs.org
MIT License
19 stars 2 forks source link

[gem] Update docs #182

Closed mantou132 closed 3 months ago

mantou132 commented 4 months ago

esm.sh v136 上线就要发布 v2,因为例子中的装饰器都是使用 v1,例外需要更新:

mantou132 commented 4 months ago

v2 介绍

经过漫长的开发实践,Gem 终于开始迈入了 v2,Gem 以让用户简单的方式编写自定义元素为宗旨进行了此次迭代。下面将介绍 v2 的一些重大更新。

装饰器

v2 使用 ES 装饰器代替了以前的 TS 装饰器,并且将 GemElement.constructor 的参数用装饰器代替:

@customElement('my-element')
+@aria({ foucusable: true, role: 'button' })
+@shadow()
+@asnyc()
class MyElement extends GemElement {
-  constructor() {
-    super({ focusable: true, isAsync: true, isLight: false });
-    this.internals.role = 'button';
-  }
}

使用装饰器具有更好的可扩展性,另外也降低了代码复杂度。基于同样的目的,还添加了 @effect @memo 等装饰器让你编写更简洁的自定义元素:

@customElement("my-element")
class MyElement extends GemElement {
  @attribute name: string;

  #content: string;

  @memo((myElement) => [myElemnt.name])
  #caleContent() {
    this.#content = this.name;
  }

  @effect((myElement) => [myElement.name])
  #fetchData() {
    // request
  }
}

[!WARNING] 未来 Gem 可能会弃用生命周期回调函数,全面使用装饰器代替

内部状态

v1 使用特定的字段 state 来表示元素内部状态,并使用 this.setState 来更新状态,在 v2 中,可以使用任意字段,因为定义状态的同时定义了更新方法:

@customElement("my-element")
class MyElement extends GemElement {
  #state = createState({ a: true })

  render() {
    this.#state({ a: false });
    console.log(this.#state.a);
  }
}

[!NOTE] v2 支持在任意地方更新状态,这在 v1 中将造成死循环

默认使用 Light DOM

Gem 使用 Shadow DOM 的一个理由是样式隔离性,他让用户可以直接编写“模块化”的 CSS,但是使用 Shadow DOM 编写 WebApp 也有一些缺点:

如果不是写需要高度封装的自定义元素(例如 UI 库),使用 Light DOM 是更合适的选择。现在,CSS 规范带来了 @scope,所以 Gem 充分利用 @scope 并默认使用 Light DOM,并且同样具备“模块化”(v1 不支持 Light DOM 样式“模块化”),下面的例子中,div 选择器将只应用在 <my-element> 的内容上:

const styles = createCSSSheet(css`
  :scope {
    display: block;
  }
  div {
    color: red;
  }
`);

@customElement('my-element')
@adoptedStyle(styles)
class MyElement extends GemElement {}

[!NOTE] 就像开头的例子,如果想要使用 Shadow DOM,需要添加 @shadow,并将 :scope 替换成 :host

主题增强

v1 只支持全局主题,v2 支持范围主题,并且支持主题覆盖:

// 全局主题将自动添加到 `document`
const [theme] = useTheme({ textColor: '#eee' });

const [scopedTheme] = useScopedTheme({ scopeTextColor: '#333' });

const [overrideTheme] = overrideTheme(theme, { textColor: '#eff' })

const styles = createCSSSheet(css`
  :scope {
    color: ${theme.textColor};
    background: ${scopedTheme.scopeTextColor};
  }
`);

@customElement('my-element')
@adoptedStyle(styles)
@adoptedStyle(scopedTheme)
@adoptedStyle(overrideTheme)
class MyElement extends GemElement {}

此外,得益于相对颜色语法,主题中以 Color 结尾的颜色直接支持使用“重量”(类似字重)调节亮度:theme.textColor500,这是一个比原 textColor 稍亮的颜色。

一起创造更好的 Gem

希望 Gem 能以卓越的设计成为创建自定义元素的首选方案,如果你有任何建议和想法,请创建 Issue

mantou132 commented 3 months ago

从 React 迁移到 Gem

React 是一个非常优秀的 UI 构建库,其生态中也有很多优秀的工具,Gem 在许多地方都有借鉴,目的是打造一个基于原生、无需编译、易于使用的 WebApp 开发框架。

从编写组件到编写自定义元素

先来看一个简单的 React 组件,这里的函数名称可以作为标签名在其他组件中使用,该组件的属性使用 IProps 标记,最后返回组件的渲染内容:

interface IProps {
  name: string;
  data?: Record<string, string>;
}

function MyComponent(props: IProps) {
  const str = JSON.stringify(props.data);

  return (
    <div>
      {props.name}
      <pre>{str}</pre>
    </div>
  );
}

在 Gem 中使用 Classes 定义自定义元素,并且必须使用 @customElement 注册元素,这让元素能在模板中使用;使用类字段来定义元素属性,例如 @attribute 等装饰器来比较属性为反应性,这让字段具有上面 React 组件中 IProps 中属性类似的作用;最后定义一个 render 方法,该方法最终返回元素内容模板,它和 React 组件返回的内容相当,只不过模板使用 JavaScript 模板字符串而非 JSX,正是因为这样,Gem 才不需要编译,能直接运行在 Vanilla JavaScript 中。

@customElement("my-element")
class MyElement extends GemElement {
  @attribute name: string;
  @property data?: Record<string, string>;

  get str() {
    return JSON.stringify(this.data);
  }

  render() {
    return html`
      <div>
        ${this.name}
        <pre>${this.str}</pre>
      </div>
    `;
  }
}

一般情况下 React 组件没有那么简单,组件很可能有副作用,或者一些重计算需要记忆化,这些需求在 React 中使用 Hooks,例如将上面 React 组件中的序列化结果记忆化,并在挂载后打印日志:

function MyComponent(props: IProps) {
  userEffect(() => {
    console.log("mounted!");
  }, []);

  const str = React.useMemo(() => {
    return JSON.stringify(props.data);
  }, [props.data]);

  return (
    <div>
      {props.name}
      <pre>{str}</pre>
    </div>
  );
}

在 Gem 中,是通过装饰器装饰函数来完成中:

@customElement("my-element")
class MyElement extends GemElement {
  @attribute name: string;
  @property data?: Record<string, string>;

  @effect(() => [])
  log() {
    console.log("mounted!");
  }

  // 注意:不能从 `this` 访问 `data`
  @memo((e) => [e.data])
  get str() {
    return JSON.stringify(this.data);
  }

  render() {
    return html`
      <div>
        ${this.name}
        <pre>${this.str}</pre>
      </div>
    `;
  }
}

总的来说 Gem 写的自定义组件要比 React 组件复杂,带来的好处是可以在 Vanilla JavaScript 中使用。

从 CSS Modules 迁移到 Gem

编写一个组件绕不开样式,在 React 中,为了让样式模块化,通常使用 CSS Modules 或者 CSS in JS 方案如 Styled Components,来看看在 React 组件中使用 CSS Modules:

.title {
  font-size: medium;
}
import styles from "./styles.css";

function MyComponent() {
  return <div className={styles.title}></div>;
}

在 Gem 中,需要手动创建对象并应用到元素上(应该自动应用?):

const styles = createCSSStyle({
  title: `
    font-size: medium;
  `,
});

@customElement("my-element")
@adoptedStyle(styles)
class MyElement extends GemElement {
  render() {
    return html`<div class=${styles.title}></div>`;
  }
}

其他工具迁移到 Gem