React SSR 專案建置
緣起
一直以來不管是寫 react 或是 vue 的 side project ,都是用官網基本的 cli 來建置專案,幾乎沒有機會自己利用 webpack 來建立開發環境,剛好也接觸了一陣子的 typescript 跟 react,剛好找個機會來從零開始實作一趟並做個紀錄。
Start with config
-
init npm, git, gitignore.
-
install and set the things below.
babel:
npm install @babel/cli @babel/core @babel/preset-env @babel/preset-react
在與 package.json
同層的資料夾下建立 .babelrc
// .babelrc
{
"presets": [
"@babel/env",
"@babel/preset-react"
]
}
webpack:
npm install webpack webpack-cli webpack-dev-server
npm install babel-loader css-loader style-loader source-map-loader
npm install html-webpack-plugin mini-css-extract-plugin webpack-node-externals
在與 package.json
同層的資料夾下分別建立 webpack.config.js
, webpack.config.client.js
, webpack.config.server.js
。
// webpack.config.js
const client = require('./webpack.config.client')
const server = require('./webpack.server.client')
module.exports = [client, server]
Webpack 幾本用法:
-
entry
: 告訴 webpack 我們專案的進入點。 -
mode
: 分別有 development, production, none 三種 mode, webpack 會對不同的 mode 在 build 的時候做不同的優化。 -
module
: 告訴 webpack 如何處理不同的 module。例如:- An ES2015
import
statement - A CommonJS
require()
statement - An
@import
statement inside of a css/sass/less file.
另外 loader 的設定可以讓 webpack 處理非 Javascript 的程式碼轉換成一包可以讓
target
(下述)執行的檔案。 - An ES2015
-
target
: 告訴 webpack 使用哪種環境編譯程式碼(default 為web
orbrowserslist
)。 -
resolve
: 設定 webpack 如何 resolve 各種 module,resolve
的 config option 十分多種,例如:alias
: 建立require
orimport
的別名,在引入時方便使用。extensions
: 按照順序解析 list 裡面的附檔名,可以在引入時不寫副檔名。
-
output
: 編譯完程式碼後,輸出的檔名以及存放的位置。 -
plugins
: 客製 webpack 的建構過程。
// webpack.config.client.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
entry: "./src/react/index.tsx",
mode: "development",
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules|bower_components)/,
loader: "babel-loader",
options: { presets: ["@babel/env"] }
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
},
{
test: /\.(ts|tsx)$/,
loader: "awesome-typescript-loader",
},
]
},
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx"]
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
plugins: [new webpack.HotModuleReplacementPlugin()],
mode: 'development'
};
// webpack.config.server.js
const path = require("path");
const webpackNodeExternals = require("webpack-node-externals");
module.exports = {
target: "node",
entry: "./src/express/index.tsx",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "build"),
},
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/,
},
{
test: /\.(ts|tsx)$/,
loader: "awesome-typescript-loader",
},
],
},
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx"]
},
externals: [webpackNodeExternals()],
mode: 'development'
};
TypeScript
npm install typescript @types/react @types/react-dom @types/express @types/node @types/jest
npm install awesome-typescript-loader
在與 package.json
同層的資料夾下建立 tsconfig.json
// tsconfig.json
{
"compilerOptions": {
"jsx": "react",
"module": "commonjs",
"noImplicitAny": true,
"outDir": "./dist/",
"preserveConstEnums": true,
"removeComments": true,
"sourceMap": true,
"target": "es5",
"esModuleInterop": true
},
"include": [
"src/react/index.tsx"
]
}
App
npm install express nodemon npm-run-all react react-dom
加入啟動 app 的腳本: 修改 package.json
裡的 script
。
// package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server",
"dev": "npm-run-all --parallel dev:server dev:build:*",
"dev:server": "nodemon --inspect build/bundle.js",
"dev:build:server": "webpack --config webpack.config.server.js --watch",
"dev:build:client": "webpack --config webpack.config.client.js --watch"
},
至此基本的 config 都已完成,可以開始 app 的建置了。
Start building the app
在與 package.json
同層的資料夾下建立 src 資料夾,並在 src 資料夾底下建立 react
以及 express
兩個資料夾,分別在兩個資料夾底下都建立一個 index.tsx
,並在 react 底下多建立一個 app.tsx
。
// src/express/index.tsx
import express from "express";
import React from "react";
import { renderToString } from "react-dom/server";
import App from '../react/app'
const app = express();
app.use(express.static("dist"));
app.get("*", (req, res) => {
const content = renderToString(
<App />
);
const html = `
<html>
<head></head>
<body>
<div id="root">${content}</div>
<script src="bundle.js"></script>
</body>
</html>
`;
res.send(html);
});
app.listen(3000, () => {
console.log("listening on port 3000");
});
// /src/react/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
ReactDOM.hydrate(<App />, document.getElementById("root"));
// /src/react/app.tsx
import React from 'react';
import Test from './components/test'
interface Ipage {
path: string
component: any
key:string
}
class App extends React.Component {
render(){
return(
<Test />
)
}
}
export default App;
最後在 src/react
底下建立一個 components
資料夾,並加入 test.tsx
component
// src/react/components/test.tsx
import React from 'react';
const Test = () => (<div>test</div>)
export default Test
執行 npm run dev
之後,就可以看到建置成功的專案了喔。