EJSをやめてReactでHTMLを書く
こんにちはスクラップ
この記事を参考にReactでHTMLを生成する
リポジトリ
しょっぱなからタイポしてた😂
$ npm init -y
npmの雛形を作る
{
"name": "react-stati-generator",
"version": "1.0.0",
"license": "MIT",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
いらない記述を削除して"private": true,
を追記する
一旦プッシュする
ブランチはdevelop
webpackは4.x系を使うので4.4を入れるcliは最新でも問題無さそう
$ npm i -D webpack@4.44.2 webpack-cli
webpackのconfigファイルを作成する
$ touch webpack.config.js
忘れないうちにコマンドを登録しておく
{
"name": "react-stati-generator",
"version": "1.0.0",
"license": "MIT",
"private": true,
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
+ "webpack-watch": "webpack -w",
+ "webpack-build": "webpack"
},
"devDependencies": {
"webpack": "^4.44.2",
"webpack-cli": "^4.2.0"
}
}
普段はGUIから作るけど今回はコマンドでファイルを作ってみる
$ mkdir src && mkdir src/pages && touch src/pages/index.jsx
一応普通のjsも作っておく
$ mkdir src/js && touch src/js/main.js
適当にmain.jsに書いとく
console.log('test');
とりあえずいつも使ってる設定ファイルからいらないところを消して、webpack.config.jsの中身を書く
const webpack = require('webpack');
const path = require('path');
module.exports = () => {
const MODE = process.env.NODE_ENV;
const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
return {
mode: MODE,
devtool: IS_DEVELOPMENT ? 'inline-source-map' : false,
entry: {
"dist/assets/js/main": "./src/js/main",
},
resolve: {
extensions: ['.ts', '.tsx', '.jsx', '.js'],
},
output: {
filename: '[name].js',
path: path.join(__dirname),
},
module: {},
plugins: [
new webpack.ProgressPlugin(),
],
}
};
赤いびよびよが出てくるので@typesを入れる
$ npx typesync
@types/nodeもいるな?🤔
$ npm i -D @types/node
process.env.NODE_ENVを使うのでnpm scriptsを増やす
npm run allを入れる
$ npm i -D npm-run-all
とりあえずstartとbuildを作る
{
"name": "react-stati-generator",
"version": "1.0.0",
"license": "MIT",
"private": true,
"scripts": {
+ "start": "NODE_ENV=development run-p _start",
+ "build": "NODE_ENV=production run-p _build",
"webpack-watch": "webpack -w",
"webpack-build": "webpack",
+ "_start": "run-s webpack-watch",
+ "_build": "run-s webpack-build"
},
"devDependencies": {
"@types/node": "^14.14.10",
"@types/webpack": "^4.41.25",
"npm-run-all": "^4.1.5",
"webpack": "^4.44.2",
"webpack-cli": "^4.2.0"
}
}
忘れないうちにReadMeに書いとかないと
react-static-generator
Command
Install
$ npm ci
Watch
$ npm start
Build
$ npm run build
コマンドが動くか確認する
[webpack-cli] Compilation starting...
98% after emitting[webpack-cli] Compilation finished
Hash: af324363234bc97f2776
Version: webpack 4.44.2
Time: 60ms
Built at: 2020/12/06 16:30:02
Asset Size Chunks Chunk Names
dist/assets/js/main.js 8.62 KiB dist/assets/js/main [emitted] dist/assets/js/main
Entrypoint dist/assets/js/main = dist/assets/js/main.js
[./src/js/main.js] 20 bytes {dist/assets/js/main} [built]
[webpack-cli] watching files for updates...
$ NODE_ENV=production run-p _build
$ run-s webpack-build
$ webpack
98% after emitting SizeLimitsPlugin[webpack-cli] Compilation finished
Hash: b1ec4be4a855a7482c28
Version: webpack 4.44.2
Time: 188ms
Built at: 2020/12/06 16:30:12
Asset Size Chunks Chunk Names
dist/assets/js/main.js 949 bytes 0 [emitted] dist/assets/js/main
Entrypoint dist/assets/js/main = dist/assets/js/main.js
[0] ./src/js/main.js 20 bytes {0} [built]
✨ Done in 2.70s.
コミットしておく
gitignore作るの忘れたから危うくnode_modules上げるところだった😂
$ touch .gitignore
node_modules
一応distもignoreしておこう🤔
node_modules
+ dist
まだ本編にすら到達していない😂
tsxでやりたいから全部リネームするか
まずはts-loaderの設定をちゃちゃっと
$ npm i -D ts-loader fork-ts-checker-webpack-plugin
めんどくさいので設定は普段使ってるやつを持ってくる
$ touch tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"jsx": "react",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"incremental": true,
"tsBuildInfoFile": "./.cache/ts/.tsbuildinfo",
"importHelpers": true,
"downlevelIteration": true,
"lib": ["dom", "es2015", "dom.iterable"],
"typeRoots": ["src/@types", "node_modules/@types"],
"baseUrl": ".",
"strict": true,
"moduleResolution": "node",
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true
}
}
const webpack = require('webpack');
const path = require('path');
+const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = () => {
const MODE = process.env.NODE_ENV;
const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
return {
mode: MODE,
devtool: IS_DEVELOPMENT ? 'inline-source-map' : false,
entry: {
"dist/assets/js/main": "./src/js/main",
},
resolve: {
extensions: ['.ts', '.tsx', '.jsx', '.js'],
},
output: {
filename: '[name].js',
path: path.join(__dirname),
},
module: {
+ rules: [
+ {
+ test: /\.(tsx?|jsx?)$/,
+ use: [
+ {
+ loader: 'ts-loader',
+ options: {
+ transpileOnly: true,
+ experimentalWatchApi: true,
+ }
+ }
+ ],
+ }
+ ]
},
plugins: [
+ new ForkTsCheckerWebpackPlugin(),
new webpack.ProgressPlugin(),
],
}
};
ここも普段はGUIだけどCUIで拡張子を変えてみる
$ mv src/js/main.js src/js/main.ts && mv src/pages/index.jsx src/pages/index.tsx
ちゃんとtypescriptかどうか適当にテストしてみる
function test(args: number): number {
return args;
}
console.log(test(3));
console.log(test('3'));
$ NODE_ENV=production run-p _build
$ run-s webpack-build
$ webpack
94% after seal[webpack-cli] Compilation finished
Hash: c4ee805885803dff91a5
Version: webpack 4.44.2
Time: 1038ms
Built at: 2020/12/06 16:56:39
Asset Size Chunks Chunk Names
dist/assets/js/main.js 976 bytes 0 dist/assets/js/main
Entrypoint dist/assets/js/main = dist/assets/js/main.js
[0] ./src/js/main.ts 101 bytes {0} [built]
ERROR in src/js/main.ts:6:18
TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
4 |
5 | console.log(test(3));
> 6 | console.log(test('3'));
| ^^^
うまくいったのでmain.ts
の内容をエラーが起きないようにする
console.log('ああああああああ');
なんかこのスクラップハンズオンでなにかするのに良さそう(?)
tsに変えた分を全部コミットしておく
.cache/ts/.tsbuildinfo
はgitで共有する必要が無いのでignoreする
node_modules
dist
+.cache
やっと本編!
html-webpack-pluginとReactを入れる
$ npm i -D html-webpack-plugin
Reactは普通に使うかもしれないので、dependenciesに入れる
$ npm i react react-dom
@typesはめんどくさいのでnpx typesyncで一気に入れる
$ npx typesync
webpacckとindex.tsxを書き換える
const webpack = require('webpack');
const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
+const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = () => {
const MODE = process.env.NODE_ENV;
const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
return {
mode: MODE,
devtool: IS_DEVELOPMENT ? 'inline-source-map' : false,
entry: {
"dist/assets/js/main": "./src/js/main",
},
resolve: {
extensions: ['.ts', '.tsx', '.jsx', '.js'],
},
output: {
filename: '[name].js',
path: path.join(__dirname),
},
module: {
rules: [
{
test: /\.(tsx?|jsx?)$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true,
}
}
],
}
]
},
plugins: [
+ new HtmlWebpackPlugin({
+ title: "My App",
+ template: "src/pages/index.tsx",
+ }),
new ForkTsCheckerWebpackPlugin(),
new webpack.ProgressPlugin(),
],
}
};
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
const App = () => <div>Use React !!</div>;
export default ({ htmlWebpackPlugin }) => `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${htmlWebpackPlugin.options.title}</title>
</head>
<body>
${renderToStaticMarkup(<App />)}
</body>
</html>
`;
$ NODE_ENV=production run-p _build
$ run-s webpack-build
$ webpack
94% after seal[webpack-cli] Compilation finished
Hash: 85dd6deab4f9bfb87e91
Version: webpack 4.44.2
Time: 1330ms
Built at: 2020/12/06 17:31:55
Asset Size Chunks Chunk Names
dist/assets/js/main.js 984 bytes 0 dist/assets/js/main
Entrypoint dist/assets/js/main = dist/assets/js/main.js
[0] ./src/js/main.ts 55 bytes {0} [built]
ERROR in src/pages/index.tsx:1:19
TS7016: Could not find a declaration file for module 'react'. '/Users/name/github/react-static-generator/node_modules/react/index.js' implicitly has an 'any' type.
Try `npm install @types/react` if it exists or add a new declaration (.d.ts) file containing `declare module 'react';`
> 1 | import React from "react";
| ^^^^^^^
2 | import { renderToStaticMarkup } from "react-dom/server";
3 |
4 | const App = () => <div>Use React !!</div>;
ERROR in src/pages/index.tsx:2:38
TS7016: Could not find a declaration file for module 'react-dom/server'. '/Users/name/github/react-static-generator/node_modules/react-dom/server.js' implicitly has an 'any' type.
Try `npm install @types/react-dom` if it exists or add a new declaration (.d.ts) file containing `declare module 'react-dom/server';`
1 | import React from "react";
> 2 | import { renderToStaticMarkup } from "react-dom/server";
| ^^^^^^^^^^^^^^^^^^
3 |
4 | const App = () => <div>Use React !!</div>;
5 |
ERROR in src/pages/index.tsx:4:19
TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.
2 | import { renderToStaticMarkup } from "react-dom/server";
3 |
> 4 | const App = () => <div>Use React !!</div>;
| ^^^^^
5 |
6 | export default ({ htmlWebpackPlugin }) => `
7 | <!DOCTYPE html>
ERROR in src/pages/index.tsx:4:36
TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.
2 | import { renderToStaticMarkup } from "react-dom/server";
3 |
> 4 | const App = () => <div>Use React !!</div>;
| ^^^^^^
5 |
6 | export default ({ htmlWebpackPlugin }) => `
7 | <!DOCTYPE html>
ERROR in src/pages/index.tsx:6:19
TS7031: Binding element 'htmlWebpackPlugin' implicitly has an 'any' type.
4 | const App = () => <div>Use React !!</div>;
5 |
> 6 | export default ({ htmlWebpackPlugin }) => `
| ^^^^^^^^^^^^^^^^^
7 | <!DOCTYPE html>
8 | <html lang="ja">
9 | <head>
Child HtmlWebpackCompiler:
Asset Size Chunks Chunk Names
__child-HtmlWebpackPlugin_0 33.5 KiB 0 HtmlWebpackPlugin_0
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
[0] ./node_modules/react/index.js 190 bytes {0} [built]
[1] ./node_modules/object-assign/index.js 2.06 KiB {0} [built]
[2] ./node_modules/react-dom/server.browser.js 228 bytes {0} [built]
[3] ./node_modules/html-webpack-plugin/lib/loader.js!./src/pages/index.tsx 601 bytes {0} [built]
[4] ./node_modules/react/cjs/react.production.min.js 6.3 KiB {0} [built]
[5] ./node_modules/react-dom/cjs/react-dom-server.browser.production.min.js 19.8 KiB {0} [built]
めちゃくちゃ怒られた
Try `npm install @types/react` if it exists or add a new declaration (.d.ts) file containing `declare module 'react';
typesyncしたのに@types/reactが無いらしいのでもう一度npm installしてみる
とりあえずエラーを消していく
FC付ける必要あるのかわからないけどとりあえず
+import React, {FC} from "react";
import {renderToStaticMarkup} from "react-dom/server";
+const App: FC = () => <div>Use React !!</div>;
export default ({htmlWebpackPlugin}) => `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${htmlWebpackPlugin.options.title}</title>
</head>
<body>
${renderToStaticMarkup(<App/>)}
</body>
</html>
`;
export default ({htmlWebpackPlugin}) =>
の型がわからないけど、html-webpack-pluginの型定義を見に行くと
/**
* The plugin options after adding default values
*/
interface ProcessedOptions {
...
}
この部分っぽい
import React, {FC} from "react";
import {renderToStaticMarkup} from "react-dom/server";
+import htmlWebpackPlugin from 'html-webpack-plugin';
const App: FC = () => <div>Use React !!</div>;
+export default ({htmlWebpackPlugin}: { htmlWebpackPlugin: htmlWebpackPlugin.ProcessedOptions }) => `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${htmlWebpackPlugin.options.title}</title>
</head>
<body>
${renderToStaticMarkup(<App/>)}
</body>
</html>
`;
とりあえず、エラーは全部消えた
$ NODE_ENV=production run-p _build
$ run-s webpack-build
$ webpack
98% after emitting SizeLimitsPlugin[webpack-cli] Compilation finished
Hash: 87c7784d007b2445c334
Version: webpack 4.44.2
Time: 1765ms
Built at: 2020/12/06 17:41:55
Asset Size Chunks Chunk Names
dist/assets/js/main.js 984 bytes 0 [emitted] dist/assets/js/main
index.html 243 bytes [emitted]
Entrypoint dist/assets/js/main = dist/assets/js/main.js
[0] ./src/js/main.ts 55 bytes {0} [built]
Child HtmlWebpackCompiler:
Asset Size Chunks Chunk Names
__child-HtmlWebpackPlugin_0 46.1 KiB 0 HtmlWebpackPlugin_0
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
[0] ./node_modules/react/index.js 193 bytes {0} [built]
[1] ./node_modules/object-assign/index.js 2.43 KiB {0} [built]
[2] ./node_modules/react-dom/server.browser.js 231 bytes {0} [built]
[3] ./node_modules/html-webpack-plugin/lib/loader.js!./src/pages/index.tsx 601 bytes {0} [built]
[4] ./node_modules/react/cjs/react.production.min.js 8.12 KiB {0} [built]
[5] ./node_modules/react-dom/cjs/react-dom-server.browser.production.min.js 30.2 KiB {0} [built]
✨ Done in 4.78s.
一旦コミットしておこう
index.htmlをdistに吐き出したいので設定を変える
タイトルいらないので消す
scriptの自動挿入もいらないので消す
const webpack = require('webpack');
const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = () => {
const MODE = process.env.NODE_ENV;
const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
return {
mode: MODE,
devtool: IS_DEVELOPMENT ? 'inline-source-map' : false,
entry: {
"dist/assets/js/main": "./src/js/main",
},
resolve: {
extensions: ['.ts', '.tsx', '.jsx', '.js'],
},
output: {
filename: '[name].js',
path: path.join(__dirname),
},
module: {
rules: [
{
test: /\.(tsx?|jsx?)$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true,
}
}
],
}
]
},
plugins: [
new HtmlWebpackPlugin({
- title: "My App",
template: "src/pages/index.tsx",
+ filename: 'dist/index.html',
+ inject: false,
}),
new ForkTsCheckerWebpackPlugin(),
new webpack.ProgressPlugin(),
],
}
};
$ NODE_ENV=production run-p _build
$ run-s webpack-build
$ webpack
98% after emitting SizeLimitsPlugin[webpack-cli] Compilation finished
Hash: 87c7784d007b2445c334
Version: webpack 4.44.2
Time: 1357ms
Built at: 2020/12/06 17:52:24
Asset Size Chunks Chunk Names
dist/assets/js/main.js 984 bytes 0 [emitted] dist/assets/js/main
dist/index.html 202 bytes [emitted]
Entrypoint dist/assets/js/main = dist/assets/js/main.js
[0] ./src/js/main.ts 55 bytes {0} [built]
Child HtmlWebpackCompiler:
Asset Size Chunks Chunk Names
__child-HtmlWebpackPlugin_0 46.1 KiB 0 HtmlWebpackPlugin_0
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
[0] ./node_modules/react/index.js 193 bytes {0} [built]
[1] ./node_modules/object-assign/index.js 2.43 KiB {0} [built]
[2] ./node_modules/react-dom/server.browser.js 231 bytes {0} [built]
[3] ./node_modules/html-webpack-plugin/lib/loader.js!./src/pages/index.tsx 601 bytes {0} [built]
[4] ./node_modules/react/cjs/react.production.min.js 8.12 KiB {0} [built]
[5] ./node_modules/react-dom/cjs/react-dom-server.browser.production.min.js 30.2 KiB {0} [built]
✨ Done in 3.40s.
distに出てきた!
pages/の中でheadを書きたくないのでLayoutsを作る
$ mkdir src/layouts && touch src/layouts/index.tsx
先にさっきの吐き出し先を変えた変更をpushしとこう
とりあえずレイアウトに切り出し
import React, {FC, ReactNode} from 'react'
type LayoutType = {
children: ReactNode
}
export const Layout: FC<LayoutType> = ({children}) => {
return (
<>
<html lang="ja">
<head>
<meta charSet="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>タイトル</title>
</head>
<body>
{children}
</body>
</html>
</>
)
}
残ったもの
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import {Layout} from "../layouts";
export default () => {
return '<!DOCTYPE html>' + renderToStaticMarkup(
<Layout>
テスト
</Layout>
)
};
Reactで<!DOCTYPE html>できない?よくわからないから
return '<!DOCTYPE html>' + renderToStaticMarkup(
<Layout>
テスト
</Layout>
)
こうしたけど、ダサいな🤔
renderToStaticMarkup()をwrapした関数を作る事で(いいかどうかは置いといて)とりあえず、解決だ😎
名前も思いつかないしhooksフォルダに入れても良いかわからないけどとりあえず作る
$ mkdir src/hooks && touch src/hooks/index.ts && touch src/hooks/staticRender.ts
renderToStaticMarkupなのでmyRenderToStaticMarkupにする
$ mv src/hooks/staticRender.ts src/hooks/myRenderToStaticMarkup.ts
renderToStaticMarkupのwrapper
import {renderToStaticMarkup} from 'react-dom/server';
import {ReactElement} from "react";
export function myRenderToStaticMarkup(element: ReactElement): string {
return '<!DOCTYPE html>' + renderToStaticMarkup(element);
}
hooksのエントリーポイント
export {myRenderToStaticMarkup} from './myRenderToStaticMarkup';
pagesのindex.tsx
import React from 'react';
-import {renderToStaticMarkup} from 'react-dom/server';
+import {myRenderToStaticMarkup} from "../hooks";
import {Layout} from "../layouts";
export default () => {
+ return myRenderToStaticMarkup(
<Layout>
テスト
</Layout>
+ )
};
myRenderToStaticMarkupの型はここから取ってきた
/**
* Similar to `renderToString`, except this doesn't create extra DOM attributes
* such as `data-reactid`, that React uses internally. This is useful if you want
* to use React as a simple static page generator, as stripping away the extra
* attributes can save lots of bytes.
*/
export function renderToStaticMarkup(element: ReactElement): string;
他のページ今回はabout pageも出てくるようにする
その前にコミット
名前が思いつかないので一旦作る
$ mkdir .config && mkdir .config/webpack && touch .config/webpack/glob.js
目標
plugins: [
- new HtmlWebpackPlugin({
- template: "src/pages/index.tsx",
- filename: 'dist/index.html',
- inject: false,
- }),
+ pages.forEach((page) => {
+ new HtmlWebpackPlugin(page)
+ }),
new ForkTsCheckerWebpackPlugin(),
new webpack.ProgressPlugin(),
],
とりあえず名前はpagesでいいので変更
$ mv .config/webpack/glob.js .config/webpack/pages.js
中身
globを使ってsrc/pages/**/.tsxをすべて取得しmapで加工して返す
const glob = require('glob');
const pages = glob.sync(`**/*.tsx`, {
ignore: `**/_*.tsx`,
cwd: `./src/pages`
}).map((n) => {
const ext = n.replace(/tsx$/,'html')
return {
template: "src/pages/" + n,
filename: 'dist/' + ext,
inject: false,
}
});
module.exports = pages;
変数名とか雑だけど、とりあえずこれでいいっしょ
const webpack = require('webpack');
const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin");
+const Pages = require(path.resolve('.config/webpack/pages.js'));
module.exports = () => {
console.log(Pages);
const MODE = process.env.NODE_ENV;
const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
return {
mode: MODE,
devtool: IS_DEVELOPMENT ? 'inline-source-map' : false,
entry: {
"dist/assets/js/main": "./src/js/main",
},
resolve: {
extensions: ['.ts', '.tsx', '.jsx', '.js'],
},
output: {
filename: '[name].js',
path: path.join(__dirname),
},
module: {
rules: [
{
test: /\.(tsx?|jsx?)$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true,
}
}
],
}
]
},
plugins: [
- new HtmlWebpackPlugin({
- template: "src/pages/index.tsx",
- filename: 'dist/index.html',
- inject: false,
- }),
+ Pages.forEach((page) => {
+ new HtmlWebpackPlugin(page);
+ }),
new ForkTsCheckerWebpackPlugin(),
new webpack.ProgressPlugin(),
],
}
};
😇😇😇😇😇😇
$ NODE_ENV=production run-p _build
$ run-s webpack-build
$ webpack
[
{
template: 'src/pages/about/index.tsx',
filename: 'dist/about/index.html',
inject: false
},
{
template: 'src/pages/index.tsx',
filename: 'dist/index.html',
inject: false
}
]
[webpack-cli] Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
- configuration.plugins[1] should be one of these:
object { apply, … } | function
-> Plugin of type object or instanceof Function
Details:
* configuration.plugins[1] should be an object.
-> Plugin instance
* configuration.plugins[1] should be an instance of function
-> Function acting as plugin
forEachじゃだめみたいなので、mapでnew HtmlWebpackPluginを返してpluginsにマージさせればいいので
const webpack = require('webpack');
const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const Pages = require(path.resolve('.config/webpack/pages.js'));
module.exports = () => {
- const test = Pages.map((page) => new HtmlWebpackPlugin(page));←debugした時の残骸
- console.log(test);←debugした時の残骸
const MODE = process.env.NODE_ENV;
const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
return {
mode: MODE,
devtool: IS_DEVELOPMENT ? 'inline-source-map' : false,
entry: {
"dist/assets/js/main": "./src/js/main",
},
resolve: {
extensions: ['.ts', '.tsx', '.jsx', '.js'],
},
output: {
filename: '[name].js',
path: path.join(__dirname),
},
module: {
rules: [
{
test: /\.(tsx?|jsx?)$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true,
}
}
],
}
]
},
plugins: [
- Pages.forEach((page) => {
- new HtmlWebpackPlugin(page);
- }),
+ ...Pages.map((page) => new HtmlWebpackPlugin(page)),
+ ...[
new ForkTsCheckerWebpackPlugin(),
new webpack.ProgressPlugin(),
+ ]
],
}
};
無事about pageも出るようになりました😤
$ NODE_ENV=production run-p _build
$ run-s webpack-build
$ webpack
98% after emitting SizeLimitsPlugin[webpack-cli] Compilation finished
Hash: f18a6300809936ad70c8
Version: webpack 4.44.2
Time: 1403ms
Built at: 2020/12/06 19:20:02
Asset Size Chunks Chunk Names
dist/about/index.html 179 bytes [emitted]
dist/assets/js/main.js 984 bytes 0 [emitted] dist/assets/js/main
dist/index.html 177 bytes [emitted]
Entrypoint dist/assets/js/main = dist/assets/js/main.js
[0] ./src/js/main.ts 55 bytes {0} [built]
Child HtmlWebpackCompiler:
Asset Size Chunks Chunk Names
__child-HtmlWebpackPlugin_0 47.5 KiB 0 HtmlWebpackPlugin_0
__child-HtmlWebpackPlugin_1 47.5 KiB 1 HtmlWebpackPlugin_1
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
Entrypoint HtmlWebpackPlugin_1 = __child-HtmlWebpackPlugin_1
[0] ./node_modules/react/index.js 193 bytes {0} {1} [built]
[1] ./node_modules/object-assign/index.js 2.43 KiB {0} {1} [built]
[2] ./node_modules/react-dom/server.browser.js 231 bytes {0} {1} [built]
[3] ./src/layouts/index.tsx 575 bytes {0} {1} [built]
[4] ./src/hooks/index.ts + 1 modules 243 bytes {0} {1} [built]
| ./src/hooks/index.ts 67 bytes [built]
| ./src/hooks/myRenderToStaticMarkup.ts 171 bytes [built]
[5] ./node_modules/react/cjs/react.production.min.js 8.12 KiB {0} {1} [built]
[6] ./node_modules/react-dom/cjs/react-dom-server.browser.production.min.js 30.2 KiB {0} {1} [built]
[7] ./node_modules/html-webpack-plugin/lib/loader.js!./src/pages/about/index.tsx 234 bytes {0} [built]
[8] ./node_modules/html-webpack-plugin/lib/loader.js!./src/pages/index.tsx 241 bytes {1} [built]
✨ Done in 3.16s.
というかglobってビルドインだったっけ?間違えてグローバルに入れちゃっててそれ参照してるのかも?
ということでglobを入れる
$ npm i glob -D
ちょっと分割代入に変えたほうがオプション渡しやすいから変えよう
const glob = require('glob');
const pages = glob.sync(`**/*.tsx`, {
ignore: `**/_*.tsx`,
cwd: `./src/pages`
}).map((n) => {
const ext = n.replace(/tsx$/,'html')
return {
template: "src/pages/" + n,
filename: 'dist/' + ext,
- inject: false,
}
});
module.exports = pages;
const webpack = require('webpack');
const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const Pages = require(path.resolve('.config/webpack/pages.js'));
module.exports = () => {
const MODE = process.env.NODE_ENV;
const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
return {
mode: MODE,
devtool: IS_DEVELOPMENT ? 'inline-source-map' : false,
entry: {
"dist/assets/js/main": "./src/js/main",
},
resolve: {
extensions: ['.ts', '.tsx', '.jsx', '.js'],
},
output: {
filename: '[name].js',
path: path.join(__dirname),
},
module: {
rules: [
{
test: /\.(tsx?|jsx?)$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true,
}
}
],
}
]
},
plugins: [
+ ...Pages.map(({template, filename}) => new HtmlWebpackPlugin({
+ template,
+ filename,
+ inject: false,
+ })),
...[
new ForkTsCheckerWebpackPlugin(),
new webpack.ProgressPlugin(),
]
],
}
};
そういえばせっかく作ったmain.jsを読み込んでなかった
いつものBrowserSync入れよう
$ npm i -D browser-sync
$ touch .config/bs-config.js
const BROWSER_SYNC = {
files: [
`dist/assets/**/*`,
`dist/**/*.html`,
],
ghostMode: {
clicks: false,
scroll: false,
forms: false
},
server: {
baseDir: 'dist',
middleware: [],
},
logFileChanges: false
};
module.exports = BROWSER_SYNC;
{
"name": "react-stati-generator",
"version": "1.0.0",
"license": "MIT",
"private": true,
"scripts": {
+ "start": "NODE_ENV=development run-p _start server",
"build": "NODE_ENV=production run-p _build",
+ "server": "browser-sync start --config ./.config/bs-config.js",
"webpack-watch": "webpack -w",
"webpack-build": "webpack",
"_start": "run-s webpack-watch",
"_build": "run-s webpack-build"
}
}
疲れたの今日はここまで
typescriptが無いっぽいので入れる
$ npm i typescript -D
pathの情報をpage全体で使用したのでuseContext
を使う
$ touch src/config.ts
import React from "react";
type currentPageValueType = {
path: '/' | './' | '../' | '../../',
}
const currentPageValue: currentPageValueType = {
path: '/',
}
export const CurrentPage = React.createContext(currentPageValue);
とりあえずpagesに反映させるためにProviderで囲う
import React from 'react';
import {myRenderToStaticMarkup} from "../hooks";
import {Layout} from "../layouts";
+import {CurrentPage} from "../config";
export default () => {
return myRenderToStaticMarkup(
- <Layout>
- テスト
- </Layout>
+ <CurrentPage.Provider value={{path: './'}}>
+ <Layout>
+ テスト
+ </Layout>
+ </CurrentPage.Provider>
)
};
import React from 'react';
import {myRenderToStaticMarkup} from "../../hooks";
import {Layout} from "../../layouts";
+import {CurrentPage} from "../../config";
export default () => {
return myRenderToStaticMarkup(
- <Layout>
- About
- </Layout>
+ <CurrentPage.Provider value={{path: '../'} as const}>
+ <Layout>
+ about
+ </Layout>
+ </CurrentPage.Provider>
)
};
ちゃんとできているか確認するためにダミーのコンポーネントを作成する
$ mkdir src/components && touch src/components/index.ts && touch src/components/App.tsx
import React, {useContext} from "react";
import {CurrentPage} from "../config";
export const App = () => {
const {path} = useContext(CurrentPage);
return (
<div>
{path}
</div>
)
}
export {App} from './App';
import {myRenderToStaticMarkup} from "../hooks";
import {Layout} from "../layouts";
import {CurrentPage} from "../config";
+import {App} from "../components";
export default () => {
return myRenderToStaticMarkup(
<CurrentPage.Provider value={{path: './'}}>
<Layout>
テスト
+ <App/>
</Layout>
</CurrentPage.Provider>
)
import {myRenderToStaticMarkup} from "../../hooks";
import {Layout} from "../../layouts";
import {CurrentPage} from "../../config";
+import {App} from "../../components";
export default () => {
return myRenderToStaticMarkup(
<CurrentPage.Provider value={{path: '../'} as const}>
<Layout>
about
+ <App/>
</Layout>
</CurrentPage.Provider>
)
pages
を起点に現在のページの相対pathを割り出す
フォルダから相対パスを出すのは履修済みなので実装する
path
をrequireしてpath.relative()
してやればいい
const glob = require('glob');
+const path = require('path');
+
+const PAGE_ROOT = 'src/pages/';
const pages = glob.sync(`**/*.tsx`, {
ignore: `**/_*.tsx`,
- cwd: `./src/pages`
-}).map((n) => {
- const ext = n.replace(/tsx$/,'html')
+ cwd: PAGE_ROOT
+}).map((currentPagePath) => {
+ const pageCurrentPath = PAGE_ROOT + currentPagePath.replace(/index\.tsx$/, '');
+ const relativePath = `${path.relative(pageCurrentPath, PAGE_ROOT) || '.'}/`;
+ const currentPageHTMLPath = currentPagePath.replace(/tsx$/, 'html')
return {
- template: "src/pages/" + n,
- filename: 'dist/' + ext,
+ template: PAGE_ROOT,
+ filename: 'dist/' + currentPageHTMLPath,
+ relativePath
}
});
currentPagePath.replace(/index\.tsx$/, '')
の部分が/index\.tsx$/
なので、index以外もできるように今後変更したい
すぐに正規表現が思いつかない
というかこれどっかで一回やったことあるので探してみる
Gatsbyですべての画像から特定の画像を抜き出す時に使った((?!.*\/).+\.(png|jpe?g))$
これを((?!.*\/).+\.tsx)$
こうじゃ
余計な括弧もいらないからこうじゃ!(?!.*\/).+\.tsx$
ignore: `**/_*.tsx`,
cwd: PAGE_ROOT
}).map((currentPagePath) => {
- const pageCurrentPath = PAGE_ROOT + currentPagePath.replace(/index\.tsx$/, '');
+ const pageCurrentPath = PAGE_ROOT + currentPagePath.replace(/(?!.*\/).+\.tsx$/, '');
const relativePath = `${path.relative(pageCurrentPath, PAGE_ROOT) || '.'}/`;
const currentPageHTMLPath = currentPagePath.replace(/tsx$/, 'html')
return {
とりあえず、適当に変数名も名前も読みやすく変えるのと
pathType
を別の所でも使うので、抜き出せるようにする
import React from "react";
-type currentPageValueType = {
- path: '/' | './' | '../' | '../../',
+export type pathType = '/' | './' | '../' | '../../';
+
+type initialPageValueType = {
+ path: pathType,
}
-const currentPageValue: currentPageValueType = {
+const initialPageValue: initialPageValueType = {
path: '/',
}
-export const CurrentPage = React.createContext(currentPageValue);
+export const CurrentPage = React.createContext(initialPageValue);
webpackで先程のrelativePathを受け取れるり、HtmlWebpackPluginに投げる
plugins: [
- ...Pages.map(({template, filename}) => new HtmlWebpackPlugin({
+ ...Pages.map(({template, filename, relativePath}) => new HtmlWebpackPlugin({
template,
filename,
+ relativePath,
inject: false,
})),
...[
投げ方的にtemplateParameters
もあるみたいだけど、普通に投げても受け取れるみたいなので普通に投げる
一旦pages/index.tsxに読み込ませる
型とか分割代入で汚いのでなんとかしたい
import React from 'react';
+import htmlWebpackPlugin from 'html-webpack-plugin';
+import {pathType} from "../config";
import {myRenderToStaticMarkup} from "../hooks";
import {Layout} from "../layouts";
import {CurrentPage} from "../config";
import {App} from "../components";
-export default () => {
+export default ({htmlWebpackPlugin}: { htmlWebpackPlugin: htmlWebpackPlugin.ProcessedOptions }) => {
+ const relativePath: pathType = htmlWebpackPlugin.options.relativePath;
return myRenderToStaticMarkup(
- <CurrentPage.Provider value={{path: './'}}>
+ <CurrentPage.Provider value={{path: relativePath}}>
<Layout>
テスト
<App/>
aboutにも読み込ませる
import React from 'react';
+import htmlWebpackPlugin from 'html-webpack-plugin';
+import {pathType} from "../../config";
import {myRenderToStaticMarkup} from "../../hooks";
import {Layout} from "../../layouts";
import {CurrentPage} from "../../config";
import {App} from "../../components";
-export default () => {
+export default ({htmlWebpackPlugin}: { htmlWebpackPlugin: htmlWebpackPlugin.ProcessedOptions }) => {
+ const relativePath: pathType = htmlWebpackPlugin.options.relativePath;
return myRenderToStaticMarkup(
- <CurrentPage.Provider value={{path: '../'} as const}>
+ <CurrentPage.Provider value={{path: relativePath}}>
<Layout>
about
<App/>
Provider
までは絶対共通なのでProvider
までを切り出す。
import {renderToStaticMarkup} from 'react-dom/server';
import React, {ReactElement} from "react";
import {ProcessedOptions} from 'html-webpack-plugin';
import {pathType} from "../config";
import {CurrentPage} from "../config";
function myRenderToStaticMarkup(element: ReactElement): string {
return '<!DOCTYPE html>' + renderToStaticMarkup(element);
}
export function newRenderToStaticMarkup(element: ReactElement) {
return (htmlWebpackPlugin: ProcessedOptions) => {
const relativePath: pathType = htmlWebpackPlugin.options.relativePath;
return myRenderToStaticMarkup(
<CurrentPage.Provider value={{path: relativePath}}>
{element}
</CurrentPage.Provider>
)
}
}
拡張子と名前が変わったので変える
$ mv src/hooks/myRenderToStaticMarkup.ts src/hooks/newRenderToStaticMarkup.tsx
なんとなくかっこよさでカリー化してみたけど、他にいい方法があるかもしれない。
ファイル分割とかその辺も
共通部分を切り出したのでだいぶスッキリした😎
import React from 'react';
-import htmlWebpackPlugin from 'html-webpack-plugin';
-import {pathType} from "../config";
-import {myRenderToStaticMarkup} from "../hooks";
+import {ProcessedOptions} from 'html-webpack-plugin';
+import {newRenderToStaticMarkup} from "../hooks";
import {Layout} from "../layouts";
-import {CurrentPage} from "../config";
import {App} from "../components";
-export default ({htmlWebpackPlugin}: { htmlWebpackPlugin: htmlWebpackPlugin.ProcessedOptions }) => {
- const relativePath: pathType = htmlWebpackPlugin.options.relativePath;
- return myRenderToStaticMarkup(
- <CurrentPage.Provider value={{path: relativePath}}>
- <Layout>
- テスト
- <App/>
- </Layout>
- </CurrentPage.Provider>
- )
+export default ({htmlWebpackPlugin}: ProcessedOptions) => {
+ return newRenderToStaticMarkup(
+ <Layout>
+ テスト
+ <App/>
+ </Layout>
+ )(htmlWebpackPlugin);
};
import React from 'react';
-import htmlWebpackPlugin from 'html-webpack-plugin';
-import {pathType} from "../../config";
-import {myRenderToStaticMarkup} from "../../hooks";
+import {ProcessedOptions} from 'html-webpack-plugin';
+import {newRenderToStaticMarkup} from "../../hooks";
import {Layout} from "../../layouts";
-import {CurrentPage} from "../../config";
import {App} from "../../components";
-export default ({htmlWebpackPlugin}: { htmlWebpackPlugin: htmlWebpackPlugin.ProcessedOptions }) => {
- const relativePath: pathType = htmlWebpackPlugin.options.relativePath;
- return myRenderToStaticMarkup(
- <CurrentPage.Provider value={{path: relativePath}}>
- <Layout>
- about
- <App/>
- </Layout>
- </CurrentPage.Provider>
- )
+export default ({htmlWebpackPlugin}: ProcessedOptions) => {
+ return newRenderToStaticMarkup(
+ <Layout>
+ about
+ <App/>
+ </Layout>
+ )(htmlWebpackPlugin);
};
もしかしてこれ引数の順番を変えると一発でいけるのでは?
この形にしないといけなさそうなので、だめかも?
export default ({htmlWebpackPlugin}) => test(htmlWebpackPlugin)(jsx);
いい加減pathを書くのがめんどくさいのでaliasを設定する
},
resolve: {
extensions: ['.ts', '.tsx', '.jsx', '.js'],
+ alias: {
+ 'src': path.resolve('./src'),
+ },
},
output: {
filename: '[name].js',
"lib": ["dom", "es2015", "dom.iterable"],
"typeRoots": ["src/@types", "node_modules/@types"],
"baseUrl": ".",
+ "paths": {
+ "src/*": ["src/*"]
+ },
"strict": true,
"moduleResolution": "node",
"noUnusedLocals": true,
リアタイじゃないけど追記
headタグを書くページで追記したい時の事を考えてreact-helmetを採用する
$ npm i -D react-helmet
$ npx typesync
$ npm i
結論として普通に使用すると<head></head>になって中身が出なかった
SSRの場合と同じなのでこちらで対応していく
renderToStaticMarkup(element);
の後に
const helmet = Helmet.renderStatic();
を追記してください!
const html = `
<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
</head>
<body ${helmet.bodyAttributes.toString()}>
<div id="content">
// React stuff here
</div>
</body>
</html>
`;
つまりこうじゃ!
+import {Helmet} from "react-helmet";
export const Layout: FC<LayoutType> = ({children}) => {
return (
<>
- <html lang="ja">
- <head>
+ <Helmet>
+ <html lang="ja" />
<meta charSet="UTF-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>タイトル</title>
- </head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+ <meta name="description" content="説明文"/>
+ </Helmet>
<body>
{children}
</body>
- </html>
</>
)
}
import {renderToStaticMarkup} from 'react-dom/server';
import React, {ReactElement} from "react";
import {Helmet} from "react-helmet";
import {ProcessedOptions} from 'html-webpack-plugin';
import {CurrentPage, pathType} from "src/config";
function myRenderToStaticMarkup(element: ReactElement): string {
const staticMarkup = renderToStaticMarkup(element);
const helmet = Helmet.renderStatic();
function replaceDataHelmet(helmet: string) {
return helmet.replace(/data-react-helmet="true"/g, '')
}
return `
<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
<head>
${replaceDataHelmet(helmet.title.toString())}
${replaceDataHelmet(helmet.meta.toString())}
${replaceDataHelmet(helmet.link.toString())}
</head>
${staticMarkup}
</html>
`;
}
export function newRenderToStaticMarkup(element: ReactElement) {
return (htmlWebpackPlugin: ProcessedOptions) => {
const relativePath: pathType = htmlWebpackPlugin.options.relativePath;
return myRenderToStaticMarkup(
<CurrentPage.Provider value={{path: relativePath}}>
{element}
</CurrentPage.Provider>
)
}
}
renderToStaticMarkup(element);
を返してた部分を変数に打ち込んで、doc通りにするbody
にclassは付けないのでbodyはLayoutの中のまま
data-react-helmet="true"
はいらないので削除する
function replaceDataHelmet(helmet: string) {
return helmet.replace(/data-react-helmet="true"/g, '')
}
年も変わったので、webpack5にする
$ npm install -g npm-check-updates
$ ncu
Upgrading /Users/hisho/github/react-static-generator/package.json
[====================] 19/19 100%
@types/node ^14.14.10 → ^14.14.17
fork-ts-checker-webpack-plugin ^6.0.5 → ^6.0.8
ts-loader ^8.0.11 → ^8.0.13
typescript ^4.1.2 → ^4.1.3
webpack ^4.44.2 → ^5.11.1
webpack-cli ^4.2.0 → ^4.3.1
Run npm install to install new versions.
$ ncu -u
$ npm i
filename: '[name].js',
path: path.join(__dirname),
},
+ target: ['web', 'es5'],
module: {
rules: [
{
TODO
- 各ページを一言管理するjsonの読み込み
- ESlintの設定
- htmlhintの設定
- prettierの設定
pages.jsがバグっていたので、修正する
const glob = require('glob');
const path = require('path');
const PAGE_ROOT = 'src/pages/';
const pages = glob.sync(`**/*.tsx`, {
ignore: `**/_*.tsx`,
cwd: PAGE_ROOT
}).map((currentPagePath) => {
const pageCurrentPath = PAGE_ROOT + currentPagePath.replace(/(?!.*\/).+\.tsx$/, '');
const relativePath = `${path.relative(pageCurrentPath, PAGE_ROOT) || '.'}/`;
const currentPageHTMLPath = currentPagePath.replace(/tsx$/, 'html')
return {
- template: PAGE_ROOT,
+ template: PAGE_ROOT + currentPagePath,
filename: 'dist/' + currentPageHTMLPath,
relativePath
}
});
module.exports = pages;