2022年了你必须要学会搭建微前端项目及部署方式

一、微前端简介

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用可以独立运行、独立开发、独立部署。

微前端的好处

  • 应用自治。只需要遵循统一的接口规范或者框架,以便于系统集成到一起,相互之间是不存在依赖关系的。
  • 单一职责。每个前端应用可以只关注于自己所需要完成的功能。
  • 技术栈无关。你可以使用 Angular 的同时,又可以使用 React 和 Vue。

微前端的缺点

  • 应用的拆分基础依赖于基础设施的构建,一旦大量应用依赖于同一基础设施,那么维护变成了一个挑战。
  • 拆分的粒度越小,便意味着架构变得复杂、维护成本变高。
  • 技术栈一旦多样化,便意味着技术栈混乱

微前端由哪些模块组成

当下微前端主要采用的是组合式应用路由方案,该方案的核心是“主从”思想,即包括一个基座(MainApp)应用若干个微(MicroApp)应用,基座应用大多数是一个前端SPA项目,主要负责应用注册,路由映射,消息下发等,而微应用是独立前端项目,这些项目不限于采用React,Vue,Angular或者JQuery开发,每个微应用注册到基座应用中,由基座进行管理,但是如果脱离基座也是可以单独访问,基本的流程如下图所示

image

是否要用微前端

微前端最佳的使用场景是一些B端的管理系统,既能兼容集成历史系统,也可以将新的系统集成进来,并且不影响原先的交互体验

二、微前端实战

image

微前端现有的落地方案可以分为三类,自组织模式、基座模式以及模块加载模式

2.1 SingleSpa实战

官网 https://zh-hans.single-spa.js.org/docs/configuration

适用场景:项目庞大,多个子项目整合在一个大的项目中。即使子项目的所用的技术栈不同,比如vue,react, angular有相应的single-spa的轮子,可以进行整合

1.构建子应用

首先创建一个vue子应用,并通过single-spa-vue来导出必要的生命周

vue create spa-vue  
npm install single-spa-vue
// main.js

import singleSpaVue from 'single-spa-vue';

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

// 在非子应用中正常挂载应用
if(!window.singleSpaNavigate){
 delete appOptions.el;
 new Vue(appOptions).$mount('#app');
}

const vueLifeCycle = singleSpaVue({
   Vue,
   appOptions
});


// 子应用必须导出以下生命周期:bootstrap、mount、unmount
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
export default vueLifeCycle;
// router.js

// 配置子路由基础路径
const router = new VueRouter({
  mode: 'history',
  base: '/vue',   //改变路径配置
  routes
})
image

2. 将子模块打包成类库

//vue.config.js
module.exports = {
    configureWebpack: {
    // 把属性挂载到window上方便父应用调用 window.singleVue.bootstrap/mount/unmount
        output: {
            library: 'singleVue',
            libraryTarget: 'umd'
        },
        devServer:{
            port:10000
        }
    }
}
image

3. 主应用搭建

<div id="nav">
    <router-link to="/vue">vue项目<router-link> 
      
    <!--将子应用挂载到id="vue"标签中-->
    <div id="vue">div>
div>
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from 'single-spa'

Vue.config.productionTip = false

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

// 注册应用
registerApplication('myVueApp',
  async ()=>{
    console.info('load')
    // singlespa问题 
    // 加载文件需要自己构建script标签 但是不知道应用有多少个文件
    // 样式不隔离
    // 全局对象没有js沙箱的机制 比如加载不同的应用 每个应用都用同一个环境
    // 先加载公共的
    await loadScript('http://localhost:10000/js/chunk-vendors.js')
    await loadScript('http://localhost:10000/js/app.js')

    return window.singleVue // bootstrap mount unmount
  },
  // 用户切换到/vue下 我们需要加载刚才定义的子应用
  location=>location.pathname.startsWith('/vue'),
)

start()

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

4. 动态设置子应用publicPath

if(window.singleSpaNavigate){
  __webpack_public_path__ = 'http://localhost:10000/'
}

2.2 qiankun实战

文档 https://qiankun.umijs.org/zh/guide

  • qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
  • qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过一批线上应用的充分检验及打磨后,我们将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统,同时也希望通过社区的帮助将 qiankun 打磨的更加成熟完善。
  • 目前 qiankun 已在蚂蚁内部服务了超过 200+ 线上应用,在易用性及完备性上,绝对是值得信赖的。

1. 主应用搭建

<template>
  <!--注意这里不要写app 否则跟子应用的加载冲突
  <div id="app">-->
  <div>
    <el-menu :router="true" mode="horizontal">
      <!-- 基座中可以放自己的路由 -->
      <el-menu-item index="/">Home</el-menu-item>

      <!-- 引用其他子应用 -->
      <el-menu-item index="/vue">vue应用</el-menu-item>
      <el-menu-item index="/react">react应用</el-menu-item>
    </el-menu>
    <router-view />

    <!-- 其他子应用的挂载节点 -->
    <div id="vue" />
    <div id="react" />
  </div>
