发布于 2026-01-06 12 阅读
0

使用 Hyperapp 在 JavaScript 中进行函数式编程

使用 Hyperapp 在 JavaScript 中进行函数式编程

考虑到我使用 Vue 和 React 的悠久历史,我感觉自己有点像个叛徒,但我现在找到了新的首选前端框架。Hyperapp 正是我梦寐以求的 Elm——它易于编写代码,结构清晰,状态管理也完美无瑕。话虽如此,它目前在生产环境的成熟度还不如前面提到的那些框架,但我相信一旦成熟,它将会大放异彩。

让我们从零开始学习如何构建 Hyperapp 应用——对于仍在发展中的工具,我通常会讲解得更深入一些。对于 Hyperapp,我也会这样做。

入门

几周前,我看到几篇关于 Hyperapp 的文章,当时他们发布了 1.0 版本,并在 GitHub 上获得了超过 10,000 个 star。我简单地浏览了一下他们文档中的“hello world”计数器。我非常喜欢它简洁明了的界面,所以很想亲自试用一下!

const { h, app } = hyperapp

const state = {
  count: 0
}

const actions = {
  down: value => state => ({ count: state.count - value }),
  up: value => state => ({ count: state.count + value })
}

const view = (state, actions) =>
  h("div", {}, [
    h("h1", {}, state.count),
    h("button", { onclick: () => actions.down(1) }, ""),
    h("button", { onclick: () => actions.up(1) }, "+")
  ])

window.main = app(state, actions, view, document.body)
Enter fullscreen mode Exit fullscreen mode

你也可以使用 JSX 而不是调用h函数来创建元素。我就是这么做的,因为我之前用 React 的时候就习惯了!我浏览了Hyperapp 的 Codepen上的示例。最后我使用了一个样板代码,这样就不用配置 webpack 来转译 JSX,也不用处理其他设置了。这很棒,使用起来没有任何问题,而且它的文件结构也正合我意。

我使用的文件结构

Hyperapp 采用了一种受 Elm 启发的架构——它包含视图、模型和更新。它也遵循与 Elm 类似的函数式编程理念。这意味着状态是不可变的,并且 action 没有副作用。由于状态是集中式的而不是组件特定的,因此其状态管理感觉更像是 Redux 而不是标准的 React。此外,你需要使用thunk来构建非纯函数。该架构和配置使用起来非常流畅,我几乎没有遇到任何问题。

由于我过去曾使用过 Elm、React、Redux、Vue 和 Vuex,所以我认出了其中的模式,并且在阅读了文档(文档很少)和查看了代码示例后,感觉可以顺利地进行最终项目。

最终项目

我想构建一个可以从 API 获取数据的东西——这在 Redux 中可能是一个相对繁琐的过程。我当时没有想到合适的 API,所以浏览了这个列表,希望能找到一个。最终我选择了FavQs API——我的想法是创建一个轮播的语录列表,并提供搜索不同语录标签的功能。这样我就可以对状态进行相当多的交互。

我写的第一段代码是状态模型。我为项目中需要的属性设置了初始属性:

export default {
  quotes: [],
  term: '',
  index: 0
}
Enter fullscreen mode Exit fullscreen mode

这里如果能用 TypeScript 或 Flow 之类的工具来强制类型就好了。我相信它们很容易就能集成到超应用项目中。

报价是一个数组,其中包含从 API 返回的报价;术语是用户指定的搜索词;索引是用户正在查看的报价的当前索引。

我有一个配置文件,其中定义了一些要在整个程序中使用的常量:

export const API_URL = 'https://favqs.com/api/quotes/'
export const COLORS = ['#DBEBFF', '#FFBBDD', '#e6f9ff', '#BBBBFF', '#F7FFFD', '#fff8e1']
export const FONT_COLORS = ['#62D0FF', '#FF62B0', '#33ccff', '#5757FF', '#03EBA6', '#ffb300']
Enter fullscreen mode Exit fullscreen mode

我还创建了一个 services 文件,其中包含用于搜索的 Axios(一个极简的 AJAX 库)请求:

import axios from 'axios'
import { API_URL } from './constants'

const getRequest = url => {
  return axios.get(url, {
    headers: {'Authorization': `Token token="XXXXXXXX"`}
  }).catch(
    err => console.log(err)
  )
}

export default {
  getAll: _ => getRequest(API_URL),
  getQuery: query => getRequest(API_URL + `?filter=${query}&type=tag`)
}
Enter fullscreen mode Exit fullscreen mode

以上文件与框架无关,但我还是想把它们包含在内以提供上下文。

可能最为关键的文件包含了操作步骤:

import request from '../config/request'

