Code Monkey home page Code Monkey logo

micro-app's Introduction

logo

version downloads license WeChat travis coveralls

English|简体中文DocumentationDiscussionsWeChat

📖Introduction

micro-app is a micro front-end framework launched by JD Retail. It renders based on webcomponent-like and realizes the micro front-end from component thinking, it aiming to reduce the difficulty of getting started and improve work efficiency.

It is the lowest cost framework for accessing micro front-end, and provides a series of perfect functions such as JS sandbox, style isolation, element isolation, preloading, resource address completion, plugin system, data communication and so on.

micro-app has no restrictions on the front-end framework, and any framework can be used as a base application to embed any type of micro application of the framework.

How to use

Base application

1、Install

yarn add @micro-zoe/micro-app

2、import at the entrance

// main.js
import microApp from '@micro-zoe/micro-app'

microApp.start()

3、Use components in page

<!-- my-page.vue -->
<template>
  <!-- 👇 name is the app name, url is the app address -->
  <micro-app name='my-app' url='http://localhost:3000/'></micro-app>
</template>

Sub application

Set cross-domain support in the headers of webpack-dev-server

devServer: {
  headers: {
    'Access-Control-Allow-Origin': '*',
  },
},

The above micro front-end rendering is completed, and the effect is as follows:

image

More detailed configuration can be viewed Documentation.

🤝 Contribution

If you're interested in this project, you're welcome to mention pull request, and also welcome your "Star" ^_^

development

1、Clone

git clone https://github.com/micro-zoe/micro-app.git

2、Install dependencies

yarn bootstrap

3、Run project

yarn start

For more commands, see DEVELOP

FAQ

What are the advantages of micro-app? It is easy to use and low invasive. It only needs to change a small amount of code to access the micro front-end, and provides rich functions at the same time.
How compatible? The micro-app relies on two newer APIs, CustomElements and Proxy.

For browsers that do not support CustomElements, they can be compatible by introducing polyfills. For details, please refer to: webcomponents/polyfills

However, Proxy is not compatible for the time being, so the micro-app cannot be run on browsers that do not support Proxy.

Browser compatibility can be viewed: Can I Use

The general is as follows:

  • desktop: Except IE browser, other browsers are basically compatible.
  • mobile: ios10+、android5+
Must micro applications support cross-domain? yes!

If it is a development environment, you can set headers in webpack-dev-server to support cross-domain.

devServer: {
  headers: {
    'Access-Control-Allow-Origin': '*',
  },
}

If it is a production environment, you can support cross-domain through Configuration nginx.

Does it support vite?

Yes, please see adapt vite for details.

Does it support ssr?

Yes, please see nextjs, nuxtjs for details.

Contributors

License

MIT License

micro-app's People

Contributors

awesomedevin avatar bailicangdu avatar chengzhuo5 avatar cuitianze avatar deviltea avatar dream2023 avatar hetianhe avatar heuulzp avatar i-artist avatar icksky avatar jardenliu avatar keuby avatar kunl avatar linfeng1997 avatar lionad-morotar avatar lolipop99 avatar loveqin avatar minbaoer111 avatar nieaowei avatar olivewind avatar preflower avatar raoenhui avatar sabera1ter avatar sepveneto avatar sunjingh avatar tinymins avatar vaniship avatar xiyuew avatar xuhongbo avatar yuxuan-ctrl avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

micro-app's Issues

从零开始写一个微前端框架-数据通信篇

前言

自从微前端框架micro-app开源后,很多小伙伴都非常感兴趣,问我是如何实现的,但这并不是几句话可以说明白的。为了讲清楚其中的原理,我会从零开始实现一个简易的微前端框架,它的核心功能包括:渲染、JS沙箱、样式隔离、数据通信。由于内容太多,会根据功能分成四篇文章进行讲解,这是系列文章的最终篇:数据通信篇。

通过这些文章,你可以了解微前端框架的具体原理和实现方式,这在你以后使用微前端或者自己写一套微前端框架时会有很大的帮助。如果这篇文章对你有帮助,欢迎点赞留言。

相关推荐

开始

架构设计

微前端各个应用本身是独立运行的,通信系统不应该对应用侵入太深,所以我们采用发布订阅系统。但是由于子应用封装在micro-app标签内,作为一个类webComponents的组件,发布订阅系统的弱绑定和它格格不入。

最好的方式是像普通属性一样通过micro-app元素传递数据。但自定义元素无法支持对象类型的属性,只能传递字符串,例如<micro-app data={x: 1}></micro-app> 会转换为 <micro-app data='[object Object]'></micro-app>,想要以组件化形式进行数据通信必须让元素支持对象类型属性,为此我们需要重写micro-app原型链上setAttribute方法处理对象类型属性。

流程图

代码实现

创建文件data.js,数据通信的功能主要在这里实现。

发布订阅系统

实现发布订阅系统的方式很多,我们简单写一个,满足基本的需求即可。

// /src/data.js

// 发布订阅系统
class EventCenter {
  // 缓存数据和绑定函数
  eventList = new Map()
  /**
   * 绑定监听函数
   * @param name 事件名称
   * @param f 绑定函数
   */
  on (name, f) {
    let eventInfo = this.eventList.get(name)
    // 如果没有缓存,则初始化
    if (!eventInfo) {
      eventInfo = {
        data: {},
        callbacks: new Set(),
      }
      // 放入缓存
      this.eventList.set(name, eventInfo)
    }

    // 记录绑定函数
    eventInfo.callbacks.add(f)
  }

  // 解除绑定
  off (name, f) {
    const eventInfo = this.eventList.get(name)
    // eventInfo存在且f为函数则卸载指定函数
    if (eventInfo && typeof f === 'function') {
      eventInfo.callbacks.delete(f)
    }
  }

  // 发送数据
  dispatch (name, data) {
    const eventInfo = this.eventList.get(name)
    // 当数据不相等时才更新
    if (eventInfo && eventInfo.data !== data) {
      eventInfo.data = data
      // 遍历执行所有绑定函数
      for (const f of eventInfo.callbacks) {
        f(data)
      }
    }
  }
}

// 创建发布订阅对象
const eventCenter = new EventCenter()

发布订阅系统很灵活,但太过于灵活可能会导致数据传输的混乱,必须定义一套清晰的数据流。所以我们要进行数据绑定,基座应用一次只能向指定的子应用发送数据,子应用只能发送数据到基座应用,至于子应用之间的数据通信则通过基座应用进行控制,这样数据流就会变得清晰

通过格式化订阅名称来进行数据的绑定通信。

// /src/data.js
/**
 * 格式化事件名称,保证基座应用和子应用的绑定通信
 * @param appName 应用名称
 * @param fromBaseApp 是否从基座应用发送数据
 */
 function formatEventName (appName, fromBaseApp) {
  if (typeof appName !== 'string' || !appName) return ''
  return fromBaseApp ? `__from_base_app_${appName}__` : `__from_micro_app_${appName}__`
}

由于基座应用和子应用的数据通信方式不同,我们分开定义。

// /src/data.js

// 基座应用的数据通信方法集合
export class EventCenterForBaseApp {
  /**
   * 向指定子应用发送数据
   * @param appName 子应用名称
   * @param data 对象数据
   */
  setData (appName, data) {
    eventCenter.dispatch(formatEventName(appName, true), data)
  }

  /**
   * 清空某个应用的监听函数
   * @param appName 子应用名称
   */
  clearDataListener (appName) {
    eventCenter.off(formatEventName(appName, false))
  }
}

// 子应用的数据通信方法集合
export class EventCenterForMicroApp {
  constructor (appName) {
    this.appName = appName
  }

  /**
   * 监听基座应用发送的数据
   * @param cb 绑定函数
   */
  addDataListener (cb) {
    eventCenter.on(formatEventName(this.appName, true), cb)
  }

  /**
   * 解除监听函数
   * @param cb 绑定函数
   */
  removeDataListener (cb) {
    if (typeof cb === 'function') {
      eventCenter.off(formatEventName(this.appName, true), cb)
    }
  }

  /**
   * 向基座应用发送数据
   * @param data 对象数据
   */
  dispatch (data) {
    const app = appInstanceMap.get(this.appName)
    if (app?.container) {
      // 子应用以自定义事件的形式发送数据
      const event = new CustomEvent('datachange', {
        detail: {
          data,
        }
      })

      app.container.dispatchEvent(event)
    }
  }

  /**
   * 清空当前子应用绑定的所有监听函数
   */
  clearDataListener () {
    eventCenter.off(formatEventName(this.appName, true))
  }
}

在入口文件中创建基座应用通信对象。

// /src/index.js

+ import { EventCenterForBaseApp } from './data'
+ const BaseAppData = new EventCenterForBaseApp()

在沙箱中创建子应用的通信对象,并在沙箱关闭时清空所有绑定的事件。

// /src/sandbox.js

import { EventCenterForMicroApp } from './data'

export default class SandBox {
  constructor (appName) {
    // 创建数据通信对象
    this.microWindow.microApp = new EventCenterForMicroApp(appName)
    ...
  }

  stop () {
    ...
    // 清空所有绑定函数
    this.microWindow.microApp.clearDataListener()
  }
}

到这里,数据通信大部分功能都完成了,但还缺少一点,就是对micro-app元素对象类型属性的支持。

我们重写Element原型链上setAttribute方法,当micro-app元素设置data属性时进行特殊处理。