</template>

2. 注册子应用


import { registerMicroApps,start } from 'qiankun'
// 基座写法
const apps = [
  {
    name: 'vueApp', // 名字
    // 默认会加载这个HTML,解析里面的js动态执行 (子应用必须支持跨域)
    entry: '//localhost:10000',  
    container: '#vue', // 容器
    activeRule: '/vue', // 激活的路径 访问/vue把应用挂载到#vue上
    props: { // 传递属性给子应用接收
      a: 1,
    }
  },
  {
    name: 'reactApp',
    // 默认会加载这个HTML,解析里面的js动态执行 (子应用必须支持跨域)
    entry: '//localhost:20000',  
    container: '#react',
    activeRule: '/react' // 访问/react把应用挂载到#react上
  },
]

// 注册
registerMicroApps(apps)
// 开启
start({
  prefetch: false // 取消预加载
})

3. 子Vue应用

// src/router.js

const router = new VueRouter({
  mode: 'history',
  // base里主应用里面注册的保持一致
  base: '/vue',
  routes
})

不要忘记子应用的钩子导出。

// main.js

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

Vue.config.productionTip = false

let instance = null
function render() {
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount('#app') // 这里是挂载到自己的HTML中 基座会拿到挂载后的HTML 将其插入进去
}

// 独立运行微应用
// https://qiankun.umijs.org/zh/faq#%E5%A6%82%E4%BD%95%E7%8B%AC%E7%AB%8B%E8%BF%90%E8%A1%8C%E5%BE%AE%E5%BA%94%E7%94%A8%EF%BC%9F
if(!window.__POWERED_BY_QIANKUN__) {
  render()
}

// 如果被qiankun使用 会动态注入路径
if(window.__POWERED_BY_QIANKUN__) {
  // qiankun 将会在微应用 bootstrap 之前注入一个运行时的 publicPath 变量,你需要做的是在微应用的 entry js 的顶部添加如下代码:
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

// 子应用的协议 导出供父应用调用 必须导出promise
export async function bootstrap(props) {} // 启动可以不用写 需要导出方法
export async function mount(props) {
  render()
}
export async function unmount(props) {
  instance.$destroy()
}

4. 配置vue.config.js

// vue.config.js

module.exports = {
    devServer:{
        port:10000,
        headers:{
            'Access-Control-Allow-Origin':'*' //允许访问跨域
        }
    },
    configureWebpack:{
        // 打umd包
        output:{
            library:'vueApp',
            libraryTarget:'umd'
        }
    }
}

5.子React应用

使用react作为子应用

// app.js

import logo from './logo.svg';
import './App.css';
import {BrowserRouter,Route,Link} from 'react-router-dom'

function App() {
  return (
    // /react跟主应用配置保持一致
    <BrowserRouter basename="/react">
      <Link to="/">首页</Link>
      <Link to="/about">关于</Link>

      <Route path="/" exact render={()=>(
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <p>
              Edit <code>src/App.js</code> and save to reload.
            </p>
            <a
              className="App-link"
              href="https://reactjs.org"
              target="_blank"
              rel="noopener noreferrer"
            >
              Learn React
            </a>
          </header>
        </div>
      )} />
      
      <Route path="/about" exact render={()=>(
        <h1>About Page</h1>
      )}></Route>
    </BrowserRouter>
  );
}

export default 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() {
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );
}

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

// 独立运行
if(!window.__POWERED_BY_QIANKUN__){
  render()
}

// 子应用协议
export async function bootstrap() {}
export async function mount() {
  render()
}
export async function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}

重写react中的webpack配置文件 (config-overrides.js)

yarn add react-app-rewired --save-dev

修改package.json文件

// react-scripts 改成 react-app-rewired
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },

在根目录新建配置文件

// 配置文件重写
touch config-overrides.js
// config-overrides.js

module.exports = {
  webpack: (config) => {
    // 名字和基座配置的一样
    config.output.library = 'reactApp';
    config.output.libraryTarget = "umd";
    config.output.publicPath = 'http://localhost:20000/'
    return config
  },
  devServer: function (configFunction) {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);

      // 配置跨域
      config.headers = {
        "Access-Control-Allow-Origin": "*",
      };
      return config;
    };
  },
};

配置.env文件

根目录新建.env

PORT=30000
# socket发送端口
WDS_SOCKET_PORT=30000

路由配置

import { BrowserRouter, Route, Link } from "react-router-dom"

const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";

