React+Typescript+Webpackの環境構築

最近、React+Typescriptによるweb開発を加速するため、まずは色々と使いまわしができるwebブラウザ上で動くHTMLエディタを開発していました。

エディターの基本部分は、draft.jsを利用して、リッチスタイルの機能と、Webページに必須なLink, 分割するための線、画像、文字色変更などの機能を自作して追加をしました。

さて、今回の記事の本題ですが、

  • このエディターをReactのコンポーネントとして様々なアプリケーションに組み込めるようにする
  • exampleフォルダにコンポーネントを動かすサンプルプログラムを置いておき、簡単に利用できるようにする。

を実現したときのまとめです。

最終的なフォルダ構成(一部抜粋)はこのとおりです。

.
├── dist
│   └── editor.bundle.js
├── example
│   ├── public
│   │   ├── favicon.ico
│   │   ├── index.html
│   │   ├── logo192.png
│   │   ├── logo512.png
│   │   ├── manifest.json
│   │   └── robots.txt
│   ├── src
│   │   ├── App.css
│   │   ├── App.test.tsx
│   │   ├── App.tsx
│   │   ├── index.css
│   │   ├── index.tsx
│   │   ├── logo.svg
│   │   ├── react-app-env.d.ts
│   │   ├── reportWebVitals.ts
│   │   └── setupTests.ts
│   ├── tsconfig.json
│   └── webpack.config.js
├── package-lock.json
├── package.json
├── src
│   ├── Components
│   │   ├── BlockStyle.tsx
│   │   ├── Color.tsx
│   │   ├── Divider.tsx
│   │   ├── InlineStyle.tsx
│   │   ├── Link.tsx
│   │   └── Media.tsx
│   ├── EditorMenu.tsx
│   ├── common.ts
│   ├── index.tsx
│   └── richeditor.css
├── tsconfig.json
├── types
│   ├── Components
│   │   ├── BlockStyle.d.ts
│   │   ├── Color.d.ts
│   │   ├── Divider.d.ts
│   │   ├── InlineStyle.d.ts
│   │   ├── Link.d.ts
│   │   └── Media.d.ts
│   ├── EditorMenu.d.ts
│   ├── common.d.ts
│   └── index.d.ts
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js

エディターの基本部分は./srcに保存されています。このエディターを動かすREACTのアプリケーションは./example/srcに保存されています。そして、再利用するときには./dist/editor.bundle.js を利用します。

また、reactのプロジェクトはcreate-react-appを使用してtypescriptのテンプレートを作成をしました。

npx create-react-app {プロジェクト名} --template typescript

package.json

開発用と配布用でwebpackの設定ファイルを変えています。なお、package.jsonのなかで本プロジェクトでしか使用しない固有のパッケージはdependenciesから削除しています。

組込用(リリース) > npm run build

再配布用のパッケージをビルドします。

組込用(開発) > npm run dev

組み込んで開発するためのパッケージをビルドします。

組込用(開発) > npm run watch

上記と同じ組み込んで開発するためのバージョンですが、ファイルの変更を監視して、変更があると再度ビルドを行います。

開発用 > npm run start

開発用のサーバーを立ててデバックをするようにしました。

{
	"name": "react-draftjs-editor",
	"version": "0.1.0",
	"types": "types/index.d.ts",
	"main": "dist/editor.bundle.js",
	"dependencies": {
		"@emotion/react": "^11.8.2",
		"@emotion/styled": "^11.8.1",
		"@mui/icons-material": "^5.5.1",
		"@mui/material": "^5.5.2",
		"@mui/styled-engine": "^5.5.2",
		"draft-js": "^0.11.7",
		"draft-js-export-html": "^1.4.1",
		"react": "^17.0.2",
		"react-dom": "^17.0.2",
		"web-vitals": "^2.1.4"
	},
	"scripts": {
		"build": "webpack --config webpack.prod.js",
		"dev": "webpack --config webpack.dev.js",
		"watch": "webpack --watch --config webpack.dev.js",
		"start": "webpack-dev-server --config example/webpack.config.js --port 3000 --host 127.0.0.1 --progress --profile --static example/"
	},
	"devDependencies": {
		"@babel/cli": "^7.17.6",
		"@babel/core": "^7.17.8",
		"@babel/plugin-proposal-class-properties": "^7.16.7",
		"@babel/plugin-proposal-export-default-from": "^7.16.7",
		"@babel/plugin-proposal-export-namespace-from": "^7.16.7",
		"@babel/plugin-proposal-function-bind": "^7.16.7",
		"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
		"@babel/plugin-transform-runtime": "^7.17.0",
		"@babel/preset-env": "^7.16.11",
		"@babel/preset-react": "^7.16.7",
		"@babel/preset-typescript": "^7.16.7",
		"@babel/runtime": "^7.17.8",
		"@types/draft-js": "^0.11.9",
		"@types/jest": "^27.4.1",
		"@types/node": "^16.11.26",
		"@types/react": "^17.0.40",
		"@types/react-dom": "^17.0.14",
		"babel-eslint": "^10.1.0",
		"babel-loader": "^8.2.3",
		"babel-plugin-istanbul": "^6.1.1",
		"babel-plugin-lodash": "^3.3.4",
		"babel-preset-airbnb": "^5.0.0",
		"css-loader": "^6.7.1",
		"html-webpack-plugin": "^5.5.0",
		"react-scripts": "^5.0.0",
		"react-svg-loader": "^3.0.3",
		"style-loader": "^3.3.1",
		"ts-loader": "^9.2.8",
		"typescript": "^4.6.2",
		"webpack": "^5.70.0",
		"webpack-bundle-analyzer": "^4.5.0",
		"webpack-cli": "^4.9.2",
		"webpack-dev-server": "^4.7.4"
	}
}