// /src/index.js

// 记录原生方法
const rawSetAttribute = Element.prototype.setAttribute

// 重写setAttribute
Element.prototype.setAttribute = function setAttribute (key, value) {
  // 目标为micro-app标签且属性名称为data时进行处理
  if (/^micro-app/i.test(this.tagName) && key === 'data') {
    if (toString.call(value) === '[object Object]') {
      // 克隆一个新的对象
      const cloneValue = {}
      Object.getOwnPropertyNames(value).forEach((propertyKey) => {
        // 过滤vue框架注入的数据
        if (!(typeof propertyKey === 'string' && propertyKey.indexOf('__') === 0)) {
          cloneValue[propertyKey] = value[propertyKey]
        }
      })
      // 发送数据
      BaseAppData.setData(this.getAttribute('name'), cloneValue)
    }
  } else {
    rawSetAttribute.call(this, key, value)
  }
}

大功告成,我们验证一下是否可以正常运行,在vue2项目中向子应用发送数据,并接受来自子应用的数据。

// vue2/pages/page1.vue
<template>
  ...
  <micro-app
    name='app'
    url='http://localhost:3001/'
    v-if='showapp'
    id='micro-app-app1'
    :data='data'
    @datachange='handleDataChange'
  ></micro-app>
</template>

<script>
export default {
  ...
  mounted () {
    setTimeout(() => {
      this.data = {
        name: '来自基座应用的数据'
      }
    }, 2000)
  },
  methods: {
    handleDataChange (e) {
      console.log('接受数据:', e.detail.data)
    }
  }
}
</script>

在react17项目中监听来自基座应用的数据并向基座应用发送数据。

// react17/index.js

// 数据监听
window.microApp?.addDataListener((data) => {
  console.log("接受数据:", data)
})

setTimeout(() => {
  window.microApp?.dispatch({ name: '来自子应用的数据' })
}, 3000);

查看控制抬的打印信息:

数据正常打印,数据通信功能生效。

结语

从这些文章中可以看出,微前端的实现并不难,真正难的是开发、生产环境中遇到的各种问题,没有完美的微前端框架,无论是Module Federation、qiankun。micro-app以及其它微前端解决方案,都会在某些场景下出现问题,了解微前端原理才能快速定位和处理问题,让自己立于不败之地。

从零开始写一个微前端框架-样式隔离篇

前言

自从微前端框架micro-app开源后,很多小伙伴都非常感兴趣,问我是如何实现的,但这并不是几句话可以说明白的。为了讲清楚其中的原理,我会从零开始实现一个简易的微前端框架,它的核心功能包括:渲染、JS沙箱、样式隔离、数据通信。由于内容太多,会根据功能分成四篇文章进行讲解,这是系列文章的第三篇:样式隔离篇。

通过这些文章,你可以了解微前端框架的具体原理和实现方式,这在你以后使用微前端或者自己写一套微前端框架时会有很大的帮助。如果这篇文章对你有帮助,欢迎点赞留言。

相关推荐

开始

前两篇文章中,我们已经完成了微前端的渲染和JS沙箱功能,接下来实现微前端的样式隔离。

问题示例

我们先创建一个问题,验证样式冲突的存在。在基座应用和子应用上分别使用div元素插入一段文字,两个div元素使用相同的class名text-color,分别在class中设置文字颜色,基座应用为red,子应用为blue

由于子应用是后来执行的,它的样式覆盖了基座应用,产生了样式冲突。

样式隔离实现原理

要实现样式隔离必须对应用的css进行改造,因为基座应用无法控制,我们只能对子应用进行修改。

先看一下子应用被渲染后的元素构造:

子应用的所有元素都被插入到micro-app标签中,且micro-app标签具有唯一的name值,所以通过添加属性选择器前缀micro-app[name=xxx]可以让css样式在指定的micro-app内生效。

例如:
.test { height: 100px; }

添加前缀后变为:
micro-app[name=xxx] .test { height: 100px; }

这样.test的样式只会影响到name为xxx的micro-app的元素。

渲染篇中我们将link标签引入的远程css文件转换为style标签,所以子应用只会存在style标签,实现样式隔离的方式就是在style标签的每一个CSS规则前面加上micro-app[name=xxx]的前缀,让所有CSS规则都只能影响到指定元素内部。

通过style.textContent获取样式内容是最简单的,但textContent拿到的是所有css内容的字符串,这样无法针对单独规则进行处理,所以我们要通过另外一种方式:CSSRules

当style元素被插入到文档中时,浏览器会自动为style元素创建CSSStyleSheet样式表,一个 CSS 样式表包含了一组表示规则的 CSSRule 对象。每条 CSS 规则可以通过与之相关联的对象进行操作,这些规则被包含在 CSSRuleList 内,可以通过样式表的 cssRules 属性获取。

形式如图:

所以cssRules就是由单个CSS规则组成的列表,我们只需要遍历规则列表,并在每个规则的选择器前加上前缀micro-app[name=xxx],就可以将当前style样式的影响限制在micro-app元素内部。

代码实现

创建一个scopedcss.js文件,样式隔离的核心代码都将放在这里。

我们上面提到过,style元素插入到文档后会创建css样式表,但有些style元素(比如动态创建的style)在执行样式隔离时还没插入到文档中,此时样式表还没生成。所以我们需要创建一个模版style元素,它用于处理这种特殊情况,模版style只作为格式化工具,不会对页面产生影响。

还有一种情况需要特殊处理:style元素被插入到文档中后再添加样式内容。这种情况常见于开发环境,通过style-loader插件创建的style元素。对于这种情况可以通过MutationObserver监听style元素的变化,当style插入新的样式时再进行隔离处理。

具体实现如下:

// /src/scopedcss.js

let templateStyle // 模版sytle

/**
 * 进行样式隔离
 * @param {HTMLStyleElement} styleElement style元素
 * @param {string} appName 应用名称
 */
export default function scopedCSS (styleElement, appName) {
  // 前缀
  const prefix = `micro-app[name=${appName}]`

  // 初始化时创建模版标签
  if (!templateStyle) {
    templateStyle = document.createElement('style')
    document.body.appendChild(templateStyle)
    // 设置样式表无效,防止对应用造成影响
    templateStyle.sheet.disabled = true
  }

  if (styleElement.textContent) {
    // 将元素的内容赋值给模版元素
    templateStyle.textContent = styleElement.textContent
    // 格式化规则,并将格式化后的规则赋值给style元素
    styleElement.textContent = scopedRule(Array.from(templateStyle.sheet?.cssRules ?? []), prefix)
    // 清空模版style内容
    templateStyle.textContent = ''
  } else {
    // 监听动态添加内容的style元素
    const observer = new MutationObserver(function () {
      // 断开监听
      observer.disconnect()
      // 格式化规则,并将格式化后的规则赋值给style元素
      styleElement.textContent = scopedRule(Array.from(styleElement.sheet?.cssRules ?? []), prefix)
    })

    // 监听style元素的内容是否变化
    observer.observe(styleElement, { childList: true })
  }
}

scopedRule方法主要进行CSSRule.type的判断和处理,CSSRule.type类型有数十种,我们只处理STYLE_RULEMEDIA_RULESUPPORTS_RULE三种类型,它们分别对应的type值为:1、4、12,其它类型type不做处理。

// /src/scopedcss.js

/**
 * 依次处理每个cssRule
 * @param rules cssRule
 * @param prefix 前缀
 */
 function scopedRule (rules, prefix) {
  let result = ''
  // 遍历rules,处理每一条规则
  for (const rule of rules) {
    switch (rule.type) {
      case 1: // STYLE_RULE
        result += scopedStyleRule(rule, prefix)
        break
      case 4: // MEDIA_RULE
        result += scopedPackRule(rule, prefix, 'media')
        break
      case 12: // SUPPORTS_RULE
        result += scopedPackRule(rule, prefix, 'supports')
        break
      default:
        result += rule.cssText
        break
    }
  }

  return result
}

scopedPackRule方法种对media和supports两种类型做进一步处理,因为它们包含子规则,我们需要递归处理它们的子规则。
如:

@media screen and (max-width: 300px) {
  .test {
    background-color:lightblue;
  }
}

需要转换为:

@media screen and (max-width: 300px) {
  micro-app[name=xxx] .test {
    background-color:lightblue;
  }
}

处理方式也十分简单:获取它们的子规则列表,递归执行方法scopedRule

// /src/scopedcss.js

// 处理media 和 supports
function scopedPackRule (rule, prefix, packName) {
  // 递归执行scopedRule,处理media 和 supports内部规则
  const result = scopedRule(Array.from(rule.cssRules), prefix)
  return `@${packName} ${rule.conditionText} {${result}}`
}

最后实现scopedStyleRule方法,这里进行具体的CSS规则修改。修改规则的方式主要通过正则匹配,查询每个规则的选择器,在选择前加上前缀。

// /src/scopedcss.js

/**
 * 修改CSS规则,添加前缀
 * @param {CSSRule} rule css规则
 * @param {string} prefix 前缀
 */
