multipartfile 前端怎么传_微前端设计理念与实践

  • Post author:
  • Post category:其他



一、微前端

后端微服务已经流行很久了,这块相关的内容就不做介绍了。说到微前端,其实微前端和微服务的设计理念大致一样,都是希望将某个单一的单体应用,转化为多个可以独立运行、独立开发、独立部署、独立维护的服务或者应用的聚合,从而满足业务快速变化及分布式多团队并行开发的需求。



二、iFrame

看这需求,最先想到的必然是 iFrame了,iFrame 可以创建一个全新的独立的宿主环境,iFrame 的页面和父页面是分开的,作为独立区域而不受父页面的 CSS 或者全局的 JavaScript 影响,在没有其他替代方案前这可能是最方便且高效的办法之一了,把一个大项目拆分成为一个容器项目(容器项目,通常展示为菜单选项等)和众多个子项目,再在主项目中使用iFrame加载不同的子项目,就实现了一个简易版的微前端工程。这个方法实现简单,但是也存在很多缺点。


列举:

  1. 使用iFrame加载子项目时,当前浏览器的链接无任何变化,从而导致一系列问题,比如用户刷新下次进入页面时无法打开所需项目等等,对于这些问题当然也有办法去解决,通常使用postmessage方法来解决,会有一定的工作量。


容器项目监听message事件

window.addEventListener('message', function(event) {
   // 接收到子项目发送的消息,然后使用pushState改变当前浏览器中URL
})

子项目

var state = 'url等相关信息'
window.parent.postMessage(state,'*')
  1. 每次使用iFrame打开子项目都需要重新加载资源,资源加载尽管有缓存,一般情况下有http强缓存,这种情况下还好,但如果是协商缓存或者无缓存都会耗费一定量的请求时间,从而导致子项目切换时过渡白屏效果明显,再往细一点说,资源加载完重新执行js也会有少量的耗时。

  2. 其他问题可以参考这个 https://www.yuque.com/kuitos/gky7yw/gesexv。



三、Single-spa

最近提到微前端,最应该想到就是这个库了,这也是目前微前端实现方案中最流行的一个。比如最新比较火热的qiankun微前端方案也都是基于Single-spa来二次封装,这也是本文章中最重要的一个实践步骤。

Single-spa是一个在前端应用程序中将多个javascript microfrontend集合在一起的框架。使用Single-Spa可以带来很多好处,例如:

  • 在同一页面上使用多个框架而无需刷新页面 (React,AngularJS,Angular,Ember或您使用的任何东西)。

  • 独立部署您的微前端。

  • 使用新框架编写代码,而无需重写现有应用程序。

  • 延迟加载代码可缩短初始加载时间。

上述介绍参考Single-Spa官网,看着很牛皮,但是想想iFrame好像也能给你做到,先不管这些往下看官网DEMO效果,查看DEMO资源可发现,打开页面后第一次切换tab会加载一个app.js,这个文件名一般都是一个项目的入口文件,也就是说切换tab会加载一个子项目,二次加载却不会再加载了,检查元素发现页面也没有iFrame等dom元素,再看看浏览器的url也是跟着变化。这就意味着iFrame方案的不足在这都能被完美的解决。


既然这么香,那该怎么使用呢?

这个库的实现原理先不做介绍,咱们在学习一个新框架或者库什么的不都是先学会怎么使用嘛?学会了怎么使用,真相就会慢慢浮现。


方案一:

一个项目,拆分多个入口文件,代码地址库只有一个。

容器项目入口文件如下(好好看代码中的注释!!)。

src/main.js:

// 导入该库
import { registerApplication, start } from 'single-spa'

// 容器项目用什么框架随意,vue、react等等都行,new Vue等代码就不展示了

// 注册一个子项目
registerApplication(
  'app2',    // 项目名为app2
  () => import('src/app2/main.js'),   // 导入该项目文件夹下的子项目入口文件
  (location) => location.pathname.startsWith('/app2'),    // 子项目打开触发条件,也就是检测到当浏览器中的location.pathname值开头为/app2时触发,然后加载该子项目

  {some: 'value'}    //  传给子项目的数据
)
// 注册第二个子项目
registerApplication(
  'app1',
  () => import('src/app1/main.js'),
  (location) => location.pathname.startsWith('/app1'),
  {some: 'value'}
)
start()  // Single-spa启动

src/app2/main.js:

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

let instance = null

function render() {
  instance = new Vue({
    render: h => h(App),
  }).$mount('#app2')
}

/**
 * 子项目必须暴露三个钩子函数(bootstrap, mount,unmount)出去,并且钩子函数返回一个Promis.resove(),也可以直接使用async
 */

