2020年手工webpack构建react项目,完美支持ssr,包括css和图片资源

时间:2022-07-22
本文章向大家介绍2020年手工webpack构建react项目,完美支持ssr,包括css和图片资源,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

这几天花了大量时间终于折腾出一个完美版本,并且是自己构建的webpack配置(之前失败很可能是因为react自带的webpack太复杂,构建服务端代码时有些细节没处理好)

完整代码上传到了git:https://github.com/liuxiaocong/react-self-customize-webpack-ssr

下载的话麻烦点个start,每一步的commit都有说明,下面再简单说一下:

1,基本项目结构,webpack配置

项目结构,src目录为前端开发,server目录为服务器相关,入口文件为index.js和about.js(如果是单入口站点可以忽略)

image.png

看一下package.json里面的指令设置:

"start": "cross-env NODE_ENV=development webpack-dev-server --open --mode development",
"build": "cross-env NODE_ENV=production webpack --mode production",
"server": "nodemon --exec babel-node server/index.js",
"buildServer": "NODE_ENV=development webpack --config ./server/webpack.server.config.js"

######yarn start: 前端代码开发调试.

######yarn build: 前端代码发布,配置文件为项目根目录下的webpack.config.js.

######yarn buildServer: 服务器相关代码打包,这一步是为了支持资源加载如css和image,配置文件为根目录下server目录的webpack.server.config.js

######yarn server: 服务器启动,这一步引用了yarn buildServer打包生产的ssr.js.

前端工程webpack配置,解析js,css,image,打包到根目录下的build文件夹webpack.config.js

const path = require('path');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');


//not need to use this, MiniCssExtractPlugin already support hmr
const cssLoaderLast = process.env.NODE_ENV === 'development'?
      'style-loader':
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          publicPath: '/css',
          hmr: true,
        },
      }
      
module.exports = {
  entry: {
    main: path.resolve(__dirname, "src/index.js"),
    about: path.resolve(__dirname, "src/about.js")
  },
  output: {
    path: path.resolve(__dirname, 'build'), //出口文件輸出的路徑
    filename: '[name].js' //出口文件,[name]為入口文件陣列的名稱main喔!
  },
  plugins: [
    new HtmlWebPackPlugin({
      template: './src/index.html',
      filename: './index.html',
      chunks: ['main'],
    }),
    new HtmlWebPackPlugin({
      template: './src/about.html',
      filename: './about.html',
      chunks: ['about'],
    }),
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: '[name].css',
      chunkFilename: '[id].css',
    }),
  ],
  module: {
    rules: [
      {
        test: /.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /.css$/i,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '/css',
              hmr: true,
            },
          },
          //'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[path][name]__[local]--[hash:base64:5]',
              },
            },
          }
        ],
      },
      {
        test: /.(png|jpe?g|gif|svg)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[contenthash].[ext]',
              outputPath: 'images',
            },
          },
        ],
      },
      {
        test: /.html$/,
        use: [
          {
            loader: 'html-loader',
          },
        ],
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx', '.json'],
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 10000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\/]node_modules[\/]/,
          priority: -10,
        },
        default: {
          minChunks: 1,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

再看一下server的webpack配置,跟上面很像,改了入口和输出,保证生产的css和image一致就行。

注意下面2行代码:

target: 'node',
externals: nodeExternals(),

这是让输出的js可以在node环境运行,否则会变成引用window对象进行挂接,造成错误。

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const nodeExternals = require('webpack-node-externals');

//not need to use this, MiniCssExtractPlugin already support hmr
const cssLoaderLast = process.env.NODE_ENV === 'development' ?
  'style-loader' :
  {
    loader: MiniCssExtractPlugin.loader,
    options: {
      publicPath: '/css',
      hmr: true,
    },
  };

module.exports = {
  entry: {
    main: path.resolve(__dirname, '../src/ssr.js'),
  },
  output: {
    path: path.resolve(__dirname, '../buildSsr'), //出口文件輸出的路徑
    filename: '[name].js', //出口文件,[name]為入口文件陣列的名稱main喔!
    libraryTarget: 'commonjs2',
  },
  plugins: [
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: '[name].css',
      chunkFilename: '[id].css',
    }),
  ],
  target: 'node',
  externals: nodeExternals(),
  module: {
    rules: [
      {
        test: /.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /.css$/i,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '/css',
              hmr: true,
            },
          },
          //'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[path][name]__[local]--[hash:base64:5]',
              },
            },
          },
        ],
      },
      {
        test: /.(png|jpe?g|gif|svg)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[contenthash].[ext]',
              outputPath: 'images',
            },
          },
        ],
      },
      {
        test: /.html$/,
        use: [
          {
            loader: 'html-loader',
          },
        ],
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx', '.json'],
  },
};
2,服务器启动代码