function scopedStyleRule (rule, prefix) {
  // 获取CSS规则对象的选择和内容
  const { selectorText, cssText } = rule

  // 处理顶层选择器,如 body,html 都转换为 micro-app[name=xxx]
  if (/^((html[\s>~,]+body)|(html|body|:root))$/.test(selectorText)) {
    return cssText.replace(/^((html[\s>~,]+body)|(html|body|:root))/, prefix)
  } else if (selectorText === '*') {
    // 选择器 * 替换为 micro-app[name=xxx] *
    return cssText.replace('*', `${prefix} *`)
  }

  const builtInRootSelectorRE = /(^|\s+)((html[\s>~]+body)|(html|body|:root))(?=[\s>~]+|$)/

  // 匹配查询选择器
  return cssText.replace(/^[\s\S]+{/, (selectors) => {
    return selectors.replace(/(^|,)([^,]+)/g, (all, $1, $2) => {
      // 如果含有顶层选择器,需要单独处理
      if (builtInRootSelectorRE.test($2)) {
        // body[name=xx]|body.xx|body#xx 等都不需要转换
        return all.replace(builtInRootSelectorRE, prefix)
      }
      // 在选择器前加上前缀
      return `${$1} ${prefix} ${$2.replace(/^\s*/, '')}`
    })
  })
}

使用

到此样式隔离的功能基本上完成了,接下来如何使用呢?

渲染篇中,我们有两处涉及到style元素的处理,一个是html字符串转换为DOM结构后的递归循环,一次是将link元素转换为style元素。所以我们需要在这两个地方调用scopedCSS方法,并将style元素作为参数传入。

// /src/source.js

/**
 * 递归处理每一个子元素
 * @param parent 父元素
 * @param app 应用实例
 */
 function extractSourceDom(parent, app) {
  ...
  for (const dom of children) {
    if (dom instanceof HTMLLinkElement) {
      ...
    } else if (dom instanceof HTMLStyleElement) {
      // 执行样式隔离
+      scopedCSS(dom, app.name)
    } else if (dom instanceof HTMLScriptElement) {
      ...
    }
  }
}

/**
 * 获取link远程资源
 * @param app 应用实例
 * @param microAppHead micro-app-head
 * @param htmlDom html DOM结构
 */
export function fetchLinksFromHtml (app, microAppHead, htmlDom) {
  ...
  Promise.all(fetchLinkPromise).then((res) => {
    for (let i = 0; i < res.length; i++) {
      const code = res[i]
      // 拿到css资源后放入style元素并插入到micro-app-head中
      const link2Style = document.createElement('style')
      link2Style.textContent = code
+      scopedCSS(link2Style, app.name)
      ...
    }

    ...
  }).catch((e) => {
    console.error('加载css出错', e)
  })
}

验证

完成以上步骤后,样式隔离的功能就生效了,但我们需要具体验证一下。

刷新页面,打印子应用的style元素的样式表,可以看到所有规则选择器的前面已经加上micro-app[name=app]的前缀。

此时基座应用中的文字颜色变为红色,子应用为蓝色,样式冲突的问题解决了,样式隔离生效🎉。

结语

从上面可以看到,样式隔离实现起来不复杂,但也有局限性。目前的方案只能隔离子应用的样式,基座应用的样式依然可以影响到子应用,这一点没有iframe和shadowDom做的那么完善,所以最好的方案还是使用cssModule之类的工具或团队之间协商好样式前缀,从源头解决问题。

从零开始写一个微前端框架-沙箱篇

前言

自从微前端框架micro-app开源后,很多小伙伴都非常感兴趣,问我是如何实现的,但这并不是几句话可以说明白的。为了讲清楚其中的原理,我会从零开始实现一个简易的微前端框架,它的核心功能包括:渲染、JS沙箱、样式隔离、数据通信。由于内容太多,会根据功能分成四篇文章进行讲解,这是系列文章的第二篇:沙箱篇。

通过这些文章,你可以了解微前端框架的具体原理和实现方式,这在你以后使用微前端或者自己写一套微前端框架时会有很大的帮助。如果这篇文章对你有帮助,欢迎点赞留言。

相关推荐

开始

前一篇文章中,我们已经完成了微前端的渲染工作,虽然页面已经正常渲染,但是此时基座应用和子应用是在同一个window下执行的,这有可能产生一些问题,如全局变量冲突、全局事件监听和解绑。

下面我们列出了两个具体的问题,然后通过创建沙箱来解决。

问题示例

1、子应用向window上添加一个全局变量:globalStr='child',如果此时基座应用也有一个相同的全局变量:globalStr='parent',此时就产生了变量冲突,基座应用的变量会被覆盖。

2、子应用渲染后通过监听scroll添加了一个全局监听事件

window.addEventListener('scroll', () => {
  console.log('scroll')
})

当子应用被卸载时,监听函数却没有解除绑定,对页面滚动的监听一直存在。如果子应用二次渲染,监听函数会绑定两次,这显然是错误的。

接下来我们就通过给微前端创建一个JS沙箱环境,隔离基座应用和子应用的JS,从而解决这两个典型的问题,

创建沙箱

由于每个子应用都需要一个独立的沙箱,所以我们通过class创建一个类:SandBox,当一个新的子应用被创建时,就创建一个新的沙箱与其绑定。

// /src/sandbox.js
export default class SandBox {
  active = false // 沙箱是否在运行
  microWindow = {} // // 代理的对象
  injectedKeys = new Set() // 新添加的属性,在卸载时清空

  constructor () {}

  // 启动
  start () {}

  // 停止
  stop () {}
}

我们使用Proxy进行代理操作,代理对象为空对象microWindow,得益于Proxy强大的功能,实现沙箱变得简单且高效。

constructor中进行代理相关操作,通过Proxy代理microWindow,设置getsetdeleteProperty三个拦截器,此时子应用对window的操作基本上可以覆盖。

// /src/sandbox.js
export default class SandBox {
  active = false // 沙箱是否在运行
  microWindow = {} // // 代理的对象
  injectedKeys = new Set() // 新添加的属性,在卸载时清空

  constructor () {
    this.proxyWindow = new Proxy(this.microWindow, {
      // 取值
      get: (target, key) => {
        // 优先从代理对象上取值
        if (Reflect.has(target, key)) {
          return Reflect.get(target, key)
        }

        // 否则兜底到window对象上取值
        const rawValue = Reflect.get(window, key)

        // 如果兜底的值为函数,则需要绑定window对象,如:console、alert等
        if (typeof rawValue === 'function') {
          const valueStr = rawValue.toString()
          // 排除构造函数
          if (!/^function\s+[A-Z]/.test(valueStr) && !/^class\s+/.test(valueStr)) {
            return rawValue.bind(window)
          }
        }

        // 其它情况直接返回
        return rawValue
      },
      // 设置变量
      set: (target, key, value) => {
        // 沙箱只有在运行时可以设置变量
        if (this.active) {
          Reflect.set(target, key, value)

          // 记录添加的变量,用于后续清空操作
          this.injectedKeys.add(key)
        }

        return true
      },
      deleteProperty: (target, key) => {
        // 当前key存在于代理对象上时才满足删除条件
        if (target.hasOwnProperty(key)) {
          return Reflect.deleteProperty(target, key)
        }
        return true
      },
    })
  }

  ...
}

创建完代理后,我们接着完善startstop两个方法,实现方式也非常简单,具体如下:

// /src/sandbox.js
export default class SandBox {
  ...
  // 启动
  start () {
    if (!this.active) {
      this.active = true
    }
  }

  // 停止
  stop () {
    if (this.active) {
      this.active = false

      // 清空变量
      this.injectedKeys.forEach((key) => {
        Reflect.deleteProperty(this.microWindow, key)
      })
      this.injectedKeys.clear()
    }
  }
}

上面一个沙箱的雏形就完成了,我们尝试一下,看看是否有效。

使用沙箱

src/app.js中引入沙箱,在CreateApp的构造函数中创建沙箱实例,并在mount方法中执行沙箱的start方法,在unmount方法中执行沙箱的stop方法。

// /src/app.js
import loadHtml from './source'
+ import Sandbox from './sandbox'

export default class CreateApp {
  constructor ({ name, url, container }) {
    ...
+    this.sandbox = new Sandbox(name)
  }

  ...
  mount () {
    ...
+    this.sandbox.start()
    // 执行js
    this.source.scripts.forEach((info) => {
      (0, eval)(info.code)
    })
  }

  /**
   * 卸载应用
   * @param destory 是否完全销毁,删除缓存资源
   */
  unmount (destory) {
    ...
+    this.sandbox.stop()
    // destory为true,则删除应用
    if (destory) {
      appInstanceMap.delete(this.name)
    }
  }
}

我们在上面创建了沙箱实例并启动沙箱,这样沙箱就生效了吗?

显然是不行的,我们还需要将子应用的js通过一个with函数包裹,修改js作用域,将子应用的window指向代理的对象。形式如:

(function(window, self) {
  with(window) {
    子应用的js代码
  }
}).call(代理对象, 代理对象, 代理对象)

在sandbox中添加方法bindScope,修改js作用域:

// /src/sandbox.js

export default class SandBox {
  ...
  // 修改js作用域
  bindScope (code) {
    window.proxyWindow = this.proxyWindow
    return `;(function(window, self){with(window){;${code}\n}}).call(window.proxyWindow, window.proxyWindow, window.proxyWindow);`
  }
}

然后在mount方法中添加对bindScope的使用

// /src/app.js

export default class CreateApp {
  mount () {
    ...
    // 执行js
    this.source.scripts.forEach((info) => {
-      (0, eval)(info.code)
+      (0, eval)(this.sandbox.bindScope(info.code))
    })
  }
}

到此沙箱才真正起作用,我们验证一下问题示例中的第一个问题。

先关闭沙箱,由于子应用覆盖了基座应用的全局变量globalStr,当我们在基座中访问这个变量时,得到的值为:child,说明变量产生了冲突。

开启沙箱后,重新在基座应用中打印globalStr的值,得到的值为:parent,说明变量冲突的问题已经解决,沙箱正确运行。

第一个问题已经解决,我们开始解决第二个问题:全局监听事件。

重写全局事件

再来回顾一下第二个问题,错误的原因是在子应用卸载时没有清空事件监听,如果子应用知道自己将要被卸载,主动清空事件监听,这个问题可以避免,但这是理想情况,一是子应用不知道自己何时被卸载,二是很多第三方库也有一些全局的监听事件,子应用无法全部控制。所以我们需要在子应用卸载时,自动将子应用残余的全局监听事件进行清空。

我们在沙箱中重写window.addEventListenerwindow.removeEventListener,记录所有全局监听事件,在应用卸载时如果有残余的全局监听事件则进行清空。

创建一个effect函数,在这里执行具体的操作

// /src/sandbox.js

// 记录addEventListener、removeEventListener原生方法
const rawWindowAddEventListener = window.addEventListener
const rawWindowRemoveEventListener = window.removeEventListener

/**
 * 重写全局事件的监听和解绑
 * @param microWindow 原型对象
 */
 function effect (microWindow) {
  // 使用Map记录全局事件
  const eventListenerMap = new Map()

  // 重写addEventListener
  microWindow.addEventListener = function (type, listener, options) {
    const listenerList = eventListenerMap.get(type)
    // 当前事件非第一次监听,则添加缓存
    if (listenerList) {
      listenerList.add(listener)
    } else {
      // 当前事件第一次监听,则初始化数据
      eventListenerMap.set(type, new Set([listener]))
    }
    // 执行原生监听函数
    return rawWindowAddEventListener.call(window, type, listener, options)
  }

  // 重写removeEventListener
  microWindow.removeEventListener = function (type, listener, options) {
    const listenerList = eventListenerMap.get(type)
    // 从缓存中删除监听函数
    if (listenerList?.size && listenerList.has(listener)) {
      listenerList.delete(listener)
    }
    // 执行原生解绑函数
    return rawWindowRemoveEventListener.call(window, type, listener, options)
  }

  // 清空残余事件
  return () => {
    console.log('需要卸载的全局事件', eventListenerMap)
    // 清空window绑定事件
    if (eventListenerMap.size) {
      // 将残余的没有解绑的函数依次解绑
      eventListenerMap.forEach((listenerList, type) => {
        if (listenerList.size) {
          for (const listener of listenerList) {
            rawWindowRemoveEventListener.call(window, type, listener)
          }
        }
      })
      eventListenerMap.clear()
    }
  }
}

在沙箱的构造函数中执行effect方法,得到卸载的钩子函数releaseEffect,在沙箱关闭时执行卸载操作,也就是在stop方法中执行releaseEffect函数

// /src/sandbox.js

export default class SandBox {
  ...
  // 修改js作用域
  constructor () {
    // 卸载钩子
+   this.releaseEffect = effect(this.microWindow)
    ...
  }

  stop () {
    if (this.active) {
      this.active = false

      // 清空变量
      this.injectedKeys.forEach((key) => {
        Reflect.deleteProperty(this.microWindow, key)
      })
      this.injectedKeys.clear()
      
      // 卸载全局事件
+      this.releaseEffect()
    }
  }
}

这样重写全局事件及卸载的操作基本完成,我们验证一下是否正常运行。

首先关闭沙箱,验证问题二的存在:卸载子应用后滚动页面,依然在打印scroll,说明事件没有被卸载。

开启沙箱后,卸载子应用,滚动页面,此时scroll不再打印,说明事件已经被卸载。

从截图中可以看出,除了我们主动监听的scroll事件,还有errorunhandledrejection等其它全局事件,这些事件都是由框架、构建工具等第三方绑定的,如果不进行清空,会导致内存无法回收,造成内存泄漏。

沙箱功能到此就基本完成了,两个问题都已经解决。当然沙箱需要解决的问题远不止这些,但基本架构思路是不变的。

结语

JS沙箱的核心在于修改js作用域和重写window,它的使用场景不限于微前端,也可以用于其它地方,比如在我们向外部提供组件或引入第三方组件时都可以使用沙箱来避免冲突。

下一篇文章我们会完成微前端的样式隔离。

nginx路由配置问题

如果在线上环境,使用nginx的话,子应用和基座应用不能同一个域名下,否则因为nginx配置的route会出现反复横跳的问题。

例如: nginx配置如下

test.xxx.com:/ => 127.0.0.1:3000 (基座应用)
test.xxx.com:/app1 => 127.0.0.1:9000 (子应用)

主应用代码路由:
image

那么浏览器会一直反复横跳,把内存吃满。

样式问题

问题1:micro-app[name=vue2], micro-app[name=vue2], micro-app[name=vue2] div, micro-app[name=vue2] span, micro-app[name=vue2] applet,
主应用给样式增加 micro-app[name=vue2] 前缀后,改变了样式原有的优先级,导致样式错乱,请问有没有更好的解法?

问题2:如果关闭沙箱的话,问题1没有了,但是会遇到另一个奇怪的问题,里面内容的宽度会逐渐拉升变宽(在自我膨胀,仿佛特效一般)

父应用的路由好像会受到 vuerouter 的 base 的影响

父应用是一个老项目,使用了 directorjs 作为路由(hash)

子应用使用了 VueRouter(hash)

当我进入子应用后,hash 会变成 #/app-vue-hash/about

但是当我点击父应用的原有菜单时,hash 会变成 #/app-vue-hash/[原来的hash]

不知道有没有人遇到过这种情况

子应用更新问题

子应用fetch过一次之后会缓存下来,如果这时子应用版本有更新,在不刷新浏览器的情况下,如何重新fetch新的子应用?

有解决基座应用和子应用共用依赖的特性吗

abc项目都有共同的依赖,vue,vuex或者react,react-dom,能让子应用bc去使用基座a上的依赖,做到给子应用减重的操作吗。
现在没有的话,未来有这个功能feature的计划吗。或者说可以在现有基础上自己通过module federation去做

个人优化意见

建议能够增加自定义子应用挂载的钩子函数,类似于 qiankun中的 getActiveRule。

[🐞 Bug]:路由跳转报错

问题描述

应用间路由跳转报错,报错信息如下:
vue-router.min.js:11 ChunkLoadError: Loading chunk loginError404Chunk failed.
(missing: http://localhost:5000/js/loginError404Chunk.js)
at Function.requireEnsure [as e] (eval at runScript (index.esm.js?fd83:1), :896:26)
at Function.fn.e (eval at runScript (index.esm.js?fd83:1), :177:40)
at component (index.js?a18c:2)
at vue-router.min.js:11
at vue-router.min.js:11
at Array.map ()
at vue-router.min.js:11
at Array.map ()
at Et (vue-router.min.js:11)
at vue-router.min.js:11

环境信息

见demo

复现步骤

  1. git clone https://github.com/junsen313/micro-app-demo.git
  2. yarn
  3. yarn serve
  4. 打开 http://localhost:5000/login/
  5. 点击页面的直接跳转按钮
  6. 点击浏览器返回按钮
  7. 点击左侧菜单切换

上传截图

image

复现仓库

demo

vue-基座绑定数据向子应用传值时,首次传值失败

如标题。
此时传值失败

<!-- 此时子应用主动接收参数时,获取不到 -->
<template>
<micro-app :name='name' :url="url" :baseurl='baseurl' :data='microData'></micro-app>
</template>
<script>
export default {
data () {
return {
name: 'customChart',
url: 'http://localhost:1888/custom_chart/#/chartGather/index',
baseurl: '/custom_chart',
microData: {
Authorization: 123,
Token: 456
}
}
},
methods: {},
}
</script>

需手动触发micro-app 的监听才可以

mounted () { console.log(this.microData); this.microData = {...this.microData} }

期望: 默认绑定的数据能在子应用中主动获取到

vue.runtime.esm.js?2b0e:619 [Vue warn]: Failed to mount component: template or render function not defined.

使用时碰到这个问题:

vue.runtime.esm.js?2b0e:619 [Vue warn]: Failed to mount component: template or render function not defined.

found in

---> <MicroApp>
       <App1> at src/components/layouts/AppLoader.vue
         <RouteView> at src/components/layouts/RouteView.vue
           <Anonymous>
             <ALayoutContent>
               <Anonymous>
                 <ALayout>
                   <Anonymous>
                     <ALayout>
                       <GlobalLayout> at src/components/page/GlobalLayout.vue
                         <TabLayout> at src/components/layouts/TabLayout.vue
                           <ALocaleProvider>
                             <LocaleReceiver>
                               <AConfigProvider>
                                 <App> at src/App.vue
                                   <Root>

这是src/components/layouts/AppLoader.vue

<template>
  <div>
    <h1>子应用</h1>
    <!-- name为应用名称,全局唯一,url为html地址-->
    <micro-app name='app1' url='http://127.0.0.1:3100/app1/' ></micro-app>
  </div>
</template>

<script>
import microApp from '@micro-zoe/micro-app'
console.log(microApp);
export default({
  name:'app1',
    components: {microApp},

})
</script>

main.js


import microApp from '@micro-zoe/micro-app'



microApp.start({
  lifeCycles: {
    created () {
      console.log('created 全局监听')
    },
    beforemount () {
      console.log('beforemount 全局监听')
    },
    mounted () {
      console.log('mounted 全局监听')
    },
    unmount () {
      console.log('unmount 全局监听')
    },
    error () {
      console.log('error 全局监听')
    }
  },
  plugins: {
    modules: {
      app1: [{
        loader(code, url) {
          if (code.indexOf('sockjs-node') > -1) {
            console.log('app1', url)
            code = code.replace('window.location.port', '3100')
          }
          return code
        }
      }],
    }
  }});

can't run at windows

设备名称 han-PC
处理器 Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz 4.20 GHz
机带 RAM 32.0 GB
设备 ID F5C3BF9B-6342-43FE-98BD-04C8A34B7F12
产品 ID 00331-10000-00001-AA107
系统类型 64 位操作系统, 基于 x64 的处理器

yarn build:watch

@micro-zoe/[email protected] build D:\DEV\github\micro-app

cross-env NODE_ENV='production' rollup -c && npm run createtype && npm run afterbuild

[!] SyntaxError: Unexpected token '.'
D:\DEV\github\micro-app\rollup.config.js:102
polyfillFiles?.forEach((file) => {
^

SyntaxError: Unexpected token '.'
at wrapSafe (internal/modules/cjs/loader.js:915:16)
at Module._compile (internal/modules/cjs/loader.js:963:27)
at Object.require.extensions. [as .js] (D:\DEV\github\micro-app\node_modules\rollup\dist\shared\loadConfigFile.js:513:20)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Module.require (internal/modules/cjs/loader.js:887:19)
at require (internal/modules/cjs/helpers.js:74:18)
at loadConfigFromBundledFile (D:\DEV\github\micro-app\node_modules\rollup\dist\shared\loadConfigFile.js:521:42)
at getDefaultFromTranspiledConfigFile (D:\DEV\github\micro-app\node_modules\rollup\dist\shared\loadConfigFile.js:505:12)
at loadConfigFile (D:\DEV\github\micro-app\node_modules\rollup\dist\shared\loadConfigFile.js:483:15)


change rollup.config.js line 89

remove the ? and it's working.

Is this a issue that using optional chaining in rollup.config.js file without any config?

vue example 不能正常运行

  1. clone 代码
  2. npm i
  3. npm run start 提示
* @micro-zoe/micro-app in ./src/main.js, ./node_modules/[email protected]@cache-loader/dist/cjs.js??ref--12-0!./node_modules/[email protected]@babel-loader/lib!./node_modules/[email protected]@cache-loader/dist/cjs.js??ref--0-0!./node_modules/[email protected]@vue-loader/lib??vue-loader-options!./src/pages/react16.vue?vue&type=script&lang=js&

To install it, you can run: npm install --save @micro-zoe/micro-app
  1. 运行:npm install --save @micro-zoe/micro-app 还是报3的错误

业务使用场景

我是一名后端开发,也略知一些前端技术,看到这个觉得挺有趣的,但是我想请教一下各位大佬,微前端的概念或者说咋们当前这个micro-app的设计,在业务上有什么使用场景,或者说能否更好的帮助业务实现?如果能有一些通俗易懂或者比较具象的例子就更好了。

基座中渲染子应用卡顿问题

接入试用了一下,发现在基座中子应用元素渲染非常卡顿,单独运行子应用非常流畅,请问是什么原因导致的?,怎么优化?

谁在用 micro-app?

大家好,
micro-app已经开源,我们将会积极响应开发者的疑问与反馈,持续不断打磨完善 ,帮助更多开发者提升开发效率,改善开发体验。如果您正在使用micro-app,非常感谢您的支持,欢迎留下公司或产品名,我们会在首页重要位置展示,以鼓励更多开发者采用 micro-app。

回复格式:

- 产品:xxx
- 公司或组织:(如果可以)
- 链接:(如果可以)
- 截图:(如果可以)

micro-app介绍

前言

MicroApp是一款基于类WebComponent进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度、提升工作效率。它是目前市面上接入微前端成本最低的框架,并且提供了JS沙箱、样式隔离、元素隔离、预加载、资源地址补全、插件系统、数据通信等一系列完善的功能。MicroApp与技术栈无关,也不和业务绑定,可以用于任何前端框架和业务。

本篇文章中我们会从业务背景、实现思路介绍MicroApp,也会详细介绍它的使用方式和技术原理。

业务背景

随着这些年互联网的飞速发展,很多企业的web应用在持续迭代中功能越来越复杂,参与的人员、团队不断增多,导致项目出现难以维护的问题,这种情况PC端尤其常见,许多研发团队也在找寻一种高效管理复杂应用的方案,于是微前端被提及的越来越频繁。

微前端并不是一项新的技术,而是一种架构理念,它将单一的web应用拆解成多个可以独立开发、独立运行、独立部署的小型应用,并将它们整合为一个应用。

在实际业务中,我们也遇到同样的问题,并且在不同的业务场景下尝试了各种解决方案,如iframe、npm包、微前端框架,并对各种方案的优劣进行了对比。

iframe:在所有微前端方案中,iframe是最稳定的、上手难度最低的,但它有一些无法解决的问题,例如性能低、通信复杂、双滚动条、弹窗无法全局覆盖,它的成长性不高,只适合简单的页面渲染。

npm包:将子应用封装成npm包,通过组件的方式引入,在性能和兼容性上是最优的方案,但却有一个致命的问题就是版本更新,每次版本发布需要通知接入方同步更新,管理非常困难。

微前端框架:流行的微前端框架有single-spa和qiankun,它们将维护成本和功能上达到一种平衡,是目前实现微前端备受推崇的方案。

由于iframe和npm包存在问题理论上无法解决,在最初我们采用qiankun作为解决方案,qiankun是在single-spa基础上进行了封装,提供了js沙箱、样式隔离、预加载等功能,并且与技术栈无关,可以兼容不同的框架。

业务诉求

qiankun虽然优秀,但依然无法满足我们的预期。第一个问题是在我们实际使用场景中,每个接入微前端的项目运行已久,且每个项目由不同的人员和团队负责,如何降低对源代码的侵入性,减少代码修改和沟通成本,这是我们非常关心的点,所以我们需要一种比qiankun接入成本更小的方案。第二个问题是在多方应用接入的情况下,沙箱并不能完美规避所有问题,但qiankun处理此类不可预料的问题的能力并不是非常高效。在不停的摸索中,我们找到一种极致简洁的实现思路,它像使用组件一样简单,只修改一点点代码就可以接入微前端,并且还提供插件系统,赋予开发者灵活处理问题的能力。

image

实现思路

微前端分为主应用和子应用,主应用也称为基座应用,是其它应用的容器载体,子应用则是被嵌入方。我们分别从主应用和子应用的角度出发,探寻一种更简洁和有效的接入微前端的方式。

关于qinkun和single-spa的思考

在single-spa和qiankun中都是通过监听url change事件,在路由变化时匹配到渲染的子应用并进行渲染。这种基于路由监听渲染是single-spa最早实现的,作为出现最早、最有影响力的微前端框架,single-spa被很多框架和公司借鉴,也导致目前实现的微前端的方式大多是基于路由监听。

同时single-spa要求子应用修改渲染逻辑并暴露出三个方法:bootstrap、mount、unmount,分别对应初始化、渲染和卸载,这也导致子应用需要对入口文件进行修改。这个特点也被qiankun继承下来,并且需要对webpack配置进行一些修改。

image

基于路由监听的实现方式和对子应用入口文件以及webpack配置的修改是必须的吗?

其实并不是,微前端的核心在于资源加载与渲染,iframe的渲染方式就是一个典型,只要能够实现一种元素隔离的功能并且路由符合要求,子应用理论上不需要修改代码就可以嵌入另外一个页面渲染,我们试图从这个角度中找到不一样的实现思路。

微前端的组件化

要想简化微前端的实现步骤,必须摒弃旧的实现思路,探索出不同的道路。

我们借鉴了WebComponent的**,以此为基础推出另一种更加组件化的实现方式:类WebComponent + HTML Entry。

image

HTML Entry:是指设置html作为资源入口,通过加载远程html,解析其DOM结构从而获取js、css等静态资源来实现微前端的渲染,这也是qiankun目前采用的渲染方案。

WebComponent:web原生组件,它有两个核心组成部分:CustomElement和ShadowDom。CustomElement用于创建自定义标签,ShadowDom用于创建阴影DOM,阴影DOM具有天然的样式隔离和元素隔离属性。由于WebComponent是原生组件,它可以在任何框架中使用,理论上是实现微前端最优的方案。但WebComponent有一个无法解决的问题 - ShadowDom的兼容性非常不好,一些前端框架在ShadowDom环境下无法正常运行,尤其是react框架。

类WebComponent:就是使用CustomElement结合自定义的ShadowDom实现WebComponent基本一致的功能。

由于ShadowDom存在的问题,我们采用自定义的样式隔离和元素隔离实现ShadowDom类似的功能,然后将微前端应用封装在一个CustomElement中,从而模拟实现了一个类WebComponent组件,它的使用方式和兼容性与WebComponent一致,同时也避开了ShadowDom的问题。并且由于自定义ShadowDom的隔离特性,Micro App不需要像single-spa和qiankun一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改webpack配置。

我们通过上述方案封装了一个自定义标签micro-app,它的渲染机制和功能与WebComponent类似,开发者可以像使用web组件一样接入微前端。它可以兼容任何框架,在使用方式和数据通信上也更加组件化,这显著降低了基座应用的接入成本,并且由于元素隔离的属性,子应用的改动量也大大降低。

使用方式

接下来我们将分别介绍主应用和子应用的接入方式。

以react代码举例

主应用

每个自定义标签micro-app渲染后就是一个微前端的子应用,它的使用方式类似于iframe标签。

我们需要给标签传递三个基础属性:

  • name:名称
  • url:子应用页面地址
  • baseurl:baseurl是基座应用分配给子应用的路由前缀

使用方式如下:

image

子应用

如果子应用只有一个页面,没有路由配置,则不需要做任何修改。

如果子应用是多页面,只需要修改路由配置,添加路由前缀。

如下:

window.__MICRO_APP_BASE_URL__是由基座应用下发的路由前缀,在非微前端环境下,这个值为undefined

image

完成以上配置即可实现微前端的渲染,对源码的改动量很少。当然MicroApp还提供了其它一些能力,如插件系统、数据通信,我们接下来做详细介绍。

核心原理

MicroApp 的核心功能在CustomElement基础上进行构建,CustomElement用于创建自定义标签,并提供了元素的渲染、卸载、属性修改等钩子函数,我们通过钩子函数获知微应用的渲染时机,并将自定义标签作为容器,微应用的所有元素和样式作用域都无法逃离容器边界,从而形成一个封闭的环境。

概念图

image

渲染流程

通过自定义元素micro-app的生命周期函数connectedCallback监听元素被渲染,加载子应用的html并转换为DOM结构,递归查询所有js和css等静态资源并加载,设置元素隔离,拦截所有动态创建的script、link等标签,提取标签内容。将加载的js经过插件系统处理后放入沙箱中运行,对css资源进行样式隔离,最后将格式化后的元素放入micro-app中,最终将micro-app元素渲染为一个微前端的子应用。在渲染的过程中,会执行开发者绑定的生命周期函数,用于进一步操作。

流程图

image

元素隔离

元素隔离源于ShadowDom的概念,即ShadowDom中的元素可以和外部的元素重复但不会冲突,ShadowDom只能对自己内部的元素进行操作。

MicroApp模拟实现了类似的功能,我们拦截了底层原型链上元素的方法,保证子应用只能对自己内部的元素进行操作,每个子应用都有自己的元素作用域。

元素隔离可以有效的防止子应用对基座应用和其它子应用元素的误操作,常见的场景是多个应用的根元素都使用相同的id,元素隔离可以保证子应用的渲染框架能够正确找到自己的根元素。

概念图

image

实际效果

image

如上图所示,micro-app元素内部渲染的就是一个子应用,它还有两个自定义元素 micro-app-headmicro-app-body,这两个元素的作用分别对应html中的head和body元素。子应用在原head元素中的内容和一些动态创建并插入head的link、script元素都会移动到micro-app-head中,在原body元素中的内容和一些动态创建并插入body的元素都会移动到micro-app-body中。这样可以防止子应用的元素泄漏到全局,在进行元素查询、删除等操作时,只需要在micro-app内部进行处理,是实现元素隔离的重要基础。

可以将micro-app理解为一个内嵌的html页面,它的结构和功能都和html页面类似。

插件系统

微前端的使用场景非常复杂,即便有沙箱机制也无法避免所有的问题,所以我们提供了一套插件系统用于解决一些无法预知的问题。

插件可以理解为符合特定规则的对象,对象中提供一个函数用于对资源进行处理,插件通常由开发者自定义。

插件系统的作用是对传入的静态资源进行初步处理,并依次调用符合条件的插件,将初步处理后的静态资源作为参数传入插件,由插件对资源内容进一步的修改,并将修改后的内容返回。插件系统赋予开发者灵活处理静态资源的能力,对有问题的资源文件进行修改。

插件系统本身是纯净的,不会对资源内容造成影响,它的作用是统筹各个插件如何执行,当开发者没有设置插件时,则传入和传出的内容是一致的。

image

js沙箱和样式隔离

js沙箱通过Proxy代理子应用的全局对象,防止应用之间全局变量的冲突,记录或清空子应用的全局副作用函数,也可以向子应用注入全局变量用于定制化处理。

样式隔离是指对子应用的link和style元素的css内容进行格式化处理,确保子应用的样式只作用域自身,无法影响外部。

MicroApp借鉴了qiankun的js沙箱和样式隔离方案,这也是目前应用广泛且成熟的方案。

预加载

MicroApp 提供了预加载子应用的功能,它是基于requestIdleCallback实现的,预加载不会对基座应用和其它子应用的渲染速度造成影响,它会在浏览器空闲时间加载应用的静态资源,在应用真正被渲染时直接从缓存中获取资源并渲染。

image

资源地址补全

微前端中经常出现资源丢失的现象,原因是基座应用将子应用的资源加载到自己的页面渲染,如果子应用的静态资源地址是相对地址,浏览器会以基座应用所在域名地址补全静态资源,从而导致资源丢失。

资源地址补全就是将子应用静态资源的相对地址补全为绝对地址,保证地址指向正确的资源路径,这种操作类似于webpack在运行时设置publicPath。

image

生命周期

在微应用渲染时,micro-app元素在不同渲染阶段会触发相应的生命周期事件,基座应用可以通过监听事件来进行相应的操作。
image

生命周期列表:

  • created:当micro-app标签被创建后,加载资源之前执行。
  • beforemount:资源加载完成,正式渲染之前执行。
  • mounted:子应用已经渲染完成后执行
  • unmount:子应用卸载时执行。
  • error:当出现破坏性错误,无法继续渲染时执行。

在卸载时,子应用也会接收到一个卸载的事件,用于执行卸载相关操作。

数据通信

数据通信是微前端中非常重要的功能,实现数据通信的技术方案很多,优秀的方案可以提升开发效率,减少试错成本。我们也研究了qiankun等微前端框架数据通信的方式,但他们的实现方式并不适合我们,我们尝试直接通过元素属性传递复杂数据的形式实现数据通信。

对于前端研发人员最熟悉的是组件化的数据交互的方式,而自定义元素micro-app作为类WebComponent,通过组件属性进行数据交互必然是最优的方式。但MicroApp在数据通信中遇到的最大的问题是自定义元素无法支持设置对象类型属性,例如<micro-app data={x: 1}></micro-app> 会转换为 <micro-app data='[object Object]'></micro-app>,想要以组件化形式进行数据通信必须让元素支持对象属性。

为了解决这个问题,我们重写了micro-app元素原型链上属性设置的方法,在micro-app元素设置对象属性时将传递的值保存到数据中心,通过数据中心将值分发给子应用。

MicroApp中数据是绑定通信的,即每个micro-app元素只能与自己指向的子应用进行通信,这样每个应用都有着清晰的数据链,可以避免数据的混乱,同时MicroApp也支持全局通信,以便跨应用传递数据。

数据通信概念图

image

框架对比

为了更直观的感受Micro App和其它框架的区别,我们使用一张图进行对比。
image
从对比图可以看出,目前开源的微前端框架中有的功能 MicroApp都有,并提供了一些它们不具备的功能,比如静态资源地址补全,元素隔离,插件系统等。

业务实践

MicroApp已经在公司内部多个项目中使用,表现良好,尤其是将一些老项目改造成微前端,在项目不受影响的情况下,即降低接入成本,又可以保证项目平稳运行,减小耦合。

如果你对这个项目感兴趣,可以通过加入组织或提pull requests的方式参与共建,非常欢迎与期待你的加入。

页面切换,内存占用不释放

问题描述

只启动了vue2的项目和基项目,在页面切换后内存占用不释放,会一直增加。只是使用的demo启动后观察到的问题。

上传截图

image

Failed to resolve component: micro-app

问题描述

问题的具体描述

复现步骤

上传截图

请上传代码截图、控制台、终端等截图以帮助我们了解您的问题。

复现仓库

请提供一个精简的代码仓库,然后上传到自己的 github,以帮助我们复现您的问题。
https://github.com/DengRiGuang/micro-frontend.git

环境信息

  • micro-app版本:0.2.5
  • 基座应用的前端框架和版本:3.2.6
  • 子应用的前端框架和版本:3.2.6
  • 构建工具&版本:vite

从零开始写一个微前端框架-渲染篇

前言

自从微前端框架micro-app开源后,很多小伙伴都非常感兴趣,问我是如何实现的,但这并不是几句话可以说明白的。为了讲清楚其中的原理,我会从零开始实现一个简易的微前端框架,它的核心功能包括:渲染、JS沙箱、样式隔离、数据通信。由于内容太多,会根据功能分成四篇文章进行讲解,这是系列文章的第一篇:渲染篇。

通过这些文章,你可以了解微前端框架的具体原理和实现方式,这在你以后使用微前端或者自己写一套微前端框架时会有很大的帮助。如果这篇文章对你有帮助,欢迎点赞留言。

相关推荐

整体架构

和micro-app一样,我们的简易微前端框架设计思路是像使用iframe一样简单,而又可以避免iframe存在的问题,其使用方式如下:

最终效果也有点类似,整个微前端应用都被封装在自定义标签micro-app中,渲染后效果如下图:

所以我们整体架构思路为:CustomElement + HTMLEntry

HTMLEntry就是以html文件作为入口地址进行渲染,入上图中的http://localhost:3000/就是一个html地址。

概念图:

前置工作

在正式开始之前,我们需要搭建一个开发环境,创建一个代码仓库simple-micro-app

目录结构

代码仓库主要分为src主目录和examples案例目录,vue2为基座应用,react17为子应用,两个项目都是使用官方脚手架创建的,构建工具使用rollup。

两个应用页面分别如下图:

基座应用 -- vue2

子应用 -- react17

在vue2项目中,配置resolve.alias,将simple-micro-app指向src目录的index.js。

// vue.config.js
...
chainWebpack: config => {
    config.resolve.alias
      .set("simple-micro-app", path.join(__dirname, '../../src/index.js'))
  },

在react17的webpack-dev-server中配置静态资源支持跨域访问。

// config/webpackDevServer.config.js
...
headers: {
  'Access-Control-Allow-Origin': '*',
},

正式开始

为了讲的更加明白,我们不会直接贴出已经完成的代码,而是从无到有,一步步实现整个过程,这样才能更加清晰,容易理解。

创建容器

微前端的渲染是将子应用的js、css等静态资源加载到基座应用中执行,所以基座应用和子应用本质是同一个页面。这不同于iframe,iframe则是创建一个新的窗口,由于每次加载都要初始化整个窗口信息,所以iframe的性能不高。

如同每个前端框架在渲染时都要指定一个根元素,微前端渲染时也需要指定一个根元素作为容器,这个根元素可以是一个div或其它元素。

这里我们使用的是通过customElements创建的自定义元素,因为它不仅提供一个元素容器,还自带了生命周期函数,我们可以在这些钩子函数中进行加载渲染等操作,从而简化步骤。

// /src/element.js

// 自定义元素
class MyElement extends HTMLElement {
  // 声明需要监听的属性名,只有这些属性变化时才会触发attributeChangedCallback
  static get observedAttributes () {
    return ['name', 'url']
  }

  constructor() {
    super();
  }

  connectedCallback() {
    // 元素被插入到DOM时执行,此时去加载子应用的静态资源并渲染
    console.log('micro-app is connected')
  }

  disconnectedCallback () {
    // 元素从DOM中删除时执行,此时进行一些卸载操作
    console.log('micro-app has disconnected')
  }

  attributeChangedCallback (attr, oldVal, newVal) {
    // 元素属性发生变化时执行,可以获取name、url等属性的值
    console.log(`attribute ${attrName}: ${newVal}`)
  }
}

/**
 * 注册元素
 * 注册后,就可以像普通元素一样使用micro-app,当micro-app元素被插入或删除DOM时即可触发相应的生命周期函数。
 */
window.customElements.define('micro-app', MyElement)

micro-app元素可能存在重复定义的情况,所以我们加一层判断,并放入函数中。

// /src/element.js

export function defineElement () {
  // 如果已经定义过,则忽略
  if (!window.customElements.get('micro-app')) {
    window.customElements.define('micro-app', MyElement)
  }
}

/src/index.js中定义默认对象SimpleMicroApp,引入并执行defineElement函数。

// /src/index.js

import { defineElement } from './element'

const SimpleMicroApp = {
  start () {
    defineElement()
  }
}

export default SimpleMicroApp

引入simple-micro-app

在vue2项目的main.js中引入simple-micro-app,执行start函数进行初始化。

// vue2/src/main.js

import SimpleMicroApp from 'simple-micro-app'

SimpleMicroApp.start()

然后就可以在vue2项目中的任何位置使用micro-app标签。

<!-- page1.vue -->
<template>
  <div>
    <micro-app name='app' url='http://localhost:3001/'></micro-app>
  </div>
</template>

插入micro-app标签后,就可以看到控制台打印的钩子信息。

以上我们就完成了容器元素的初始化,子应用的所有元素都会放入到这个容器中。接下来我们就需要完成子应用的静态资源加载及渲染。

创建微应用实例

很显然,初始化的操作要放在connectedCallback 中执行。我们声明一个类,它的每一个实例都对应一个微应用,用于控制微应用的资源加载、渲染、卸载等。

// /src/app.js

// 创建微应用
export default class CreateApp {
  constructor () {}

  status = 'created' // 组件状态,包括 created/loading/mount/unmount

  // 存放应用的静态资源
  source = { 
    links: new Map(), // link元素对应的静态资源
    scripts: new Map(), // script元素对应的静态资源
  }

  // 资源加载完时执行
  onLoad () {}

  /**
   * 资源加载完成后进行渲染
   */
  mount () {}

  /**
   * 卸载应用
   * 执行关闭沙箱,清空缓存等操作
   */
  unmount () {}
}

我们在connectedCallback函数中初始化实例,将name、url及元素自身作为参数传入,在CreateApp的constructor中记录这些值,并根据url地址请求html。

// /src/element.js
import CreateApp, { appInstanceMap } from './app'

...
connectedCallback () {
  // 创建微应用实例
  const app = new CreateApp({
    name: this.name,
    url: this.url,
    container: this,
  })

  // 记入缓存,用于后续功能
  appInstanceMap.set(this.name, app)
}

attributeChangedCallback (attrName, oldVal, newVal) {
  // 分别记录name及url的值
  if (attrName === 'name' && !this.name && newVal) {
    this.name = newVal
  } else if (attrName === 'url' && !this.url && newVal) {
    this.url = newVal
  }
}
...

在初始化实例时,根据传入的参数请求静态资源。

// /src/app.js
import loadHtml from './source'

// 创建微应用
export default class CreateApp {
  constructor ({ name, url, container }) {
    this.name = name // 应用名称
    this.url = url  // url地址
    this.container = container // micro-app元素
    this.status = 'loading'
    loadHtml(this)
  }
  ...
}

请求html

我们使用fetch请求静态资源,好处是浏览器自带且支持promise,但这也要求子应用的静态资源支持跨域访问。

// src/source.js

export default function loadHtml (app) {
  fetch(app.url).then((res) => {
    return res.text()
  }).then((html) => {
    console.log('html:', html)
  }).catch((e) => {
    console.error('加载html出错', e)
  })
}

因为请求js、css等都需要使用到fetch,所以我们将它提取出来作为公共方法。

// /src/utils.js

/**
 * 获取静态资源
 * @param {string} url 静态资源地址
 */
export function fetchSource (url) {
  return fetch(url).then((res) => {
    return res.text()
  })
}

重新使用封装后的方法,并对获取到到html进行处理。

// src/source.js
import { fetchSource } from './utils'

export default function loadHtml (app) {
  fetchSource(app.url).then((html) => {
    html = html
      .replace(/<head[^>]*>[\s\S]*?<\/head>/i, (match) => {
        // 将head标签替换为micro-app-head,因为web页面只允许有一个head标签
        return match
          .replace(/<head/i, '<micro-app-head')
          .replace(/<\/head>/i, '</micro-app-head>')
      })
      .replace(/<body[^>]*>[\s\S]*?<\/body>/i, (match) => {
        // 将body标签替换为micro-app-body,防止与基座应用的body标签重复导致的问题。
        return match
          .replace(/<body/i, '<micro-app-body')
          .replace(/<\/body>/i, '</micro-app-body>')
      })

    // 将html字符串转化为DOM结构
    const htmlDom = document.createElement('div')
    htmlDom.innerHTML = html
    console.log('html:', htmlDom)

    // 进一步提取和处理js、css等静态资源
    extractSourceDom(htmlDom, app)
  }).catch((e) => {
    console.error('加载html出错', e)
  })
}

html格式化后,我们就可以得到一个DOM结构。从下图可以看到,这个DOM结构包含link、style、script等标签,接下来就需要对这个DOM做进一步处理。

提取js、css等静态资源地址

我们在extractSourceDom方法中循环递归处理每一个DOM节点,查询到所有link、style、script标签,提取静态资源地址并格式化标签。

// src/source.js

/**
 * 递归处理每一个子元素
 * @param parent 父元素
 * @param app 应用实例
 */
function extractSourceDom(parent, app) {
  const children = Array.from(parent.children)
  
  // 递归每一个子元素
  children.length && children.forEach((child) => {
    extractSourceDom(child, app)
  })

  for (const dom of children) {
    if (dom instanceof HTMLLinkElement) {
      // 提取css地址
      const href = dom.getAttribute('href')
      if (dom.getAttribute('rel') === 'stylesheet' && href) {
        // 计入source缓存中
        app.source.links.set(href, {
          code: '', // 代码内容
        })
      }
      // 删除原有元素
      parent.removeChild(dom)
    } else if (dom instanceof HTMLScriptElement) {
      // 并提取js地址
      const src = dom.getAttribute('src')
      if (src) { // 远程script
        app.source.scripts.set(src, {
          code: '', // 代码内容
          isExternal: true, // 是否远程script
        })
      } else if (dom.textContent) { // 内联script
        const nonceStr = Math.random().toString(36).substr(2, 15)
        app.source.scripts.set(nonceStr, {
          code: dom.textContent, // 代码内容
          isExternal: false, // 是否远程script
        })
      }

      parent.removeChild(dom)
    } else if (dom instanceof HTMLStyleElement) {
      // 进行样式隔离
    }
  }
}

请求静态资源

上面已经拿到了html中的css、js等静态资源的地址,接下来就是请求这些地址,拿到资源的内容。

接着完善loadHtml,在extractSourceDom下面添加请求资源的方法。

// src/source.js
...
export default function loadHtml (app) {
  ...
  // 进一步提取和处理js、css等静态资源
  extractSourceDom(htmlDom, app)

  // 获取micro-app-head元素
  const microAppHead = htmlDom.querySelector('micro-app-head')
  // 如果有远程css资源,则通过fetch请求
  if (app.source.links.size) {
    fetchLinksFromHtml(app, microAppHead, htmlDom)
  } else {
    app.onLoad(htmlDom)
  }

  // 如果有远程js资源,则通过fetch请求
  if (app.source.scripts.size) {
    fetchScriptsFromHtml(app, htmlDom)
  } else {
    app.onLoad(htmlDom)
  }
}

fetchLinksFromHtmlfetchScriptsFromHtml分别请求css和js资源,请求资源后的处理方式不同,css资源会转化为style标签插入DOM中,而js不会立即执行,我们会在应用的mount方法中执行js。

两个方法的具体实现方式如下:

// src/source.js
/**
 * 获取link远程资源
 * @param app 应用实例
 * @param microAppHead micro-app-head
 * @param htmlDom html DOM结构
 */
 export function fetchLinksFromHtml (app, microAppHead, htmlDom) {
  const linkEntries = Array.from(app.source.links.entries())
  // 通过fetch请求所有css资源
  const fetchLinkPromise = []
  for (const [url] of linkEntries) {
    fetchLinkPromise.push(fetchSource(url))
  }

  Promise.all(fetchLinkPromise).then((res) => {
    for (let i = 0; i < res.length; i++) {
      const code = res[i]
      // 拿到css资源后放入style元素并插入到micro-app-head中
      const link2Style = document.createElement('style')
      link2Style.textContent = code
      microAppHead.appendChild(link2Style)

      // 将代码放入缓存,再次渲染时可以从缓存中获取
      linkEntries[i][1].code = code
    }

    // 处理完成后执行onLoad方法
    app.onLoad(htmlDom)
  }).catch((e) => {
    console.error('加载css出错', e)
  })
}

/**
 * 获取js远程资源
 * @param app 应用实例
 * @param htmlDom html DOM结构
 */
 export function fetchScriptsFromHtml (app, htmlDom) {
  const scriptEntries = Array.from(app.source.scripts.entries())
  // 通过fetch请求所有js资源
  const fetchScriptPromise = []
  for (const [url, info] of scriptEntries) {
    // 如果是内联script,则不需要请求资源
    fetchScriptPromise.push(info.code ? Promise.resolve(info.code) :  fetchSource(url))
  }

  Promise.all(fetchScriptPromise).then((res) => {
    for (let i = 0; i < res.length; i++) {
      const code = res[i]
      // 将代码放入缓存,再次渲染时可以从缓存中获取
      scriptEntries[i][1].code = code
    }

    // 处理完成后执行onLoad方法
    app.onLoad(htmlDom)
  }).catch((e) => {
    console.error('加载js出错', e)
  })
}

上面可以看到,css和js加载完成后都执行了onLoad方法,所以onLoad方法被执行了两次,接下来我们就要完善onLoad方法并渲染微应用。

渲染

因为onLoad被执行了两次,所以我们进行标记,当第二次执行时说明所有资源都加载完成,然后进行渲染操作。

// /src/app.js

// 创建微应用
export default class CreateApp {
  ...
  // 资源加载完时执行
  onLoad (htmlDom) {
    this.loadCount = this.loadCount ? this.loadCount + 1 : 1
    // 第二次执行且组件未卸载时执行渲染
    if (this.loadCount === 2 && this.status !== 'unmount') {
      // 记录DOM结构用于后续操作
      this.source.html = htmlDom
      // 执行mount方法
      this.mount()
    }
  }
  ...
}

mount方法中将DOM结构插入文档中,然后执行js文件进行渲染操作,此时微应用即可完成基本的渲染。

// /src/app.js

// 创建微应用
export default class CreateApp {
  ...
  /**
   * 资源加载完成后进行渲染
   */
  mount () {
    // 克隆DOM节点
    const cloneHtml = this.source.html.cloneNode(true)
    // 创建一个fragment节点作为模版,这样不会产生冗余的元素
    const fragment = document.createDocumentFragment()
    Array.from(cloneHtml.childNodes).forEach((node) => {
      fragment.appendChild(node)
    })

    // 将格式化后的DOM结构插入到容器中
    this.container.appendChild(fragment)

    // 执行js
    this.source.scripts.forEach((info) => {
      (0, eval)(info.code)
    })

    // 标记应用为已渲染
    this.status = 'mounted'
  }
  ...
}

以上步骤完成了微前端的基本渲染操作,我们看一下效果。

开始使用

我们在基座应用下面嵌入微前端:

<!-- vue2/src/pages/page1.vue -->
<template>
  <div>
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld :msg="'基座应用vue@' + version" />
    <!-- 👇嵌入微前端 -->
    <micro-app name='app' url='http://localhost:3001/'></micro-app>
  </div>
</template>

最终得到的效果如下:

可见react17已经正常嵌入运行了。

我们给子应用react17添加一个懒加载页面page2,验证一下多页面应用是否可以正常运行。

page2的内容也非常简单,只是一段标题:

在页面上添加一个按钮,点击即可跳转page2。

点击按钮,得到的效果如下:

正常渲染!🎉🎉

一个简易的微前端框架就完成了,当然此时它是非常基础的,没有JS沙箱和样式隔离。

关于JS沙箱和样式隔离我们会单独做一篇文章分享,但是此时我们还有一件事情需要做 - 卸载应用。

卸载

当micro-app元素被删除时会自动执行生命周期函数disconnectedCallback,我们在此处执行卸载相关操作。

// /src/element.js

class MyElement extends HTMLElement {
  ...
  disconnectedCallback () {
    // 获取应用实例
    const app = appInstanceMap.get(this.name)
    // 如果有属性destory,则完全卸载应用包括缓存的文件
    app.unmount(this.hasAttribute('destory'))
  }
}

接下来完善应用的unmount方法:

// /src/app.js

export default class CreateApp {
  ...
  /**
   * 卸载应用
   * @param destory 是否完全销毁,删除缓存资源
   */
  unmount (destory) {
    // 更新状态
    this.status = 'unmount'
    // 清空容器
    this.container = null
    // destory为true,则删除应用
    if (destory) {
      appInstanceMap.delete(this.name)
    }
  }
}

当destory为true时,删除应用的实例,此时所有静态资源失去了引用,自动被浏览器回收。

在基座应用vue2中添加一个按钮,切换子应用的显示/隐藏状态,验证多次渲染和卸载是否正常运行。

效果如下:

一且运行正常!🎉

结语

到此微前端渲染篇的文章就结束了,我们完成了微前端的渲染和卸载功能,当然它的功能是非常简单的,只是叙述了微前端的基本实现思路。接下来我们会完成JS沙箱、样式隔离、数据通讯等功能,如果你能耐下心来读一遍,会对你了解微前端有很大帮助。

mian-react16初始化报错

Module not found: Error: Can't resolve '@micro-zoe/micro-app' in 'C:\Users\Administrator\Desktop\micro-app\examples\main-react16\src
error in ./src/layouts/BasicLayout.jsx

Module not found: Error: Can't resolve '@micro-zoe/micro-app' in 'C:\Users\Administrator\Desktop\micro-app\examples\main-react16\src\layouts'

error in ./src/pages/angular11/angular11.js

Module not found: Error: Can't resolve '@micro-zoe/micro-app/polyfill/jsx-custom-event' in 'C:\Users\Administrator\Desktop\micro-app\examples\main-react16\src\pages\angular11'

error in ./src/pages/multiple/multiple.js

Module not found: Error: Can't resolve '@micro-zoe/micro-app/polyfill/jsx-custom-event' in 'C:\Users\Administrator\Desktop\micro-app\examples\main-react16\src\pages\multiple'

error in ./src/pages/react16/react16.js
...

[🐞 Bug]:

问题描述

问题的具体描述

环境信息

  • micro-app版本: 0.2.5
  • 基座&子应用使用的前端框架和版本: react 17.0.2
  • 构建工具&版本: webpack 4.44.2

复现步骤

  1. 当子应用出现unhandledrejection(fetch 请求返回Promise.reject未捕获)错误时,页面直接白屏

上传截图

复现仓库

feat: 服务端渲染接入 micro-app

我们公司这边的服务端选渲染框架 https://github.com/ykfe/ssr 目前正在打算整合微前端方案。
调研了一下qiankun和micro-app。看起来micro-app的侵入性是比较小的。但是不确定成熟度如何。
接入过程中出现了一些问题,初步推测是micro-app自身的问题。
不知道是否有精力能够加个微信单独沟通调试下。

执行远程scrtipt报错

如何复现?

执行远程script,script报错无法执行
image

debug信息:
控制台打日志发现,执行远程脚本的info.code被替换成了远程地址html,而不是''。

image

image

子应用中执行脚本文件有问题,但是执行text code没问题

image

文档完善建议

框架使用文档已经很清晰,建议官方出一个关于项目部署的文档,瑞思拜!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.