Giới thiệu về framework Hyperapp

Hyperapp là một framework giúp xây dựng giao diện của ứng dụng web với cơ chế quản lý trạng thái. Hyperapp sử dụng một object chứa dữ liệu (state), các thành phần trên giao diện (biểu diễn thông qua virtualDOM) sẽ hiển thị thông tin dựa trên object này. Việc thay đổi dữ liệu của state sẽ được thực hiện thông qua các action, khi state được thay đổi bởi các action, giao diện của ứng dụng sẽ thay đổi tương ứng.

Ví dụ cơ bản

Dưới đây là một ví dụ cơ bản (được viết lại từ ví dụ trên readme của hyperapp. Trên giao diện sẽ hiển thị một số và 2 nút tăng, giảm số đếm đó.

index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf8">
    </head>
    <body>
        <div id="app"></div>
        <script src="https://unpkg.com/hyperapp"></script>
        <script src="main.js"></script>
    </body>
</html>

File index.html khai báo sử dụng framework và code của main.js ở 2 thẻ script. Thẻ div với id="app" được sử dụng để framework hiển thị giao diện.

main.js:

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) => {
  return h("div", {}, [
    h("h1", {}, state.count),
    h("button", {onclick: () => actions.down(1)}, "-"),
    h("button", {onclick: () => actions.up(1)}, "+")
  ])
}

app(state, actions, view, document.getElementById("app"))

Trong file main.js:

  • hằng số state được khai báo để lưu trữ trạng thái của giao diện.
  • hằng số actions chứa các action được sử dụng để thay đổi giá trị của state. Kết quả trả về của các action này sẽ được ghép với giá trị cũ của state, tức là các key không trùng tên ở kết quả trả về và trạng thái cũ của state sẽ giữ nguyên ở state mới, còn các key trùng tên sẽ được gán giá trị mới từ kết quả trả về của action.
  • hàm view sẽ trả về virtualDOM biểu diễn giao diện của ứng dụng. Hàm h, là hàm của Hyperapp, được sử dụng để tạo virtualDOM biểu diễn phần tử HTML. Chẳng hạn h("h1", {}, state.count) sẽ biểu diễn 1 thẻ h1 với nội dung là state.count, h("button", {onclick: () => actions.down(1)}, "-") sẽ biểu diễn một button có nội dung là - và được gắn với sự kiện onclick: khi nút được nhấn thì actions.down sẽ được gọi với tham số là 1 .
  • cuối cùng, state, actions, view sẽ được sử dụng trong hàm app, hàm này giúp mount ứng dụng vào phần tử có id là "app" được lấy thông qua document.getElementById.

Ngoài các sử dụng hàm h của Hyperapp, chúng ta có thể sử dụng JSX hoặc hyperapp/html, hyperx, t7, ijk để viết phần view của ứng dụng. Nếu sử dụng JSX, bạn sẽ cần BabelJSX transform plugin.

Cài đặt

Bạn có thể cài đặt qua npm:

npm install hyperapp

sau đó import vào (khi sử dụng Webpack làm module bundler):

import {h, app} from "hyperapp"

Hoặc có thể khai báo trực tiếp trong file html qua thẻ script như ví dụ ở trên:

<script src="https://unpkg.com/hyperapp"></script>

Các khái niệm của Hyperapp

State

Như đã nói ở trên, state là object chứa dữ liệu mô tả trạng thái của giao diện ứng dụng. Chúng ta không thể thay đổi trực tiếp giá trị của state mà phải thực hiện thông qua các action. state là một object Javascript nên nó có thể chứa các object khác bên trong nó.

const state = {
  top: {
    count: 0
  },
  bottom: {
    count: 0
  }
}

Do Hyperapp thực hiện việc shallow merge các object để cập nhật state nên state phải là một object Javascript thuần, còn các giá trị bên trong có thể thuộc các kiểu khác như mảng, Maps, Sets, v..v..

Action

Các action được sử dụng để thay đổi state, và chúng chỉ nhận 1 tham số. Do vậy, nếu bạn cần truyền nhiều hơn 1 tham số, bạn nên đưa các dữ liệu đó vào một object và truyền object này vào action (bản thân mình khi làm thử đã không để ý đến vấn đề này nên đã truyền vào nhiều tham số, kết cục là các tham số từ thứ 2 trở đi đều có giá trị undefined hết mà không rõ tại sao).

