2 minute read

在上篇中讲到了,在同一个目录下创建主应用以及子应用,但是这样的话其实还是感觉像一个大应用,并且别的模块在维护的时候还需要把整个项目 clone 下来,感觉还是欠缺了点什么。

在这篇中大致上还是基于上看的结构,接着把上面的结构完全拆开来,拆分到单独的 repo,可以分为如下几个结构: 1. 根(主)应用: root-config 2. 子应用 1: app1 3. 子应用 2: app2

接着一步步来

Step 1

把 app1 和 app2 从 micro-front 文件下拆分出来,与 micro-front 同级,基本需要包含如下的文件

|—— micro-front
	|——— index.ejs // 页面
	|——— package.json
	|——— webpack.config.config
	|——— single-spa.config.js // 主应用的的 single-spa 配置
|—— app1
	|——— src
		|——— App.js // 子应用
		|——— react-mf-app1.js // 子应用的 single-spa 配置
		|——— set-public-path.js // 暴露子应用
	|——— package.json
	|——— webpack.config.config
|—— app2
	|——— src
		|——— App.js
		|——— react-mf-app2.js
		|——— set-public-path.js
	|——— package.json
	|——— webpack.config.config

Step 2

修改 mirco-front 的 webpack 配置

const path = require("path")
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = () => {
    const result = {
        entry: path.resolve(__dirname, "./single-spa.config"),
        output: {
            filename: "single-spa.config.js",
            libraryTarget: "system",
            path: path.resolve(__dirname, "dist"),
        },
        devtool: "sourcemap",
        module: {
            rules: [
                { parser: { system: false } },
                {
                    test: /\.js$/,
                    exclude: /node_modules/,
                    use: [{ loader: "babel-loader" }],
                },
            ],
        },
        devServer: {
            historyApiFallback: true,
            disableHostCheck: true,
            headers: {
                "Access-Control-Allow-Origin": "*",
            },
            https: true,
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: "./index.ejs",
            }),
            new CleanWebpackPlugin(),
        ],
        externals: ["single-spa", /^@react-mf\/.+$/],
    }

    return result
}

修改 index.ejs

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>React Microfrontends</title>
        <meta name="importmap-type" content="systemjs-importmap" />
        <script type="systemjs-importmap">
            {
                "imports": {
                    "@react-mf/root-config": "//localhost:9000/single-spa.config.js"
                }
            }
        </script>
        <script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.1.0/dist/import-map-overrides.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/systemjs@6.7.1/dist/system.min.js"></script>
        <template id="single-spa-layout">
            <single-spa-router>
                <div class="main-content mt-16">
                    <route path="app1">
                        <application name="@react-mf/app1"></application>
                    </route>
                    <route path="app2">
                        <application name="@react-mf/app2"></application>
                    </route>
                    <route default>
                        <h1 class="flex flex-row justify-center p-16">
                            <p class="max-w-md">
                                This example project shows independently built
                                and deployed microfrontends that use React and
                                single-spa. Each nav link above takes you to a
                                different microfrontend.
                            </p>
                        </h1>
                    </route>
                </div>
            </single-spa-router>
        </template>
    </head>
    <body>
        <a href="/app1">app1</a>
        <br />
        <a href="/app2">app2</a>
        <script>
            System.import("@react-mf/root-config")
        </script>
    </body>
</html>

Single-sap.config.js

import {
    constructRoutes,
    constructLayoutEngine,
    constructApplications,
} from "single-spa-layout"
import { registerApplication, start } from "single-spa"
console.log(registerApplication)

const routes = constructRoutes(document.querySelector("#single-spa-layout"), {
    loaders: {
        topNav: "<h1>Loading topnav</h1>",
    },
    errors: {
        topNav: "<h1>Failed to load topNav</h1>",
    },
})

const applications = constructApplications({
    routes,
    loadApp: ({ name }) => System.import(name),
})

const layoutEngine = constructLayoutEngine({
    routes,
    applications,
    active: false,
})

applications.forEach(registerApplication)

layoutEngine.activate()
start()

到这里,主应用的改造就算完成了,你可以单独运行一下主应用,切换到子应用路由,页面控制台会报未找到子应用的错误,是因为子应用没有改造完且没有运行,所以是找不到

Step 3

改造完主应用接下来来改造一下子应用 首先改造 webpack.config.js,因为子应用是我们直接通过 react 的 CRA 脚手架创建的,懒得把内置的配置暴露出来,所以自己就增加一个 webpack 配置,同样的在 package.json 中增加一条命令 webpack.config.js

const webpackMerge = require("webpack-merge")
const singleSpaDefaults = require("webpack-config-single-spa-react")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = (webpackConfigEnv = {}) => {
    // TODO: 这部分具体干嘛的还不怎么清楚,感觉像是对外暴露这个包名的
    const defaultConfig = singleSpaDefaults({
        orgName: "react-mf",
        projectName: "app1",
        webpackConfigEnv,
    })

    const config = webpackMerge.smart(defaultConfig, {
        devServer: {
            historyApiFallback: true,
            https: true,
        },
        plugins: [new HtmlWebpackPlugin()],
        resolve: {
            extensions: [".js", ".jsx", ".ts", ".tsx"],
        },
        externals: [/^rxjs\/?.*$/],
    })
    return config
}

Package.json 增加一条 script

"startDev": "webpack-dev-server --port 9001"

React-mf-app1.js

const webpackMerge = require("webpack-merge")
const singleSpaDefaults = require("webpack-config-single-spa-react")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = (webpackConfigEnv = {}) => {
    const defaultConfig = singleSpaDefaults({
        orgName: "react-mf",
        projectName: "app1",
        webpackConfigEnv,
    })

    const config = webpackMerge.smart(defaultConfig, {
        // customizations go here
        devServer: {
            historyApiFallback: true,
            https: true,
        },
        plugins: [new HtmlWebpackPlugin()],
        resolve: {
            extensions: [".js", ".jsx", ".ts", ".tsx"],
        },
        externals: [/^rxjs\/?.*$/],
    })

    return config
}

Set-public-path.js

import { setPublicPath } from "systemjs-webpack-interop"
setPublicPath("@react-mf/app1")

自此,子应用的也算是改造完了,app2 参考着 app1 改造就行。

Step 4

运行,配置 importmap ,分别启动主应用和子应用 ⚠️ 这里使用的是 https ,所以注意自己的 url 运行起来后,我们可以看到我们主应用的页面,切换的子应用的路由控制台会报错,这是因为我们还是实际配置 importmap 在这之前先推荐安装一下 single-spa 的一个 chrome 扩展 single-spa-inspector ,这个扩展可以在我们运行 single-spa 的微服务应用的时候帮助我进行一些配置,以及做一个子应用的切换

安装了扩展之后我们的 Chrome 控制台就会多这么一个 tab ,在这个 tab 下就能看到我们的子应用有哪些,初始 import override 那是没有配置的,这里根据我们自己的子应用做一个配置就行,上面就是我的配置,到这里算是完成了,我们切换路由就可以看到对应的子应用了。

Updated: