やりたいこと
- フロントエンドはReact、バックエンドはRailsを使いたい。
- フロントエンドとバックエンドを一つのプロジェクトで実装したい。
- Webpackの設定を直接書きたい。
Simpackerを使えば全てクリアになります。
クックパッド社が公開したSimpackerとそのサンプルを見て自分なりの最小構成でReactとRailsを組み合わせて見ました。
Webpackの詳細説明は割愛します。
Railsプロジェクトの生成
Railsのプロジェクトのフォルダを生成する。
mkdir react_with_rails
cd react_with_rails
bundle init
Gemfileを開いて「gem "rails"」のところをアンコメントする。
gem "rails"
railsをインストールする。
プロジェクト単位でgemを管理したいので、--pathオプションを付与する。
bundle install --path vendor/bundle --jobs=4
先まではプロジェクトのフォルダを生成してrails gemをインストールしただけ。
本物のプロジェクトを生成する。
Gemfileを上書きするか?のところでYを入力してEnter
-Bは--skip-bundle、gemのインストールは最後で実施するべき
-Sは--skip-sprockets、信じられないけどrails6でもまだ生き残っている。stylesheetのために残しているみたいが、postcssを使いたいので、要らない。
-Tは--skip-test、rspecを使うならminitestは要らない。
-Jは--skip-javascript、rails6ではデフォルトでwebpackerを使っているが、ここではSimpackerを使うので要らない。
-dはデータベースを指定するためのオプション
bundle exec rails new . -B -S -T -J -d mysql
Gemfileを開いてSimpackerを追加する。
gem "simpacker"
gemをインストールする。
bundle
Databaseを生成する。
bin/rails db:create
Reactをインストール
Simpackerでwebpackとwebpack-cliはインストールされるが、webpack-dev-serverはインストールされない。
webpack-dev-serverはwebpackを利用する時必須packageなので、インストールする。
npm i -D webpack-dev-server
React関連packageをインストールする。
npm i -D webpack-dev-server
npm i -S react react-dom
npm i -D babel-loader @babel/core @babel/preset-env @babel/preset-react
Typescript関連packageをインストールする。
ここではtsxは使ってないが、jsxとtsxの両方が使えるようにしたい。
またvimでコード補完のためにもTypescriptが必要(coc-tsserverを使ってる)
npm i -D typescript ts-loader
tsconfig.jsonファイルを生成する。
Typescriptをコンパイルする時の設定ファイル
ここではまだTypescriptを使ってないが、このファイルがない場合はvimのコード補完が作動しない。
{
"compilerOptions": {
"target": "es5",
"lib": ["es2019", "dom", "dom.iterable"],
"module": "es2015",
"jsx": "react",
"moduleResolution": "node",
"esModuleInterop": true,
"downlevelIteration": true,
"sourceMap": true,
"removeComments": false,
"noImplicitAny": false,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true
},
"include": [
"app/javascript/**/*"
]
}
Postcss関連packageをインストールする。
npm i -D css-loader mini-css-extract-plugin postcss-loader postcss-preset-env
Webpackのconfigファイルを編集する。
Webpackerではconfigファイルが隠蔽されているが、SimpackerではWebpackと同じく直接設定しなければならない。
const path = require("path");
const WebpackAssetsManifest = require("webpack-assets-manifest");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { NODE_ENV } = process.env;
const isProd = NODE_ENV === "production";
module.exports = {
mode: isProd ? "production" : "development",
devtool: "source-map",
entry: {
application: path.resolve(__dirname, "app/javascript/application.jsx")
},
output: {
path: path.resolve(__dirname, "public/packs"),
publicPath: isProd ? "/packs/" : "//localhost:8081/packs/",
filename: isProd ? "[name]-[hash].js" : "[name].js"
},
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx"]
},
devServer: {
contentBase: path.resolve(__dirname, "public"),
publicPath: "/packs/",
host: "localhost",
port: 8081,
headers: {
"Access-Control-Allow-Origin": "*"
}
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
plugins: loader => [require("postcss-preset-env")()]
}
}
]
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"]
}
}
},
{
test: /\.tsx?$/,
loader: "ts-loader",
options: {
transpileOnly: true
}
}
]
},
plugins: [
new WebpackAssetsManifest({ publicPath: true, writeToDisk: true }),
new MiniCssExtractPlugin({
filename: isProd ? "[name]-[hash].css" : "[name].css"
})
]
};
サンプル
Simpackerのgithubにサンプルが用意されている。
gemの公開だけではなく色んなケースに対応するサンプルが忠実に用意されている。
クックパッドさんすごい!
app/javascript/greeter.jsx
import React from "react";
export const Hello = props => <div>Hello {props.name}!</div>;
app/javascript/application.css
:root {
--mainColor: tomato;
}
body {
color: var(--mainColor);
}
app/javascript/application.jsx
import React from "react";
import ReactDOM from "react-dom";
import { Hello } from "./greeter";
import "./application.css";
document.addEventListener("DOMContentLoaded", () => {
ReactDOM.render(
<Hello name="Rails" />, document.getElementById("app")
);
})
DOMツリーの構築が完了した時点で発火する必要がある。
そうでなければ、Target container is not a DOM element.というエラーが発生する。
どうしてもDOMContentLoadedイベントを追加したくない場合は、applicationレイアウトからjavascript_pack_tagをbodyタグの一番下に移動する。
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>ReactWithRails</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_pack_tag 'application' %>
<%= javascript_pack_tag 'application' %>
</head>
<body>
<%= yield %>
</body>
</html>
適当なcontrollerとviewを作って確認する。
bin/rails g controller hello world
app/views/hello/world.html.erb
<h1>Hello#world</h1>
<p>Find me in app/views/hello/world.html.erb</p>
<div id="app"></div> # ここだけ追加
production環境ではコンパイルされたjsを使うが、developmentではwebpack-dev-serverを立ち上げる必要がある。
pumaとwebpack-dev-serverを別々に立ち上げるのは面倒なので、Procfileを作成する。
Procfile
server: bin/rails server
assets: ./node_modules/.bin/webpack-dev-server
サーバーを起動する。
ここではovermindを使う。
まだ使ったことがない人はHomebrewでインストールしよう。
overmind s
system | Tmux socket name: overmind-react-with-rails-SlxVtc-lkjdgVfnNPgfz5e
system | Tmux session ID: react-with-rails
system | Listening at /Users/devtopia/apps/study/js/react_with_rails/.overmind.sock
assets | Started with pid 42087...
server | Started with pid 42086...
assets | ℹ 「wds」: Project is running at http://localhost:8081/
assets | ℹ 「wds」: webpack output is served from /packs/
assets | ℹ 「wds」: Content not from webpack is served from /Users/devtopia/apps/study/js/react_with_rails/public
server | => Booting Puma
server | => Rails 6.0.0 application starting in development
server | => Run `rails server --help` for more startup options
server | Puma starting in single mode...
server | * Version 3.12.1 (ruby 2.6.4-p104), codename: Llamas in Pajamas
server | * Min threads: 5, max threads: 5
server | * Environment: development
server | * Listening on tcp://localhost:5000
server | Use Ctrl-C to stop
assets | ℹ 「wdm」: Hash: 1b641e1f282acd737e26
assets | Version: webpack 4.41.2
assets | Time: 1572ms
assets | Built at: 2019-11-03 1:16:33 AM
assets | Asset Size Chunks Chunk Names
assets | application.css 131 bytes application [emitted] application
assets | application.css.map 289 bytes application [emitted] [dev] application
assets | application.js 1.39 MiB application [emitted] application
assets | application.js.map 1.61 MiB application [emitted] [dev] application
assets | manifest.json 266 bytes [emitted]
assets | Entrypoint application = application.css application.js application.css.map application.js.map
assets | [0] multi (webpack)-dev-server/client?http://localhost:8081 ./app/javascript/application.jsx 40 bytes {application} [built]
assets | [./app/javascript/application.css] 39 bytes {application} [built]
assets | [./app/javascript/application.jsx] 291 bytes {application} [built]
assets | [./app/javascript/greeter.jsx] 146 bytes {application} [built]
assets | [./node_modules/react-dom/index.js] 1.33 KiB {application} [built]
assets | [./node_modules/react/index.js] 190 bytes {application} [built]
assets | [./node_modules/webpack-dev-server/client/index.js?http://localhost:8081] (webpack)-dev-server/client?http://localhost:8081 4.29 KiB {application} [built]
assets | [./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.51 KiB {application} [built]
assets | [./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.53 KiB {application} [built]
assets | [./node_modules/webpack-dev-server/client/utils/createSocketUrl.js] (webpack)-dev-server/client/utils/createSocketUrl.js 2.89 KiB {application} [built]
assets | [./node_modules/webpack-dev-server/client/utils/log.js] (webpack)-dev-server/client/utils/log.js 964 bytes {application} [built]
assets | [./node_modules/webpack-dev-server/client/utils/reloadApp.js] (webpack)-dev-server/client/utils/reloadApp.js 1.59 KiB {application} [built]
assets | [./node_modules/webpack-dev-server/client/utils/sendMessage.js] (webpack)-dev-server/client/utils/sendMessage.js 402 bytes {application} [built]
assets | [./node_modules/webpack-dev-server/node_modules/strip-ansi/index.js] (webpack)-dev-server/node_modules/strip-ansi/index.js 161 bytes {application} [built]
assets | [./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {application} [built]
assets | + 32 hidden modules
assets | Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js??ref--4-2!app/javascript/application.css:
assets | Entrypoint mini-css-extract-plugin = *
assets | [./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/src/index.js?!./app/javascript/application.css] ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/src??ref--4-2!./app/javascript/application.css 236 bytes {mini-css-extract-plugin} [built]
assets | [./node_modules/css-loader/dist/runtime/api.js] 2.61 KiB {mini-css-extract-plugin} [built]
assets | ℹ 「wdm」: Compiled successfully.
結果