Webpack4不求人(2) ——手把手搭建TypeScript+React16+ReactRouter5同構應用腳手架

同構應用

使用同一份應用代碼,同時提供瀏覽器環境和服務器環境下的應用,解決傳統瀏覽器單頁應用的兩個頑固問題:javascript

  • 不利於SEO,瀏覽器環境代碼是在客戶端渲染,大部分爬蟲都只能爬到一個空白的入口文件
  • 代碼在瀏覽器渲染,低端機可能會卡頓

接下來咱們一塊兒從零開始搭建基於Webpack的React同構應用腳手架。css

SSR流程

  1. Web應用構建完成後輸出CSS、JS和HTML
  2. SSR應用構建完成後輸出一個CommonJs模塊文件,能夠將虛擬DOM在服務端渲染爲HTML字符串
  3. Node.js新建HTTP服務器,收到請求後調用SSR模塊導出的render函數輸出HTML到客戶端

初始化項目

mkdir react-ssr-example
cd react-ssr-example
yarn init -y

yarn add webpack webpack-cli webpack-dev-server -D # 安裝Webpack
yarn add react react-dom react-router-dom # 安裝React
yarn add @types/react @types/react-dom @types/react-router-dom -D # 安裝React聲明文件
yarn add express # 安裝express
yarn add css-loader sass-loader node-sass mini-css-extract-plugin # 安裝CSS相關模塊
yarn add ts-loader typescript # 安裝TypeScript
yarn add html-webpack-plugin # 安裝HTML處理插件

目錄結構

腳手架的完整目錄以下:(這些文件一步步都會有)html

|----build # 構建結果目錄
        |----styles # 樣式
                |----main.css
        |----bundle.ssr.js # SSR應用文件
        |----bundle.web.js # Web應用文件
        |----index.html # Web應用入口HTML
|----src # 應用源碼
        |----home # 首頁組件
                |----index.scss # 首頁SCSS
                |----index.tsx # 首頁組件
        |----signin # 登陸頁組件
                |----index.scss # 登陸頁SCSS
                |----index.tsx # 登陸頁組件
        |----App.tsx # 應用路由設置
        |----index.html # Web應用入口HTML
        |----main.ssr.tsx # SSR入口文件
        |----main.web.tsx # Web入口文件
|----index.js # express服務器入口
|----package.json
|----tsconfig.json # TypeScript配置文件
|----webpack.config.js # Web應用webpack配置
|----webconfig.ssr.config.js # SSR應用Webpack配置

工具配置

1.TypeScript配置,新建tsconfig.jsonjava

{
    "compilerOptions": {
      "target": "es5", 
      "module": "commonjs", 
      "jsx": "react", 
      "strict": true,
      "lib": [
        "DOM"
      ],
      "esModuleInterop": true
    },
    "include": [
      "./src/**/*.ts",
      "./src/**/*.tsx"
    ],
    "exclude": [
      "node_modules"
    ]
  }

主要是添加了jsx設置和include設置node

2.Web環境webpack配置,新建webpack.config.jsreact

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssPlugin = require('mini-css-extract-plugin');

module.exports = {
    entry: './src/main.web', // 入口文件
    output: {
        path: path.resolve(__dirname, 'build'), // 輸出目錄
        filename: 'bundle.web.js' // 輸出文件
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/, // ts文件處理
                use: 'ts-loader'
            },
            {
                test: /\.scss$/, // scss文件處理
                use: [MiniCssPlugin.loader, 'css-loader', 'sass-loader']
            },
            {
                test: /\.css$/, // css文件處理
                use: [MiniCssPlugin.loader, 'css-loader']
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            chunks: ['main'], // chunk名稱,entry是字符串類型,所以chunk爲main
            filename: 'index.html', // 輸出到build目錄的文件名
            template: 'src/index.html' // 模板路徑
        }),
        new MiniCssPlugin({
            filename: 'styles/[name].[contenthash:8].css', // 輸出的CSS文件名
            chunkFilename: 'styles/[name].[contenthash:8].css'
        })
    ],
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.json'] // 添加ts和tsx後綴
    }
};

3.SSR環境Webpack配置,新建webpack.ssr.config.jswebpack

const path = require('path');
const MiniCssPlugin = require('mini-css-extract-plugin');

