SODA Engineering Blog
🫠

SolidJSでサクッと作るGithubPagesサイト

2024/07/30に公開3

こんにちは。FEチームのMapleです。私たちのチームは、現在のシステムアーキテクチャを見直し、Reactを用いた新しいアーキテクチャへの移行を検討しています。
他のフレームワークも視野を広げたいので、今回はSolidJSでサクッとGithubPagesにデプロイを行っていこうと思います。

リポジトリ

https://github.com/fuuki12/maple-dev

SolidJSとは

SolidJSは、JavaScriptライブラリの一つで、シンプルで高性能なユーザーインターフェースを構築するために設計されています。以下にSolidJSの特徴をいくつか挙げます。

  • リアクティブプログラミング: SolidJSは、リアクティブプログラミングに基づいており、効率的に状態を管理し、UIを更新します。
  • 高速なレンダリング: 仮想DOMを使用せずに、直接DOMを操作することで、高速なレンダリングを実現。
  • 小さなバンドルサイズ: SolidJSは非常に軽量で、バンドルサイズが小さい。

詳しい内容は以下の記事で解説しておりますので、ご一読ください。
https://zenn.dev/maple_siro/articles/f186909c89de95

このブログでは、SolidJSを使って簡単なサイトを作成し、GitHub Pagesにデプロイする方法を説明します。

SolidJSプロジェクトのセットアップ

pnpm create solid

あとは質問に回答していくとサクッとプロジェクトが作成されます。

一部プログラム抜粋

以下は、SolidJSの簡単なサンプルコードです。このコードでは、スクロールに応じてプログレスバーが動的に更新されるリアクティブなUIを実装しました。

import { For, createEffect, onCleanup } from "solid-js";
import { css } from "solid-styled";

import logo from "~/assets/logo.jpg";
import nextIcon from "~/assets/nextdotjs.svg";
import reactIcon from "~/assets/react.svg";
import solidIcon from "~/assets/solid.svg";
import typescriptIcon from "~/assets/typescript.svg";
import svelteIcon from "~/assets/svelte.svg";
import pythonIcon from "~/assets/python.svg";
import githubActionIcon from "~/assets/githubactions.svg";
import javascriptIcon from "~/assets/javascript.svg";
import amazonawsIcon from "~/assets/amazonaws.svg";
import goIcon from "~/assets/go.svg";
import kotlinIcon from "~/assets/Kotlin logo.svg";
import vueIcon from "~/assets/Vue.js.svg";

export default function Profile() {
  css`
    #icon {
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      width: 80%;
      gap: 12px;
      padding: 14px;
      border: 0.0625rem solid #dee2e6;
    }

    #profile {
      border-radius: 50%;
      width: 100%;
      max-width: 12rem;
      height: auto;
    }

    .network-icon {
      border-radius: 30%;
      width: 100%;
      max-width: 3rem;
      height: auto;
      pointer: cursor;

      &:hover {
        opacity: 0.5;
      }
    }

    .progress-bar {
      width: 100%;
      background-color: #f1f1f1;
      border-radius: 8px;
      overflow: hidden;
    }

    .progress {
      height: 20px;
      background-color: #b5b5ff;
      text-align: center;
      line-height: 20px;
      color: white;
      border-radius: 8px;
    }

    #gray {
      color: #999;
    }
  `;

  createEffect(() => {
    const handleScroll = () => {
      // ドキュメントの高さ
      const docHeight =
        document.documentElement.scrollHeight -
        document.documentElement.clientHeight;
      // 現在のスクロール位置
      const scrollTop = document.documentElement.scrollTop;
      // スクロール位置に基づいてプログレスを計算(0から100まで)
      const scrollPercent = (scrollTop / docHeight) * 100;

      // 各プログレスバーの長さを更新
      iconObj.forEach((icon, index) => {
        const progressBar = document.querySelector(
          `#progress-${index}`
        ) as HTMLElement;
        if (progressBar) {
          progressBar.style.width = `${Math.min(scrollPercent, icon.level)}%`;
        }
      });
    };

    // スクロールイベントリスナーを追加
    window.addEventListener("scroll", handleScroll);

    // コンポーネントのアンマウント時にイベントリスナーを削除
    onCleanup(() => {
      window.removeEventListener("scroll", handleScroll);
    });
  });

  const iconObj = [
    { img: javascriptIcon, name: "JavaScript", level: 95 },
    { img: typescriptIcon, name: "TypeScript", level: 90 },
    { img: reactIcon, name: "React", level: 90 },
    { img: vueIcon, name: "Vue.js", level: 80 },
    { img: nextIcon, name: "Next.js", level: 80 },
    { img: solidIcon, name: "SolidJS", level: 75 },
    { img: svelteIcon, name: "Svelte", level: 60 },
    { img: pythonIcon, name: "Python", level: 70 },
    { img: kotlinIcon, name: "Kotlin", level: 60 },
    { img: goIcon, name: "Go", level: 30 },
    { img: githubActionIcon, name: "GitHub Actions", level: 65 },
    { img: amazonawsIcon, name: "AWS", level: 50 },
  ];

  return (
    <section>
      <div class="card">
        <h3>Skills</h3>
        <For each={iconObj}>
          {(key, index) => (
            <div id="icon">
              <img class="network-icon" src={key.img} />
              <div class="progress-bar">
                <div
                  id={`progress-${index()}`}
                  class="progress"
                  style={{ width: "0%" }}
                >
                  {key.name}
                </div>
              </div>
            </div>
          )}
        </For>
      </div>
    </section>
  );
}