再配布用 > webpack.prod.js

javascriptをmidifiedするTerserPluginの設定だけ行っています。

const {merge} = require('webpack-merge') // webpack-merge
const common = require('./webpack.common.js') // 汎用設定をインポート
const path = require('path');
const TerserPlugin = require("terser-webpack-plugin");

module.exports = merge(common,{
	mode: 'production',
	optimization: {
		minimizer: [
			new TerserPlugin({
				extractComments: false, //falseでライセンスコメントを抽出し削除。 trueだと抽出したファイルを生成する
				terserOptions: {
					compress: {
						drop_console: true, //console.logは削除する
					},
				},
			}),
		],
	},
});

組込開発用 > webpack.dev.js

javascriptをmidifiedするTerserPluginとデバック用のsourceMapの出力を行っています。

const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");
const path = require('path');
const TerserPlugin = require("terser-webpack-plugin");

module.exports = merge(common, {
	mode: 'development',
	devtool: 'source-map',
	optimization: {
		minimizer: [
			new TerserPlugin({
				extractComments: false, //falseでライセンスコメントを抽出し削除。 trueだと抽出したファイルを生成する
				terserOptions: {
					compress: {
						drop_console: false, //console.logを残す
					},
				},
			}),
		],
	},
});

再配布(共通) > webpack.common.js

const path = require('path');
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
	entry: './src/index.tsx',
	output: {
		filename: 'editor.bundle.js',
		library: 'EditorApp',
		globalObject: 'this',
		libraryTarget: 'umd',
		path: path.resolve(__dirname, 'dist'),
		clean: true,
	},
	resolve: {
		extensions: [".ts", ".js", ".tsx", ".jsx"], // コンパイルする言語指定
	},
	module: {
		rules: [{
			test: /\.(js|ts|tsx)$/,
			use: {
				loader: 'babel-loader',
			},
			exclude: /node_modules/,
			include: [
				__dirname,
				path.join(__dirname, 'src'),
			],
		}, {
			test: /\.(ts|tsx)$/,
			exclude: /node_modules/,
			include: [
				path.resolve(__dirname, 'src'),
			],
			use: {
				loader: 'ts-loader',
			}
		}, {
			test: /\.css$/,
			use: ["style-loader", "css-loader"]
		}, {
			test: /\.svg$/,
			use: [
				{
					loader: "babel-loader"
				},
				{
					loader: "react-svg-loader",
					options: {
						jsx: true // true outputs JSX tags
					}
				}
			]
		}],
	}
};

開発用 > example/webpack.config.js

  • ライブラリ単体のビルドと比べてソースのentryが異なります。
  • HtmlWebpackPluginでReactのindex.htmlを指定しています。
  • resolve.aliasで、本プロジェクトの保存場所を置き換えしています。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { LoaderOptionsPlugin } = require('webpack');

module.exports = {
    mode: 'development',
    context: __dirname,
    devtool: 'inline-source-map',       //sourceMapを出力する
    entry: [
        path.join(__dirname, 'src/index.tsx'),
    ],
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.js',
    },
    cache: true,
    plugins: [
        new LoaderOptionsPlugin({
            debug: true
        }),
        new HtmlWebpackPlugin({                              //reactのテンプレートを読み込む
            template: './public/index.html',
            filename: 'index.html',
        })
    ],
    resolve: {
        alias: {
            "react-draftjs-editor": path.join(__dirname, '..', 'src/index.tsx'),    // 開発しているプロジェクトは../src/index.tsxを読み込む
        },
        extensions: ['.json', '.js', '.jsx', '.ts', '.tsx'],
    },
    module: {
        rules: [{
            test: /\.(js|ts|tsx)$/,       // ts, tsxはbabel-loaderとts-loaderを利用してロードする(両方併用しないとエラーが出た)
            use: {
                loader: 'babel-loader',
            },
            exclude: /node_modules/,
            include: [
                __dirname,
                path.join(__dirname, '..', 'src'),
            ],
        }, {
            test: /\.(ts|tsx)$/,
            exclude: /node_modules/,
            include: [
                path.resolve(__dirname, 'src'),
            ],
            use: {
                loader: 'ts-loader',
            }
        }, {
            test: /\.css$/,     // cssのファイル読み込みモジュールを指定
            use: ["style-loader", "css-loader"]
        },
        {
            test: /\.svg$/,
            use: [
                {
                    loader: "babel-loader"
                },
                {
                    loader: "react-svg-loader",
                    options: {
                        jsx: true // true outputs JSX tags
                    }
                }
            ]
        }
        ],
    },
};

%PUBLIC_URL%の修正

create-react-appで作成したexample/public/index.htmlでエラーが発生してしまいました。%PUBLIC_URL%という記述が3箇所あります。これはreact-scriptsで作成した環境だと%PUBLIC_URL%を実在するURLに置き換えてくれるのですが、それをwebpackの環境に変えたら変換してくれなくなりました。これは解決方法が解らなかったので、%PUBLIC_URL%をpublicに置き換えました。

example/public/index.html

    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--