乐趣区

关于前端:2022-你还不会微前端吗-上-从巨石应用到微应用

前言

微前端系列分为 上 / 下 两篇,本文为 上篇 次要还是理解微前端的由来、概念、作用等,以及基于已有的微前端框架进行实际,并理解微前端的外围性能所在,而在下篇 2022 你还不会微前端吗 (下) — 揭秘微前端外围原理 中次要就是通过自定义实现一个微前端框架来加深了解。

微前端是什么?

微前端 是一种相似于 微服务 的概念,因而要想更好的理解微前端,就必须先理解一下微服务。

微服务

微服务架构 是将一个宏大的业务零碎依照业务模块拆分成 若干个独立的子系统 ,每个子系统都是一个独立的利用,它是一种将利用构建成一系列按 业务畛域 划分模块的、小的自治服务的软件架构形式,提倡将 简单的单体利用 拆分成 若干个性能繁多、松偶合的服务,目标是升高开发难度、加强扩展性、便于麻利开发,及继续集成与交付流动。

与微服务绝对的另一个概念是传统的 单体式应用程序 (Monolithic application),单体式利用外部蕴含了 所有须要的服务 ,且各个服务功能模块具备 强耦合性(相互依赖),导致难以进行拆分和扩容。

简略来说,单体式应用程序 其实就是一台服务器解决了须要所有的性能, 微服务 就是将性能依照业务模块划分成了不同的独立服务,各个微服务间通过 HTTP 协定进行通信,通过注册核心观测微服务状态。

微服务 概念次要存在于后端开发,但这个概念是不是和你据说过的 微前端 很像了。

微前端

随着大前端的疾速倒退 和 SPA 的大规模利用,也带来了新的问题,而这些问题都催化出了 微前端 的概念:

  • 我的项目性能一直增多、体积一直增大(巨石利用 ),导致打包工夫成正比例增长,是否能保障更好的 我的项目扩大
  • 前端技术更新太快,一个我的项目历经一两年兴许就须要进行我的项目降级,甚至是切换技术栈,但仍须要老我的项目的代码,是否能进行 新老版本的兼容
  • 团队技术栈不一,又须要保障同一我的项目的开发,是否能保障不同团队的 独立开发
  • SPA 我的项目的任何变动都需执行残缺的打包、部署,是否能保障不同内容 独立部署

微前端 是一种相似于 微服务 的架构,是一种由独立交付的 多个前端利用 组成整体的架构格调,将前端利用分解成一些更小、更简略的可能 独立开发、测试、部署 的利用,而对外体现仍是 单个内聚的产品

微前端框架

微前端框架的外围

一个微前端框架至多要保障如下的外围性能:

  • 技术栈无关

    • 主框架不限度接入子利用的技术栈,微利用具备齐全自主权
  • 独立开发、独立部署

    • 微利用仓库独立,前后端可独立开发,部署实现后主框架主动实现同步更新
  • 增量降级

    • 在面对各种简单场景时,通常很难对一个曾经存在的零碎做全量的技术栈降级或重构,而微前端是一种十分好的施行渐进式重构的伎俩和策略
  • 独立运行时

    • 每个微利用之间状态隔离,运行时状态不共享

    single-spa

    single-spa 是一个将多个单页面利用聚合为一个整体利用的 JavaScript 微前端框架。

外围原理

基座 (主) 利用 中注册所有 App 的路由,single-spa 保留各子利用的路由映射关系,充当微前端控制器 Controler,当对应的 URL 变换时,除了匹配 基座利用 自身的路由外,还会匹配 子利用 路由并加载渲染子利用。

子利用会通过如下过程

  • 下载 (loaded)
  • 初始化 (initialized)
  • 挂载 (mounted)
  • 卸载 (unmounted)

single-spa 还会通过 生命周期 为这些过程提供对应的 钩子函数

qiankun

qiankun 是一个基于 single-spa 的 微前端 实现库,目标是提供更简略、无痛的构建一个生产可用微前端架构零碎。

包含在 single-spa 文档中也有举荐应用 qiankun

single-spa 实际

创立子利用

Vue3 子利用

为了疾速的创立利用,这里通过 vue create vue3-micro-app 来疾速创立技术栈为 Vue3子利用

以下在子利用中的解决形式可用 single-spa-vue 来简化

页面成果如下

子利用入口文件

