微前端实践一

微前端起源

微前端的概念最早由 thoughtworks 在 2016 年提出。其核心思路是借鉴后端微服务架构理念,将一个单体的庞大的前端应用拆分为多个简单独立的前端工程。每个前端工程可以独立开发、测试、部署。最终再由一个容器应用,将拆分后的微前端工程组合为一个整体,面向用户提供服务

微前端的价值

  • 技术栈无关
    主框架不限制接入应用的技术栈,子应用具备完全自主权
  • 独立开发、独立部署
    子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 独立运行时
    每个子应用之间状态隔离,运行时状态不共享

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

解决方案:

MPA: 多页面应用(Multi page web application)
SPA: 单页面应用(Single page web appliction)

MPA:

  • 优点: 部署简单、各应用之间硬隔离,天生具备技术栈无关、独立开发、独立部署的特性。
  • 缺点则也很明显,应用之间切换会造成浏览器重刷,由于产品域名之间相互跳转,流程体验上会存在断点。

SPA

  • 优点: 则天生具备体验上的优势,应用直接无刷新切换,能极大的保证多产品之间流程操作串联时的流程性。
  • 缺点则在于各应用技术栈之间是强耦合的。

常见的实现方式

  • 路由分发式。通过 HTTP 服务器的反向代理功能,来将请求路由到对应的应用上。
  • 前端微服务化。在不同的框架之上设计通讯、加载机制,以在一个页面内加载对应的应用。
  • 微应用。通过软件工程的方式,在部署构建环境中,组合多个独立应用成一个单体应用。
  • 微件化。开发一个新的构建系统,将部分业务功能构建成一个独立的 chunk 代码,使用时只需要远程加载即可。
  • 前端容器化。通过将 iFrame 作为容器,来容纳其它前端应用。
  • 应用组件化。借助于 Web Components 技术,来构建跨框架的前端应用。

路由分发式

路由分发式微前端,即通过路由将不同的业务分发到不同的、独立前端应用上。其通常可以通过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。

前端微服务化

前端微服务化,是微服务架构在前端的实施,每个前端应用都是完全独立(技术栈、开发、部署、构建独立)、自主运行的,最后通过模块化的方式组合出完整的前端应用。其

组合式集成:微应用化

微应用化,即在开发时,应用都是以单一、微小应用的形式存在,而在运行时,则通过构建系统合并这些应用,组合成一个新的应用。

微件化

微件(widget),指的是一段可以直接嵌入在应用上运行的代码,它由开发人员预先编译好,在加载时不需要再做任何修改或者编译。

前端容器化

前端容器 iframe 或 web components

Systemjs模块化解决方案
https://github.com/systemjs/systemjs

systemjs 是一个最小系统加载工具,用来创建插件来处理可替代的场景加载过程,包括加载 CSS 场景和图片,主要运行在浏览器和 NodeJS 中。它是 ES6 浏览器加载程序的的扩展,将应用在本地浏览器中。通常创建的插件名称是模块本身,要是没有特意指定用途,则默认插件名是模块的扩展名称。

通常它支持创建的插件种类有:

// CSS 
System.import('my/file.css!')

// Image 
System.import('some/image.png!image')

// JSON 
System.import('some/data.json!').then(function(json){})

// Markdown 
System.import('app/some/project/README.md!').then(function(html) {})

// Text 
System.import('some/text.txt!text').then(function(text) {})

// WebFont 
System.import('google Port Lligat Slab, Droid Sans !font')

System.register('name', [], function () { ... });

示例

<script src="system.js"></script>
<script type="systemjs-importmap">
{
  "imports": {
    "lodash": "https://unpkg.com/lodash@4.17.10/lodash.js"
  }
}
</script>
<script type="systemjs-module" src="/js/main.js"></script>

webpack5 Module Federation

https://indepth.dev/posts/1173/webpack-5-module-federation-a-game-changer-in-javascript-architecture

1、模块联邦是什么
简单来说就是允许运行时动态决定代码的引入和加载。

app1
---index.js 入口文件
---bootstrap.js 启动文件 // 特殊处理
---App.js react组件

app2
---index.js 入口文件
---bootstrap.js 启动文件 // 特殊处理
---App.js react组件
---User.js react组件
---News.js react组件

2、代码结构

/** app1 **/

/**
 * index.js
 **/
import React from 'react'
import ReactDom from 'react-dom'

import App from './App'

ReactDom.render(<App />, document.getElementById('root'))


/**
 * App.js
 **/

 import React from 'react'

const User = React.lazy(() => import("app2/User"))

let _onbind = () => {
  console.log('onBind')
}
const App = () => (
  <div>
    <h2>App1 Content</h2>
    <hr/>
    <React.Suspense fallback="Loading app2">
      <User name={'app1 named'} onbind={ _onbind}/>
    </React.Suspense>
  </div>
)

export default App

暂时不用关心app2的代码,问题关键是: app1是如何引入app2的代码的?

3、Module federation的配置

/**
 * app1/webpack.config.js
 **/
 {
  plugins:[
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'public/index.html')
    }),
    new Mfp({
      filename:'app1.js',// 对外提供打包后的文件名,导入时会使用
      name:'app1',// 微应用的名字
      remotes: {  // 引用外部的组件
        app2: "app2@http://localhost:3001/app2.js",
      },
      // shared: ["react", "react-dom"],
      shared: {
        react: { singleton: true }, // singleton 只实例化一次
        "react-dom": { singleton: true }
      }
    })
  ]
 }
  • 配置:exposes/remotes
    app1项目引入 app2 的 News组件 User组件
/**
 * app2/webpack.config.js
 **/
new Mfp({
  filename:'app2.js',// 对外提供打包后的文件名,导入时会使用
  name:'app2',// 微应用的名字
  exposes:{ // 暴露外部的组件
    './News':'./src/News.js', // 名字:具体那个一个组件
    './User':'./src/User.js',
  },
})

/**
 * app1/webpack.config.js
 **/
new Mfp({
  filename:'app1.js',// 对外提供打包后的文件名,导入时会使用
  name:'app1',// 微应用的名字
  remotes: { // 引用外部的组件
    app2: "app2@http://localhost:3001/app2.js",
  },
})

我们重点关注 exposes/remotes

  • 提供了 exposes 选项的表示当前应用是一个 Remoteexposes 内的模块可以被其他的 Host 引用,引用方式为 import(${name}/${expose})
  • 提供了 remotes 选项的表示当前应用是一个 Host,可以引用 remoteexpose 的模块。

项目中如何使用

/**
 * app1/App.js中通过 React.lazy 引用
 * 使用 <React.Suspense></React.Suspense>包括
 **/
import React from 'react'
const User = React.lazy(() => import("app2/User"))

const App = () => (
  <div>
    <h2>App1 Content</h2>
    <hr/>
    
    <React.Suspense fallback="Loading app">
      <User/>
    </React.Suspense>
  </div>
)

export default App
  • 配置:shared
    除了前面提到的模块引入和模块暴露相关的配置外,还有个 shared 配置,主要是用来避免项目出现多个公共依赖。
    例如,我们当前的项目 app1,已经引入了一个 react/react-dom,而项目 app2 暴露的User组件也依赖了 react/react-dom。如果不解决这个问题,项目 app1 就会加载两个 react 库。
    1. remotes的代码自己不打包,类似external,例如app2/button就是加载app2打包的代码
    1. shared的代码自己是有打包的
  • 问题及解决方案
    1、配置shared后报错: Shared module is not available for eager consumption
    [图片上传失败...(image-989a0e-1628564569501)]

解决方案:
增加bootstrap.js 通过 index.js 异步加载页面

/**
 * webpack.config.js
 **/
const config = {
  module: {
    rules: [
      {
        test: /bootstrap\.js$/,
        loader: 'bundle-loader',
        options: {
          lazy: true,
        },
      },
    ]
  }
}

/**
 * index.js
 **/
import bootstrap from './bootstrap'
bootstrap()

/**
 * bootstrap.js
 **/
import React from 'react'
import ReactDom from 'react-dom'

import App from './App'
ReactDom.render(<App />, document.getElementById('root'))


主要原因是 remote 暴露的 js 文件需要优先加载,如果 bootstrap.js 不是一个异步逻辑,在 import User 的时候,会依赖 app2app2.js,如果直接在 index.js 执行,app2app2.js 根本没有加载,所以会有问题。

  • 双向共享
/**
 * app1/webpack.config.js
 **/
new Mfp({
  filename:'app1.js',
  name:'app1',
  exposes:{
    // 名字:具体那个一个组件
    './Button':'./src/Button.js',
  },
})

/**
 * app2/webpack.config.js
 **/
 new Mfp({
  filename:'app2.js',
  name:'app2',
  // 引用外部的组件
  remotes: {
    app1: "app1@http://localhost:3000/app1.js",
  },
})

/**
 * app2/News.js
 **/
 import React from 'react'

const Button = React.lazy(() => import("app1/Button"))
const News = () => (
  <div>
    App2 News组件
    <React.Suspense fallback="loading app1">
      <Button />
    </React.Suspense>
  </div>
)

export default News

这里有一个点需要特别注意,就是入口文件 index.js 本身没有什么逻辑,反而将逻辑放在了 bootstrap.js 中,index.js 去动态加载 bootstrap.js。

参考文档:
https://micro-frontends.org/

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

推荐阅读更多精彩内容