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" />
<!--