React工程如何实现多入口多SPA?

大家入门react想必都是从这行命令开始的吧!

npx create-react-app my-app

这个命令会生成一个工程骨架,小伙伴就可以愉快的板砖了。搬久了发现工程越来越大,一打包好几兆,严重影响加载速度。这时候就需要多入口打包,将工程分为几个小的SPA。比如登录时一个SPA,内容管理是一个SPA,产品管理是一个SPA,每个SPA加载共同的JS,和自己的JS,无关的不加载。我们就需要配置webpack来改造工程,结果发现项目里根本没有webpack.config.js。

React团队就像你妈一样为你做好了饭,你吃就好了。把webpack进行了封装,把你当傻瓜。

在package.json你会发现有个react-scripts包,打开源码就可以找到相见恨晚的webpack.config.js。但是我们不能直接修改这个文件,可以使用以下两种方法来对webpack进行配置。

第一种方式 eject

执行命令

yarn eject

把封装的配置脚本释放到当前工程里,在config目录下就有webpack.config.js了。此过程是不可逆的,你也将失去react-scripts未来升级能给你带来的好处。

第二种方式 craco

感谢社区提供第二种方案,让你的代码还可以继续保持纯洁的肉体。首先安装一下@craco/craco 这个包。


yarn add @craco/craco 

项目根目录下新建craco.config.js,在这个文件中覆盖默认的webpack.config即可。

craco.config.js如何配置请参阅文档:https://www.npmjs.com/package/@craco/craco
我们通过第二种方式实现多入口改造。改造思路:

  1. 保留react-create-app默认的入口,入口名为main,js为src/index.js,使用模板public/index.html生成index.html
  2. 为src/entries/下的每个子目录,创建一个同名的入口,入口js为src/entries/{entry-name}/index.js, 使用模板public/{entry-name}.html生成{entry-name}.html

按照这个思路我们需要把webpack config的entry项改为多入口,配置插件HtmlWebpackPlugin为每个entry生成对应的HTML。
其它要点:

  • output.filename需要修改为按入口名生成bundle.js
  • optimization.runtimeChunk需要修改为single 这样不同的入口会共用同一个runtime.js而不是每个生成一个
    废话不多说,直接放码!
  • 默认webpack runtime代码会内嵌到HTML,可以在.env文件中设置INLINE_RUNTIME_CHUNK=false来禁用,或者通过代码删除InlineChunkHtmlPlugin
  • devServerConfig.historyApiFallback中disableDotRule设置为true,要不然开发服务器会把/xxx.html的请求重定向到/xxx上,无法打开入口html
const path = require('path');
const fs = require('fs');
const CracoLessPlugin = require('craco-less');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
function configureWebpack(webpackConfig, {env, paths}) {
    const isEnvDevelopment = env === 'development';
    const isEnvProduction = env === 'production';
    //配置HtmlWebpackPlugin用来产生一个独立的HTML
    function mkHtmlWebpackPlugin(chunks, filename, template) {
        return new HtmlWebpackPlugin({
            inject: true,
            template: template || paths.appHtml,
            chunks,
            filename,
            ...isEnvProduction ? {
                minify: {
                    removeComments: true,
                    collapseWhitespace: true,
                    removeRedundantAttributes: true,
                    useShortDoctype: true,
                    removeEmptyAttributes: true,
                    removeStyleLinkTypeAttributes: true,
                    keepClosingSlash: true,
                    minifyJS: true,
                    minifyCSS: true,
                    minifyURLs: true,
                }
            } : undefined
        });
    }

    //遍历src/entries为所有子目录创建一个webpack入口,并配置对应的HtmlWebpackPlugin
    const entriesDir = path.join(paths.appSrc, 'entries');
    const fileNames = fs.readdirSync(entriesDir);
    const entries = {};
    const htmlWebpackPlugins = [];
    fileNames.forEach(fileName => {
        const filePath = path.join(entriesDir, fileName);
        const file = fs.statSync(filePath);
        if(file.isDirectory()){
            entries[fileName] = path.join(filePath, "index.js");
            let template = path.join(paths.appPublic, fileName + ".html");
            if (!fs.existsSync(template))
                template = undefined;

            htmlWebpackPlugins.push(mkHtmlWebpackPlugin([fileName], fileName + ".html", template));
        }
    });

    //main为create-react-app默认创建的入口,保留下来。这样既可以实现原始的单入口,又可以实现多入口
    webpackConfig.entry = {
        main: webpackConfig.entry,
        ...entries
    };

    //覆盖默认的plugins配置
    const defaultHtmlWebpackPluginIndex = webpackConfig.plugins.findIndex(plugin => plugin instanceof HtmlWebpackPlugin);
    webpackConfig.plugins.splice(defaultHtmlWebpackPluginIndex, 1, mkHtmlWebpackPlugin(["main"], "index.html"), ...htmlWebpackPlugins);

    //create-react-app默认用的是一个固定文件名,不适合多入口!改为按入口名生成输出文件名
    if (isEnvDevelopment)
        webpackConfig.output.filename = 'static/js/[name].bundle.js';

    //共用runtime bundle
    webpackConfig.optimization.runtimeChunk = "single";

    // react-scripts默认在生产环境会将runtime chunk内嵌到html中
    // 禁用该行为,使用独立的js
    // 也可以在根目录新建.env文件,设置INLINE_RUNTIME_CHUNK=false来禁用
    // 不过配置入口太多了,不方便管理,直接这里用代码禁用好了
    const inlineChunkHtmlPluginIndex = webpackConfig.plugins.findIndex(plugin => plugin instanceof InlineChunkHtmlPlugin);
    if (inlineChunkHtmlPluginIndex >= 0)
        webpackConfig.plugins.slice(inlineChunkHtmlPluginIndex, 1);
    return webpackConfig;
}

function configureDevServer(devServerConfig, { env, paths, proxy, allowedHost }) {
    devServerConfig.historyApiFallback = {
        disableDotRule: true, //禁用,否则当访问/xxx.html时服务器会自动去掉.html重写url为/xxx
        index: paths.publicUrlOrPath,
        verbose: true,
    };
    return devServerConfig;
}

module.exports = {
    devServer: configureDevServer,
    webpack: {
        configure: configureWebpack,
    }
};

通过上述代码即可完成多入口改造。具体实践代码可以参考
https://github.com/ikeyit/ikeyit-management-web
假设你有入口/src/entries/your-loved-mp4/index.js,访问http://localhost:3000/your-loved-mp4.html即可打开该入口。

执行编译

yarn build

在build文件夹中可以看到生成了好多js,webpack会把共用和不共用的代码分到不同的包里。

执行命令分析Bundle Size

yarn analyze

(完)

欢迎关注作者的github项目,学习微服务:
一个支持多店铺的电商系统,基于Spring Boot的微服务构架
https://github.com/ikeyit/ikeyit-services
一个基于React的电商管理后台
https://github.com/ikeyit/ikeyit-management-web

你可能感兴趣的:(React工程如何实现多入口多SPA?)