女王控的博客

微前端适配demo的实践

需求背景

如下图所示,由于多个产品共存,当初设计产品架构时,侧边栏、打电话组件是存在于每个项目的子模块中的。迫于产品越来越多,需要动到侧边栏的时候越来越频繁,随着产品的不断增加导致了需要编译每个项目的工作越来越重,急需一种新的架构来分离侧边栏与各自产品端。

2020 04 01 11 05 14

技术方案

基本原理类似于之前的文章 记一次组件打包为链接的实践,也就是主框架也就是侧边栏项目提前设定好元素的位置,再用 ReactDOM.render 方法去渲染结点,当然还有许多其他要注意的地方,比如链接的缓存规则、路由之间的跳转、子框架应用间的通信、样式和脚本的隔离等等。

参考市面上已有的解决的方案,发现比较符合微前端框架的适用场景,于是开始用以下微前端框架来做出一个 demo 级别的应用

  1. ice-stark
  2. single-spa
  3. qiankun

具体实施

ice-stark

主框架

以路径作为识别子应用的方式,子应用需要配置 publicPath,否则访问不到

这里假设产品 1 是采用了旧框架的子应用,产品 2 采用了新的

以下是在 ice/stark 1.4.1 的版本上开始做的,需要添加代码如下:

src\platform\layout\index.js

import { AppRouter, AppRoute } from '@ice/stark';
<AppRouter
  NotFoundComponent={<div>未找到</div>}
  LoadingComponent={<div>加载中</div>}
  onRouteChange={this.handleRouteChange}
>
  <AppRoute
    path="/product1"
    basename="/product1"
    title="产品1"
    entry="产品1的域名/index.html"
  />
  <AppRoute
    path="/product2"
    basename="/product2"
    title="产品2"
    entry="产品2的域名/index.html"
  />
</AppRouter>

结合公司实际的应用场景,不同产品对应着不同的域名,这就不能用路径的方式来识别子应用的加载,需要作出如下改动

当然对应的子应用就不需要路径别名的配置了,这样实现的缺点是 onRouteChange 这个函数不再起作用,而且不能控制子应用的缓存,这些不同域名的缓存全部交给对应的 nginx 来做了

import { AppRouter, AppRoute } from '@ice/stark';
<AppRouter
  NotFoundComponent={<div>未找到</div>}
  LoadingComponent={<div>加载中</div>}
  onRouteChange={this.handleRouteChange}
>
  {
    产品1的域名 ? <AppRoute
      path="/"
      basename="/"
      title="产品1"
      entry="产品1的域名/index.html"
    /> : null
  }

  {
    产品2的域名 ? <AppRoute
      path="/"
      basename="/"
      title="产品2"
      entry="产品2的域名/index.html"
    /> : null
  }
</AppRouter>

子框架

老项目

老项目采用了 react-router 3.2.1,区别于新项目

以下是在 ice/stark-app 1.2.0 的版本上开始做的,需要添加代码如下:

入口文件

app\app.js

import { applyRouterMiddleware, Router, browserHistory, useRouterHistory } from 'react-router';
import { createHistory } from 'history';
import { isInIcestark, getMountNode, registerAppEnter, registerAppLeave, getBasename } from '@ice/stark-app';

const baseHistory = useRouterHistory(createHistory)({
  basename: getBasename(),
});

const isIE9 = utils.getBrowserInfo().msie && utils.getBrowserInfo().version === '9.0';

const history = syncHistoryWithStore(baseHistory, store, {
  selectLocationState: makeSelectLocationState(),
  adjustUrlOnReplay: isIE9 ? false : undefined,
});

const router = () => (<ConfigProvider locale={zhCN}>
  <Provider store={store}>
    <Router
      history={history}
      routes={rootRoute}
      render={
        // Scroll to top when going to a new page, imitating default browser behaviour
        applyRouterMiddleware(useScroll())
      }
    />
  </Provider>
</ConfigProvider>);

if (isInIcestark()) {
  registerAppEnter(() => {
    ReactDOM.render(router(), getMountNode());
  });
  registerAppLeave(() => {
    ReactDOM.unmountComponentAtNode(getMountNode());
  });
} else {
  ReactDOM.render(router(), document.getElementById('app'));
}
网络请求

网络请求、资源都需要绝对路径,这就涉及到资源的跨域,需要处理

app\utils\httpFetch.js

export default function httpFetch(url, options) {
  const newUrl = __COMMON_VARS__.robots + url;
  // already forced sign out and has shown modal, cancel fetch
  if (hasShownForcedSignOutModal) {
    return new Promise((resolve, reject) => {
      const error = new Error();
      error.status = 401;
      reject(error);
    });
  }
  // eslint-disable-next-line no-param-reassign
  options = {
    ...options,
    credentials: 'include',
  };
  return runFetch(newUrl, options)
    .then((response) => checkStatus(response, newUrl, options));
}
webpack配置

internals\webpack\webpack.base.babel.js

output: Object.assign({ // Compile into js/build.js
  path: path.resolve(process.cwd(), 'build'),
  publicPath: `${globalVars.robots}/product1/`,
}, options.output), // Merge with env dependent settings
nodejs服务器配置

server\index.js

// 设置允许资源文件跨域访问该服务
app.all('*', (req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  next();
});

// Copy proxy.config.example.json to proxy.config.json to setup proxy
const proxy = require('http-proxy-middleware');
config.proxies.forEach((item) => {
  app.use(item.match, proxy({
    target: item.host,
    changeOrigin: true,
    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes(proxyRes, req, res) {
      res.header('Access-Control-Allow-Origin', req.headers.origin);
      res.header('Access-Control-Allow-Credentials', 'true');
      res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With, Pragma, Cache-Control');
    },
    // 修改响应信息中的cookie域名
    // cookieDomainRewrite: 'www.domain1.com', // 可以为false,表示不修改
  }));
});

// In production we need to pass these values in instead of relying on webpack
setup(app, {
  outputPath: resolve(process.cwd(), 'build'),
  publicPath: '/product1',
});

新项目

新项目采用了 react-router 3.2.1,相同的部分不再阐述,只有入口文件不同于老项目

入口文件

src\index.js

import { BrowserRouter, Router } from 'react-router-dom';
import { isInIcestark, getMountNode, registerAppEnter, registerAppLeave, getBasename } from '@ice/stark-app';

const router = () => (<Provider
  {...stores}
>
  <Router history={history}>
    <BrowserRouter basename={getBasename()}>
      <ConfigProvider locale={zhCN}>
        <App />
      </ConfigProvider>
    </BrowserRouter>
  </Router>
</Provider>);

if (isInIcestark()) {
  registerAppEnter(() => {
    ReactDOM.render(router(), getMountNode());
  });
  registerAppLeave(() => {
    ReactDOM.unmountComponentAtNode(getMountNode());
  });
} else {
  ReactDOM.render(router(), document.getElementById('app'), renderCallback);
}

single-spa

待研究,不过这个框架只兼容 IE 10

qiankun

待研究,不过这个框架不兼容 IE

实现效果

2020 04 01 15 21 23

2020 04 01 15 20 24

评论

阅读下一篇

移动端适配汇总
2020-03-16 17:47:42
0%