小而美的框架—hyperapp

寫在前面

沒錯,又是一個新的前端框架,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
        })
    }
});

(完整demo能夠參見這裏
這樣就完成了一個Counter,基本由stateviewactions構成:node

  • state: 與react中的一模一樣,state的改變會引發從新渲染
  • view: 至關於react中的render
  • actions: 對state進行改變

h至關於reactcreateElement,來看下h接收的參數:react

  • tag: 標籤名,或者一個函數,傳入函數也就意味着無狀態組件
  • data: 至關於react中的props
  • children: 子節點

須要注意的一點是,hyperapp並不支持boolean類型,對於boolean類型會忽略,使用時注意將其轉化爲string類型,如:git

<h3>Test {true}</h3>             // Test
<h3>Test {String(true)}</h3>     // Test true

至於爲何?能夠參見源碼github

生命週期

下面來看一下其生命週期,對於hyperapp的整個運行過程,能夠參見下圖:數組

圖片描述

  • load:至關於reactcomponentWillMount
  • update:至關於reactcomponentWillUpdate
  • 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);

對於每個節點來講,有着三個特殊的屬性:promise

  • oncreate:至關於componentDidMount
  • onupdate:至關於componentDidUpdate
  • onremove:與componentWillUnMount相似,須要注意的是,加入有了這個屬性,那麼當節點須要被移除時,也不會被移除,須要本身來從dom中移除,這樣設計是爲了便於作一些淡入淡出等效果,具體源碼能夠參見這裏,更多的使用方式以及討論能夠參見這裏

三個屬性均爲函數,接收一個參數,就是這個節點前端框架

自定義組件

經過上面,基本上能夠了解hyperapp的基本寫法,下面來看一下如何自定義組件:app

「木偶」組件

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,就像在reactjsx翻譯爲createElementhyperappjsx會被翻譯爲以下形式:

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 />來講,tagapp函數返回的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

相關文章
相關標籤/搜索