Webpackerはもう要らない〜 Simpacker

## やりたいこと

* フロントエンドはReact、バックエンドはRailsを使いたい。
* フロントエンドとバックエンドを一つのプロジェクトで実装したい。
* Webpackの設定を直接書きたい。

Simpackerを使えば全てクリアになります。
クックパッド社が公開したSimpackerとそのサンプルを見て自分なりの最小構成でReactとRailsを組み合わせて見ました。
Webpackの詳細説明は割愛します。

## Railsプロジェクトの生成

Railsのプロジェクトのフォルダを生成する。

```sh
mkdir react_with_rails
cd react_with_rails
bundle init
```

Gemfileを開いて「gem "rails"」のところをアンコメントする。

```ruby
gem "rails"
```

railsをインストールする。
プロジェクト単位でgemを管理したいので、--pathオプションを付与する。

```sh
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はデータベースを指定するためのオプション

```sh
bundle exec rails new . -B -S -T -J -d mysql
```

Gemfileを開いてSimpackerを追加する。

```sh
gem "simpacker"
```

gemをインストールする。

```sh
bundle
```

Databaseを生成する。

```sh
bin/rails db:create
```

## Reactをインストール

Simpackerでwebpackとwebpack-cliはインストールされるが、webpack-dev-serverはインストールされない。
webpack-dev-serverはwebpackを利用する時必須packageなので、インストールする。

```sh
npm i -D webpack-dev-server
```

React関連packageをインストールする。

```sh
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を使ってる）

```sh
npm i -D typescript ts-loader
```

tsconfig.jsonファイルを生成する。
Typescriptをコンパイルする時の設定ファイル
ここではまだTypescriptを使ってないが、このファイルがない場合はvimのコード補完が作動しない。

```json
{
  "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をインストールする。

```sh
npm i -D css-loader mini-css-extract-plugin postcss-loader postcss-preset-env
```

Webpackのconfigファイルを編集する。
Webpackerではconfigファイルが隠蔽されているが、SimpackerではWebpackと同じく直接設定しなければならない。

```javascript
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

```ruby
import React from "react";

export const Hello = props => <div>Hello {props.name}!</div>;
```

app/javascript/application.css

```css
:root {
  --mainColor: tomato;
}

body {
  color: var(--mainColor);
}
```

app/javascript/application.jsx

```ruby
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

```ruby
<!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を作って確認する。

```sh
bin/rails g controller hello world
```

app/views/hello/world.html.erb

```ruby
<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

```sh
server: bin/rails server
assets: ./node_modules/.bin/webpack-dev-server
```

サーバーを起動する。
ここではovermindを使う。
まだ使ったことがない人はHomebrewでインストールしよう。

```sh
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.
```

結果

http://localhost:5000/hello/world

<img width="1072" alt="スクリーンショット 2019-11-03 1.33.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93752/8d93e47e-88ba-c623-cc5b-13cad8923d32.png">

## 参考

* https://techlife.cookpad.com/entry/2019/07/08/100000
* https://github.com/hokaccha/simpacker