IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 三、如何手动实现一个微前端框架雏形 -> 正文阅读

[JavaScript知识库]三、如何手动实现一个微前端框架雏形

如何手动实现一个微前端框架雏形

一、了解微前端

1. 什么是微前端

为了解决一整块儿庞大的前端服务所带来的变更和拓展方面的限制,将整体前端服务拆分成一些更小、更简单的,能够独立开发、测试部署的小块儿。但是在整体表现上还是一个整体的产品的服务称为微前端。

2. 为什么要学习微前端

2.1 关键优势

每个分模块的规模更小,更利于维护

松散各个模块之间的耦合。

可以实现增量升级,避免在重构的时候影响整体逻辑

技术栈无关,可以在每个子模块之间选取合适的技术栈进行开发

独立开发独立部署

2.2 为什么要学

在重构项目的时候,总会有各种各样的问题,如

项目技术栈落后,重构时混用多种技术栈,导致项目技术栈混杂。

各个模块之间耦合严重,动一处,可能影响整体项目运转

因为各种历史问题不得不做出各种妥协,或者是添加各种兼容条件限制。

当你也有以上问题的时候,不妨考虑使用微前端,不仅可以拜托繁重的历史包袱,让你可以进行轻松重构,而且不会出现重构不彻底的情况,可以根据需求的实际情况进行重构工作,而不是基于项目历史债务的问题进行考虑。

这是需要学习微前端一个很重要的前提,如果你的项目没有任何历史包袱,或者说项目是从零开始的,这样就不推荐你引入微前端这个东西,这样或许不能达到预期的目的,或许只会加重自己的开发负担。

3. 手写一个框架可以给我们带来什么

可以从框架作者的角度去考虑,为什么框架的架构要这么设计,从中学习作者的设计思想,对于模型概念的理解。

二、微前端实现方式对比

1.iframe

优势:

天生支持沙箱隔离、独立运行,这是最大的优势。不用做任何沙箱的处理。

劣势:

无法预加载缓存 iframe

无法共享基础库

事件通信限制较多

快捷键劫持

事件无法冒泡到顶层

跳转路径无法保持统一

登录状态无法共享

iframe 加载失败,主应用无法感知

性能问题难以计算

2.基于 SPA 的微前端架构

优势

可以规避 iframe 现存的问题点

可缓存和预加载

共享登录状态

主应用感知加载状态

快捷键劫持

通信设计

共享基础库

劣势:

实现难度较高。需要实现以下几项内容

路由系统

沙箱隔离

样式隔离

通信

html 加载和 js 解析能力

调试开发能力

三、项目介绍

主要用到koa、vue2、vue3、react15、react16

应用:主应用、子应用、后端服务和发布应用

主应用-选定vue3技术栈

vue2子应用:实现新能源页面

vue3子应用:首页、选车

react15子应用:资讯、视频、视频详情

react16子应用:新车、排行、登录

服务端接口:koa实现

发布应用:express

service

npm install koa-generator -g
koa -v
Koa2 service
// 监听修改文件自动重启
npm install supervisor —save-dev
处理跨域问题
npm install koa2-cors —save-dev

build>run.js快速启动所有的应用

四、子应用接入微前端

子应用接入微前端-vue2

// 1. vue2 > vue.config.js,子应用设置允许跨域,主应用需要获取子应用内容,防止资源拦截
  devServer: {
    contentBase: path.join(__dirname, 'dist'), // contentBase必须要配置
    hot: false,
    disableHostCheck: true,
    port,
    headers: {
      'Access-Control-Allow-Origin': '*', // 本地服务的跨域内容
    },
  },
    // 自定义webpack配置
  configureWebpack: {
    resolve: {
      alias: {
        '@': resolve('src'),
      },
    },
    output: {
      // library配置vue2后浏览器可以通过window.vue2获取到打包的内容,之后在微前端框架里也会用到这个信息,这个配置成子应用的名称
      library: `${packageName}`,
      // 把子应用打包成 umd 库格式 commonjs 浏览器,node环境
      libraryTarget: 'umd',
    },
  },


2. //vue2 > main.js  配置如果不是微前端环境下才执行render函数,微前端环境下需要根据微前端生命周期触发函数,暴露一组生命周期,window.vue2里会有这几个生命周期内容,后续会在微前端框架里使用,生命周期何时执行,需要在微前端框架里进行控制
  const render = () => {
  new Vue({
    router,
    render: h => h(App)
  }).$mount('#app-vue')
}

if (!window.__MICRO_WEB__) {
  render()
}
// 一般不做处理,特殊情况,例如加载之前需要做参数的处理再处理
export async function bootstrap() {
  console.log('bootstrap');
}
// 调用render方法,render方法执行成功之后就可以得到我们的整体vue2项目的vue实例,用实例就可以做一些其他东西
export async function mount() {
  render()
}
// 处理卸载上的事,如撤销监听事件,撤销这个容器显示的所有内容
export async function unmount(ctx) {
  const { container } = ctx
  if (container) {
    document.querySelector(container).innerHTML = ''
  }
}

子应用接入微前端 - vue3

配置与vue2同理

子应用接入微前端 - react15

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'react15.js',
    library: 'react15',
    libraryTarget: 'umd',
    umdNamedDefine: true,
    publicPath: 'http://localhost:9002/'
  },
  devServer: {
    // 配置允许跨域
    headers: { 'Access-Control-Allow-Origin': '*' },
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9002,
    historyApiFallback: true,
    hot: true,
  }


  const render = () => {
  ReactDOM.render((
    <BasicMap />
  ), document.getElementById('app-react'))
}

if (!window.__MICRO_WEB__) {
  render()
}

export const bootstrap = () => {
  console.log('bootstrap')
}

export const mount = () => {
  render()
}