为了子利用既能够独立运行,也能够在基座利用中运行,须要在子利用入口文件进行一些批改,具体如下:

  • 将本来的初始化内容封装在自定义的 render 函数中,目标是能够在不同的环境执行初始化操作

    • 若以后在基座利用中进行渲染,则其页面内容对应的挂载容器须要指定为基座容器中对应的 DOM 节点
    • window.singleVue3 不存在时意味着是子利用独立运行,此时间接依照本来的初始化形式进行即可,即间接调用 render() 函数
  • 子利用必须导出 bootstrap、mount、unmount 等生命周期函数,且其返回值类型要为 fullfilled 状态的 Promise,否则后续操作不会执行
  • 定义 instance 变量存储实例对象,不便在以后子利用在基座利用中被切换时能够执行真正的卸载子利用

    // main.ts
    
    import {createApp} from 'vue'
    import type {App as AppType} from 'vue'
    import App from './App.vue'
    import router from './router'
    
    let instance: AppType
    
    function render(container?: string) {instance = createApp(App)
      instance.use(router).mount(container || '#micro-vue-app')
    }
    
    // 当 window.singleVue3 不存在时,意味着是子利用独自运行
    if (!window.singleVue3) {render();
    }
    
    // 子利用必须导出 以下生命周期 bootstrap、mount、unmount
    export const bootstrap = () => {return Promise.resolve()
    };
    export const mount = (props: any) => {render(props.container);
      return Promise.resolve()};
    export const unmount = () => {instance.unmount();
      return Promise.resolve()};

    为什么要将 x.mount('#app') 换成 x.mount('#micro-vue-app') ?

如果你明确 子利用 基座利用 中的渲染形式就不难理解了,因为以后这个子利用的挂载容器的 id="app" 而基座利用中的默认挂载容器也是 id="app",这显然会导致抵触,初始化渲染时会渲染基座利用自身,然而当你切换到 vue3 的子利用时,就会发现以后子利用的内容整个笼罩了基座利用的内容,因为此时子利用在进行挂挂载的时候,会把曾经渲染 基座利用 的容器再一次作为 子利用 的容器进行渲染,于是内容就会被齐全替换成子利用的内容。

基座利用被子利用替换成果如下:

路由配置

路由模式为 hash 模式,默认路由配置:

import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router'
import HomeView from '../views/HomeView.vue'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  }
]

const router = createRouter({history: createWebHashHistory(),
  routes
})

export default router

打包配置

vue.config.js 下必须将打包后的输入格局指定为 umd 格局:

module.exports = {
  configureWebpack: {
    output: {
      library: 'singleVue3',
      libraryTarget: 'umd',
      globalObject: 'window',
    },
    devServer: {port: 5000,},
  },
}

React 子利用

相似的,这里通过 npx create-react-app react-micro-app 来疾速创立技术栈为 React子利用

以下在子利用中的解决形式可用 single-spa-react 来简化

页面成果如下

子利用入口文件

此局部核心内容和上述的 vue3 子利用统一,不在额定阐明,入口文件代码如下:

// index.js

import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'

let root = null

function render(props = {}) {
  const container = document.getElementById(props.container ? props.container.slice(1) : 'root',
  )

  if(!container) return
  
  root = ReactDOM.createRoot(container)
  root.render(
    <React.StrictMode>
      <App {...props} />
    </React.StrictMode>,
  )
}

// 当 window.singleReact 不存在时,意味着是子利用独自运行
if (!window.singleReact) {render()
}

// 子利用必须导出 以下生命周期 bootstrap、mount、unmount
export const bootstrap = () => {return Promise.resolve()
}
export const mount = (props) => {render(props)
  return Promise.resolve()}
export const unmount = () => {root.unmount()
  return Promise.resolve()}

路由配置

路由模式为 hash 模式,自定义路由配置:

<Router>
    <Switch>
      <Route path="/" exact>
        <Home />
      </Route>
      <Route path="/about" exact>
        <About />
      </Route>
    </Switch>
</Router>

打包配置

对于 React 利用来讲,要么是一开始就是自定义 webpack 相干配置,要么是基于 crate-react-app 内置的 webpack 配置进行批改,因为下面是通过 crate-react-app 的形式创立的我的项目,因而能够基于其内置的配置文件进行批改:

不想通过 eject 的形式去批改配置文件,可通过 react-app-rewired 进行重写

  • 执行 npm run eject 将内置的 webpack 配置像裸露进去,会生成一个 scripts 目录 和 config 目录
  • 这里只须要关注 config 目录即可,因为 webpack.config.js 在该目录下
  • 为其中的 output 配置增加上如下两行配置项即可(需重新启动)

     output: {
        library: 'singleReact',
        libraryTarget: 'umd',
        globalObject: 'window',
        ....
     }

    创立基座利用

    基座利用的技术栈这里抉择 Vue2,同样能够通过 vue create vue2-main-app 的形式创立对应的基座利用:

路由配置

为了让 基座利用 的路由看起来更好看,这里抉择 history 模式(也可选 hash 模式),接着配置具体路由:

  • 基座利用的路由

    • 配置对应的路由门路
    • 指定对应组件作为路由渲染视图,如 HomeView 组件
  • 微利用的路由

    • 只须要设定对应的路由门路,不须要指定对应的具体组件
    import Vue from 'vue'
    import VueRouter, {RouteConfig} from 'vue-router'
    import HomeView from '../views/HomeView.vue'
    
    Vue.use(VueRouter)
    
    const routes: Array<RouteConfig> = [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/vue3-micro-app',
      name: 'about',
    },
    {
      path: '/react-micro-app',
      name: 'about',
    }
    ]
    
    const router = new VueRouter({
    mode: 'history',
    base: process.env.BASE_URL,
    routes
    })
    
    export default router

    子利用挂载容器

    基座利用中的 <router-view /> 是为了渲染基座利用本身的路由视图,而子利用是不能通过 <router-view /> 来渲染,因而,咱们须要在基座利用中设定一个 Dom 节点专门用于渲染子利用的视图,这里就将子利用的内容挂载在 <div id="micro-content"></div> 的节点中:

    <!-- 主内容 -->
    <main class="content">
    <!-- 基座利用路由视图渲染 -->
    <router-view />
    
    <!-- 子利用挂载容器 -->
    <div id="micro-content"></div>
    </main>

    菜单配置

    菜单配置理论就是指定路由的跳转,具体蕴含内容如下:

  • Home 菜单渲染的视图内容是 基座利用 中对应的 HomeView 组件
  • Vue3-micro-app 菜单渲染视图是名为 vue3-micro-app子利用
  • Home 菜单渲染视图是基名为 react-micro-app子利用

    基座利用注册子利用

    基座利用 中尚未注册 子利用 时的页面成果如下:

为了切换菜单路由时,对应的子利用可能被正确的渲染在基座利用中,须要咱们在基座利用中注册子利用:

  • 通过 pnpm install single-spa -S 装置 single-spa
  • 通过 single-spa 中提供的 registerApplication()start() 函数实现注册和启动,该逻辑可抽离到 registerApplication.ts

    // registerApplication.ts
    
     import {registerApplication, start} from 'single-spa';
    
    // 子利用
    export const applications = [{
        name: 'singleVue3',
        async activeWhen() {await loadScript('http://localhost:5000/js/chunk-vendors.js');
            await loadScript('http://localhost:5000/js/app.js');
            return window.singleVue3
        },
        app(location: Location) {return location.pathname.startsWith('/vue3-micro-app')
        },
        customProps: {container: '#micro-content'}
    },
    {
        name: 'singleReact',
        async activeWhen() {await loadScript('http://localhost:3000/static/js/main.js');
            return window.singleReact
        },
        app(location: Location) {return location.pathname.startsWith('/react-micro-app')
        },
        customProps: {container: '#micro-content'}
    }]
    
    // 加载子利用 script
    export const loadScript = async (url: string) => {await new Promise((resolve, reject) => {const script = document.createElement('script');
            script.src = url;
            script.onload = resolve;
            script.onerror = reject;
            document.head.appendChild(script)
        });
    }
    
    // 注册子利用
    export const registerApps = (apps: any[] = applications) => {
        apps.forEach(({
            name,
            activeWhen,
            app,
            customProps = {}}) => registerApplication(name, activeWhen, app, customProps)
        );
    
        start();}
  • main.ts 中导入并执行 registerApplication() 即可

成果预览

源码地址

其中蕴含了 子利用 在基座利用中 特定地位的渲染 ,以及 子利用 本身路由的切换时的成果,能够看出子利用路由和主利用路由互不影响。

qiankun 实际

有了后面 single-spa 的根底,通过 qiankun 来实现微前端更加简略了,因为 qiankun 自身就是基于 single-spa 实现的微前端架构零碎,目标是提供更简略、简洁的形式接入。

上面咱们还是应用上述的三个我的项目来通过 qiankun 的模式来实现微前端。

配置基座利用

vue2-main-app 的入口文件 registerApplication.ts 中应用 qiankun 进行简略配置即可,相比于下面 single-spa 的形式来说更简略:

// registerApplication.ts

import {registerMicroApps, start} from 'qiankun';

// 默认子利用
export const applications = [
    {
        name: 'singleVue3', // app name registered
        entry: 'http://localhost:5000',
        container: '#micro-content',
        activeRule: '/vue3-micro-app',
      },
      {
        name: 'singleReact', // app name registered
        entry: 'http://localhost:3000',
        container: '#micro-content',
        activeRule: '/react-micro-app',
      },
]

// 注册子利用
export const registerApps = (apps: any[] = applications) => {registerMicroApps(applications);

    start();}

配置子利用

子利用 局部该导出的生命周期还是要导出,值得注意的是生命周期中的 container 曾经是对应基座利用中的 实在 DOM 节点,而不是 CSS 选择器,因而只须要进行简略的批改即可,具体如下所示:

vue3-micro-app 子利用

// src/main.ts

import {createApp} from 'vue'
import type {App as AppType} from 'vue'
import App from './App.vue'
import router from './router'

let instance: AppType

function render(container?: string) {instance = createApp(App)
    // 这里的 container 曾经是对应基座利用中的实在 DOM 节点,而不是 CSS 选择器
    instance.use(router).mount(container || '#micro-vue-app')
}

// 当 window.singleVue3 不存在时,意味着是子利用独自运行
if (!window.singleVue3) {render();
}

// 子利用必须导出 以下生命周期 bootstrap、mount、unmount
export const bootstrap = () => {return Promise.resolve()
};
export const mount = (props: any) => {render(props.container);
    return Promise.resolve()};
export const unmount = () => {instance.unmount();
    return Promise.resolve()};

react-micro-app 子利用

// src/index.js

import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'

let root = null

function render(props = {}) {
  // 这里的 container 曾经是对应基座利用中的实在 DOM 节点,而不是 CSS 选择器
  const container = props.container || document.getElementById('root')

  if(!container) return

  root = ReactDOM.createRoot(container)
  root.render(
    <React.StrictMode>
      <App {...props} />
    </React.StrictMode>,
  )
}

// 当 window.singleReact 不存在时,意味着是子利用独自运行
if (!window.singleReact) {render()
}

// 子利用必须导出 以下生命周期 bootstrap、mount、unmount
export const bootstrap = () => {return Promise.resolve()
}
export const mount = (props) => {render(props)
  return Promise.resolve()}
export const unmount = () => {root.unmount()
  return Promise.resolve()}

子利用配置 CORS

后面说过基座利用是须要将子利用的入口文件加载到以后利用下来执行的,这个过程第一步就是申请对应的入口文件,因为浏览器 同源策略 的限度,咱们必须要在子利用中配置以后子利用的资源是容许被跨域申请的。

子利用没有配置 CORS 产生跨域

vue3-micro-app 配置 CORS

vue.config.js 中配置 devServer 既可,其中的 devServer 能够配置所有合乎 webpack-dev-server 的选项:

module.exports = {
  publicPath: '//localhost:5000',
  configureWebpack: {
    output: {
      library: 'singleVue3',
      libraryTarget: 'umd',
      globalObject: 'window',
    },
    devServer: {
      port: 5000,
      headers: {'Access-Control-Allow-Origin': '*',},
    },
  },
}

react-micro-app 配置 CORS

因为之前是通过 npm run eject 的形式裸露进去和 webpack 相干的配置,在查看对应的 config\webpackDevServer.config.js 配置发现其外部曾经默认做了 CORS 配置

最初

通过以上的实际,上面简略地对微前端框架核心内容进行本人的了解:

  • 技术栈无关

    • 任何一个子利用不管应用什么技术栈,最终都会被编译为 JavaScript 代码,因而真正在执行时无论基座利用还是子利用都曾经是同一种语言模式
  • 独立开发、独立部署

    • 子利用实质上就是广泛应用的 spa 单页面利用,因而当然能够领有独立代码仓库进行关联,可独立公布运行,也可作为子利用运行,只须要做好不同环境的兼容即可
  • 增量降级

    • 子利用可能独立开发部署,天然反对本身利用的性能的独立扩大,又或者是接入新的子利用
  • 独立运行时

    • 保障多个子利用在基座利用中运行时,本身的状态不受其余子利用的影响

本篇文章就这里就完结了,下一篇文章再去聊聊微前端的实现原理,以及通过本人实现一个微前端的形式加深了解。

心愿本篇文章能对你有所帮忙!!!

参考

  • 微前端架构的几种技术选型
  • 2022 年你必须要会的微前端
  • 什么叫做微服务?它和传统的我的项目之间有什么区别?
  • 什么是微服务架构?
退出移动版