Kết quả trả về của action là một object chứa một phần của state mới. state mới là kết quả của việc shallow merge giữa state hiện tại và object kết quả của action. Ngoài cách trả về một object, action còn có thể trả về một hàm nhận tham số là state hiện tại và các action với kết quả trả về là một object chứa một phần của state mới, như ví dụ ở đầu bài:

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

Action bất đồng bộ

Các action có các hiệu ứng phụ như ghi dữ liệu vào CSDL hay lấy dữ liệu tử server không cần trả về object chứa state mới. Trong action có thể gọi đến các action khác, chẳng hạn sau khi lấy dữ liệu từ server về xong thì gọi một action để cập nhật dữ liệu đó vào state. Các action trả về một Promise, undefined hay null sẽ không kích hoạt việc cập nhật state.

const actions = {
  upLater: value => (state, actions) => {
    setTimeout(actions.up, 1000, value)
  },
  up: value => state => ({ count: state.count + value })
}

Một action có thể là một hàm async, và do hàm async trả về một Promise, chúng ta cần gọi một action khác trong action này để cập nhật state.

const actions = {
  upLater: () => async (state, actions) => {
    await new Promise(done => setTimeout(done, 1000))
    actions.up(10)
  },
  up: value => state => ({ count: state.count + value })
}

Action nằm trong namespace

Để cập nhật state con nằm trong một namespace của state ngoài cùng, ta có thể khai báo các action nằm trong namespace cùng tên với namespace của state con đó:

const state = {
  counter: {
    count: 0
  }
}

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

View

Mỗi khi state thay đổi, hàm view sẽ được gọi để cập nhật virtualDOM biểu diện giao diện của ứng dụng. Hàm view sẽ trả về virtualDOM mới, Hyperapp sẽ đảm nhiệm việc cập nhật lại virtualDOM cho ứng dụng.

Mounting

Hyperapp sẽ mount ứng dụng vào một phần tử DOM. Như với ví dụ đầu bài, phần tử DOM đó là thẻ dividapp:

app(state, actions, view, document.getElementById("app"))

Component

Một component là một hàm thuần (pure function) trả về một virtual node.Component không liên kết trực tiếp tới stateaction của ứng dụng, chúng chỉ là các đoạn code có thể tái sử dụng. Ví dụ:

const {h} = hyperapp

const PaginationItem = ({page, text, loadDataAction}) => {
  return h("div", {class: "pagination-item", onclick: () => loadDataAction(page)}, text)
}

const view = (state, action) => {
  return h("div", {}, [
    PaginationItem({page: state.currentPage - 1, text: "Prev", loadDataAction: actions.loadData})
    PaginationItem({page: state.currentPage + 1, text: "Next", loadDataAction: actions.loadData})
  ])
}

Lazy component

Component thông thường chỉ nhận thuộc tính và component con từ component cha. Các lazy component, tương tự với hàm view, có thể nhận stateaction của ứng dụng. Để tạo một lazy component, khai báo một component thông thường và trả về một hàm view bên trong component này.

const {h} = hyperapp

const PaginationItem = ({page, text}) => (state, actions) {
  return h("div", {class: "pagination-item", onclick: () => actions.loadData(page)}, text)
}

const view = (state, action) => {
  return h("div", {}, [
    PaginationItem({page: state.currentPage - 1, text: "Prev"})
    PaginationItem({page: state.currentPage + 1, text: "Next"})
  ])
}

Children composition

Component nhận các phần tử con của nó thông qua tham số thứ 2, cho phép truyền phần tử con tùy ý vào component đó.

const FlashMessage = ({flashType}, children) => (
  return h("div", {class: `alert alert-${flashType}`}, children)
)

const Panel = () => (
  return FlashMessage({flashType: "warning"}, h("p", {}, "Warning!!!"))
)

Các thuộc tính được hỗ trợ

Các thuộc tính được hỗ trợ bao gồm các thuộc tính HTML, các thuộc tính của SVG, các sự kiện của DOM, các sự kiện của Lifecycle và khóa. Các thuộc tính HTML không chuẩn không được hỗ trợ, chẳng hạn onClick hay className.

Style

