RedwoodJSというフレームワークがめちゃくちゃ良さそうなので入門してみます。

RedwoodJS is 何?

Redwood is the full-stack web framework designed to help you grow from side project to startup. Redwood features an end-to-end development workflow that weaves together the best parts of React, GraphQL, Prisma, TypeScript, Jest, and Storybook.

サイドプロジェクトからスタートアップくらいのプロダクトの成長を支援するためのフルスタックフレームワークとのことで、Railsと近い立ち位置にいるような印象。

React、GraphQL、Prisma、TypeScript、Jest、Storybookを用いたフレームワーク。 選定しているライブラリがとても自分好みです。

GitHub共同創設者、jekyllの作者、Gravatarの作者、Semantic Versioningの著者、TOMLの発明者など錚々たるメンバーに寄って立ち上げられたOSSプロジェクト。

まずはインストール

> yarn create redwood-app try-redwoodjs --typescript

フォルダ構成

.redwood
.vscode
api
├ db
├ src
│ ├ directives
│ ├ functions
│ ├ graphql
│ ├ lib
│ └ services
└ types
scripts
web
└ public
  └ src
    ├ components
    ├ layouts
    └ pages

.redwoodディレクトリ

Auto-generated Types

The CLI automatically generates types for you. These generated types not only include your GraphQL queries, but also your named routes, Cells, scenarios, and tests.

いきなりめちゃくちゃ良さそうなこと書いてある GraphQLだけでなく、ルートなども型を生成してくれるとか。すごい

.vscodeディレクトリ

良いですねー。create時点でlaunch.jsonもあるのは良いですねー。

apiディレクトリ

The /api Directory

api/db

Prismaのスキーマが入ってるここをいじってdbを作っていく

api/src

サーバーサイドコードを入れる箱中で更に分類する

api/src/directives

GraphQLのディレクティブを定義するところ

api/src/functions

自動生成されるgraphql.jsファイルと、lambdaファンクションが入るらしい、リンクがNetlifyのfunctionsになっているがそこにデプロイされるの?どういうこと?

api/src/graphql

GraphQLスキーマを定義するところっぽい。コードファーストはできない?

api/src/lib

認証や、db接続に使える関数が置いてある。そういうレイヤーの関数をおくところっぽい

api/src/services

ビジネスロジックを格納するところ。scriptからも呼べるような依存性にしておくと良いっぽい

webディレクトリ

web/public

よくある公開ディレクトリ

web/src/components

コンポーネントとCellコンポーネントが格納されている

web/src/layouts

ページ間で共有されるレイアウト部品ラッパー

web/src/pages

ページを構成する要素を集めたページコンポーネント

Cell?

web/src/components/Post/PostCell/PostCell.tsx

規約通りの命名でexportすると、Redwoodがライフサイクル管理や、型の生成をしてくれるらしい。 特に特徴的なのがGraphQLクエリのvariables

