一个用原生 JavaScript 编写 UI 的库,支持组件化和基于 @vue/reactivity 的响应式(目前准备去掉它,因为它体积太大了,我想到了另一种更好的更新策略),且不需要虚拟 DOM,所以整体代码非常精简。状态更新后,直接更新相应 DOM,高性能更新 UI。
在script
标签的src
属性中引入https://unpkg.com/@hopejs/hope@0.6.7/dist/index.min.js
,所有 API 都在全局变量 Hope
中。
<script src="https://unpkg.com/@hopejs/hope@0.6.7/dist/index.min.js"></script>
例如用hopejs
写一个 Hello World 是这样的:
const { div, $div, hText } = Hope
div()
hText('hello hope!')
$div()
mount(document.body)
不要怀疑,虽然上面的写法有些“古怪”,不过这可是纯原生 JavaScript !
我把 html 的版本写出来对比一下就很容易看明白:
// html 版本
<div>
hello hope!
</div>
// hopejs 版本
div()
hText('hello hope!')
$div()
怎么样,是不是很相似?这是我故意模仿 html 的语法设计的,便于使用者的理解,降低学习成本。
用 js 写 HTML 的好处是可以实现模块化,响应式,组件化开发页面,相比 HTML 更灵活。虽然有这些好处,但如何用 js 写 HTML 是个难题。可以用函数传参的形式,类似于虚拟 DOM 中的 h 函数。
// 描述一个 div 元素
h('div', props, ...children);
如上所示,单个函数的形式可以完备的描述一个 DOM 树的整体结构,但当 DOM 树比较复杂时,这种写法的可读性比较低,写起来也不方便,对习惯了 HTML 语法的初学者来说不是很友好。
于是我想到了下面的这种类似于 HTML 语法的写法。
const { div, $div, hText } = Hope
// 描述一个 div 元素
div()
hText('hello')
$div()
上面的这种写法比较类似于 HTML 的语法,有“开始标签”和“结束标签”,并且还可以在“标签”之间写其它的元素,如下所示:
// 描述一个拥有一个 span 子元素的 div 元素
div()
span()
$span()
$div()
// HTML 版本
<div>
<span>
</span>
</div>
通过上面简单的比较,可以发现这种写法像 HTML 的写法,所以对习惯 HTML 语法的初学者来说比较容易入门,在写复杂的 DOM 结构的时候也会像 HTML 一样富有层次感。以后还会支持自动缩进和语法补全(通过 vscode 插件支持)。
如何设置元素的 class 或者 DOM 的属性呢?
请看下面的例子:
// 设置 div 的 class name
div({ class: 'class-name' })
hText('hello')
$div()
// HTML 版本
<div class="class-name">
hello
</div>
可以看出,我是尽可能的模仿 HTML 的语法,便于更快的上手使用。有一点与 HTML 不同的是 hopejs 会自动判断设置的属性是 attribute 还是 property,所以如果想设置 DOM 元素的 property,也可以直接写到开始标签参数中。
例如:
// 设置 div 的 innerHTML 属性
div({ innerHTML: 'hello hope!' })
hText('hello')
$div()
// HTML 这样写是无效的,并不会设置其 DOM 元素的 innerHTML
<div innerHTML="hello hope!">
hello
</div>
如何绑定事件呢,很简单,如下所示:
// 绑定 div 的 click 事件,'on' 后面的首字母需要大写
div({ onClick: () => console.log('Say hello!') })
hText('hello')
$div()
// HTML 版本,'on' 后面的首字母不需要大写
<div onclick="hello hope!">
hello
</div>
这里要注意 onClick
中的 Click
的首字母是需要大写的,其底层使用的是 DOM 元素的 addEventListener
属性设置的。
前端的组件化已经深入人心,所以 hopejs 也支持组件化开发。hopejs 暴露的有一个生成组件的 API:defineComponent
,用该 API 可以封装自己的组件。
const { defineComponent, div, $div, hText, s } = Hope
// 接收一个函数作为参数,在该函数中书写该组件的 HTML 结构和 CSS 样式
const [com, $com] = defineComponent(({ props, slot, emit }) => {
// 直接写 “HTML",不用 return
div({ class: 'class-name' })
hText('Hello Component!')
$div()
// hopejs 暴露了一个 s 接口,s 是 style 的缩写,用来写 CSS
// 该语法也是模仿的 CSS 的语法,CSS 的版本是这样的:
// .class-name {
// width: 100px;
// height: 100px;
// }
s('.class-name', {
width: '100px',
height: '100px'
})
})
// 然后就像普通标签一样使用组件
com()
$com()
// 最后需要挂在到 DOM 树中
mount(document.body)
在组件中可以使用三个生命周期函数,分别是:
当组件被挂在到 DOM 树中时被触发。
当组件被卸载时触发该生命周期函数。
当组件的视图更新时触发该生命周期函数。
我比较喜欢 单向数据流
的概念,父组件与子组件通过子组件的 props 通信,子组件与父组件通过发出一个事件的方式进行通信,这样的代码更易于维护。
在 hopejs 中组件也可以发出一个事件,供用户使用的时候监听该事件,处理一些逻辑。事件是通过 emit
在组件中发出的。
const { defineComponent, div, $div, hText, s } = Hope
const [com, $com] = defineComponent(({ props, slot, emit }) => {
const handler = () => {
// 使用 emit 发出一个事件
emit('clickText', '这里可以传参数')
}
// 监听组件根元素的 click 事件
div({ onClick: handler })
hText('Hello Component!')
$div()
})
// 监听事件时注意字母的大小写,必须要 'on' 开头
com({ onClickText: (param) => console.log(param) })
$com()
// 最后需要挂在到 DOM 树中
mount(document.body)
插槽在组件中也是很重要的,可以更灵活的使用组件。现在来看一下 hopejs 中组件的插槽是如何实现的。
const { defineComponent, div, $div, hText, hSlot, s } = Hope
const [com, $com] = defineComponent(({ props, slot, emit }) => {
// 通过 slot 参数,在组件中可以获取到插入到组件中的插槽,
// 所谓插槽就是一个函数,直接在某个位置上调用即可,default
// 表示的是没有提供具体名字的插槽,如果提供了具体的名字,
// 则需要更改为那个具体的名字,如 slot.name()
div()
slot.default()
$div()
})
// 使用组件时,需要通过 hSlot 指令来指定组件的插槽
com()
hSlot(() => {
div()
hText('这里是插槽中的内容')
$div()
})
$com()
// 也可以指定插槽的名字,使用时这样用 slot.name()
com()
hSlot('name', () => {
div()
hText('这里是插槽中的内容')
$div()
})
$com()
// 最后需要挂在到 DOM 树中
mount(document.body)
有时候我们会根据某个状态值的不同,去显示不同的 DOM 结构,比如根据路由的不同渲染不同的组件。hopejs 提供了 block
API 来进行 DOM 树的结构更新。如下所示:
const { div, $div, hText, reactive, block } = Hope
const state = reactive({ show: true })
div()
// 在 block 中声明 DOM 结构与状态之间的关系,
// 当状态更新时,DOM 树结构也会自动进行更新
block(() => {
if (state.show) {
hText('show 为 true 时显示')
} else {
hText('show 为 false 时显示')
}
})
$div()
响应式在现在的前端开发中已经是不可缺少的一部分了,它极大的简化了前端页面的开发难度。来看一下 hopejs 的响应式是怎么写的。
const { div, $div, reactive } = Hope
const state = reactive({ color: 'red' })
// HTML
// 在 hopejs 中,只要把属性的值写成函数的形式,并返回,
// 当状态值更改时,对应的 UI 也会自动更新
div({style: {
color: () => state.color
}})
$div()
// 挂在到 DOM 树
mount(document.body)
可以看到,在 hopejs 中响应式需要满足两个条件,一个是响应式对象,就是用 reactive API 生成的对象,一个是属性值是一个返回状态值的函数,这两个条件缺一不可,否则就不会状态值更改时自动更新 UI。
demo1: elastic-header
demo2: markdown
demo3: todomvc