Closed mantou132 closed 3 months ago
经过漫长的开发实践,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 中将造成死循环
Gem 使用 Shadow DOM 的一个理由是样式隔离性,他让用户可以直接编写“模块化”的 CSS,但是使用 Shadow DOM 编写 WebApp 也有一些缺点:
document.activeElement
无效如果不是写需要高度封装的自定义元素(例如 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 能以卓越的设计成为创建自定义元素的首选方案,如果你有任何建议和想法,请创建 Issue。
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 中使用。
编写一个组件绕不开样式,在 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-light-route>
Gem.useStore
@mantou/gem/helper/theme
@mantou/gem/helper/i18n
@mantou/gem/helper/request
esm.sh v136 上线就要发布 v2,因为例子中的装饰器都是使用 v1,例外需要更新: