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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容