このサンプルコードは、以下のSolidJSの特徴を示しています。

  • リアクティブなUI: createEffectフックを使用して、スクロールイベントに基づいてプログレスバーを動的に更新しています。依存配列は必要ありません。
  • より視覚的なJSX:<For each={iconObj}>をObject.mapの代わりにしようすることができます。よりわかりやすく良いですね。
  • スタイリング: solid-styledを使用して、CSSスタイルをコンポーネント内で定義し、スタイルのスコープを限定しています。

GitHub ActionsでCI/CDを設定

GitHub Actionsを使用して、自動デプロイを設定します。

name: Build and Deploy

on:
  push:
    branches:
      - main # Set a branch to trigger deployment

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v4.1.7

      - uses: pnpm/action-setup@v4.0.0
        with:
          version: 8.6.10
      - run: |
          pnpm install
          pnpm build

      - name: Move files  📂
        run: |
          ls -la ./dist/public
          cp -R ./dist/public/* ./docs/

      - name: Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@4.1.0
        with:
          token: ${{ secrets.MAPLE_TOKEN }}
          branch: gh-pages
          folder: ./docs

デプロイの確認

コードをmainブランチにプッシュするたびに、GitHub Actionsが自動的にプロジェクトをビルドし、GitHub Pagesにデプロイします。

デプロイ完了後以下のURLを確認します。
https://fuuki12.github.io/maple-dev/

まとめ

  • リアーキテクチャに向けてより多角的に視野を広げてみるために今回はReact以外の別のフレームワークに触れました。
  • 今後もより視野を広げてキャッチアップできたらなと思います。
SODA Engineering Blog
SODA Engineering Blog

Discussion

eyemono.moeeyemono.moe

失礼します。
ご提示されているサンプルコードのように、signalを使っておらずaddEventListenerの実行のみを行う用途では、createEffectを使用する必要はなく、代わりにonMountを使用するのが("レンダリング直後に1回のみ実行する"という意図を明確にできるという点で *)適切かと思います。

https://docs.solidjs.com/reference/lifecycle/on-mount

使用例:

https://playground.solidjs.com/anonymous/6f17152c-1a06-4e0f-a77b-4bd5ba677a44

(createEffectの紹介のためにあえて使用していたのでしたら申し訳ありません🙇)

* 返信を受けて追記いたしました

MapleMaple

eyemono.moeさん
コメントありがとうございます🙇‍♀️
ドキュメント確認いたしました。
正しくない書き方になってしまっているので修正いたします。

本日はもう遅いので後日対応させてください🙇‍♀️
不適切なコードを投稿してしまい大変申し訳ございませんした。

eyemono.moeeyemono.moe

いえいえ、こちらこそ深夜に失礼しました🙇‍♀️

正しくない書き方になってしまっているので修正いたします。

  • onMountcreateEffect(() => untrack(callbackFn));のaliasであることを踏まえると、signalが使われていない本サンプルではonMountcreateEffectが等価である
  • 仮にeffect内でsignalが使われており、effectが複数回走った場合でも、
    • 追加しているイベントリスナーが無名関数ではないので、多重に追加されたりはしない
    • 同一reactive scope内のonCleanupwindow.removeEventListenerが実行されているためイベントリスナが残ることはない

少なくとも本記事のサンプルコードにおいては上記の理由から、createEffect/onMountいずれを使用しても期待通りに動作するため、"正しくない"/"不適切"なコードとまではいかないかと思います👍
むしろ あたかも不適切なコードであるかのような指摘になってしまい申し訳ありませんでした🙇‍♀️

ですので記事内容を修正するか否かは白色さんにおまかせいたします🙇‍♀️