micro-app 微前端实践

3,820 阅读4分钟

micro-app

micro-app 一款轻量、高效、功能强大的微前端框架 真的超简单~

前言

本文章使用 micro-app 以及 react (create-react-app 生成的项目) 实现文章中的内容。

具体会实现两个项目之间通过基座实现跳转,跳转后两个页面中的状态不会丢失。

名词解释

基座应用:用来放置子应用(业务项目)的容器,可以和各个子应用进行通信,主要是负责展示子应用。

子应用:就是前端项目,可以是 react 项目、vue 项目、ng 项目等

项目结构

-- root         #项目根目录

  -- main     #基座应用
    -- react 项目目录

-- one      #子应用1
    -- react 项目目录

-- two      #子应用2
    -- react 项目目录

子应用代码编写

简单实现页面中有一个输入框,并且有一个按钮可以用来跳转到另一个项目。

直接在项目入口文件编写如下代码

// 第一个项目 one/src/index.js

import './public-path'
import React from "react";
import { render } from "react-dom";
import { HashRouter, Switch, Route } from "react-router-dom";

const App = () => {
    return (
        <HashRouter>
            <Switch>
                <Route path="/Test001">
                    <input type="text" />
                    <button
                        onClick={() => { 
                            const data = window.microApp?.getData();  
                            data?.pushState("/two/#/Test002");
                        }}
                    >  到 two 项目 </button>
                </Route>
            </Switch>
        </HashRouter>
    );
};
const rootDom = document.getElementById("root");
render(<App />, rootDom);

ps: react-router-dom@6.x 版本的写法和上面是不一样的,Switch 变成 Routes 即可。

// 第二个项目 two/src/index.js

import './public-path'
import React from "react";
import { render } from "react-dom";
import { HashRouter, Switch, Route } from "react-router-dom";

const App = () => {
    return (
        <HashRouter>
            <Switch>
                <Route path="/Test002">
                    <input type="text" />
                    <button
                        onClick={() => { 
                            const data = window.microApp?.getData();  
                            data?.pushState("/one/#/Test001");
                        }}
                    >  到 one 项目 </button>
                </Route>
            </Switch>
        </HashRouter>
    );
};
const rootDom = document.getElementById("root");
render(<App />, rootDom);

上面两个子项目几乎是一样的,只是按钮中的文字不同和页面的路由不同。

需要注意的是按钮点击后执行的 getData 函数这时候是没有的,它是由基座注入的一个方法。

运行两个项目可以看到页面如下(我分别运行的 81和82端口)

image.png

public-path.js

    // __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量 
    if (window.__MICRO_APP_ENVIRONMENT__) {
         // eslint-disable-next-line 
         __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
    }

子应用设置跨域

本地开发时需要给 webpack 服务器设置跨域,线上生产环境需要给 nginx 设置跨域。

webpack 配置跨域

使用create-react-app脚手架创建的项目,在 config/webpackDevServer.config.js 文件中添加 headers(这里是使用 yarn rejest 将 webpack 暴露出来才会有的文件,也可以用其他方法来解决这个问题)。

其它项目在webpack-dev-server中添加headers。

headers: {
  'Access-Control-Allow-Origin': '*',
}

nginx 配置跨域

location / {  
    add_header Access-Control-Allow-Origin *; 
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    if ($request_method = 'OPTIONS') {
        return 204;
    }
} 

基座应用代码编写

为了更方便的阅读各个代码段的关系,所以把所有代码都放到了一个代码文件中


// 基座项目 main/src/index.js

import React from "react";
import { render } from "react-dom";
import { BrowserRouter, Switch, Route, Link, useHistory } from "react-router-dom"; 

// 因为React不支持自定义事件,所以需要引入一个polyfill。
/** @jsxRuntime classic */
/** @jsx jsxCustomEvent */
import jsxCustomEvent from "@micro-zoe/micro-app/polyfill/jsx-custom-event";

// entry
import microApp from "@micro-zoe/micro-app";

// 定义好两个子应用的数据
const apps = [
    { name: "appOne", url: "http://localhost:81/" },
    { name: "appTwo", url: "http://localhost:82/" },
];

// 预加载子应用
microApp.preFetch(apps);
microApp.start();

// 子应用1
const Page = () => { 
   
    // 子应用之间的跳转必须由基座实现
    // 如果在页面中使用 location.href = 'xxx' 跳转,将会导致 keep-alive 无法缓存
    const history = useHistory();
    function pushState(path) {
        history.push(path);
    }
    return (
        <>
            <span style={{ fontSize: 15, paddingRight: 6 }}>当前在应用1</span> 
            <micro-app
                // name(必传):应用名称
                name={apps[0].name} 
                // url(必传):应用地址会被自动补全为http://localhost:3000/index.html
                url={apps[0].url} 
                // 切换应用后页面状态不会丢失
                keep-alive
                // 传入到子应用的数据
                data={{
                    pushState,
                }} 
            ></micro-app>
        </>
    );
};

// 子应用2
const PageTwo = () => { 
    const history = useHistory();
    function pushState(path) {
        history.push(path);
    }
    return (
        <>
            <span style={{ fontSize: 15, paddingRight: 6 }}>当前在应用2</span> 
            <micro-app
                name={apps[1].name} 
                url={apps[1].url}  
                keep-alive
                data={{ 
                        pushState,
                }} 
            ></micro-app>
        </>
    );
};

// 路由
const RouterLay = () => {
    return (
        <BrowserRouter basename="/main">
            <Switch>
                <Route path="/one">
                    <Page />
                </Route>
                <Route path="/two">
                    <PageTwo />
                </Route>
            </Switch>
        </BrowserRouter>
    );
};

const App = () => {
    return <RouterLay />;
};

const rootDom = document.getElementById("root");
render(<App />, rootDom);

简单分析一波上面的代码

首先项目有两个路由 /one/two

两个路由分别对应了两个函数组件,两个函数组件中的内容几乎一致,都是调用micro-app组件,然后传入一个 data 给子应用,提供子应用跳转到另一个子应用的能力。

打开基座项目

微前端.gif

show 一波代码 image.png

😑 是不是超简单(感谢京东提供的车轮)~

虽然用起来简单,但是要用到生产环境中还是需要花一些时间的,主要的时间还是花在路由处理、各个子应用直接数据的共享...。 子应用需要调用基座提供的方法跳转(如果不考虑缓存页面的话就不用考虑这么多了~)。

如下面这种,如果系统顶部有页签功能,各个子系统之间切换的话,同步这个页签...🤔

image.png