query FindPostById($id: Int!) { ... というクエリを書くと <PostCell id={3} /> のようにidのpropが生えるみたい。なるほど。なかなかよいかもしれない。

また、CLIで生成できる。

yarn rw generate cell post

このコマンドでいかが生成された

  • PostCell.tsx
  • PostCell.mock.ts
  • PostCell.stories.tsx
  • PostCell.test.tsx

生成されたファイルはこんな感じ

PostCell.tsx

import type { FindPostQuery } from 'types/graphql'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'

export const QUERY = gql`
  query FindPostQuery($id: Int!) {
    post: post(id: $id) {
      id
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>Empty</div>

export const Failure = ({ error }: CellFailureProps) => (
  <div style={{ color: 'red' }}>Error: {error.message}</div>
)

export const Success = ({ post }: CellSuccessProps<FindPostQuery>) => {
  return <div>{JSON.stringify(post)}</div>
}

PostCell.mock.ts

// Define your own mock data here:
export const standard = (/* vars, { ctx, req } */) => ({
  post: {
    id: 42,
  },
})

PostCell.stories.tsx

各状態のストーリーをそのまま書けるのでこれはめっちゃはかどるのでは…

import { Loading, Empty, Failure, Success } from './PostCell'
import { standard } from './PostCell.mock'

export const loading = () => {
  return Loading ? <Loading /> : null
}

export const empty = () => {
  return Empty ? <Empty /> : null
}

export const failure = () => {
  return Failure ? <Failure error={new Error('Oh no')} /> : null
}

export const success = () => {
  return Success ? <Success {...standard()} /> : null
}

export default { title: 'Cells/PostCell' }

PostCell.test.tsx

storybookと同じく各状態のコンポーネントのレンダリングを即テストできるので捗りそう

import { render, screen } from '@redwoodjs/testing/web'
import { Loading, Empty, Failure, Success } from './PostCell'
import { standard } from './PostCell.mock'

// Generated boilerplate tests do not account for all circumstances
// and can fail without adjustments, e.g. Float and DateTime types.
//           Please refer to the RedwoodJS Testing Docs:
//        https://redwoodjs.com/docs/testing#testing-cells
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations

describe('PostCell', () => {
  it('renders Loading successfully', () => {
    expect(() => {
      render(<Loading />)
    }).not.toThrow()
  })

  it('renders Empty successfully', async () => {
    expect(() => {
      render(<Empty />)
    }).not.toThrow()
  })

  it('renders Failure successfully', async () => {
    expect(() => {
      render(<Failure error={new Error('Oh no')} />)
    }).not.toThrow()
  })

  // When you're ready to test the actual output of your component render
  // you could test that, for example, certain text is present:
  //
  // 1. import { screen } from '@redwoodjs/testing/web'
  // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()

  it('renders Success successfully', async () => {
    expect(() => {
      render(<Success post={standard().post} />)
    }).not.toThrow()
  })
})

どんなコマンドがあるのか

良さそう

> rw <command>

Commands:
  rw build [side..]          Build for production
  rw check                   Get structural diagnostics for a Redwood project
                             (experimental)               [aliases: diagnostics]
  rw console                 Launch an interactive Redwood shell (experimental)
                                                                    [aliases: c]
  rw data-migrate <command>  Migrate the data in your database
                                                      [aliases: dm, dataMigrate]
  rw deploy <target>         Deploy your Redwood project
  rw destroy <type>          Rollback changes made by the generate command
                                                                    [aliases: d]
  rw dev [side..]            Start development servers for api, and web
  rw exec [name]             Run scripts generated with yarn generate script
  rw generate <type>         Generate boilerplate code and type definitions
                                                                    [aliases: g]
  rw info                    Print your system environment information
  rw lint [path..]           Lint your files
  rw prerender               Prerender pages of your Redwood app at build time
                                                               [aliases: render]
  rw prisma [commands..]     Run Prisma CLI with experimental features
  rw record <command>        Setup RedwoodRecord for your project. Caches a JSON
                             version of your data model and adds
                             api/src/models/index.js with some config.
  rw serve [side]            Run server for api or web in production
  rw setup <commmand>        Initialize project config and install packages
  rw storybook               Launch Storybook: a tool for building UI components
                             and pages in isolation                [aliases: sb]
  rw test [filter..]         Run Jest tests. Defaults to watch mode
  rw ts-to-js                Convert a TypeScript project to JavaScript
  rw type-check [sides..]    Run a TypeScript compiler check on your project
                                                              [aliases: tsc, tc]
  rw upgrade                 Upgrade all @redwoodjs packages via interactive CLI

Options:
  --help     Show help                                                 [boolean]
  --version  Show version number                                       [boolean]
  --cwd      Working directory to use (where `redwood.toml` is located.)

Examples:
  yarn rw g page home /  "Create a page component named 'Home' at path '/'"

感想

思ったよりとても重厚なフレームワークでした。 思想はとても良く、元々Apolloのベストプラクティスに共感できる場合は結構マッチするのではないかと思います。

良さそうなポイント

  • 基本的にはRedwoodJSが全てのツールをラップしているので、パッケージ依存バージョンの動作保証をRedwoodJSにある程度任せられそう
  • test, storybookを即書ける
  • 単純なWebアプリケーションはRailsのActionView並に高速に実装できそう
    • それでいてReact, GraphQLを使っているのでUIを柔軟にできる

懸念

  • RedwoodJSへのロックイン

特にtest, storybookを即かけるのはめちゃくちゃ良さそう。その上単純なページを作る速度もRails並なので、かなり強力なフレームワークだと感じました。 なんか軽いアプリケーション書いてみたい。