export const unmount = () => {
  console.log('卸载')
  // 可以通过index.html里的根结点根元素下直接置空,或者将传入的容器内容置空
}

子应用接入微前端 - react16

webpack打包回打包成
(function(){……})()
配置library之后会打包成
var react16 = (function(){……})()
当前变量内容是存在全局的

五、微前端框架开发

主框架—子应用注册

// main > src > components > MainNav.vue导航跳转时要跳转对应的链接,用useRouter, useRoute


// main > src > router > index.js,outer路由里配置react15、react16、vue2、vue3对应的组件都设置成app.vue
const routes = [
  {
    path: '/',
    component: () => import('../App.vue'),
  },
  {
    path: '/react15',
    component: () => import('../App.vue'),
  },
  {
    path: '/react16',
    component: () => import('../App.vue'),
  },
  {
    path: '/vue2',
    component: () => import('../App.vue'),
  },
  {
    path: '/vue3',
    component: () => import('../App.vue'),
  },
];


// 1. main>src>main.js
import { subNavList } from './store/sub'
import { registerApp } from './util'
registerApp(subNavList)



// 2. main > src > store > sub.js抛出子应用列表
export const subNavList = [
  {
    name: 'react15',// 唯一标识
    entry: '//localhost:9002/', // 去哪个入口获取到子应用的文件
    container: '#micro-container', // 子应用渲染容器container
    activeRule: '/react15', // 激活规则,子应用激活的路由
  },
  {
    name: 'react16',
    entry: '//localhost:9003/',
    container: '#micro-container',
    activeRule: '/react16',
  },
  {
    name: 'vue2',
    entry: '//localhost:9004/',
    container: '#micro-container',
    activeRule: '/vue2',
  },
  {
    name: 'vue3',
    entry: '//localhost:9005/',
    container: '#micro-container',
    activeRule: '/vue3',
  },
];



// 3.main > src > util > index.js里registerApp注册子应用并调用registerMicroApps注册到微前端框架里
import { registerMicroApps } from '../../micro'
export const registerApp = (list) => {
  // 注册到微前端框架里
  registerMicroApps(list)
}

// 4.main > micro > start.js有了微前端框架的第一个方法
import { setList } from './const/subApps'
export const registerMicroApps = (appList) => {
  setList(appList)
}

// 5.main > micro>const>subApps.js在微前端框架里定义了一个subApps去统一一管理子应用列表
let list = []

export const getList = () => list

export const setList = appList => list = appList // 通过setList将子应用列表注册到list上,之后在整体微前端框架运行期间都可以通过getList去获取

微前端框架 - 路由拦截

// 1. main > micro > start.js
import { rewriteRouter } from './router/rewriteRouter'
// 实现路由拦截
rewriteRouter()



// 2.main > micro  > router > rewriteRouter重写路由跳转
  1. main > micro > start.js调用rewriteRouter路由拦截
import { rewriteRouter } from './router/rewriteRouter'
// 实现路由拦截
rewriteRouter()

//2. main > micro  > router > rewriteRouter重写路由跳转,将window.history.pushState与replaceState同时做了更换,用patchRouter做了新的函数,有两个参数,第一个是原生的事件,第二个是起的事件名称
import { patchRouter } from '../utils'
import { turnApp } from './routerHandle'
// 重写window的路由跳转
export const rewriteRouter = () => {
  window.history.pushState = patchRouter(window.history.pushState, 'micro_push')
  window.history.replaceState = patchRouter(window.history.replaceState, 'micro_replace')
  window.addEventListener('micro_push', turnApp)
  window.addEventListener('micro_replace', turnApp)
  // 监听返回事件
  window.onpopstate = async function () {
    await turnApp()
  }
}


// 3.main > micro  > utils > index.js
// 给当前的路由跳转打补丁
export const patchRouter = (globalEvent, eventName) => {
  return function () {
    const e = new Event(eventName)  // new Event创建新的事件
    globalEvent.apply(this, arguments) // globalEvent传递过来的原生事件来代替当前return函数的执行,return函数是被  window.history.replaceState的下面window.addEventListener('micro_replace', turnApp)监听所调用的,this指向的就是监听的函数,将所有的参数传递过去
    window.dispatchEvent(e)// window.dispatchEvent触发刚创建的事件
    // 这样就实现了路由的拦截,所有能触发pushState和replaceState的地方我们都可以监听到
  }
}



// 4.main > micro  > router > routerHandle.js
export const turnApp = async () => {
  console.log('路由切换了')
}

微前端框架-获取首个子应用

// main > src > util > index.js
import { registerMicroApps } from '../../micro'
export const registerApp = (list) => {
  // 注册到微前端框架里
  registerMicroApps(list)
  // 开启微前端框架
  start()
}


// main > micro > start.js里写start函数
import { setList, getList } from './const/subApps'
import { currentApp } from './utils'
export const start = () => {
  // 首先验证当前子应用列表是否为空
  const apps = getList()
  if (!apps.length) {
    // 子应用列表为空
    throw Error('子应用列表为空, 请正确注册')
  }
  // 有子应用的内容, 查找到符合当前路由的子应用
  const app = currentApp()
  const { pathname, hash } = window.location
  if (app) {
    const url = pathname + hash
    window.__CURRENT_SUB_APP__ = app.activeRule // 加对应的标记,之后可以在turnApp里判断isTurnChild,有变动再进行接下来的操作
    window.history.pushState('', '', url) // app有内容就触发pushState方法
  }
}


// main > micro > utils > index.js
//获取的规则是window.location.pathname与子应用的activeRule做对比,返回命中的子应用
export const currentApp = () => {
  const currentUrl = window.location.pathname
  return filterApp('activeRule', currentUrl)
}
export const filterApp = (key, value) => {
  const currentApp = getList().filter(item => item[key] === value)
  return currentApp && currentApp.length ? currentApp[0] : {}
}


// main > micro  > router > routerHandle.js
import { isTurnChild } from '../utils'
export const turnApp = async () => {
  if (isTurnChild()) {
    console.log('路由切换了')
  }
}



// main > micro > utils > index.js
// 子应用是否做了切换,有变动才进行接下来的操作
export const isTurnChild = () => {
  if(window.__CURRENT_SUB_APP__ === window.location.pathname) {
    return false
  }
  return true;
}

微前端框架主应用生命周期

// main > src > util > index.js的registerMicroApps第二个参数添加生命周期,将生命周期注册奥微前端框架里
import { registerMicroApps, start } from '../../micro'
import { loading } from '../store'
export const registerApp = (list) => {
  // 注册到微前端框架里
  registerMicroApps(list, {
    beforeLoad: [
      () => {
        loading.changeLoading(true) // 控制loading状态
        console.log('开始加载')
      }
    ],
    mounted: [
      () => {
        loading.changeLoading(false) // 控制loading状态
        console.log('渲染完成')
      }
    ],
    destoryed: [
      () => {
        console.log('卸载完成')
      }
    ]
  })
  // 开启微前端框架
  start()
}


// 微前端框架中main > micro > start.js里registerMicroApps接收第二个参数
export const registerMicroApps = (appList, lifeCycle) => {
  setList(appList)
  setMainLifecycle(lifeCycle) // 设置lifeCycle
}



// main > micro>const>mainLifeCycle.js
let lifecycle = {}
export const getMainLifecycle = () => lifecycle
export const setMainLifecycle = data => lifecycle = data

微前端框架-微前端生命周期

// main > micro > router > routerHandle.js判断子应用是否切换里执行微前端的生命周期
export const turnApp = async () => {
  if (isTurnChild()) {
    // 微前端的生命周期执行
    await lifecycle()
  }
}


// main > micro > lifeCycle > index.js
import { findAppByRoute } from '../utils'
import { getMainLifecycle } from '../const/mainLifeCycle'
export const lifecycle = async () => {
  // 获取到上一个子应用
  const prevApp = findAppByRoute(window.__ORIGIN_APP__)
  // 获取到要跳转到的子应用
  const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__)
  if (!nextApp) {
    return
  }
  if (prevApp && prevApp.unmount) {
    // 其他子应用切换过来的,卸载上一个子应用
    await destoryed(prevApp)
  }
  const app = await beforeLoad(nextApp) // 加载下一个子应用框架
  await mounted(app) // 渲染
}
export const beforeLoad = async (app) => {
  await runMainLifeCycle('beforeLoad') // 运行主应用的生命周期
  app && app.beforeLoad && app.beforeLoad() // 运行子应用生命周期内容
  const appContext = null
  return appContext
}
export const mounted = async (app) => {
  app && app.mount && app.mount()
  await runMainLifeCycle('mounted')
}
export const destoryed = async (app) => {
  app && app.unmount && app.unmount()
  // 对应的执行以下主应用的生命周期
  await runMainLifeCycle('destoryed')
}
export const runMainLifeCycle = async (type) => {
  const mainlife = getMainLifecycle() // 主应用生命周期是一个数组
  await Promise.all(mainlife[type].map(async item => await item())) // 需要等到所有内容都执行完成才可以继续
}



// main > micro > utils > index.js
export const isTurnChild = () => {
  window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__;
  if(window.__CURRENT_SUB_APP__ === window.location.pathname) {
    // 如果当前子应用没有变动,返回的是false,window.__ORIGIN_APP__ 与window.__CURRENT_SUB_APP__是相同的
    return false
  }
  // 如果当前子应用有变动,需要对window.__CURRENT_SUB_APP__进行更换
  // 这样就可以获取上一个子应用和下一个子应用
  window.__CURRENT_SUB_APP__ = window.location.pathname
  return true;
}

// main > micro > utils > index.js
export const findAppByRoute = (router) => {
  return filterApp('activeRule', router)
}
export const filterApp = (key, value) => {
  const currentApp = getList().filter(item => item[key] === value)
  return currentApp && currentApp.length ? currentApp[0] : {}
}

获取需要展示的页面 - 加载和解析html

//访问子应用http://localhost:9005/#/, f12发现有个get请求,get请求路径就是子应用路口

// main > micro > lifeCycle > index.js的beforeLoad 里执行loadHtml
import { loadHtml } from '../loader'
export const beforeLoad = async (app) => {
  await runMainLifeCycle('beforeLoad')
  app && app.beforeLoad && app.beforeLoad()

  const subApp = await loadHtml(app) // 获取的是子应用的内容
  subApp && subApp.beforeLoad && subApp.beforeLoad()

  return subApp
}


// main > micro > loader > index.js
import { fetchResource } from '../utils/fetchResource'
// 加载html的方法
export const loadHtml = async (app) => {
  // 第一个,子应用需要显示在哪里
  let container = app.container // #id 内容
  // 子应用的入口
  let entry = app.entry
  const html = await parseHtml(entry)
  const ct = document.querySelector(container)
  if (!ct) {
    throw new Error('容器不存在,请查看')
  }
  ct.innerHTML = html
  return app
}
export const parseHtml = async (entry, name) => {
  const html = await fetchResource(entry)
  return html
}


// main > micro > utils > fetchResource.js
export const fetchResource = url => fetch(url).then(async res => await res.text())

// 刷新页面,页面是空白的,什么都没有,在micro-container微前端容器里有子应用的信息,没有显示子应用内容是当前js所加载的内容是加载不到的,微前端处理里并没有获取js的内容,并没有执行
// 切换路由,micro-container微前端容器内容会有变化

加载和解析js

// main > micro > loader > index.js
export const loadHtml = async (app) => {
  // 第一个,子应用需要显示在哪里
  let container = app.container // #id 内容
  // 子应用的入口
  let entry = app.entry
  const [dom, scripts] = await parseHtml(entry, app.name)
  const ct = document.querySelector(container)
  if (!ct) {
    throw new Error('容器不存在,请查看')
  }
  ct.innerHTML = dom
  return app
}
const cache = {} // 根据子应用的name来做缓存
export const parseHtml = async (entry, name) => {
  if (cache[name]) {
    return cache[name]
  }
  const html = await fetchResource(entry)
  let allScript = []
  const div = document.createElement('div')
  div.innerHTML = html
  // 接下来针对div这个伪元素做对应的html处理,处理内容包含标签、link、script(src,js)
  const [dom, scriptUrl, script] = await getResources(div, entry)
  const fetchedScripts = await Promise.all(scriptUrl.map(async item => fetchResource(item)))
  allScript = script.concat(fetchedScripts)
  cache[name] = [dom, allScript]
  return [dom, allScript]
}
export const getResources = async (root, entry) => {
  const scriptUrl = [] // js 链接  src  href
  const script = [] // 写在script中的js脚本内容
  const dom = root.outerHTML
  // 深度解析
  function deepParse(element) {
    const children = element.children
    const parent = element.parent;
    // 第一步处理位于 script 中的内容
    if (element.nodeName.toLowerCase() === 'script') {
      const src = element.getAttribute('src');
      if (!src) {
        // script不是通过外部链接引用的
        script.push(element.outerHTML)
      } else {
        // scriptUrl要做是否以http开头的判断,因为可能子应用没有配置publicPath
        if (src.startsWith('http')) {
          scriptUrl.push(src)
        } else {
          scriptUrl.push(`http:${entry}/${src}`)
        }
      }
      if (parent) {
        parent.replaceChild(document.createComment('此 js 文件已经被微前端替换'), element)
      }
    }
    // link 也会有js的内容
    if (element.nodeName.toLowerCase() === 'link') {
      const href = element.getAttribute('href');
      if (href.endsWith('.js')) {
        if (href.startsWith('http')) {
          scriptUrl.push(href)
        } else {
          scriptUrl.push(`http:${entry}/${href}`)
        }
      }
    }
    for (let i = 0; i < children.length; i++) {
      deepParse(children[i])
    }
  }
  deepParse(root)
  return [dom, scriptUrl, script]
}

执行js脚本

// main > micro > utils > index.js 中
// pathname 里符合activeRule的规则
export const isTurnChild = () => {
  const { pathname } = window.location
  // 当前路由无改变。
  let prefix = pathname.match(/(\/\w+)/)
  if(prefix) {
    prefix = prefix[0]
  }
  window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__;
  if (window.__CURRENT_SUB_APP__===prefix) {
    return false
  }
  const currentSubApp = window.location.pathname.match(/(\/\w+)/)

  if (!currentSubApp) {
    return false
  }
  // 当前路由以改变,修改当前路由
  window.__CURRENT_SUB_APP__ = currentSubApp[0];
  return true;
}

// main > micro > loader > index.js
import { performScript } from "../sandbox/performScript";
// 加载html的方法
export const loadHtml = async (app) => {
  // 第一个,子应用需要显示在哪里
  let container = app.container // #id 内容

  // 子应用的入口
  let entry = app.entry

  const [dom, scripts] = await parseHtml(entry, app.name)

  const ct = document.querySelector(container)

  if (!ct) {
    throw new Error('容器不存在,请查看')
  }

  ct.innerHTML = dom
// 所有js文件的执行放在dom执行完成之后
  scripts.forEach(item => {
    performScript(item)
  })

  return app
}


// main > micro > sandbox > performScript.js
// 执行js脚本
export const performScript = (script) => {
   eval(script)
  //  new Function(script).call(window,window)// 需要设置全局对象,将this指向window,传入当前的window对象
}
eval('var a=1;var b=2;console.log(a+b)')
var s = 'return `hello ${name}`'
var func = new Function('name', s)
func('小明') // “hello 小明”

微前端环境变量

// main > micro > loader > index.js
import { sandBox } from "../sandbox";
// 加载html的方法
export const loadHtml = async (app) => {
  // 第一个,子应用需要显示在哪里
  let container = app.container // #id 内容

  // 子应用的入口
  let entry = app.entry

  const [dom, scripts] = await parseHtml(entry, app.name)

  const ct = document.querySelector(container)

  if (!ct) {
    throw new Error('容器不存在,请查看')
  }

  ct.innerHTML = dom

  scripts.forEach(item => {
    sandBox(app, item)
  })

  return app
}



// main > micro > sandbox > index.js
// 子应用生命周期处理, 环境变量设置
export const sandBox = (app, script) => {
  // 1. 设置环境变量
  window.__MICRO_WEB__ = true
  // 2. 运行js文件
  // 不仅仅执行了子应用脚本,执行完成之后也获取到子应用生命周期
  const lifecycle = performScriptForEval(script, app.name)
  // 运行完js后需要得到生命周期内容,将所有的生命周期函数挂载到app上,挂载完成后就可以在微前端框架lifeCycle里获取到app的生命周期内容并执行
  if (isCheckLifeCycle(lifecycle)) {
    app.bootstrap = lifecycle.bootstrap
    app.mount = lifecycle.mount
    app.unmount = lifecycle.unmount
  }
}
const isCheckLifeCycle = lifecycle => lifecycle &&
  lifecycle.bootstrap &&
  lifecycle.mount &&
  lifecycle.unmount



// main > micro > sandbox > performScript.js
export const performScriptForEval = (script, appName) => {
  // 之前配置的library可以获取window.appName有生命周期函数
  const scriptText = `
    () => {
      ${script}
      return window['${appName}'] 
    }
  `
  return eval(scriptText).call(window,window)// app module mount
}
export const performScriptForFunction = (script, appName) => {
  const scriptText = `
      ${script}
      return window['${appName}']
  `
  return new Function(scriptText).call(window,window)
}

