🎩

Eleventy v2で、ページ内の一部要素をPartial Hydration(w/Preact + JSX)する

2023/03/13に公開

BEENOSの三上です。
今回は社内の新規サイト構築でEleventyの使用を検討している過程で、Preactで作ったcomponentのPartial Hydrationを試してみたので、その知見を共有させて頂きます。
なお、Preact componentのbuildにはRollupBabelを用い、フロントエンドでのロードの制御には is-land を使用しています。
(bundle toolについては、昨今様々な選択肢がありますが、執筆者三上の学びを兼ねてRolllupを使っています。)

今回の検証の目的

弊社内で新規にサイト構築する見込みがあり、その際に何で作っていこうかというのが事の発端でした。
下記3点が、技術選定するにあたり意識していた判断軸です。

  • 多言語化を行う
  • サイト内コンテンツは基本静的なものだが、ほんの一部動的要素が見込まれる
  • サイトの開発、運用にはエンジニアだけでなく、デザイナーさんも直接関われると良さそう
    • なお、他サイトの構築などでEJSを用いており、デザイナーさんの学習コスト的にEJSは使い回せると嬉しい

上記の条件を満たすツールがないかの検証を進め、その過程で得られたEleventyに関する知見があまり日本語で共有されていなかったため、今回執筆に至った経緯です。

Eleventyとは?

EleventyはA simpler static site generatorです。(11ty/eleventyのREADMEより)

Eleventy自体の説明については、他に詳しい記事があるため、そちらに譲ります。

因みに2023/02/08にEleventy v2がリリースされています。
ELEVENTY V2.0.0, THE STABLE RELEASE - Eleventy Blog
この記事では最新のv2で検証をしていきます。
他記事を参照される際はメジャーバージョンの違いに気をつけて頂けると良さそうです。

環境

モノ version
Node.js 18.14.1 (2023/02/19時点での最新のLTS)
npm 9.5.0
Eleventy 2.0.0
Rollup 3.17.1
11ty/is-land 3.0.1

Initialize

まず適当なdirectoryを作ってnpmのinitializeをしておきます。

mkdir my-11ty-prj
cd my-11ty-prj
npm init -y

Eleventyをセットアップしていきます。
この後Eleventyの設定をいじるんですが、is-landに関する設定も行うため、先にinstallしています。

npm i @11ty/eleventy @11ty/is-land

Eleventyに関するディレクトリ構成を少しばかりいじっていきます。
この辺りはお好みでいじってください。

.eleventy.js
module.exports = function (eleventyConfig) {
  // なんらか最終成果物にコピーしていきたいリソースがある場合は、`addPassthroughCopy`に渡していきます。
  // cf.: https://www.11ty.dev/docs/copy/
  eleventyConfig.addPassthroughCopy({
    // `is-land.js`を使いたいので、`node_modules`配下から引っ張ってきます。
    './node_modules/@11ty/is-land/is-land.js': 'assets/js/is-land.js',
  });

  return {
    dir: {
      input: './src/contents',
      output: './dist',
    },
  };
};

上記.eleventy.js内のdir.inputで指定したパスに、ページ描画の元となるテンプレートを配置します。

src/contents/index.ejs
---
title: Eleventy experimental implementation w/Preact partial hydration.
---

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%- title %></title>
  </head>
  <body>
    <h3>
      <%- title %>
    </h3>

    <script type="module" src="/assets/js/is-land.js"></script>
  </body>
</html>

動作確認をするためにpackage.jsonのscriptsにEleventyのコマンドを追加して走らせてみます。
まずnpm scriptsとしてEleventyのコマンドを追加しましょう。

package.json
 {
   "name": "my-11ty-prj",
   "version": "1.0.0",
   "description": "",
   "main": "index.js",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1",
+    "dev:web": "eleventy --serve"
   },
   "keywords": [],
   "author": "",
   "license": "ISC",
   "dependencies": {
     "@11ty/eleventy": "^2.0.0",
     "@11ty/is-land": "^3.0.1"
   }
 }

npm run dev:webで起動してみます。

npm run dev:web

> my-11ty-prj@1.0.0 dev:web
> eleventy --serve

[11ty] Writing ./dist/index.html from ./src/contents/index.ejs
[11ty] Copied 1 file / Wrote 1 file in 0.04 seconds (v2.0.0)
[11ty] Watching…
[11ty] Server at http://localhost:8080/

http://localhost:8080/にアクセスして、下記のようなページが表示されていれば正しくページを構成できています。
8080が既に使用されている場合は、8081などのportでlistenする可能性があります。
EleventyがServer at ~~~の用に表示したURLに対してアクセスしてください。

最初のテンプレートを作った際のスクリーンショット

Rollupの導入

JSXを含むPreact componentのjsをtranspile&bundleするためにRollupを用います。
ついでに、RollupとEleventyのプロセスを同時に走らせるためにconcurrentlyを使用するので、このタイミングで導入しておきます。

