Contents

React SSR 專案建置

緣起

一直以來不管是寫 react 或是 vue 的 side project ,都是用官網基本的 cli 來建置專案,幾乎沒有機會自己利用 webpack 來建立開發環境,剛好也接觸了一陣子的 typescript 跟 react,剛好找個機會來從零開始實作一趟並做個紀錄。

Start with config

  1. init npm, git, gitignore.

  2. install and set the things below.

babel:

What is babel?

npm install @babel/cli @babel/core @babel/preset-env @babel/preset-react

在與 package.json 同層的資料夾下建立 .babelrc

// .babelrc

{
  "presets": [
    "@babel/env",
    "@babel/preset-react"
  ]
}

webpack:

What is 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(下述)執行的檔案。

  • target : 告訴 webpack 使用哪種環境編譯程式碼(default 為 web or browserslist)。

  • resolve : 設定 webpack 如何 resolve 各種 module,resolve 的 config option 十分多種,例如:

    • alias : 建立 require or import 的別名,在引入時方便使用。
    • 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

What is 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 之後,就可以看到建置成功的專案了喔。