运行环境隔离 - 快照沙箱

// 为什么要隔离运行环境
//如在vue3>main.js的mount中设置window.a=1;在加载应用时,切换到其他子应用window.a还存在,这样对公共的变量没问题,但是如果变量只在某个子应用里应用,切换到其他应用变量还存在对逻辑获取、变量设置都会有影响,所以我们需要将运行时变量维护在子应用里,切换子应用变量消失掉;如果有子应用公共的变量,可以放在主应用里,或者通过主应用的方法设置到全局上
//如何实现将我们的子应用运行在沙箱环境中
// main > micro > sandbox > snapShotSandbox.js
// 快照沙箱:针对于给当前的全局变量实现一个快照的方式来记录我们沙箱的内容,在子应用切换后将所有沙箱的变量设为初始值
// 消耗和执行性能都是比较差的,一个window上会挂很多属性,
// 应用场景:比较老版本的浏览器,
export class SnapShotSandbox {
  constructor() {
    // 1. 代理对象
    this.proxy = window // 之后就可以使用proxy这个代理对象完成全局对象的替换

    this.active()
  }
  // 沙箱激活
  active() {
    // 创建一个沙箱快照
    this.snapshot = new Map()

    // 遍历全局环境
    for(const key in window) {
      this.snapshot[key] = window[key] // 将window所有的key值记录在快照对象上
    }
  }
  // 沙箱销毁
  inactive () {
    for (const key in window) {
      if (window[key] !== this.snapshot[key]) {
        // 还原操作
        window[key] = this.snapshot[key]
      }
    }
  }
}



// main > micro > sandbox > index.js
import { performScriptForEval } from './performScript'
import { SnapShotSandbox } from './snapShotSandbox'
// 子应用生命周期处理, 环境变量设置
export const sandBox = (app, script) => {
  const proxy = new SnapShotSandbox() // 记录所有的window值,激活沙箱
  if (!app.proxy) {
    app.proxy = proxy // proxy挂载到app上
  }
  // 1. 设置环境变量
  window.__MICRO_WEB__ = true
  // 2. 运行js文件,需要传递全局对象app.proxy.proxy,之后所有的运行环境都在代理对象里
  const lifecycle = performScriptForEval(script, app.name, app.proxy.proxy)
  // 生命周期,挂载到app上
  if (isCheckLifeCycle(lifecycle)) {
    app.bootstrap = lifecycle.bootstrap
    app.mount = lifecycle.mount
    app.unmount = lifecycle.unmount
  }
}


// main > micro > sandbox > performScript.js,接收global参数,代替window进行执行
export const performScriptForEval = (script, appName,global) => {
  // 之前配置的library可以获取window.appName有生命周期函数
  const scriptText = `
    () => {
      ${script}
      return window['${appName}'] 
    }
  `
  return eval(scriptText).call(global,global)// app module mount
}
export const performScriptForFunction = (script, appName,global) => {
  const scriptText = `
      ${script}
      return window['${appName}']
  `
  return new Function(scriptText).call(global,global)
}


// main > micro > lifeCycle > index.js中将代理销毁
export const lifecycle = async () => {
  // 获取到上一个子应用
  const prevApp = findAppByRoute(window.__ORIGIN_APP__)
  // 获取到要跳转到的子应用
  const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__)
  if (!nextApp) {
    return
  }
  if (prevApp && prevApp.unmount) {
    if (prevApp.proxy) {
      prevApp.proxy.inactive() // 将沙箱销毁
    }
    await destoryed(prevApp)
  }
  const app = await beforeLoad(nextApp)
  await mounted(app)
}
// 运行时变量维护在子应用里,子应用公共变量放在主应用里,或者可以通过主应用方法设在全局里
// 如何将子应用运行在沙箱环境中
// 要设置一个沙箱环境在运行整体js时就需要将沙箱环境给设置上
// 创建一个文件snapShotSandbox快照沙箱,针对给当前全局变量实现一个快照的方式,来记录我们沙箱的内容,在子应用切换之后,将所有沙箱的变量置为初始值
// 在生命周期上一个子应用销毁前将沙箱环境重置
// Window.a只在当前子应用生效

运行环境隔离 - 代理沙箱

// 快照沙箱是不支持多实例的,同一时间我们的页面只可以显示一个子应用,如果一个页面需要显示多个子应用快照沙箱不支持,要用代理沙箱
// 对于Proxy可以查下具体文档的使用,可以将它代理的对象做个拦截,我们之后对代理的对象做任何操作都在它拦截上处理一次,举个例子,Proxy可以做到13种拦截,下面举get和set例子
let a = {}
const proxy = new Proxy(a, {
  get() {
    console.log(111)
  },
  set(){
    console.log(222)
    return true
  }
})
console.log(proxy.b)
proxy.a = 1
// 结果为111 undefined 222,proxy.b获取proxy上属性会受到get方法的拦截,会先执行get方法,返回111,proxy.b并没有返回任何数据输出undefined,proxy.a = 1赋值的操作触发了set操作,输出222
// 如果get方法return 333;console.log(proxy.b)都会返回333
// main > micro > sandbox > proxySandbox.js
let defaultValue = {} // 子应用的沙箱容器
export class ProxySandbox{
  constructor() {
    this.proxy = null;

    this.active()
  }
  // 沙箱激活
  active() {
    // 子应用需要设置属性,
    this.proxy = new Proxy(window, {
      get(target, key) {
        // 处理Illegal invocation非法操作
        if (typeof target[key] === 'function') {
          return target[key].bind(target)
        }
        return defaultValue[key] || target[key] // 做兼容操作,如果defaultValue找不到去target上找,比如location
      },
      set(target, key, value) {
        defaultValue[key] = value
        return true
      }
    })

  }