function App() {
  return (
    <BrowserRouter basename={BASE_NAME}><Link to="/">首页Link><Link to="/about">关于Link><Route path="/" exact render={() => <h1>hello homeh1>}>Route><Route path="/about" render={() => <h1>hello abouth1>}>Route>BrowserRouter>
  );
}
image

2.3 飞冰微前端实战

官方接入指南 https://micro-frontends.ice.work/docs/guide

  • icestark 是一个面向大型系统的微前端解决方案,适用于以下业务场景:
  • 后台比较分散,体验差别大,因为要频繁跳转导致操作效率低,希望能统一收口的一个系统内
  • 单页面应用非常庞大,多人协作成本高,开发/构建时间长,依赖升级回归成本高
  • 系统有二方/三方接入的需求

icestark 在保证一个系统的操作体验基础上,实现各个微应用的独立开发和发版,主应用通过 icestark 管理微应用的注册和渲染,将整个系统彻底解耦。

1. react主应用编写

$ npm init ice icestark-layout @icedesign/stark-layout-scaffold
$ cd icestark-layout
$ npm install
$ npm start
// src/app.jsx中加入

const appConfig: IAppConfig = {

  ...
  
  icestark: {
    type: 'framework',
    Layout: FrameworkLayout,
    getApps: async () => {
      const apps = [
      {
        path: '/vue',
        title: 'vue微应用测试',
        sandbox: false,
        url: [
          // 测试环境
          // 请求子应用端口下的服务,子应用的vue.config.js里面 需要配置headers跨域请求头
          "http://localhost:3001/js/chunk-vendors.js",
          "http://localhost:3001/js/app.js",
        ],
      },
      {
        path: '/react',
        title: 'react微应用测试',
        sandbox: true,
        url: [
          // 测试环境
          // 请求子应用端口下的服务,子应用的webpackDevServer.config.js里面 需要配置headers跨域请求头
          "http://localhost:3000/static/js/bundle.js",
        ],
      }
    ];
      return apps;
    },
    appRouter: {
      LoadingComponent: PageLoading,
    },
  },
};
// 侧边栏菜单
// src/layouts/menuConfig.ts 改造

const asideMenuConfig = [
  {
    name: 'vue微应用测试',
    icon: 'set',
    path: '/vue' 
  },
  {
    name: 'React微应用测试',
    icon: 'set',
    path: '/react'
  },
]

2. vue子应用接入

# 创建一个子应用
vue create vue-child
// 修改vue.config.js

module.exports = {
  devServer: {
    open: true, // 设置浏览器自动打开项目
    port: 3001, // 设置端口
    // 支持跨域 方便主应用请求子应用资源
    headers: {
      'Access-Control-Allow-Origin' : '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
    }
  },
  configureWebpack: {
    // 打包成lib包 umd格式
    output: {
      library: 'icestark-vue',
      libraryTarget: 'umd',
    },
  }
}

src/main.js改造

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

import {
  isInIcestark,
  getMountNode,
  registerAppEnter,
  registerAppLeave,
  setLibraryName
} from '@ice/stark-app'

let vue = createApp(App)
vue.use(store)
vue.use(router)

// 注意:`setLibraryName` 的入参需要与 webpack 工程配置的 output.library 保持一致
//  重要 不加不生效 和 vue.config.js中配置的一样
setLibraryName('icestark-vue')

export function mount({ container }) {
  // ![](http://img-repo.poetries.top/images/20210731130030.png)
  console.log(container,'container')
  vue.mount(container);
}

export function unmount() {
  vue.unmount();
}
  
if (!isInIcestark()) {
  vue.mount('#app')
}

router改造

import { getBasename } from '@ice/stark-app';


const router = createRouter({
  // 重要 在主应用中的基准路由
  base: getBasename(),
  routes
})

export default router

3. react子应用接入

create-react-app react-child
// src/app.js

import { isInIcestark, getMountNode, registerAppEnter, registerAppLeave } from '@ice/stark-app';

export function mount(props) {
  ReactDOM.render(<App />, props.container);
}

export function unmount(props) {
  ReactDOM.unmountComponentAtNode(props.container);
}

if (!isInIcestark()) {
  ReactDOM.render(<App />, document.getElementById('root'));
}

if (isInIcestark()) {
  registerAppEnter(() => {
    ReactDOM.render(<App />, getMountNode());
  })
  registerAppLeave(() => {
    ReactDOM.unmountComponentAtNode(getMountNode());
  })
} else {
  ReactDOM.render(<App />, document.getElementById('root'));
}

npm run eject后,改造 config/webpackDevServer.config.js

hot: '',
port: '',
...

// 支持跨域
headers: {
  'Access-Control-Allow-Origin' : '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
  'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
},

微前端部署

微前端部署实践总结

更多干货只在公号:「前端进阶之旅」分享

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

推荐阅读更多精彩内容