Thuộc tính style sẽ nhận một object thay vì string như trong HTML. Tên thuộc tính trong style sẽ được viết dưới dạng camelCase.

const Title = ({text}) => {
  return h("div", {style: {padding: "5px 10px", color: "white", backgroundColor: "#5d8ec9"}}, text)
}

Các sự kiện của Lifecycle

Có 4 sự kiện trong Lifecycle của một phần tử được quản lý bỏi virtualDOM, các sự kiện này được kích hoạt khi phần tử này được tạo, cập nhật hay bị xóa.

oncreate

Sự kiện này được kích hoạt sau khi phần tử được tạo vào gắn vào DOM. Sử dụng sự kiện này để xử lý DOM node, tạo network request hay các hiệu ứng slide/fade

h("div", {oncreate: (element) => (element.style = "color: red;")}, "New element")

onupdate

Sự kiện này được kích hoạt khi chúng ta cập nhật các thuộc tính của phần tử. Sử dụng tham số oldAttributes bên trong hàm xử lý để kiểm tra xem thuộc tính nào bị thay đổi.

export const Textbox = ({ placeholder }) => (
  <input
    type="text"
    placeholder={placeholder}
    onupdate={(element, oldAttributes) => {
      if (oldAttributes.placeholder !== placeholder) {
        // Handle changes here!
      }
    }}
  />
)

onremove

Sự kiện này được kích hoạt trước khi phần tử bị xóa khỏi DOM. Sử dụng sự kiện này để tạo các hiệu ứng slide/fade out. Gọi tham số done bên trong hàm xử lý để xóa phần tử. Sự kiện này sẽ không kích hoạt bên trong phần tử con của phần tử bị xóa.

export const MessageWithFadeout = ({ title }) => (
  <div onremove={(element, done) => fadeout(element).then(done)}>
    <h1>{title}</h1>
  </div>
)

ondestroy

Sự kiện này được kích hoạt sau khi phần tử bị xóa khỏi DOM một cách trực tiếp hoặc phần tử cha của phần tử này bị xóa. Sử dụng sự kiện này để xóa bỏ các timer, hủy network request, bỏ các hàm bắt sự kiện, v..v..

export const Camera = ({ onerror }) => (
  <video
    poster="loading.png"
    oncreate={element => {
      navigator.mediaDevices
        .getUserMedia({ video: true })
        .then(stream => (element.srcObject = stream))
        .catch(onerror)
    }}
    ondestroy={element => element.srcObject.getTracks()[0].stop()}
  />
)

Khóa

Khóa giúp xác định node mỗi khi DOM được cập nhật.Bằng cách chỉ định khóa trên một virtual node, node đó sẽ tương ứng với một phần tử DOM nào đó. Điều này cho phép phần tử được dời đến vị trí mới nếu vị trí của nó thay đổi, thay vì bị xóa bỏ.

export const ImageGallery = ({ images }) =>
  images.map(({ hash, url, description }) => (
    <li key={hash}>
      <img src={url} alt={description} />
    </li>
  ))

Khóa phải duy nhất giữa các node cùng cha. Không sử dụng index của mảng làm khóa nếu index dùng để chỉ định thứ tự của các node cùng cha này. Nếu thứ tự và số lượng các phần tử này cố định thì không có vấn đề gì, nhưng nếu chúng không cố định, khóa sẽ bị thay đổi mỗi khi DOM được tạo lại.

export const PlayerList = ({ players }) =>
  players
    .slice()
    .sort((player, nextPlayer) => nextPlayer.score - player.score)
    .map(player => (
      <li key={player.username} class={player.isAlive ? "alive" : "dead"}>
        <PlayerProfile {...player} />
      </li>
    ))

Demo

Dưới đây là một demo cho phép tìm kiếm các repo public trên github bằng tên. Kết quả trả về sẽ được phân trang, mỗi trang 10 repo, dưới cùng sẽ có 2 nút cho phép trở về phân trang trước hoặc tiến tới phân trang tiếp theo.

Kết luận

Hyperapp là một framework nhỏ gọn và đơn giản nhưng khá hiệu quả và rõ ràng với tính năng quản lý trạng thái và các thành phần tách bạch. Hy vọng mọi người sẽ có dịp trải nghiệm framework này.

Cảm ơn các bạn đã theo dõi bài viết.

Tham khảo