HTM - JSX 的替代品?还是另一种选择?

Preact Omi
hyperscript tagged markup demo hyperscript tagged markup demo

htm 全称是 Hyperscript Tagged Markup,是一款与 JSX 语法类似的东西,相比 JSX 它最大的优点是:

  • 不需要编译器
  • 直接在现代浏览器中运行,只要你的浏览器支持 Tagged templates 就行

所以,你可以直接在 react、preact 或者 omi 中使用并且直接在浏览器中运行,不需要任何编译。它利用了 Tagged templates 和浏览器自带的 HTML parser。

极小的尺寸

  • 直接在浏览器中使用只有 700 字节,1KB 都不到
  • 在 preact 中使用只有 500 字节
  • 如果使用 babel-plugin-htm 只需要 0 字节

语法 - 像 JSX 也像 lit-html

htm 是受 lit-html 启发,但是包含了 JSX 里的这些特性:

  • 剩余扩展: <div ...${props}>
  • 标签自关闭: <div />
  • 动态标签名: <${tagName}> ( tagName 是元素的引用)
  • 布尔属性: <div draggable />

对 JSX 的改进

htm 确确实实地基于 JSX 之上做了大量改进,比如下面这些特性是 JSX 所没有的:

  • 不需要编译器,直接在浏览器中运行
  • HTML 的可选分号的方式: <div class=foo>
  • HTML 的自关闭: <img src=${url}>
  • 可选的关闭标签: <section><h1>this is the whole template!
  • 组件关闭标签: <${Footer}>footer content<//>
  • 支持 HTML 注释: <div><!-- don't delete this! --></div>
  • 安装 lit-html VSCode extension 语法高亮

项目状态

HTM最初的目标是在Preact周围创建一个包装器,使用它在浏览器中不受干扰。我想使用虚拟DOM,但我不想用构建工具,直接使用ES模块。

这意味着要放弃JSX,最接近的替代方案是 Tagged templates。所以,我写了这个库来修补两者之间的差异。事实证明,该技术是框架无关的,因此它应该与大多数虚拟 DOM 库一起工作。

安装

htm 发布到了 npm, 也可以访问 unpkg.com 的 CDN:

  1. npm i htm

从unpkg获取:

  1. import htm from 'https://unpkg.com/htm?module'
  2. const html = htm.bind(React.createElement);
  1. // just want htm + preact in a single file? there's a highly-optimized version of that:
  2. import { html, render } from 'https://unpkg.com/htm/preact/standalone.mjs'

使用指南

既然 htm 是一个通用的库,我们需要告诉它怎么“编译”我们的模板。

目标应该是形式 h(tag, props, ...children) (hyperscript), 的函数,并且可以返回任何东西。

  1. // 这是我们的 h 函数。现在,它只返回一个描述对象。
  2. function h(tag, props, ...children) {
  3. return { tag, props, children };
  4. }

为了使用那个 h 函数,我们需要通过绑定htm到我们的h函数来创建我们自己的 HTML 标签函数:

  1. import htm from 'htm';
  2. const html = htm.bind(h);

现在我们有一个html模板标签,可以用来生成上面创建的格式的对象,比如:

  1. import htm from 'htm';
  2. function h(tag, props, ...children) {
  3. return { tag, props, children };
  4. }
  5. const html = htm.bind(h);
  6. console.log( html`<h1 id=hello>Hello world!</h1>` );
  7. // {
  8. // tag: 'h1',
  9. // props: { id: 'hello' },
  10. // children: ['Hello world!']
  11. // }

举个例子

好奇地想看看这一切是什么样子的?这是一个工作应用程序!

它是单个HTML文件,没有构建或工具。你可以用Nano编辑它。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <title>htm Demo</title>
  4. <script type="module">
  5. import { html, Component, render } from 'https://unpkg.com/htm/preact/standalone.mjs';
  6. class App extends Component {
  7. addTodo() {
  8. const { todos = [] } = this.state;
  9. this.setState({ todos: todos.concat(`Item ${todos.length}`) });
  10. }
  11. render({ page }, { todos = [] }) {
  12. return html`
  13. <div class="app">
  14. <${Header} name="ToDo's (${page})" />
  15. <ul>
  16. ${todos.map(todo => html`
  17. <li>${todo}</li>
  18. `)}
  19. </ul>
  20. <button onClick=${() => this.addTodo()}>Add Todo</button>
  21. <${Footer}>footer content here<//>
  22. </div>
  23. `;
  24. }
  25. }
  26. const Header = ({ name }) => html`<h1>${name} List</h1>`
  27. const Footer = props => html`<footer ...${props} />`
  28. render(html`<${App} page="All" />`, document.body);
  29. </script>
  30. </html>

这是一个Preact 线上版本.

那真是太好了?注意,只有一个导入-这里我们只使用了 import 与 Preact 集成,因为它更容易导入和更小。

同样的示例在没有预构建版本的情况下运行良好,只需使用两个导入:

  1. import { h, Component, render } from 'preact';
  2. import htm from 'htm';
  3. const html = htm.bind(h);
  4. render(html`<${App} page="All" />`, document.body);

其他使用方式

因为htm被设计成满足JSX的相同需求,所以您可以使用JSX的任何地方使用它。

使用 vhtml 生成 HTML:

  1. import htm from 'htm';
  2. import vhtml from 'vhtml';
  3. const html = htm.bind(vhtml);
  4. console.log( html`<h1 id=hello>Hello world!</h1>` );
  5. // '<h1 id="hello">Hello world!</h1>'

Webpack configuration via jsxobj: (details here)

  1. import htm from 'htm';
  2. import jsxobj from 'jsxobj';
  3. const html = htm.bind(jsxobj);
  4. console.log(html`
  5. <webpack watch mode=production>
  6. <entry path="src/index.js" />
  7. </webpack>
  8. `);
  9. // {
  10. // watch: true,
  11. // mode: 'production',
  12. // entry: {
  13. // path: 'src/index.js'
  14. // }
  15. // }

omi-html

在 omi 中使用 htm

→ 在线例子

Usage of omi-html

  1. import { define, render, WeElement } from 'omi'
  2. import 'omi-html'
  3. define('my-counter', class extends WeElement {
  4. static observe = true
  5. data = {
  6. count: 1
  7. }
  8. sub = () => {
  9. this.data.count--
  10. }
  11. add = () => {
  12. this.data.count++
  13. }
  14. render() {
  15. return html`
  16. <div>
  17. <button onClick=${this.sub}>-</button>
  18. <span>${this.data.count}</span>
  19. <button onClick=${this.add}>+</button>
  20. </div>`
  21. }
  22. })
  23. render(html`<my-counter />`, 'body')

直接运行在浏览器

  1. <script src="https://unpkg.com/omi"></script>
  2. <script src="https://unpkg.com/omi-html"></script>
  3. <script>
  4. const { define, WeElement, render } = Omi
  5. define('my-counter', class extends WeElement {
  6. install() {
  7. this.constructor.observe = true
  8. this.data.count = 1
  9. this.sub = this.sub.bind(this)
  10. this.add = this.add.bind(this)
  11. }
  12. sub() {
  13. this.data.count--
  14. }
  15. add() {
  16. this.data.count++
  17. }
  18. render() {
  19. return html`
  20. <div>
  21. <button onClick=${this.sub}>-</button>
  22. <span>${this.data.count}</span>
  23. <button onClick=${this.add}>+</button>
  24. </div>
  25. `}
  26. })
  27. render(html`<my-counter />`, 'body')
  28. </script>