Webpack4不求人(2) ——手把手搭建TypeScript+React16+ReactRouter5同构应用脚手架

算法云阙使
• 阅读 3117

同构应用

使用同一份应用代码,同时提供浏览器环境和服务器环境下的应用,解决传统浏览器单页应用的两个顽固问题:

  • 不利于SEO,浏览器环境代码是在客户端渲染,大部分爬虫都只能爬到一个空白的入口文件
  • 代码在浏览器渲染,低端机可能会卡顿

接下来我们一起从零开始搭建基于Webpack的React同构应用脚手架。

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处理插件

目录结构

脚手架的完整目录如下:(这些文件一步步都会有)

|----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.json

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

主要是添加了jsx设置和include设置

2.Web环境webpack配置,新建webpack.config.js

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.js

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命令

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

应用编码

src/home/index.tsx

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

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

src/home/index.scss

.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服务器

查看结果

首页样式

Webpack4不求人(2) ——手把手搭建TypeScript+React16+ReactRouter5同构应用脚手架

首页代码

Webpack4不求人(2) ——手把手搭建TypeScript+React16+ReactRouter5同构应用脚手架

登录页样式

Webpack4不求人(2) ——手把手搭建TypeScript+React16+ReactRouter5同构应用脚手架

登录页代码

Webpack4不求人(2) ——手把手搭建TypeScript+React16+ReactRouter5同构应用脚手架

源码地址

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

Webpack4不求人(2) ——手把手搭建TypeScript+React16+ReactRouter5同构应用脚手架

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
美凌格栋栋酱 美凌格栋栋酱
10个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Easter79 Easter79
4年前
SpringBoot学习:整合shiro自动登录功能(rememberMe记住我功能)
首先在shiro配置类中注入rememberMe管理器!复制代码(https://oscimg.oschina.net/oscnet/675f5689159acfa2c39c91f4df40a00ce0f.gif)/cookie对象;rememberMeCookie()方法是设置Cookie的生成模
Wesley13 Wesley13
4年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
算法云阙使
算法云阙使
Lv1
还君明珠双泪垂,恨不相逢未嫁时。
文章
3
粉丝
0
获赞
0