微前端调研记录

What? 什么是微前端?

微前端就是将不同的功能按照不同的维度拆分成多个子应用,通过主应用来加载这些子应用。

微前端的核心在于拆,拆完后在合!

Why? 为什么去使用它?

  • 不同的团队间开发同一个应用技术栈不同的问题
  • 希望每个团队都可以独立开发,独立部署问题
  • 项目中还需要老的应用代码集成问题

我们是不是可以将一个应用划分成若干个子应用,将子应用打包成一个个的 lib,当路劲切换的时候,加载不用的子应用,这样每一个子应用都是独立的,技术栈也不用做限制了,从从而解决了前端协同开发的问题。

How? 怎么落地微前端?

举个例子:

image

2018年 Single-SPA 的出现,single-spa 是一个用于前端为服务化的 JavaScript 前端解决方案(本身没有处理样式隔离,js 执行隔离)只实现了路由劫持和应用加载。

2019年 QianKun 基于 Single-SPA 提供了更加开箱即用的 API ( single-spa + sandbox + import-html-entry )做到了,技术栈无关,并且接入简单(像 iframe 一样简单)

总结:子应用可以独立构建,运行时动态加载主子应用完全解耦,技术栈无关,靠的是协议接入(子应用必须导出 bootstarp、mount、unmount 方法)

为什么不用 iframe?

其实 iframe 也不是不可以用,毕竟也是原生的支持应用的隔离,如果不考虑体验问题,iframe 是最完美的微前端解决方案,天生的沙盒模式,但就是应为太过于原生的沙盒支持使得很多时候应用间的交互变得困难,即使采用各种方式解决了交互上的困难,但也会由此带来应用间耦合性越来越高,带来更多其他的问题,比如:

  • iframe 中的子应用切换路由的时候用户刷新了页面导致路由状态丢失
  • cookie 共享还得保证顶层父子应用域名一致
  • Url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用
  • DOM 结构不共享,想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..

微前端通信

  • 基于 URL 来进行数据的传递,但是传递消息能力弱
  • 基于 CustomEvent 实现通信
  • 基于 props 主子应用通信
  • 使用全局变量、Vue、Redux 进行通信
  • MessageChannel 通信

微前端公共依赖

  • CDN
  • Webpack 联邦模块
  • Npm 包

Single Spa

Parent App

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { Menu } from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
import { registerApplication, start } from 'single-spa'

async function loadScript(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = url
    script.onload = resolve
    script.onerror = reject
    document.head.appendChild(script)
  })
}

registerApplication('childVueApp',
  async () => {
    await loadScript(`http://localhost:8086/js/chunk-vendors.js`)
    await loadScript(`http://localhost:8086/js/app.js`)
    return window.singleVue // 包含:bootstarp、mount、unmount
  },
  location => location.pathname.startsWith('/vue'), // 路径劫持
  {
    app: 'child-vue-app'
  }
)

registerApplication('childReactApp',
  async () => {
    await loadScript(`http://localhost:8087/static/js/bundle.js`)
    await loadScript(`http://localhost:8087/static/js/vendors~main.chunk.js`)
    await loadScript(`http://localhost:8087/static/js/main.chunk.js`)
    return window.singleReact // 包含:bootstarp、mount、unmount
  },
  location => location.pathname.startsWith('/react'), // 路径劫持
  {
    app: 'child-react-app'
  }
)

start();


Vue.config.productionTip = false

Vue.use(Menu);

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

复制代码

Child Vue App

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import SingleSpaVue from 'single-spa-vue'

Vue.config.productionTip = false

const appOptions = {
  el: '#vue',
  router,
  render: h => h(App)
}

const vueLifeCycles = SingleSpaVue({
  Vue,
  appOptions
})

if (window.singleSpaNavigate) {
  __webpack_public_path__ = 'http://localhost:8086/'
} else {
  delete appOptions.el
  new Vue(appOptions).$mount('#app');
}


// 协议接入,定义好协议,父应用会调用这些方法
export const bootstarp = vueLifeCycles.bootstrap
export const mount = vueLifeCycles.mount
export const unmount = vueLifeCycles.unmount

// webpack config
module.exports = {
  configureWebpack: {
    output: {
      library: 'singleVue',
      libraryTarget: 'umd'
    }
  },
  devServer: {
    port: 8086
  }
}
复制代码

Child React App

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>Child React App</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

// webpack config
module.exports = {
  webpack: config => {
    config.output.library = 'singleReact'
    config.output.libraryTarget = 'umd'
    config.output.publicPath = 'http://localhost:8087/'
    return config
  },
  devServer: configFunction => {
    return (proxy, allowedHost) => {
      const config = configFunction(proxy, allowedHost)
      config.headers = {
        'Access-Control-Allow-Origin': '*'
      }
      return config
    }
  }
}
复制代码

效果

image

Qian Kun

qiankun 的案例图示其实和上面的是一样的,只不过 qiankun 在 single-spa 上进行了封装,包括接入方式的简化,样式隔离,js 沙箱等优化

Parent App

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { Menu } from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'childVueApp', // app name registered
    entry: 'http://localhost:8086',
    container: '#vue',
    activeRule: '/vue',
  },
  {
    name: 'childReactApp',
    entry: 'http://localhost:8087',
    container: '#react',
    activeRule: '/react',
  },
]);

start();

Vue.config.productionTip = false

Vue.use(Menu)

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
复制代码

Child Vue App

// mian.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

let instance = null
function render(props) {
  const { container } = props;
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount(container ? container.querySelector('#app'):'#app');
}

if (window.singleSpaNavigate) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
} else {
  render();
}

export async function bootstrap() {
  console.log('react app bootstraped');
}

export async function mount(props) {
  render(props)
}

export async function unmount(props) {
  instance.$destroy();
}
// webpack config
module.exports = {
  configureWebpack: {
    output: {
      library: 'qiankunVue',
      libraryTarget: 'umd'
    }
  },
  devServer: {
    port: 8086,
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  }
}
复制代码

Child React App

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

function render(props) {
  const { container } = props;
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    container ? container.querySelector('#root') : document.getElementById('root')
  );
};

if (!window.singleSpaNavigate) {
  render();
}

export async function bootstrap() {
  console.log('react app bootstraped');
}

export async function mount(props) {
  render(props);
}

export async function unmount(props) {
  const { container } = props;
  ReactDOM.unmountComponentAtNode(
    container ? container.querySelector('#root') : document.getElementById('root')
  );
}
// webpack config
module.exports = {
  webpack: config => {
    config.output.library = 'qiankunReact'
    config.output.libraryTarget = 'umd'
    config.output.publicPath = 'http://localhost:8087/'
    return config
  },
  devServer: configFunction => {
    return (proxy, allowedHost) => {
      const config = configFunction(proxy, allowedHost)
      config.headers = {
        'Access-Control-Allow-Origin': '*'
      }
      return config
    }
  }
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享