npm i rollup @rollup/plugin-commonjs @rollup/plugin-babel @babel/plugin-transform-react-jsx @babel/preset-react @rollup/plugin-node-resolve preact concurrently

Preactでちょっとした描画するcomponentを作っておきます。

src/assets/js/partials/preact-component.js
import { h, render } from 'preact'

function HelloComponent () {
  return (
    <p>
      <strong>
        Hello from Preact!
      </strong>
    </p>
  )
}

const Component = (el) => {
  render(<HelloComponent />, el)
}

export default Component

Rollupの設定を書いていきます。

rollup.config.mjs
import { dirname, resolve } from 'path';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import { nodeResolve } from '@rollup/plugin-node-resolve';

const extensions = ['.ts', '.tsx', '.json', '.js'];

const projectRoot = dirname(new URL(import.meta.url).pathname)

export default {
  input: [
    resolve(projectRoot, './src/assets/js/partials/preact-component.js')
  ],
  output: {
    entryFileNames: '[name].js',
    dir: 'dist/assets/js/partials',
  },
  plugins: [
    nodeResolve({
      extensions: ['.js'],
    }),
    babel({
      presets: ['@babel/preset-react'],
      plugins: [
        ['@babel/plugin-transform-react-jsx', { "pragma":"h" }]
      ],
      babelHelpers: 'bundled',
    }),
    commonjs(),
  ],
};

EleventyとRollupを同時に起動するためのnpm scriptsを追加しておきます。

package.json
...
   "scripts": {
-    "dev:web": "eleventy --serve"
+    "dev": "concurrently 'npm:dev:*'",
+    "dev:web": "eleventy --serve",
+    "dev:js": "rollup -c --watch"
   }
...

npm run devを実行して動かしてみましょう。

npm run dev

> my-11ty-prj@1.0.0 dev
> concurrently 'npm:dev:*'

[web]
[web] > my-11ty-prj@1.0.0 dev:web
[web] > eleventy --serve
[web]
[js]
[js] > my-11ty-prj@1.0.0 dev:js
[js] > rollup -c --watch
[js]
[js] rollup v3.17.2
[js] bundles [プロジェクトのパス]/src/assets/js/partials/preact-component.js → dist/assets/js/partials...
[web] [11ty] Writing ./dist/index.html from ./src/contents/index.ejs
[web] [11ty] Copied 1 file / Wrote 1 file in 0.04 seconds (v2.0.0)
[web] [11ty] Watching...
[web] [11ty] Server at http://localhost:8080/
[js] created dist/assets/js/partials in 192ms

エラーなく起動出来ているようです。
次はbuild出来たPreactのcomponentを描画していきます。

Preact Componentを表示するページを作成

dist/assets/js/partials/preact-component.jsをページで読み込んで表示してみます。

src/contents/index.ejs
...
   <body>
     <h3>
       <%- title %>
     </h3>

+    <is-land on:visible autoinit="preact" import="/assets/js/partials/preact-component.js"></is-land>
+
     <script type="module" src="/assets/js/is-land.js"></script>

   </body>
...

下記のような表示になっていれば成功です。

PreactのComponentがロード/表示できている

is-landによるlazy loadの挙動を確認

今回、is-landを用いており、描画対象領域がviewport内にあるかどうかをIntersection Observerにより判定してjsのload&描画を行っています。
これが想定通り動作しているかも検証してみましょう。

まず、ページの初期表示時はPreact componentの描画領域が画面外に出るように、div要素を足して適当な高さを設定しましょう。

src/contents/index.ejs
...
   <body>
     <h3>
       <%- title %>
     </h3>

+    <div style="height: 500px;"></div>
     <is-land on:visible autoinit="preact" import="/assets/js/partials/preact-component.js"></is-land>

     <script type="module" src="/assets/js/is-land.js"></script>

   </body>
...

ファーストビューではPreact componentが表示されないような形になっていればOKです。
このファーストビューから、スクロールしてPreact componentを表示してみた例が↓のgifです。
スクロールして描画領域が視覚的viewport内に入ったタイミングで、preact-component.jsファイルがロードされていることがわかります🎉

PreactのComponentがロード/表示できている

まとめ

お疲れさまです。
ここまでで、Eleventyを使って生成している静的ページ内の一部を、Preactを使って動的なComponentとして描画することが出来ました。
あくまで「Preactから描画できているよね」という単純な検証をしただけですが、似たようなことをしようと考えている方の参考になれば幸いです。

Wanted!!

BEENOSグループでは一緒に働いて頂けるエンジニアを強く求めております!
少しでも気になった方は、社内の様子や大事にしていることなどをThe BEENOSにて発信しておりますので、是非ご覧ください。

https://beenos.com/blog/

とても気になった方はこちらで求人も公開しておりますので、お気軽にご応募ください!
「自分に該当する職種がないな...?」と思った方はオープンポジションとしてご応募頂けると大変嬉しいです。🙌


世界で戦えるサービスを創っていきたい方、是非ご連絡ください!よろしくお願い致します!!

世界で戦えるサービスを創っていく


BEENOS Tech Blog

Discussion