  // 沙箱销毁
  inactive () {
    defaultValue = {}
  }
}


// main > micro > sandbox > index.js中使用ProxySandbox
import { performScriptForEval } from './performScript'
import { ProxySandbox } from './proxySandbox'
// 子应用生命周期处理, 环境变量设置
export const sandBox = (app, script) => {
  const proxy = new ProxySandbox() // 记录所有的window值,激活沙箱
  if (!app.proxy) {
    app.proxy = proxy // proxy挂载到app上
  }
  // 1. 设置环境变量
  window.__MICRO_WEB__ = true
  // 2. 运行js文件,需要传递全局对象app.proxy.proxy,之后所有的运行环境都在代理对象里
  const lifecycle = performScriptForEval(script, app.name, app.proxy.proxy)
  // 生命周期,挂载到app上
  if (isCheckLifeCycle(lifecycle)) {
    app.bootstrap = lifecycle.bootstrap
    app.mount = lifecycle.mount
    app.unmount = lifecycle.unmount
  }
}


// main > micro > sandbox > performScript.js
// 执行js脚本
export const performScriptForFunction = (script, appName, global) => {
  window.proxy = global
  console.log(global)
  const scriptText = `
    return ((window) => {
      ${script}
      return window['${appName}']
    })(window.proxy)
  `
  return new Function(scriptText)()
}
export const performScriptForEval = (script, appName, global) => {
  // library window.appName
  window.proxy = global
  const scriptText = `
    ((window) => {
      ${script}
      return window['${appName}'] 
    })(window.proxy)
  `
  return eval(scriptText)// app module mount
}
// 在vue3路由获取window.a是undefined,vue3对window的操作并没有作用到我们全局的window对象上,而是作用到代理沙箱里

// 报Illegal invocation非法操作
const proxy = new Proxy(window,{})// 通过代理对象代理了window,但是没有传递任何方式,那所有的操作都不会经过代理
proxy.addEventListener('a',()=>{
  console.log(1)
}) // addEventListener并不是指向window,而是指向proxy内容,在正常处理回调函数和添加监听事件时会有很多问题,这使需要对它get做些特殊的操作
const proxy = new Proxy(window,{
      get(target, key) {
        if (typeof target[key] === 'function') {
          return target[key].bind(target)// 得到this指向target指向window的函数。才可以正常使用原生的一些方法
        }
      },
})

css样式隔离

// 1.css modules在子应用里配置css-loader设置module为true
// 2.shadow dom,通过mode-attachShadow方法,这是比较新的语法,对浏览器兼容性支持性不是很高,将我们的元素、样式统一隔离到虚拟的dom上,这个dom集中我们所有的内容,和其他内容没有任何冲突的
const box1 = document.getElementById('box1')
// 开启shadow dom模式
const shadow1 = box1.attachShadow({mode:'open'})
const one = document.createElement('div')
one.className = 'one'
one.innerText = '第一个内容'
const style1 = document.createElement('style')
style1.textContent = `
.one{
  color:red;
}
`
shadow1.appendChild(one1);
shadow1.appendChild(style1);