这部分可以看一下之前的文件https://www.jianshu.com/p/eba973875d22

入口文件是index.js

import express from 'express';
//import compression from 'compression';
import path from 'path';
import { renderToString } from 'react-dom/server';
//https://www.babeljs.cn/docs/babel-register
require('@babel/register')();

require('@babel/polyfill');
require.extensions['.less'] = () => {
  return;
};
require.extensions['.css'] = () => {
  return;
};
require.extensions['.svg', '.png'] = () => {
  return;
};

const renderReact = require('./renderReact.js');

//const router = express.Router();

const app = express();
//app.use(compression());
renderReact(app);
app.use(express.static(path.resolve(__dirname, '../build/')));

const port = process.env.PORT || 4000;

app.listen(port, function listenHandler() {
  console.info(`Running on ${ port }`);
});

用的是express,babel require是让后续的运行支持es6语法

######babel/register模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码:http://www.ruanyifeng.com/blog/2016/01/babel.html

renderReact.js为主要服务端路由配置,为什么要分开的原因上一篇文章也提过了babel-register doesn't process the file it is called from, see https://stackoverflow.com/a/29425761/1795821

import React from 'react';
import fs from 'fs';
import { StaticRouter } from 'react-router-dom';
import Main from '../src/container/main';

const render = require('../buildSsr/main').default;
const reactDomServer = require('react-dom/server');
console.log('server start ....');
console.log(render);
const useServerBuildFile = true;
let buildHtml;
module.exports = function(app) {
  const routerArray = ['/', '/todo', 'about'];
  routerArray.forEach((item) => {
    if (useServerBuildFile) {
      app.get(item, render);
    } else {
      app.get(item, (req, res) => {
        const context = {};
        const appHtml = reactDomServer.renderToString(
          <StaticRouter location={ req.url } context={ context }>
            <Main/>
          </StaticRouter>,
        );
        if (!buildHtml) {
          buildHtml = fs.readFileSync('./build/index.html', 'utf8');
        }
        let result = buildHtml.replace('#body', appHtml);
        res.send(result);
      });
    }
  });
};

注意render方法的引用,来源于yarn buildServer生成的ssr.js文件,通过webpack对js和资源进行解析,然后export一个方法给服务器调用

3,前端提供给服务器的入口文件

这个就是核心,src目录下的ssr.js文件,网上其他资料基本没涉及到,很好的一个思路

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Main from './container/main';
import fs from "fs";
let buildHtml;
export default function render(req, res) {
  const context = {};
  const appString = renderToString(
    <StaticRouter location={ req.url } context={ context }>
      <Main/>
    </StaticRouter>
  );
  if (!buildHtml) {
    buildHtml = fs.readFileSync('./build/index.html', 'utf8');
  }
  let result = buildHtml.replace('#body', appString);
  console.log(appString);
  res.send(result);
};

服务器代码引用的就是render函数,同时资源打包和css解析跟原本的前端js一致,因为基本是同一个webpack配置打包出来的。