module.exports = {
    entry: './src/main.ssr',
    target: 'node', // 必須指定爲Node.js,不然會打包Node.js內置模塊
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.ssr.js',
        libraryTarget: 'commonjs2' // 打包爲CommonJs模塊才能被Node.js加載
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader'
            },
            {
                test: /\.scss$/,
                use: [MiniCssPlugin.loader, 'css-loader', 'sass-loader']
            },
            {
                test: /\.css$/,
                use: [MiniCssPlugin.loader, 'css-loader']
            }
        ]
    },
    plugins: [
        new MiniCssPlugin({
            filename: 'styles/[name].[contenthash:8].css',
            chunkFilename: 'styles/[name].[contenthash:8].css'
        })
    ],
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.json']
    }
};

4.package.json添加npm命令git

{
    "scripts": {
    "build": "webpack",
    "start": "webpack-dev-server",
    "build-ssr": "webpack --config webpack.ssr.config.js"
  }
}

應用編碼

src/home/index.tsxgithub

import React from 'react';
import './index.scss';

export default class Home extends React.Component {
    render() {
        return (
            <div className="main">首頁</div>
        )
    }
}

src/home/index.scssweb

.main {
    color: red;
}

src/signin/index.tsx

import React from 'react';
import { withRouter } from 'react-router-dom';

function SignIn(props: any) {
    return (
        <button onClick={() => props.history.replace('/')}>登陸</button>
    )
}
export default withRouter(SignIn);

src/App.tsx

import React from 'react';
import { Switch, Route, Link } from 'react-router-dom'; // router

// 導入頁面組件
import Home from './home';
import SignIn from './signin';

// 導出路由組件配置
export default function App() {
    return (
        <Switch>
            <Route path="/signin" component={SignIn} />
            <Route path="/" component={Home} />
        </Switch>
    )
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

src/main.ssr.tsx

import React from 'react';
import { StaticRouter, Link } from 'react-router-dom';
import { renderToString } from 'react-dom/server';
import App from './App'; // 將路由組件導入進來

export function render(req: any) { // 導出一個渲染函數,根據請求連接進行分發
    const context = {};
    const html = renderToString(
        <StaticRouter location={req.url} context={context}>
            <header>
                <nav>
                    <ul>
                        <li><Link to="/">首頁</Link></li>
                        <li><Link to="/signin">登陸</Link></li>
                    </ul>
                </nav>
            </header>
            <App />
        </StaticRouter>
    );
    return [html, context]; // 導出context和html渲染結果
}

src/main.web.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Link } from 'react-router-dom';
import App from './App';

ReactDOM.render( // 渲染路由
    <BrowserRouter>
        <header>
            <nav>
                <ul>
                    <li><Link to="/">首頁</Link></li>
                    <li><Link to="/signin">登陸</Link></li>
                </ul>
            </nav>
        </header>
        <App />
    </BrowserRouter>, document.querySelector('#app'))

index.js

const express = require('express'); // 加載express
const { render } = require('./build/bundle.ssr'); // 加載ssr

const app = express();

app.use(express.static('.')) // 靜態資源配置


app.get('/*', (req, res) => { // 全部請求都走這裏處理,必須加*
    const [html, context] = render(req)
    console.log(context) // context目前沒發現啥用處
    res.send(`
    <html>
    <head>
        <meta charset="UTF-8">
        <title>SSR</title>
        <link href="build/styles/main.8f173ff5.css" rel="stylesheet">
    </head>
    <body>
        <div id="app">${html}</div>
        <script src="build/bundle.web.js"></script>
    </body>
    </html>
    `);
    console.log(context)
});

app.listen(8080)

注意:

  • 靜態資源配置必須在最上面
  • app.get('/')必須有
  • HTML字符串必須手動引入CSS和Web構建結果

執行構建

npm run build # 構建Web
npm run build-ssr # 構建SSR
node index.js # 啓動Express服務器

查看結果

首頁樣式

image-20200302173258430

首頁代碼

image-20200302173322601

登陸頁樣式

image-20200302173344488

登陸頁代碼

image-20200302173405907

源碼地址

Https://github.com/xialeistud...

0.jpeg

相關文章
相關標籤/搜索