- 投稿日:2021-10-31T21:02:12+09:00
Rails+React+MySQL+nginx+puma on Docker
はじめまして。 初投稿です。 よろしくお願いします。 概要 Docker 自己紹介 初投稿なので軽く自己紹介します。 現在横浜の大学に通うB3で絶賛就活中です。 Qiitaの方々の記事に大変お世話になっているのでぜひ自分も書いて誰かの役に立てればと思い書いてみました。 誰かのtipsになれば幸いです。 承認欲求強めです。いいね、ストック、スター、フォローしてくれると飛び跳ねるくらい嬉しいです。 よろしくお願いします。 すぐはじめたい方へ 完成版 github clone後setup.shを実行すればコンテナが立ち上がります。 構成 バック version Ruby 2.6.2 Rails(API モード) 6.1.4 nginx 1.20.1 MySQL 5.7 フロント version Node.js 12.6.1 React 17.0.2 nginx 1.20.1 投稿時テーブルを上から持ってきたままになっておりバージョンの編集が行われておりませんでしたので編集を行いました。 ディレクトリ . ├── api │ ├── nginx │ │ ├── Dockerfile │ │ └── default.conf │ └── server │ ├── Dockerfile │ ├── Gemfile │ ├── Gemfile.lock │ ├── entrypoint.sh │ └── run.sh ├── frontend │ ├── Dockerfile │ └── run.sh │ ├── front │ │ ├ │ ├── nginx │ │ ├── Dockerfile │ │ └── default.conf ├── mysql │ ├── Dockerfile │ └── my.cnf ├── docker-compose.yml └── setup.sh ポートは以下のようです。 rails 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp api-nginx 0.0.0.0:8080->80/tcp, :::8080->80/tcp react 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp front-nginx 0.0.0.0:80->80/tcp, :::80->80/tcp db 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp ReactとRailsのデフォルトポートが3000でかぶるので、今回はReactのポートを8000にしました。 このポートって慣習とかルールってあるんですかね? qiitaの記事とか見ると色んな設定ですけど、分かる方がいらっしゃればぜひ教えてください?♂️ バック まずはバックを構築していきます。はじめにRailsのコンテナ作って、MySQLを作ってからnginx作ります。 Rails Gemfile Gemfile source 'https://rubygems.org' gem 'rails', '~> 6.1.4' Dockerfile Dockerfile FROM ruby:2.6.2 RUN apt-get update -qq && \ apt-get install -y build-essential libpq-dev nodejs vim RUN mkdir /code WORKDIR /code COPY Gemfile /code/Gemfile COPY Gemfile.lock /code/Gemfile.lock RUN bundle install COPY . /code COPY entrypoint.sh /usr/bin/ ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 CMD ["bundle", "exec", "puma", "-C", "/code/config/puma.rb"] よく見る構成のDockerfileだと思います。 Dockerfileの最後のコマンドは、今回pumaを使用するのでこちらのような形式となっています。 -Cオプションの後ろで実行するファイルを指定します。 今回はルートディレクトリ下の/codeに諸々のアプリケーションファイルを置くので、このような指定としています。 Dockerfile内apt-get installの最後のvimはお好みで入っているだけなのでなくても動きます。 entrypoint.sh entrypoint.sh #!/bin/bash set -e rm -f /code/tmp/pids/server.pid exec "$@" 実行 run.sh #!/bin/bash cd /code rm -f /code/tmp/pids.server.pid bundle exec puma -C /code/config/puma.rb MySQL .env MYSQL_DATABASE=server_dev MYSQL_USER=docker MYSQL_ROOT_PASSWORD=DockerMysql1.0! MYSQL_PASSWORD=DockerMysql1.0! MySQLの環境変数設定です。 今回はユーザー名 docker、パスワード DockerMysql1.0!としてMySQLに接続します。 Dockerfile Dockerfile FROM mysql:5.7 EXPOSE 3306 COPY ./my.cnf /etc/mysql/conf.d/my.cnf CMD ["mysqld"] 特に注意する点はありません。 MySQLイメージを持ってきて、3306ポートを開け、confファイルをコンテナ内にコピーし、mysqldで起動します。 my.cnf [mysqld] explicit_defaults_for_timestamp = 1 character-set-server = utf8mb4 collation-server = utf8mb4_bin [mysql] default-character-set = utf8mb4 [client] default-character-set = utf8mb4 データベースの文字設定を行っています。 nginx(Rails側) Dockerfile Dockerfile FROM nginx:1.20.1 RUN rm /etc/nginx/conf.d/default.conf COPY default.conf /etc/nginx/conf.d CMD ["/usr/sbin/nginx", "-g", "daemon off;", "-c", "/etc/nginx/conf.d/default.conf"] Rails側のnginxコンテナです。 nginxイメージを持ってきて、/etc/nginx/conf.d下にあるデフォルトのconfファイルを削除し、これから作成するconfファイルを置きます。 その後nginxを起動します。 default.cnf default.cnf events { worker_connections 1024; } http { upstream rails { server qiita-rails:3000; } server { listen 80; server_name localhost; root /code/public; location / { try_files $uri @app; } location @app { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://rails; } } } nginxのconfファイルです。 今回はバックエンドでnginxコンテナに来たリクエストをRailsコンテナに流すので、こちらの設定を行います。 upstreem内でrailsコンテナ(これからdocker-compose.yml内で設定するサービス名qiita-railsの3000番ポートサーバー)を設定し、locationのproxy_passで流します。 docker-compose.yml docker-compose.yml version: "3.4" services: qiita-db: build: ./mysql/ container_name: qiita_db environment: MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_PASSWORD: ${MYSQL_PASSWORD} volumes: - db-data:/var/log/mysql healthcheck: test: ["CMD-SHELL", "mysqladmin ping --host=127.0.0.1 --user=${MYSQL_USER} --password=${MYSQL_PASSWORD} --port=3306"] ports: - "3306:3306" qiita-rails: build: ./api/server/ container_name: wt4u_api_rails command: ["./run.sh"] container_name: qiita_rails env_file: - ./api/server/.env volumes: - ./api/server/:/code/ - public:/code/public/ environment: TZ: Asia/Tokyo RAILS_ENV: development depends_on: qiita-db: condition: service_healthy ports: - "3000:3000" healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"] qiita-api-nginx: build: ./api/nginx/ container_name: wt4u_api_nginx volumes: - public:/code/public/ ports: - "8080:80" depends_on: qiita-rails: condition: service_healthy volumes: db-data: public: バック+DBまでのdocker-composeです。 特に変わった点はないですが、注意点としてqiita-db/qiita-rails両サービスにヘルスチェックを設定します。 depends_onで設定できるのはコンテナの起動順のみですので、コンテナ起動後アプリケーションが起動してから新たなコンテナを起動するように設定します。 起動順はdb→rails→nginxです。 DBの方はなくてもいけましたが、Railsのサービスの方はその後のnginxのほうが早く立ち上がり、リクエストを流す先がないエラーとなってしまうことを防ぐために設定しています。 内容としては、DBの方はmysqladminコマンドで接続できるかどうか、Railsの方はcurlコマンドでレスポンスが帰ってくるかどうかです。 詳細はDockerのドキュメントに書かれているのでこちらを参照してください。 コマンドだけでなく試行回数やインターバルなども設定できます。 またDBのデータを永続化するためvolumeを設定します。 そしてローカルの編集をコンテナへ反映させるためapi/serverをマウントします。これでコンテナ内でbundle installしたものなどが永続化できます。 さらにRailsコンテナとnginxコンテナでpublicディレクトリを共有するため同様にvolumeを作成します。 フロント 続いてフロント側のコンテナ達を作成します。 React Dockerfile FROM node:12.16.1 RUN apt-get update && \ apt-get install -y vim COPY run.sh / RUN mkdir /code WORKDIR /code EXPOSE 8000 CMD ["PORT=8000", "yarn", "start"] これもよくあるReactのDockerfileです。 注意点として、今回React側のポートを8000にするので、yarn startコマンドでポートを指定します。 run.sh #!/bin/bash cd /code PORT=8000 yarn start 上と同じくです。 nginx(React側) Dockerfile FROM nginx:1.20.1 RUN rm /etc/nginx/conf.d/default.conf COPY default.conf /etc/nginx/conf.d CMD ["/usr/sbin/nginx", "-g", "daemon off;", "-c", "/etc/nginx/conf.d/default.conf"] React側のnginxのDockerfileはRails側と同じです。 default.cnf events { worker_connections 1024; } http { upstream react { server qiita-react:8000; } server { listen 80; location / { proxy_pass http://react; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; } location /sockjs-node { proxy_pass http://react; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } } confもRails側と同じです。今回Reactのコンテナ名をqiita-reactとするのでupstreamで設定します。 docker-compose.yml docker-compose.yml version: "3.4" services: qiita-db: build: ./mysql/ container_name: qiita_db environment: MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_PASSWORD: ${MYSQL_PASSWORD} volumes: - db-data:/var/log/mysql healthcheck: test: ["CMD-SHELL", "mysqladmin ping --host=127.0.0.1 --user=${MYSQL_USER} --password=${MYSQL_PASSWORD} --port=3306"] ports: - "3306:3306" qiita-rails: build: ./api/server/ container_name: wt4u_api_rails command: ["./run.sh"] container_name: qiita_rails env_file: - ./api/server/.env volumes: - ./api/server/:/code/ - public:/code/public/ environment: TZ: Asia/Tokyo RAILS_ENV: development depends_on: qiita-db: condition: service_healthy ports: - "3000:3000" healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"] qiita-api-nginx: build: ./api/nginx/ container_name: wt4u_api_nginx volumes: - public:/code/public/ ports: - "8080:80" depends_on: qiita-rails: condition: service_healthy qiita-react: build: ./frontend/ container_name: qiita_react volumes: - ./frontend/front/:/code/ ports: - "8000:8000" command: ["/run.sh"] healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8000 || exit 1"] qiita-front-nginx: build: ./frontend/nginx/ container_name: qiita_front_nginx ports: - "80:80" depends_on: qiita-react: condition: service_healthy volumes: db-data: public: docker-composeの最終的な構成です。 フロント側もnginxを立てるので同様にヘルスチェックを行い起動を確認後nginxのコンテナを起動します。 RailsもReactもコンテナ内の/codeディレクトリにコードを一式置きます。 volumesの設定でマウントしているので、コンテナ内のbundle(yarn) installやローカルでの編集が同期されます。 立ち上げ Build $ docker-compose build Rails $ docker-compose run qiita-rails rails new . --api -d mysql --force --path vendor/bundle Railsのアプリケーションを新規作成します。 今回はRailsをAPIモードで使用し、データベースにMySQLを使用するのでオプションで指定します。 またbundle installしたgem達を永続化したいので、/code/vendor/bundleにインストールしvolumeの対象となるようにするため--pathオプションで指定します。 Rails DB接続情報 database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= ENV.fetch("MYSQL_USER") %> password: <%= ENV.fetch("MYSQL_PASSWORD") %> host: qiita-db development: <<: *default database: <%= ENV.fetch("MYSQL_DATABASE") %> RailsのDB接続情報を設定します。 hostにDBのコンテナ名を指定します。 React $ docker-compose run qiita-react npx create-react-app . npx create-react-appでReactアプリケーションを作成します。 起動 $ sudo chmod -R a=rx,u+wx . $ docker-compose up --build dockerがsudo権限で実行されている場合、コンテナ内で作成されvolumeでローカルにマウントされたRails、Reactアプリケーションファイルには実行権限がありません(でした)。 ユーザーに実行権限を設定するため1つ目のコマンドを実行します。 最後にdocker-compose up --buildを実行することですべてのコンテナたちが立ち上がります。 localhostの3000、8080ポートでRailsのトップが、8000、80ポートでReactのトップページが表示されます。 最後に 拙い文章でしたが最後までお読みいただきありがとうございます。 修正/加筆依頼/アドバイスは大歓迎です! qiitaでもissueでもぜひお願いします。 ちょこちょこ記事書きたいと思っているのでぜひよろしくお願いします。
- 投稿日:2021-10-31T17:59:22+09:00
Next.jsで実装 SPAでキャッシュ更新の時に便利な方法
SWRで十分な場面もありますが、今回僕はSWRが認証トークンの関係上使えなくて、そしてデプロイ後に更新した分が反映されなくて困った時にうまくいったので載せようと思います。 next.config.jsに以下を追記 generateBuildId: async () => { const date = new Date() const Y = date.getFullYear() const M = ('00' + (date.getMonth() + 1)).slice(-2) const D = ('00' + date.getDate()).slice(-2) const h = ('00' + date.getHours()).slice(-2) const m = ('00' + date.getMinutes()).slice(-2) const s = ('00' + date.getSeconds()).slice(-2) return Y + M + D + h + m + s }, こうすることで、ビルドした時のディレクトリにビルドした時のDateを入れることができますので、のちに生きてきます。 続いて、 JSファイルを作って、ビルド時のコマンドに&&で繋げて実行します require('dotenv').config() var fs = require('fs') const date = new Date() const Y = date.getFullYear() const M = ('00' + (date.getMonth() + 1)).slice(-2) const D = ('00' + date.getDate()).slice(-2) const h = ('00' + date.getHours()).slice(-2) const m = ('00' + date.getMinutes()).slice(-2) const s = ('00' + date.getSeconds()).slice(-2) var jsonData = { deploy: Y + M + D + h + m + s, } // ファイルの書き込み関数 function writeFile(path, data) { const jsonStr = JSON.stringify(data) fs.writeFile(path, jsonStr, (err) => { if (!err) { console.log('更新成功') console.log(data) } }) } function main(path, input) { writeFile(path, input) } //実行例 main(`build/deploytimestamp.json`, jsonData) main(`public/deploytimestamp.json`, jsonData) dotenvは.envを使うためにインストールしておきましょう。 あとは、deploytimestamp.jsonのファイルをfetchしていきます。 その際にとても大事なのはfetchしているファイルへのキャッシュを更新させることです。 const hash = new Date().getTime() fetch('/deploytimestamp.json?' + hash) //ここです .then((res) => res.json()) .then( (result) => { // JSONの内容が受け取れる }, (error) => { console.log(error) } ) イメージは上記のようにqueryにDateを合体させていただければできるかと思います。 同じ悩みを持った方のアイデアの力に慣れたら嬉しいです。
- 投稿日:2021-10-31T15:17:37+09:00
React-Hook-Formの使い方(Part3:MUIのバリデーション)?
はじめに Part1ではRHFの使い方。Part2ではRHFとyupを合わせて利用する方法を見ていきました。のPart3ではMUIとの併用を見ていきます。 パッケージインストール 今回インストールするものはありません。 MUIのバリデーションは今までのやり方にひと手間加えるだけなので簡単です! MUIとRHFの使い方 方法 MUIはregisterを使った登録はできません。ControllerコンポーネントをimportしてControllerコンポーネントのpropsにMUIを渡すことでバリデーションやvalueの取得を行います。 Controllerコンポーネントのimport react-hook-formからControllerをimportします。 import { useForm, SubmitHandler, Controller } from "react-hook-form"; Controllerコンポーネントの定義 Controllerを定義します。propsには「name」・「control」・「render」を定義します。それぞれのpropsを説明します。 name nameはregisterと同様の意味を持ちます。name属性を登録してRHFから制御できるようにします。よって、nameのpropsには"email"やpassword"などの自由な値を渡すことが出来ます。 control controlのPropsにはcontrolを渡します。controlはuseFormから取得できます。 const {control} = useForm(); render renderには関数を指定します。指定した関数はfieldを受け取ります。fieldにはcontrolで指定したpropsの値が入っています。また、指定した関数はMUIを返します。 以下の例では戻り値のMUIとしてTextFieldを返しています。また、TextFieldのpropsにはfieldをスプレッドして指定します。こうすることで、MUIのvalue値をRHFで取得できるようになります。 render={({ field }) => ( <TextField {...field} /> )} 以上の定義をまとめたコードが以下になります。これで、RHFからMUIのvalue値を取得できます。 <Controller name="email" control={control} render={({ field }) => ( <TextField {...field} /> )} /> エラーメッセージの出力 あとはエラーメッセージを出せるようにします。part2の知識があればココを読まなくても書けそうですね。 以下の例のコードではemailの入力を受け付けるMUIとして定義しています。 ※テキストフィールドの右の3点リーダーと丸のやつはNortonが勝手に表示しているやつです。 <Controller name="email" control={control} defaultValue="example@gmail.com" render={({ field }) => ( <TextField {...field} type="email" label="Email" variant="outlined" error={!!errors.email} helperText={errors.email ? errors.email?.message : ""} /> )} /> 注意点 defaultのvalueを指定しないとエラーが出てしまいます。defaultValue=""などを指定することで回避できます。 全体のコード import { Button, TextField } from "@mui/material"; import { useForm, SubmitHandler, Controller } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; // submitされる値の型定義 type IFormInputs = { email: String; password: string; }; // submitをハンドリング const formSubmitHandler: SubmitHandler<IFormInputs> = (data) => { console.log(data); }; // スキーマ const schema = yup.object().shape({ email: yup .string() .email("メールの書式に従ってください") .required("emailは必須です"), password: yup .string() .min(4, "4桁以上必須") .max(20, "最大20桁") .required("必須です") }); export default function App() { // useForm const { register, handleSubmit, watch, control, formState: { errors } } = useForm({ resolver: yupResolver(schema) }); // JSX return ( <form onSubmit={handleSubmit(formSubmitHandler)}> <Controller name="email" control={control} defaultValue="example@gmail.com" render={({ field }) => ( <TextField {...field} type="email" label="Email" variant="outlined" error={!!errors.email} helperText={errors.email ? errors.email?.message : ""} /> )} /> <br /> <br /> <Controller name="password" control={control} render={({ field }) => ( <TextField {...field} type="password" label="Password" variant="outlined" error={!!errors.password} helperText={errors.password ? errors.password?.message : ""} /> )} /> <br /> <br /> <input type="submit" /> </form> ); } 終わりに 以上になります。最後まで読んでくださりありがとうございました。 ※part4があれば、formProviderについて書きます。
- 投稿日:2021-10-31T14:08:29+09:00
React-Hook-Formの使い方(Part2:RHFとyupでバリデーション)?
はじめに Part1ではRHFの使い方。Part2でははRHFとyupを合わせて利用する方法を見ていきます。次回のPart3ではさらにMUIとの併用を見ていきます。 ※これはpart2になります。part1は以下リンク https://qiita.com/shunnami/items/98c0c03e3eb59b5c9c57 パッケージインストール yarn add yup hookform/resolvers yupの使いかた RHFの使い方はpart1で勉強しました。あとはyupの使い方を理解すればすぐに合体させて利用することができます。またyup自体も使い方は簡単です。 すでに詳細な使い方を説明している記事があるので以下を参照してください。 ※2ページ目まで読めば次の項目をスムーズに読むことが出来ると思います。 https://codezine.jp/article/detail/13518 組み合わせる yupの定義 まずは、バリデーションをyupで定義します。 yup.object().shapeに渡すオブジェクトのプロパティをpasswordにしています。これはregisterで登録したname属性にしてください。 import * as yup from "yup"; // スキーマ const schema = yup.object().shape({ password: yup .string() .min(4, "4桁以上必須") .max(20, "最大20桁") .required("必須です") }); useFormのオプションを設定する useFormの引数に値を渡すことでオプションを設定できます。yupを利用する際にはuseFormのオプションを設定する必要があります。その設定に必要なのが冒頭でインストールしたyupResolverです。yupResolverの引数には先ほど定義したスキーマを渡します。 import { yupResolver } from "@hookform/resolvers/yup"; useForm<IFormInputs>({ // useFormの引数にyupResolverを設定 resolver: yupResolver(schema) }); 全体のコードを確認 上記の2つの工程でyupバリデーションが実現できました。最後にコード全体を見てみます。RHF単体のときよりyupを組み合わせたほうがエラーメッセージがスマートに出し分けできます。 import { useForm, SubmitHandler } from "react-hook-form"; // yupを利用するなら以下をimportする import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; // submitされる値の型定義 type IFormInputs = { password: string; }; // submitをハンドリング const formSubmitHandler: SubmitHandler<IFormInputs> = (data) => { console.log(data); }; // スキーマ const schema = yup.object().shape({ password: yup .string() .min(4, "4桁以上必須") .max(20, "最大20桁") .required("必須です") }); export default function App() { const { register, handleSubmit, watch, formState: { errors } } = useForm<IFormInputs>({ // useFormの引数にyupResolverを設定 resolver: yupResolver(schema) }); return ( <form onSubmit={handleSubmit(formSubmitHandler)}> <input {...register("password")} /> {errors.password && ( // errors.password.messageにはyupのスキーマで設定した値が入る <span style={{ color: "red" }}>{errors.password.message}</span> )} <br /> <input type="submit" /> </form> ); }
- 投稿日:2021-10-31T11:42:10+09:00
Gatsbyでの画像表示方法
画像の用意 src/imagesディレクトリに画像ファイルを保存する。 今回は、Qiitaのロゴ画像を使用する。 ダウンロード元 - https://help.qiita.com/ja/articles/others-brand-guideline src/images/qiita.pingで保存 書き方 src/components/ExampleComponent.jsx import React from 'react'; import QiitaIcon from '../images/qiita.png'; const ExampleComponent = () => ( <div> <img src={ QiitaIcon } alt="Qiita" /> </div> ); export default ExampleComponent; 参考サイト 入門者でもわかるGatsbyでの画像の設定方法
- 投稿日:2021-10-31T00:15:47+09:00
コンポーネントは compnents 以下にフラットに全部置くのが良い
はじめに 巷では Vue.js や React など、コンポーネント指向フレームワークにおいて、どのようにコンポーネントのディレクトリを切り分けるか について議論がなされているようです。 Atomic Design ベースの Vue コンポーネント設計 Atomic Designをやめてディレクトリ構造を見直した話 なに?マクドナルドから学ぶ、優れたcomponentsディレクトリ構造?! 私自身も、ここ 1.5 年ほどフロントエンドの開発を行っており、いくつかのディレクトリ構造を経験しました。 結論、components 以下に全てのコンポーネントをフラットに配置するのが一番しっくり来ています。 フラット構造がオススメの理由 とりあえず概要だけ先に書いておきます。 1年ほど前から全社的に導入しているが、この方法で全く困ったことがない。 どのフォルダに入れるかを全く考えなくていい。 検索が容易。 フラットなディレクトリ構造を実践してみる こんな感じです。 src ├── components │ ├── AppBackButton │ ├── AppSubmitButton │ ├── ArticleCard │ ├── ArticleTextEditor │ ├── ArticleTextColorPallet │ ├── BaseInput │ ├── BaseSelect │ ├── BaseTextArea │ ├── ProfileCard │ ├── ProfileForm │ ├── ProfileFormLayout │ └── ProfileGenderSelect └── pages ├── ArticleListPage ├── ArticleEditPage ├── ProfilePage └── ProfileEditPage ポイント: 命名規則 全てのコンポーネントは原則として 属性 + 要素 という形式で命名されています。 ArticleTextColorPallet Article = 記事投稿機能に関連する TextColorPallet = テキストの色選択要素 AppBackButton App = アプリケーション全体で共通の BackButton = 戻るボタン このように名前を付けることで、どの機能に関連するものなのか、どのような要素なのか、が明確になります。 機能をまたがって使う場合は、App やプロダクト名等のプレフィックスを付けましょう。 検索するときも、機能 → 要素 と絞り込んでいけるので楽です。 ポイント: Base コンポーネント Base という名前がついているコンポーネントは、プロジェクト全体で使われる最も基本的な要素 を指しています。 Atomic Design で言うところの Atoms に相当します。 もちろん Base 以外のプレフィクスを付けても OK です。 チームで合意が取れていれば Core でも Hoge でも何でも OK です。 ポイント: page ディレクトリ 各ページ最上位に当たるコンポーネントは page 以下で管理します。 「全て」フラットじゃないのかよ!!というツッコミもあるかと思います。すいません。 別に components/ だけ問題はありません。Page という要素であることが明確だからです。 我々のチームでは、Page 以外で api 等のリソースを直接使ってはいけない、というルールを敷いていたので、ディレクトリを分けて、Page コンポーネントが特別であるということを表現しました。 他のやり方との比較 Atomic Design 私自身 Atomic Design のディレクトリ構造を試したことがありますが、どのディレクトリかを考えるのは結構大変です。 Atoms は割と決めやすいですが、 molecules と organisms は基準が曖昧になりがちです。 時間をかけて分類しても、得られるものが少なすぎました。 機能ごとにディレクトリを切る方法 先程の例の、Article Profile のような機能ごとにディレクトリを切ってみたりもしていました。 このやり方は、機能ごとにコンポーネントが見やすくなるという点では優れています。 一方で、次のような状況が発生します。 src └── components ├── article | ├── Card // 名前が同じ! | ├── TextEditor | └── TextColorPallet └── profile ├── Card // 名前が同じ! ├── Form └── GenderSelect この状態でファイル名で検索すると Card コンポーネントが複数表示されます。 もちろんディレクトリ名を見れば特定できますが、VsCode の ctrl + p での検索では、ディレクトリ名はやや見づらいです。 細かいことですが、コーディング中は何回も繰り返す動作なので、効果はかなり大きいです。 その他 命名規則は徹底する チーム内で命名規則を決めて、徹底することが大切です。 徹底できれば、名前だけでスコープと機能を伝えることができるようになります! 車輪の再開発を防げる 「こういうコンポーネントつくらないとな...」と思ったときに、ファイル検索をすると、実はもう既に存在していた! みたいなことがちょいちょいあります。 components ディレクトリが巨大になる問題 また、components ディレクトリが巨大なりすぎるんじゃないの? という疑問もあるかも知れません。 はい、なります。開くとズラーッとコンポーネントが表示されます。 でも問題となったことはありません。コンポーネントファイルは検索して開けば良いのです。 命名が長くなりすぎる問題 ArticleTextColorPallet とかは長いな、と感じた方もいるかも知れません。 確かに長いですが、それで問題になったことはありません。 むしろ名前が長いコンポーネントは、それだけ狭い範囲で使われているコンポーネントなんだ、ということが分かります。 逆に名前の短いものは、汎用的で広い範囲で使われているコンポーネントであることが分かります。(例外もありますが。) まとめ いろんなコンポーネント管理方法が記事になっていますが、フラットに管理する、というものはあまり見ないので書いてみました。 もちろんプロジェクトの規模によってベストな方法は変わってくるとは思います。 ですが、我々の社内では小さいプロジェクトから大きいプロジェクトまでこの方法を採用していますが、困ったことは一度もありません。 迷っていたら、是非試してみてください。