const box2 = document.getElementById('box2')
// 开启shadow dom模式
const shadow2 = box2.attachShadow({mode:'open'})
const two = document.createElement('div')
two.className = 'one'
two.innerText = '第一个内容'
const style2 = document.createElement('style')
style2.textContent = `
.one{
  color:blue;
}
`
shadow2.appendChild(one2);
shadow2.appendChild(style2);
// 第一个第二个同时位于#shadow-root下,类名一样,同样的元素位于不同的shadow-root下,他们的样式和内容不会发生冲突
// 3.minicss:minicssectractplugin,把所有css打包成单独的css文件,然后渲染子应用时会通过link标签引入我们的css文件,文件的引用是放在子应用容器,切换子应用时把容器内的所有内容清空,对于css文件的引入也是清空的,不会存在两个子应用样式相互影响的问题
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
{
  test: /\.(cs|scs)s$/,
  use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
// 4.css-in-js,css内容通过js进行设置,每一个子应用的css都位于js文件里,如果清空了所有子应用内容,也就清空了css的样式

应用间通信 - 父子通信

如登录状态父子应用不同步问题,主应用状态变更可以通过某种方法及时通知我们的子应用,子应用状态变更也要通过方法通知主应用进行修改

//应用间通信主要有以下两种方式
// 1.props(主要)
// 访问http://localhost:8080/react16#/login时,头部与导航不需要
// main>src>store>header.js
import { ref } from 'vue';
export const headerStatus = ref(true)
export const changeHeader = type => headerStatus.value = type;

// main>src>store>nav.js
import { ref } from 'vue';
export const navStatus = ref(true)
export const changeNav = type => navStatus.value = type;


// main>src>store>index.js
// 暴露header的方法
export * as header from './header'
// 暴露nav的方法
export * as nav from './nav'


// main>src>App.vue状态控制导航显示隐藏
// 如何在子应用里(如react16里)修改主应用导航和头部的显示,需要将所有内容通过props传递给你子应用
// main > src > store > sub.js中引入store,将appInfo赋值到每一个子应用上
import { loading } from '../store'
import * as appInfo from '../store'
export const subNavList = [
  {
    name: 'react15',// 唯一
    entry: '//localhost:9002/',
    loading,
    container: '#micro-container',
    activeRule: '/react15',
    appInfo,
  },
  {
    name: 'react16',
    entry: '//localhost:9003/',
    loading,
    container: '#micro-container',
    activeRule: '/react16',
    appInfo,
  },
  {
    name: 'vue2',
    entry: '//localhost:9004/',
    loading,
    container: '#micro-container',
    activeRule: '/vue2',
    appInfo,
  },
  {
    name: 'vue3',
    entry: '//localhost:9005/',
    loading,
    container: '#micro-container',
    activeRule: '/vue3',
    appInfo,
  },
];
// 微前端框架对于子应用的处理基本集中在main>micro>lifeCycle>index.js里,执行里app所有的生命周期,获取里dom结构,加载里资源,接下来需要在对应生命周期里来将app信息传递给子应用里,在执行子应用app.mount的时候将appInfo传递过去
// main>src>util>index.js的registerApp注册子应用的方法里将store内容传递给子应用
export const mounted = async (app) => {
  app && app.mount && app.mount({
    appInfo: app.appInfo,
    entry: app.entry
  })

  await runMainLifeCycle('mounted')
}



// react16>src>index.js接下来就可以在子应用react16的mounted事件里获取到主应用的内容
export const mount = (app) => {
  app.appInfo.header.changeHeader(false)
  app.appInfo.nav.changeNav(false)
  render()
}

//对于react16而言,只需要在登录页面进行隐藏,目前其他页面也是隐藏的
// react16>src>utils>main.js对app做个缓存
let main = null
export const setMain = (data) => {
  main = data
}
export const getMain = () => {
  return main
}


// react16>src>index.js中将app设置在main对象上
import {setMain} from './src/utils/main';
export const mount = (app) => {
  setMain(app)
  render()
}


//react16>src>pages>login>index.jsx中隐藏,登录页面隐藏,其他页面还是显示
import { getMain } from '../../utils/main'
  useEffect(() => {
    // 不仅仅可以获取到主应用方法,同时也可以通过方法向主应用传递我们的参数
    const main = getMain()
    main.appInfo.header.changeHeader(false)
    main.appInfo.nav.changeNav(false)
  }, [])

// 好莱坞原则 - 不用联系我,当我需要的时候会打电话给你
// 依赖注入 - 主应用的显示隐藏,注入到子应用内部,通过子应用内部的方法进行调用

// 2.customevent
//main>micro>customevent>index.js
export class Custom {
  // 事件监听
  on (name, cb) {
    window.addEventListener(name, (e) => {
      cb(e.detail)
    })
  }
  // 事件触发
  emit(name, data) {
    const event = new CustomEvent(name, {
      detail: data
    })
    window.dispatchEvent(event)
  }
}


//main>micro>start.js
import { Custom } from './customevent'
const custom = new Custom()
custom.on('test', (data) => {
  console.log(data)
})
window.custom = custom

// vue3>src>main.js
export const mount = () => {
  window.custom.emit('test',{
    a:1
  })
  render()
}
//可以看到打印的a:1,子应用向主应用传递事件,同样道理也可以从主应用向子应用发送事件,但是一定要注意监听和触发的时机,监听一定要在触发之前,否则会产生一些监听不到的问题

应用间通信 - 子应用间通信

对于微前端而言,需要获取上一个应用的内容,表明当前的应用设计是有一些缺陷的,理论上来说,我们每一个应用之间都不会依赖于上一个或者下一个应用间的信息,这些信息我们都可以通过主应用或者数据服务来获取,但也避免不了一些及其特殊的情况,我们需要用到上一个子应用处理过的信息,在这子应用里做其他处理

// 1.props
// 子应用1 - 父应用交互 - 子应用2,子应用1和父应用进行交互,得到子应用1传递的参数,父应用与子应用2进行交互,将子应用1传递的参数通过父应用的转发传递给子应用2
// 2.customevent
// 例如vue3与vue2进行通信
//vue3>src>main.js
export const mount = () => {
  window.custom.on('test1',(data)=>{
    console.log(data)
  })
  render()
}


//vue2>src>main.js触发一个事件将参数传递到vue3里
export const mount = () => {
  window.custom.emit('test1',{
    a:1
  })
  render()
}
// vue3切换到vue2子应用里会打印a:1对象

//如果需要从vue3向vue2发传递数据
//vue2>src>main.js
export const mount = () => {
  window.custom.on('test2',(data)=>{
    console.log(data,'======')
  })
  window.custom.emit('test1',{
    a:1
  })
  render()
}
//vue3>src>main.js
export const mount = () => {
  //先有监听再有触发
  window.custom.on('test1',(data)=>{
      window.custom.emit('test2',{
        b:2
      })
  })
  render()
}
// vue3切换到vue2页面,b:2
//vue2子应用里先做里test2的监听,再做了test1的触发,在vue3里只要接收到了test1的事件,之后我们就可以在它回调函数里将test2事件进行触发并且传递我们需要的参数
//从vue3向vue2传递数据,下一个子应用是没有加载过的,没有自己的监听事件,如果在vue3里直接做了test2事件的触发,就会出现在vue2监听不到的情况,需要在vue2里添加test2的监听事件,添加完成之后向vue3发一个事件,告诉它你现在可以触发test2的事件了,这样在vue3里做事件的触发,先监听test1,回调函数里触发test2

全局状态管理 - 全局store

//上节讲到的监听如果当前监听的方法非常多,会出现监听方法重叠,需要做到name的唯一化管理,极大提升开发难度,所以我们需要一个管理系统,不需要定义我们监听的事件,也可以触发我们所有监听的内容
//main>micro>store>index.js
export const createStore = (initData = {}) => (() => {
  let store = initData
  const observers = [] // 管理所有的订阅者,依赖
  // 获取store
  const getStore = () => store
  // 更新store
  const update = (value) => {
    if (value !== store) {
      // 执行store的操作
      const oldValue = store
      // 将store更新
      store = value
      // 通知所有的订阅者,监听store的变化
      observers.forEach(async item => await item(store, oldValue))
    }
  }
  // 添加订阅者
  const subscribe = (fn) => {
    observers.push(fn)
  }
  return {
    getStore,
    update,
    subscribe,
  }
})()
//在微前端框架里实现这样的状态管理,与所有的框架都是没有关系的,每套框架都可以使用这套状态管理体系来做


//main>src>utils>index.js
import { createStore } from '../../micro'
const store = createStore()
window.store = store
store.subscribe((newValue, oldValue) => {
  console.log(newValue, oldValue, '---')
}) // 添加订阅者


//vue3>src>main.js
export async function mount(app) {
  const storeData = window.store.getStore() // 获取默认的data
  //更新数据,更新完成后会通知订阅者
  window.store.update({
    ...storeData, // store里是完全做替换的,需要将之前所有数据传递过去
    a:11
  })
  render();
}

// 好处:
//1.不使用任何的eventName就可以做到事件监听
//2.同个事件可以添加很多observers,添加狠多订阅者,之后通过订阅者来遍历,修改所有我们的订阅者依赖
//没有之前所说的命名会重复的问题,但是也要注意添加订阅者和update操作也有先后顺序,只有先添加订阅者在操作之后才可以通知到订阅者,如果没有添加订阅者直接进行update操作可以通知到其他订阅者,但是你当前需要的数据没有了

提高加载性能 - 应用缓存

// main > micro > loader > index.js里加载过的资源不再进行加载
export const loadHtml = async (app) => {
  // 第一个,子应用需要显示在哪里
  let container = app.container // #id 内容
  // 子应用的入口
  let entry = app.entry
  const [dom, scripts] = await parseHtml(entry, app.name)
  const ct = document.querySelector(container)
  if (!ct) {
    throw new Error('容器不存在,请查看')
  }
  ct.innerHTML = dom
  return app
}
const cache = {} // 根据子应用的name来做缓存
export const parseHtml = async (entry, name) => {
  if (cache[name]) {
    return cache[name]
  }
  const html = await fetchResource(entry)
  let allScript = []
  const div = document.createElement('div')
  div.innerHTML = html
  // 接下来针对div这个伪元素做对应的html处理,处理内容包含标签、link、script(src,js)
  const [dom, scriptUrl, script] = await getResources(div, entry)
  const fetchedScripts = await Promise.all(scriptUrl.map(async item => fetchResource(item)))
  allScript = script.concat(fetchedScripts)
  cache[name] = [dom, allScript] // 
  return [dom, allScript]
}
// 节省了很多解析子应用资源的步骤,省了解析和加载的步骤

提高加载性能 - 预加载子应用

// main>micro>start.js
import { prefetch } from './loader/prefetch'
// 启动微前端框架
export const start = () => {
  // 首先验证当前子应用列表是否为空
  const apps = getList()
  if (!apps.lenth) {
    // 子应用列表为空
    throw Error('子应用列表为空, 请正确注册')
  }
  // 有子应用的内容, 查找到符合当前路由的子应用
  const app = currentApp()
  const { pathname, hash } = window.location

  if (!hash) {
    // 当前没有在使用的子应用
    // 1. 抛出一个错误,请访问正确的连接
    // 2. 访问一个默认的路由,通常为首页或登录页面
    window.history.pushState(null, null, '/vue3#/index')
  }
  if (app && hash) {
    const url = pathname + hash
    window.__CURRENT_SUB_APP__ = app.activeRule
    window.history.pushState('', '', url)
  }
  // 预加载 - 加载接下来的所有子应用,但是不显示
  prefetch()
}

// main>micro>loader>prefetch.js
import { getList } from '../const/subApps';
import { parseHtml } from './index';
export const prefetch = async () => {
  // 1. 获取到所有子应用列表 - 不包括当前正在显示的
  const list = getList().filter(item => !window.location.pathname.startsWith(item.activeRule))
  // 2. 预加载剩下的所有子应用
  await Promise.all(list.map(async item => await parseHtml(item.entry, item.name)))
}

六、框架发布 – 通过npm发布框架

如果其他主应用也想用微前端框架,只能copy过去使用,这样效率慢,准确性得不到保障,如果微前端框架需要升级,维护起来麻烦,每一份主应用都需要更改,可以将框架通过npm发布,以后主应用通过npm安装依赖就行

// 1.需要有自己的npm账号
// 2.npm login
// 3.npm whoami 查看当前登录的账号
// 4.npm publish发布
// 5.npm unpublish 包名@版本号 取消发布 npm unpublish 包名@1.0.1
// 主版本号 1 做了不会向下兼容的改动npm version major
// 次版本号 0 做了会向下兼容的功能新增 npm version minor(次版本号会递增,修订号归0)
// 修订号 0 做了会向下兼容的问题修正 npm version patch

七、应用部署 - 创建自动部署平台

// npm install express express-generator -g
// express -e server
// Npm install supervisor —save-dev实现自动更新

八、实现应用的自动化部署

九、质量保证 - 如何实现主子应用测试

// 1.访问到线上或者测试环境
// 2.需要将对应的请求链接(页面请求链接)代理到本地
// 3.需要确保本地服务开启
// Charles
// 1.proxy》macOS Proxy开启
// 2.proxy>ssl proxying settings添加*:443
// 3.proxy>recording settings中include添加*
// 4.需要安装https证书,并且设置为信任状态help》ssl proxying〉install Charles root certificate
// 5.tools》map remote勾选enable map remote

十、使用qiankun重构项目

真正想在生辰环境实现一个微前端框架,要做的事情是很多的,要经过大量的试错,验证来验证自己的框架是没有问题的,现在有很多成熟的微前端框架,我们可以直接用

qiankun源码分析-应用注册

十一、使用single-spa重构项目

single-spa.js.org

qiankun与single-spa

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-01-04 13:20:03  更:2022-01-04 13:22:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 13:04:45-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码