//    容器项目第一次调用子项目时会触发该钩子
export async function bootstrap(props) {
  console.log('bootstrap', props)
}
//    子项目每次启动时会触发该钩子,props为容器项目传进来的数据
export async function mount(props) {
  console.log('mount', props)
  render()
}
//    切换子项目时,也就是该子项目被销毁时会触发该钩子
export async function unmount() {
  //  销毁vue实例
  instance.$destroy()
  instance = null
}

上述代码中,容器项目中注册了两个子项目,当浏览器中url改变成****/app2时,匹配到app2这个子项目,Single-spa就会加载src/app2/main.js,加载完后调用对应子项目的生命周期钩子,也就完成了这一链路,这种方案可以用,但是呢好像所有项目都在一个代码仓库下,会造成一些维护问题,发版的话也不是独立的,修改了app2项目,发版却是发容器项目,这只是对于该库的一次初探,接下来看方案二。


方案二:

子项目拆分到独立代码仓库去,子项目每次修改后就打成一个npm包,然后给容器项目使用。

容器项目入口文件如下:

// 导入该库
import { registerApplication, start } from 'single-spa'

// 注册一个子项目
registerApplication(
  'app2',
  () => import('@project/app2'),      // 导入子项目npm包
  (location) => location.pathname.startsWith('/app2'),    
  {some: 'value'}
)

start()  // Single-spa启动

该方案和方案一差不多,只不过把子项目抽离到单独的代码仓库,项目维护和迭代稍微好了一点,还有子项目打成npm包后,容器项目还得更新一次npm包和build一次再发版。显然方案二这样做还是不能做到独立发版,只是理解到一种新的项目维护方案。


方案三:

固定子项目打包出来的入口js名字,然后给容器项目使用。

容器项目入口文件如下:

// 导入该库
import { registerApplication, start } from 'single-spa'

//    动态执行js
function runScript(src) {
  return new Promise((resove) => {
    const el = document.createElement('script')
    el.src = src
    el.onload = function() {
      resove()
    }
    document.body.appendChild(el)
  })
}

// 注册一个子项目
registerApplication(
  'app2',
  () => {
      await runScript('http://localhost:3001/app2.js')    // 动态加载子项目的入口文件
     return window.app2               // 子项目入口文件执行完挂bootstrap\mount\unmount载生命周期钩子到window上
  },      // 导入子项目npm包
  (location) => location.pathname.startsWith('/app2'),    
  {some: 'value'}
)

start()  // Single-spa启动

子项目入口文件不变,还是参考方案一中的src/app2/main.js。

但是打包配置(webpack或者其他)需要修改,以webpack为例:

//    打包出来的文件名为app2.js,并且把项目入口文件中export的方法挂载到window.app2下面
output: {
  filename: `app2.js`,
  library: `app2`,
  libraryTarget: 'window',
  globalObject: 'this',
  umdNamedDefine: true,
}

这样一操作,容器项目一样能成功加载到子项目的入口文件,并且执行子项目的钩子函数。各个项目终于能做到独立部署了,撒花~。但是…如果这样把子项目打包出来的入口文件名固定了,浏览器缓存这一块就有问题了,服务器设置协商缓存还好,换成强缓存如果子项目发版了,用户打开的可能还是上一个版本的内容。当然也你可以把动态加载的app2.js后面加上一个随机参数,保证用户每次加载最新的资源,但是这就再也是用不到缓存了。还有一个种办法可以解决这个缓存问题,参考这个项目,这个项目巧妙的利用

manifest.json

来提供子项目的入口文件,该方案完成后算是入门了,再看看下一个。


方案四:

使用SystemJs来实现动态导入子项目。

容器项目html文件如下:

html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
  head>
  <body class="vue1">
    <div id="container-app">div>
    <script src="system.js">script>
    <script type="systemjs-importmap">
      {"imports": {"@container": "http://localhost:3000/app.js",  // 容器项目入口文件"@app2": "http://localhost:3001/app2.js"  // 子项目入口文件
        }
      }script>
    <script>
      System.import('@container')  // 加载容器项目script>  
  body>
html>

容器项目入口js文件:

// 导入该库
import { registerApplication, start } from 'single-spa'

registerApplication(
  'app2',    
  () => System.import('@app2'),   
  (location) => location.pathname.startsWith('/app2'),  
  {}
)

start()

子项目入口文件不变,还是参考方案一中的src/app2/main.js

但是打包配置(webpack或者其他)需要修改,以webpack为例:

// 把子应用打包成umd模块,不然SystemJs调用不了
output: {
  filename: `app2.js`,
  library: `app2`,
  libraryTarget: 'umd'
}

上述代码中,子项目的入口文件是写死的一样会有缓存问题,于是html文件中SystemJs那块可以改造成如下:



版权声明:本文为weixin_42588555原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。