Upgrade to Pro — share decks privately, control downloads, hide ads and more …

やさしくはじめるRemixとWeb

 やさしくはじめるRemixとWeb

この資料は、2023年4月23日に開催された「春のJavaScript祭り Online 2023」にて発表した資料になります。

https://javascript-fes.doorkeeper.jp/events/154047

Remixとは、2021年にバージョン1.0が登場したフルスタックWebフレームワークです。RemixはReact Routerを開発していたチームが主導で開発を行っています。RemixはSPAの次のアーキテクチャであるPESPAに沿って開発を行うことができる素晴らしいフレームワークです。
今回の発表ではRemixに備わっている機能の紹介、そして誕生することになった時代的背景などを含めて簡単に紹介します。そして、Remix CLIに搭載されているRemix Stacksという機能を使って、実際にプロジェクトを作成しデプロイするまでの過程を含めて、時間の許す範囲で紹介させていただければと思います。

たかぎとねこ

April 23, 2023
Tweet

More Decks by たかぎとねこ

Other Decks in Technology

Transcript

  1. やさしくはじめるRemixとWeb
    〜ネットワークキャズムの間に橋を架けよう〜
    たかぎとねこ

    View full-size slide

  2. whoami
    たかぎとねこ(@takagimeow)
    フロントエンドエンジニア
    最近の活動
    PESPAの翻訳記事を投稿
    ChatGPTのVSCode拡張を公開
    rsstreeというWebアプリを公開
    Androidアプリを2つ公開

    View full-size slide

  3. What is Remix?

    View full-size slide

  4. About Remix
    RemixはフルスタックWebフレームワーク
    開発者がUIを中心に開発できるように設計されている
    Web標準に従って開発を進めることができる
    React Routerをベースに開発されている
    PESPAがデフォルトのアーキテクチャ

    View full-size slide

  5. Comparison with Rails and Laravel
    従来のバックエンドフレームワークはModel、View、Controllerの3
    つを提供する
    RemixはViewとControllerのみフレームワークの機能として提供す

    RemixはModelを機能として提供しない

    View full-size slide

  6. Reasons for not providing models
    JavaScriptというエコシステムには、既にすばらしいライブラリが
    存在している
    開発者はPrismaなどのライブラリをモデルとして採用することがで
    きる
    採用するライブラリをフレームワークに強制されず、開発者が自由
    に選択できる

    View full-size slide

  7. Explanation from MVC aspects
    Remixでは、それぞれのView自身がそのコントローラーを担う設計
    データとコンポーネントをひとつのモジュールにまとめることで、
    完全なUIを構築することを目標としている(ルートモジュール)

    View full-size slide

  8. Questions about state management
    Remixでは、グローバルな状態管理が必要ない
    Reduxを使う必要がない
    GraphQLのような巨大なモジュールをクライアントに送る必要がな

    View full-size slide

  9. Misconceptions about Remix
    RemixはWebフレームワークである
    RemixはUIフレームワークではない
    RemixにおいてReactはViewの機能としてのみ使われている
    ReactはViewにとって現在サポートされているUIフレームワークの
    ひとつにすぎない
    将来的にはVueなどのUIフレームワークをサポートする予定

    View full-size slide

  10. Origins of Remix

    View full-size slide

  11. Authors
    Michael Jackson
    出典: https://remix.run/authors/profile-michael-jackson.png
    Ryan Florence
    出典: https://remix.run/authors/profile-ryan-florence.png

    View full-size slide

  12. History of Remix
    2020年にパンデミックが発生
    Ryan FlorenceとMichael Jacksonが経営していたReact Trainingの
    状況が悪化し、会社にとって最悪の状況を迎える
    React RouterをベースにしたWebフレームワークの開発を始める
    2020年10月ベータ版の開発者プレビューをライセンス形式で公開す
    る(当時はライセンスの購入が必須)

    View full-size slide

  13. Kent C. Dodds' participation in Remix
    Kent C. Dodds
    出典: https://remix.run/authors/profile-kent-c-dodds.png
    フィードバックを積極的に行う
    自分のWebサイトをRemixで作
    り直す
    Remixで人々がWebサイトを作
    ることを手助けするために、フ
    ルタイムの仕事を辞めてRemix
    に参加することを決断

    View full-size slide

  14. Let's develop an app with Remix

    View full-size slide

  15. Remix Indie Stack
    Remix Indie Stack
    出典: https://github.com/remix-run/indie-stack
    Docker & CI/CD & Fly.io
    Deployment Setup
    SQLite & Prisma
    Cypress & Vitest & Testing
    Library
    MSW
    ESLint & Prettier
    etc.
    引用: https://github.com/remix-run/indie-stack

    View full-size slide

  16. Setting up the project
    % npx create-remix@latest --template remix-run/indie-stack
    ? Where would you like to create your app? (./my-remix-app)
    ? TypeScript or JavaScript? (Use arrow keys)
    ❯ TypeScript
    JavaScript
    ? Do you want me to run `npm install`? (Y/n)
    ...
    Start development with `npm run dev `
    That's it!

    View full-size slide

  17. Start a server for development
    cd ./javascript-fes
    npm run dev
    ...
    Mock server running
    Rebuilding...
    Done in 128ms.
    Loading environment variables from .env
    Remix App Server started at http://localhost:3000 (http://192.168.xx.x:3000)

    View full-size slide

  18. Nested Routes

    View full-size slide

  19. プロジェクトをVSCodeで開いた時の状態

    View full-size slide

  20. 次のようなURLがあった場合
    http://example.com/notes/clg275yvp0003zzm9kc
    セグメントとレンダリングされるUIコンポーネントが紐づけられる
    URL ファイル UIコンポーネント
    http://example.com root.tsx App
    /notes notes.tsx NotesPage
    /clg275yvp0003zzm9kc notes.$noteId.tsx NoteDetailsPage

    View full-size slide

  21. export default function NotesPage() {
    return (

    ...

    ...

    );
    子ルートは親ルートの を通してレンダリングされる



    View full-size slide

  22. root.tsx
    root.tsxの は、すべてのルートをレンダリングする
    root.tsxはアプリケーション全体のレイアウトとして機能するた
    め、Root Routeと呼ばれる

    View full-size slide

  23. notes.tsx & notes.$noteId.tsx
    notes.tsxに存在する は、 /notes 配下のルートをレンダ
    リングする
    notes.$noteId.tsxは、notes.tsxの によってレンダリング
    される
    そのため、notes.tsxはnotes.$noteId.tsxにとっての親ルートとなる
    そして、notes.$noteId.tsxは子ルートとなる

    View full-size slide

  24. それぞれのコンポーネントが大きい箱と小さい箱のように機能する

    View full-size slide

  25. Route Module
    これらのコンポーネントのことをRouteと呼ぶ
    Routeは loader() や action() と一緒に定義され、これらをまとめて
    エクスポートするモジュール(ファイル)のことをRoute Moduleと
    呼ぶ

    View full-size slide

  26. File Name Conventions
    notes.tsx を親ルートにしたい時、子ルートのファイルは必ず
    notes. から始まるファイル名にする
    notes._index.tsx 、 notes.$noteId.tsx 、そして notes.new.tsx は必
    ず notes.tsx の子ルートとなる

    View full-size slide

  27. What is notes._index.tsx?
    /notes にアクセスした時にレンダリングする子ルートが存在しない
    ため、デフォルトの子ルートとして扱われる
    これをIndex Routeと呼ぶ

    View full-size slide

  28. Nested Routes up to V1
    V1まではディレクトリを分けることでルートの階層化を行っていた
    routes
    ├── notes
    │ ├── index.tsx
    │ ├── $noteId.tsx
    │ └── new.tsx
    └── notes.tsx

    View full-size slide

  29. Nested Routes from V2
    V2ではファイル名を使ってルートの階層化を行う
    ルートの階層化を行うために、ディレクトリを作成する必要がなくな
    った(フラットになった)
    routes
    ├── notes._index.tsx
    ├── notes.$noteId.tsx
    ├── notes.new.tsx
    └── notes.tsx

    View full-size slide

  30. loader and action
    Route Moduleには、 default export されるコンポーネントの他に
    loader() 関数と action() 関数がある
    loader() 関数と action() 関数は、サーバーサイドでのみ実行され

    コンポーネントに対してデータを提供する

    View full-size slide

  31. loader
    export async function loader({ request, params }: LoaderArgs) {
    const userId = await requireUserId(request);
    invariant(params.noteId, "noteId not found");
    const note = await getNote({ userId, id: params.noteId });
    if (!note) {
    throw new Response("Not Found", { status: 404 });
    }
    return json({ note });
    }
    json() はHTTPレスポンスを作成するためのユーティリティ関数

    View full-size slide

  32. useLoaderData hook
    loader() で返された値は useLoaderData() を使って取得できる
    export default function NoteDetailsPage() {
    const data = useLoaderData();
    return (

    {data.note.title}
    {data.note.body}
    ...
    );
    }

    View full-size slide

  33. Features of loader
    GET リクエストの時に呼び出される
    最初にSSRされるときにHTMLドキュメントに対してデータを提供
    する
    ブラウザ上でナビゲーションを行うたびに、セグメントとマッチす
    るRoute Moduleに存在する loader() 関数が fetch によって呼び出さ
    れる

    View full-size slide

  34. Problems solved by loader
    loader() がサーバー上で動作することにより、SPAでは課題だった
    プライベートキーなどの問題が発生しない
    サードパーティのAPIやDBとの通信も行える
    非同期の await を使うことができる
    つまり、クライアント上でのAPI呼び出しに関して悩む必要がなくな
    った

    View full-size slide

  35. action
    export async function action({ request, params }: ActionArgs) {
    const userId = await requireUserId(request);
    invariant(params.noteId, "noteId not found");
    await deleteNote({ userId, id: params.noteId });
    return redirect("/notes");
    }

    View full-size slide

  36. Features of action
    action() は GET 以外の POST 、 PUT 、 PATCH 、 DELETE リクエストの
    時に呼び出される
    その場合 loader() より先に呼び出される
    loader() へ GET リクエストが到達した時、ルート階層にマッチする
    すべての loader() が呼び出される
    しかし、 action() の場合は1つのみ

    View full-size slide

  37. Conditions under which an action is determined to be
    one 1 / 2
    どのルートの action() が呼び出されるかの条件は、最も深くマッチす
    るルートであること
    例えば、 /notes/$noteId の場合を考える
    notes.tsx と notes.
    $noteId.tsx の両方に action() が定義されていた
    としても、呼び出されるのは notes.
    $noteId.tsx の action()
    理由はそれがもっとも深いルートだから

    View full-size slide

  38. Conditions under which an action is determined to be
    one 2 / 2
    しかし、 /notes にリクエストが来た時の挙動は違う
    notes._index.tsx があったとしても、 notes.tsx の action() が呼び出
    される
    明確に notes._index.tsx の action() を呼び出したい場合は、 ?index
    を付与して /notes?index にリクエストを送る

    View full-size slide

  39. How to call action
    クライアントが action() を呼び出すために axios() を使う必要はな

    Web標準に従って を使えば良い

    View full-size slide

  40. Remix Form
    export default function NoteDetailsPage() {
    const data = useLoaderData();
    return (

    {data.note.title}
    {data.note.body}


    type="submit"
    className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
    >
    Delete



    );
    }

    View full-size slide

  41. Remix Form 1 / 3
    25年以上も前からWebではFormを使ってデータに対する変更を行って
    きた
    変更には作成、更新、削除が含まれる
    と action() によるデータの変更は、クライアント上で
    JavaScriptが動作していなくても実行される
    なぜならそれがWebの基本だから

    View full-size slide

  42. Remix Form 2 / 3
    によって値が action() に送信される時、送信されるデータ
    はシリアライズされる(通常のブラウザの挙動)
    action() が実行された後、ページ上のすべての loader() が再度呼び
    出される
    データに変更がある場合、UIに即座に反映される

    View full-size slide

  43. Remix Form 3 / 3
    action 属性を指定することで、現在のルート以外の action にリクエ
    ストを送信することができる
    method 属性には、 post 以外のメソッドタイプを指定することができ



    View full-size slide

  44. Let's deploy to Fly.io!

    View full-size slide

  45. 出典: https://fly.io/

    View full-size slide

  46. https://fly.io/docs/hands-on/install-flyctl/

    View full-size slide

  47. Sign up with flyctl
    flyctl のインストールが完了したら、ログインもしくはサインアップ
    を行う
    fly auth signup
    ログインしている場合は、期待通りのアカウントかどうかを確認する
    fly auth whoami

    View full-size slide

  48. Create app on Fly.io
    flyctl を使ってFly上にアプリを作成する
    % fly apps create javascript-fes
    ? Select Organization: *** (personal)
    New app created: javascript-fes
    アプリ名は必ず fly.toml の app フィールドに設定されているものと一
    致させる
    一致しない場合はデプロイできない

    View full-size slide

  49. Create a Secret on Fly.io
    プロジェクトの内部では SESSION_SECRET 環境変数が使われている
    これはアカウント管理のロジックで使われている
    secretsを設定することで環境変数として値を渡すことができる
    % fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app javascript-fes
    Secrets are staged for the first deployment%

    View full-size slide

  50. Create a persistent volume
    今回のプロジェクトではデータベースとしてSQLiteが採用されてる
    デプロイするたびにファイルシステムが再構築されるため、SQLiteの
    データベースファイルを永続的なボリュームに配置する必要がある
    % fly volumes create data --size 1 --app javascript-fes
    ? Select region: Dallas, Texas (US) (dfw)
    ID: vol_xxx
    ...
    App: javascript-fes
    ...
    Size GB: 1

    View full-size slide

  51. Deploy to Fly.io
    それでは、Fly.ioにデプロイしよう
    fly launch を実行するだけです
    % fly launch
    ...
    1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 2 total, 1 passing, 1 critical]
    --> v0 deployed successfully
    デプロイが完了しました

    View full-size slide

  52. Let's check it out!
    実際にアプリが動作しているかを確認しましょう
    https://javascript-fes.fly.dev/

    View full-size slide

  53. 出典: https://javascript-fes.fly.dev/

    View full-size slide

  54. Conclusion
    Remixで大事にされている4つの考え
    1. コードとデータを分離させたサーバー・クライアントモデルを採用
    する
    2. Webの基本に忠実に従う。例えばブラウザやHTTP、HTMLといった
    基本中の基本
    3. UXを向上させるためにJavaScriptを使い、ブラウザの動作をエミュ
    レートする
    4. 基本となる技術を抽象化させすぎない

    View full-size slide

  55. Conclusion
    Webの基本的な技術は25年以上前に作られた
    この技術は今もなお使われている素晴らしい技術
    Remixはその上にすべてが構築されている
    Remixを学ぶということはWeb標準を学ぶこと
    Web標準を学べば自然とRemixを使いこなせるようになる
    このシンプルなメンタルモデルこそがRemixの魅力である

    View full-size slide