写在前面
没错,又是一个新的前端框架,hyperapp
非常的小,仅仅1kb
,当然学习起来也是非常的简单,可以说是1分钟入门。声明式:HyperApp 的设计基于Elm Architecture
(这也意味着组件更多的是纯函数),支持自定义标签以及虚拟DOM。下面先来看下怎么使用:
hello world
import { h, app } from 'hyperapp';
app({
state: {
count: 0
},
view: (state, actions) => (
<main>
<h2>{state.count}</h2>
<button onclick={actions.down}>-</button>
<button onclick={actions.up}>+</button>
</main>
),
actions: {
down: state => ({
count: state.count - 1
}),
up: state => ({
count: state.count + 1
})
}
});复制代码
这样就完成了一个Counter
,基本由state
,view
,actions
构成:
state
: 与react
中的如出一辙,state
的改变会引起重新渲染view
: 相当于react
中的render
actions
: 对state
进行改变
h
相当于react
的createElement
,来看下h
接收的参数:
tag
: 标签名,或者一个函数,传入函数也就意味着无状态组件data
: 相当于react
中的props
children
: 子节点
需要注意的一点是,hyperapp
并不支持boolean
类型,对于boolean
类型会忽略,使用时注意将其转化为string类型,如:
<h3>Test {true}</h3> // Test
<h3>Test {String(true)}</h3> // Test true复制代码
至于为什么?可以参见源码
生命周期
下面来看一下其生命周期,对于hyperapp
的整个运行过程,可以参见下图:
load
:相当于react
的componentWillMount
update
:相当于react
的componentWillUpdate
render
:调用view
函数之前调用action
:调用actions
之前,一般用来进行log
resolve
:调用actions
之后,对于一个异步操作来说,actions
返回一个promise
,生命周期resolve
来处理,返回一个函数update => result.then(update)
,即框架内部调用update
来更新state
,重新渲染
具体代码可以参考:
// 生命周期: action -> actions[key] -> resolve
// 异步请求需要利用resolve
emit('action', { name: name, data: data });
var result = emit('resolve', action(appState, appActions, data));
return typeof result === 'function' ? result(update) : update(result);复制代码
对于每一个节点来说,有着三个特殊的属性:
oncreate
:相当于componentDidMount
onupdate
:相当于componentDidUpdate
onremove
:与componentWillUnMount
类似,需要注意的是,加入有了这个属性,那么当节点需要被移除时,也不会被移除,需要自己来从dom
中移除,这样设计是为了便于做一些淡入淡出等效果,具体源码可以参见这里,更多的使用方式以及讨论可以参见这里
三个属性均为函数,接收一个参数,就是这个节点
自定义组件
通过上面,基本上可以了解hyperapp
的基本写法,下面来看一下如何自定义组件:
“木偶”组件
const Header = ({ title, caption }) => (
<header>
<h1>
{title}
<small>{caption}</small>
</h1>
</header>
);
// 使用
<Header title="hyperapp-example" caption="demo" />复制代码
无状态组件的写法与react
基本一致,hyperapp
官方给出的自定义组件的方式仅仅有这种,但是所有的组件都要是无状态的???答案当然是否定的,如何实现“智能组件”是一个问题:
“智能”组件
利用app方法实现
我们通常的期望业务组件具有一些基本的功能,比如数据获取展现这种:
const Header = app({
state: {
caption: 'loading'
},
view(state, actions) {
return (
<header>{state.caption}</header>
);
},
actions: {
fetchData(state) {
return new Promise((resolve) => {
// 模拟fetch数据
setTimeout(() => {
state.caption = 'ok';
resolve(state);
}, 1000);
});
}
},
events: {
load(state, actions) {
actions.fetchData(state);
},
resolve(state, actions, result) {
if (result && typeof result.then === 'function') {
return update => result.then(update);
}
}
}
});
export default Header;复制代码
按照如下方式使用:
import Header from './Header';
...
state: {
count: 0
},
view: (state, actions) => (
<main>
<Header />
<h2>{state.count}</h2>
</main>
),
...复制代码
打开页面,从ui来看已经实现组件封装,但是这种是一种”曲线“的实现方式,为什么说它是不正规,可以观察其dom
层级,可能与我们理解和期望的并不相同。我们期望得到的层级是:
body
main
header
h2复制代码
但是事实上得到的层级为:
body
header
main
h2复制代码
至于为什么会产生这种情况,需要看一下源码:
app
做了什么?
// app接收一个对象
function app(props) {
...
// appRoot 就是需要挂载到的根节点
var appRoot = props.root || document.body
...
// 注意此处,下文会用到
return emit;
...
// 利用raf调用render渲染ui
function render(cb) {
element = patch(
appRoot,
...
);
}
...
function patch(parent, ...) {
if (oldNode == null) {
// 第一次渲染,将节点插入到appRoot中
// 只要是第一次挂载,element为null
element = parent.insertBefore(createElement(node, isSVG), element);
}
...
}
}复制代码
所以说将Header
组件挂载的原因并不是我们通过jsx
写出了这层结构,而是在import
的时候,就已经将其挂载到了document.body
下,main
在挂载到document.body
时,被插入到子节点的末尾。
<Header />
去哪儿了?
<Header />
就这样消失了,先来看下h
,就像在react
把jsx
翻译为createElement
,hyperapp
的jsx
会被翻译为如下形式:
h(tagName, props, children)复制代码
来简单的看下h
的实现:
function h(tag, data) {
// 根据后续参数,生成children
while (stack.length) {
if (Array.isArray((node = stack.pop()))) {
// 处理传入的child为数组
for (i = node.length; i--; ) {
stack.push(node[i]);
}
}
...
}
...
return typeof tag === 'string' ? {
tag: tag,
data: data || {},
children: children
} : tag(data, children);
}复制代码
可以得出的是,tag
接收函数传入,比如木偶组件,tag
就是一个函数,但是对于<Header />
来说,tag
为app
函数返回的emit
:
function emit(name, data) {
// 一个不常见的写法,这个写法会返回data
return (
(appEvents[name] || []).map(function(cb) {
var result = cb(appState, appActions, data);
if (result != null) {
data = result;
}
}),
data
);
}复制代码
基于目前这两点,可以得出:
<Header />
被转为了,h(emit, null)
h
返回的就是children
,也就是一个[]
- 由于
<Header />
作为子节点,会再次被h
整理一次,参照h
对数组的处理,可以得出[]
直接就被忽略掉了 - 需要
render
的节点的子节点中根本就没有<Header/>
的出现
这种实现方式可以说是非常的不好,局限性也很大,想想可不可以利用其他方法实现:
利用oncreate
实现
// 改进Header组件
const Header = (root) => app({
root,
...同上
});
// 改进引入方式
view: (state, actions) => (
<main>
<div oncreate={(e) => Header(e)}></div>
<h2>{state.count}</h2>
</main>
),复制代码
这种方式,利用了oncreate
方法,挂载后,载入组件(可以考虑通过代码分割将组件异步加载)
“木偶”组件+mixins
hyperapp
支持传入mixins
,既然天然的支持这个,那么将一个组件进行两方面分割:
view
,利用“木偶组件”实现feature
,利用mixins
实现
组件定义:
export const HeaderView = ({ text }) => (
<header>{text}</header>
);
export const HeaderMixins = () => ({
state: // 同上
actions: // 同上
events: // 同上
});复制代码
使用方式:
import { HeaderView, HeaderMixins } from './HeaderView';
...
state: {
count: 0
},
view: (state, actions) => (
<main>
<HeaderView text={state.caption} />
<h2>{state.count}</h2>
</main>
),
mixins: [
HeaderMixins()
]
...复制代码
mixins
会将其属性与本身进行一个并操作,可以理解为Object.assign(key, mixins[key])
,对于events
来说,为一个典型的发布/订阅模式,events
的某一种类型对应一个数组,emit
时会将其全部执行。本人认为利用这种方式可以实现出一个比较符合框架本意的”智能“组件,但是仍然有些问题,就是state
,在使用这个组件时不得不去看一下组件内部的state
叫什么名字,而且容易造成同名state
冲突的情况。
写在最后
总体来说,hyperapp
是一个小而美的框架,值得我们来折腾一下,以上均为本人理解,如有错误还请指出,不胜感激~
一个硬广
我所在团队(工作地点在北京)求大量前端(社招 or 实习),有意者可发简历至:zp139505@alibaba-inc.com