export default {
  getQuotes: quotes => (state, actions) => request.getAll().then(
    actions.setQuotes),
  submitSearch: quotes => (state, actions) => request.getQuery(
    state.term).then(actions.setQuotes),
  setQuotes: res => ({ quotes: res.data.quotes.filter(
    quote => quote.body && quote.body.length < 150) }),
  updateSearch: ({ term }) => ({ term }),
  next: e => ({ index, quotes }) => ({ index: index + 1 }),
  prev: e => ({ index, quotes }) => ({ index: index - 1 })
}
Enter fullscreen mode Exit fullscreen mode

我使用了 thunk 来实现getQuotes`and`操作submitSearch——也就是说,我只是从一个函数中调用一个函数,而不是调用一个值。这允许在嵌套函数中使用非纯函数,尤其是在 API 数据不如函数式编程所要求的那样可预测的情况下。由于 Axios 请求需要一些时间才能执行,因此状态实际上只有在setQuotes从 API 获取数据后调用该方法时才会更新。其他操作相对简单明了!事件处理程序会先获取事件,然后再获取当前状态——我确实觉得这有点像“魔法”,但总的来说,操作体验非常流畅。

最后,我创建了视图。主视图如下所示:

import { h, app } from 'hyperapp'
import Search from './Search'
import Quote from './Quote'
import { COLORS, FONT_COLORS } from '../config/constants'

const quote = (quotes, index) => quotes[index]
const color = index => COLORS[index % COLORS.length]
const fontColor = index => FONT_COLORS[index % FONT_COLORS.length]

export default ({ quotes, index }, { getQuotes, updateSearch, submitSearch, next, prev }) =>
  <div
    oncreate={getQuotes}
    className={ quotes ? 'body' : 'body hidden' }
    style={{ 'backgroundColor': color(index), 'color': fontColor(index) }}
  >
    <div className='centered-content'>
      <div className='container'>
        { index > 0 &&
        <div
          onclick={prev}
          className='direction left'
          style={{ 'color': fontColor(index) }}>
            &lt;
        </div> }
        { quotes.length > 0 && <Quote quote={quote(quotes, index)} /> }
        { index < quotes.length - 1 &&
        <div
          onclick={next}
          className='direction right'
          style={{ 'color': fontColor(index) }}>
            &gt;
        </div> }
        <Search
          updateSearch={updateSearch}
          submitSearch={submitSearch}
        />
      </div>
    </div>
  </div>
Enter fullscreen mode Exit fullscreen mode

它看起来几乎和 React 中的函数式组件一模一样!事件处理程序是小写的,但除此之外,JSX 代码完全相同。生命周期方法也略有不同。componentDidMount在 React 中,我通常会使用 `method` 来发起 API 请求,但在这里我使用了 ` oncreateattribute`。它们本质上做的是同一件事,只是语法不同。我也没有找到关于订阅的文档,订阅在 Elm 中非常重要。它允许你使用 WebSocket 并添加全局事件监听器。不过,一些 GitHub 问题提到了它们,所以我推测它们已经实现了,只是还没有添加到文档中。

我还有两个“子组件”,其中引用部分非常简单:

import { h, app } from 'hyperapp'

export default ({ quote }) =>
  <div className='quote'>
    <h1>{quote.body}</h1>
    <h4>{quote.author}</h4>
  </div>
Enter fullscreen mode Exit fullscreen mode

搜索结果也是如此:

import { h, app } from 'hyperapp'

export default ({ updateSearch, submitSearch }) =>
  <div className='search'>
    <input
      onkeyup={
        e => {
          e.keyCode === 13 ? submitSearch() : updateSearch({ term: e.target.value })
        }
      }
      placeholder='Search quote tags...'
    />
  </div>
Enter fullscreen mode Exit fullscreen mode

最后,将index.js其他文件中的元素合并起来,以便在操作和视图中使用状态。

import { app } from 'hyperapp'
import actions from './actions'
import state from './state'
import view from './components/View'

app(state, actions, view, document.querySelector('.hyperapp-root'))
Enter fullscreen mode Exit fullscreen mode

这种装订方式与 Elm 组合元素的方式本质上是相同的!

我喜欢把代码拆分成多个文件,而且我觉得这种方式扩展性很强。我完全可以想象未来用 HyperApp 构建更大的项目。

应用截图

后续步骤

Hyperapp 再次成为我近期学习过的最喜欢的工具之一——或许仅次于 Golang。它完美融合了我过去使用过的各种工具。它本身也是一个非常小巧且高效的库,这一点尤其令人兴奋,特别是与我上周刚学的 Angular 相比!它的 API 非常简洁,并且很好地贯彻了函数式编程的理念。我强烈推荐把它作为学习 React 和 Redux 的入门工具。我肯定会再次使用 Hyperapp,因为它简单易用,而且代码优雅流畅。我希望它的社区能够不断壮大,文档能够不断完善,并且能够实现类似 Redux/Elm 的回滚功能。总而言之,我对 Hyperapp 的体验非常棒,我已经计划再次使用它了!

应用
代码

这是我的“学习新事物系列”的一部分

其他类似文章:

文章来源:https://dev.to/aspittel/function-programming-in-javascript-done-right-with-hyperapp-570f