微前端框架qiankun源码剖析之下篇

目录
  • 引言
  • 四、沙箱隔离
    • 4.1 JS隔离
      • 1. Snapshot沙箱
      • 2. Legacy沙箱
      • 3. Proxy沙箱
    • 4.2 CSS隔离
      • 1. ShadowDOM
      • 2. Scoped CSS
  • 五、通信方式
  • 六、结语

引言

承接上文  微前端框架qiankun源码剖析之上篇

注意: 受篇幅限制,本文中所粘贴的代码都是经过作者删减梳理后的,只为讲述qiankun框架原理而展示,并非完整源码。如果需要阅读相关源码可以自行打开文中链接。

四、沙箱隔离

在基于single-spa开发的微前端应用中,子应用开发者需要特别注意的是:

要谨慎修改和使用全局变量上的属性(如window、document等),以免造成依赖该属性的自身应用或其它子应用运行时出现错误;

要谨慎控制CSS规则的生效范围,避免覆盖污染其它子应用的样式;

但这样的低级人为保证机制是无法在大规模的团队开发过程中对应用的独立性起到完善保护的,而qiankun框架给我们提供的最便利和有用的功能就是其基于配置的自动化沙箱隔离机制了。有了框架层面的子应用隔离支持,用户无论是在编写JS代码还是修改CSS样式时都不必再担心代码对于全局环境的污染问题了。沙箱机制一方面提升了微应用框架运行的稳定性和独立性,另一方面也降低了微前端开发者的心智负担,让其只需专注于自己的子应用代码开发之中。

4.1 JS隔离

在JS隔离方面,qiankun为开发者提供了三种不同模式的沙箱机制,分别适用于不同的场景之中。

1. Snapshot沙箱

该沙箱主要用于不支持Proxy对象的低版本浏览器之中,不能由用户手动指定该模式,qiankun会自动检测浏览器的支持情况并降级到Snapshot沙箱实现。由于这种实现方式在子应用运行过程中实际上修改了全局变量,因此不能用于多例模式之中(同时存在多个已挂载的子应用)。

该沙箱实现方式非常简洁,下面我们给出其简化后的实现(源码地址github.com/umijs/qiank…

// 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
export default class SnapshotSandbox implements SandBox {
  private windowSnapshot!: Window;
  private modifyPropsMap: Record<any, any> = {};
  constructor() {}
  active() {
    // 记录当前快照
    this.windowSnapshot = {} as Window;
    iter(window, (prop) => {
      this.windowSnapshot[prop] = window[prop];
    });
    // 恢复之前的变更
    Object.keys(this.modifyPropsMap).forEach((p: any) => {
      window[p] = this.modifyPropsMap[p];
    });
  }
  inactive() {
    this.modifyPropsMap = {};
    iter(window, (prop) => {
      if (window[prop] !== this.windowSnapshot[prop]) {
        // 记录变更,恢复环境
        this.modifyPropsMap[prop] = window[prop];
        window[prop] = this.windowSnapshot[prop];
      }
    });
  }
}

沙箱内部存在两个对象变量windowSnapshotmodifyPropsMap ,分别用来存储子应用挂载前原始window对象上的全部属性以及子应卸载时被其修改过的window对象上的相关属性。

Snapshot沙箱会在子应用mount前将modifyPropsMap中存储的属性重新赋值给window以恢复该子应用之前执行时的全局变量上下文,并在子应用unmount后将windowSnapshot中存储的属性重新赋值给window以恢复该子应用运行前的全局变量上下文,从而使得两个不同子应用的window相互独立,达到JS隔离的目的。

2. Legacy沙箱

当用户手动配置sandbox.loose: true时该沙箱被启用。Legacy沙箱同样会对window造成污染,但是其性能比要比snapshot沙箱好,因为该沙箱不用遍历window对象。同样legacy沙箱也只适用于单例模式之中。

下面一起来看一下其简化后的大致实现方式(源码地址github.com/umijs/qiank…

/**
 * 基于 Proxy 实现的沙箱
 * TODO: 为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换
 */
export default class LegacySandbox implements SandBox {
  /** 沙箱代理的全局变量 */
  proxy: WindowProxy;
  /** 沙箱期间新增的全局变量 */
  private addedPropsMapInSandbox = new Map<PropertyKey, any>();
  /** 沙箱期间更新的全局变量 */
  private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();
  /** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */
  private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();
  constructor() {
    const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this;
    const rawWindow = window;
    const fakeWindow = Object.create(null) as Window;
    const setTrap = (p: PropertyKey, value: any, originalValue: any) => {
      if (!rawWindow.hasOwnProperty(p)) {
        // 当前 window 对象不存在该属性,将其记录在新增变量之中
        addedPropsMapInSandbox.set(p, value);
      } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
        // 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值
        modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
      }
      // 无论何种修改都记录在currentUpdatedPropsValueMap中
      currentUpdatedPropsValueMap.set(p, value);
      // 必须重新设置 window 对象保证下次 get 时能拿到已更新的数据
      (rawWindow as any)[p] = value;
    };
    const proxy = new Proxy(fakeWindow, {
      set: (_: Window, p: PropertyKey, value: any): boolean => {
        const originalValue = (rawWindow as any)[p];
        return setTrap(p, value, originalValue, true);
      },
      get(_: Window, p: PropertyKey): any {
        // avoid who using window.window or window.self to escape the sandbox environment to touch the really window or use window.top to check if an iframe context
        if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
          return proxy;
        }
        const value = (rawWindow as any)[p];
        return value;
      },
    });
    this.proxy = proxy
  }
  active() {
    // 激活时将子应用之前的所有改变重新赋予window,恢复其运行时上下文
    this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v));
  }
  inactive() {
    // 卸载时将window上修改的值复原,新添加的值删除
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v));
    this.addedPropsMapInSandbox.forEach((_, p) => this.setWindowProp(p, undefined, true));
  }
  private setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) {
    if (value === undefined && toDelete) {
      delete (this.globalContext as any)[prop];
    } else {
      (this.globalContext as any)[prop] = value;
    }
  }
}

Legacy沙箱为一个空对象fakewindow使用proxy代理拦截了其全部的set/get等操作,并在loader中用其替换了window。当用户试图修改window属性时,fakewindow上代理的set操作生效捕获了相关修改,其分别将新增的属性和修改前的值存入addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox这两个Map之中,此外还将所有修改记录在了currentUpdatedPropsValueMap之中,并改变了window对象。

这样当子应用挂载前,legacy沙箱会将currentUpdatedPropsValueMap之中记录的子应用相关修改重新赋予window,恢复其运行时上下文。当子应用卸载后,legacy沙箱会遍历addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox这两个Map并将window上的相关值恢复到子应用运行之前的状态。最终达到了子应用间JS隔离的目的。

3. Proxy沙箱

Proxy沙箱是qiankun框架中默认使用的沙箱模式(也可以通过配置sandbox.loose: false来开启),只有该模式真正做到了对window的无污染隔离(子应用完全不能修改全局变量),因此可以被应用在单/多例模式之中。

Proxy沙箱的原理也非常简单,它将window上的所有属性遍历拷贝生成一个新的fakeWindow对象,紧接着使用proxy代理这个fakeWindow,用户对window操作全部被拦截下来,只作用于在这个fakeWindow之上(源码地址github.com/umijs/qiank…

// 便利window拷贝创建初始代理对象
function createFakeWindow(globalContext: Window) {
  const fakeWindow = {} as FakeWindow;
  Object.getOwnPropertyNames(globalContext)
    .forEach((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
      rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
    });
  return { fakeWindow };
}
/**
 * 基于 Proxy 实现的沙箱
 */
export default class ProxySandbox implements SandBox {
  // 标志该沙箱是否被启用
  sandboxRunning = true;
  constructor() {
    const { fakeWindow } = createFakeWindow(window);
    const proxy = new Proxy(fakeWindow, {
      set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
        if(this.sandboxRunning){
          // 修改代理对象的值
          target[p] = value;
          return true;
        }
      }
      get: (target: FakeWindow, p: PropertyKey): any => {
        // avoid who using window.window or window.self to escape the sandbox environment to touch the really window
        if (p === 'window' || p === 'self' || p === 'globalThis') {
          return proxy;
        }
        // 获取代理对象的值
      	const value = target[p];
        return value;
      },
    })
  }
  active() {
    if (!this.sandboxRunning) activeSandboxCount++;
    this.sandboxRunning = true;
  }
  inactive() {
    this.sandboxRunning = false;
  }
}

4.2 CSS隔离

对于CSS隔离的方式,在默认情况下由于切换子应用时,其相关的CSS内外连属性会被卸载掉,所以可以确保单实例场景子应用之间的样式隔离,但是这种方式无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。不过,qiankun也提供了两种可配置生效的内置方式供使用者选择。

1. ShadowDOM

当用户配置sandbox.strictStyleIsolation: true时,ShadowDOM样式沙箱会被开启。在这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。(源码地址github.com/umijs/qiank…

// 在子应用的DOM树最外层进行一次包裹
function createElement(
  appContent: string,
  strictStyleIsolation: boolean,
  scopedCSS: boolean,
  appInstanceId: string,
): HTMLElement {
  // 包裹节点
  const containerElement = document.createElement('div');
  containerElement.innerHTML = appContent;
  // 子应用最外层节点
  const appElement = containerElement.firstChild as HTMLElement;
  // 当开启了ShadowDOM沙箱时
  if (strictStyleIsolation) {
    const { innerHTML } = appElement;
    appElement.innerHTML = '';
    let shadow: ShadowRoot;
		// 判断浏览器兼容的创建ShadowDOM的方式,并使用该方式创建ShadowDOM根节点
    if (appElement.attachShadow) {
      shadow = appElement.attachShadow({ mode: 'open' });
    } else {
      // createShadowRoot was proposed in initial spec, which has then been deprecated
      shadow = (appElement as any).createShadowRoot();
    }
    // 将子应用内容挂在ShadowDOM根节点下
    shadow.innerHTML = innerHTML;
  }
	// 。。。。。。
  return appElement;
}

这种方式虽然看起来清晰简单,还巧妙利用了浏览器对于ShadowDOM的CSS隔离特性,但是由于ShadowDOM的隔离比较严格,所以这并不是一种无脑使用的方案。例如:如果子应用内存在一个弹出时会挂在document根元素的弹窗,那么该弹窗的样式是否会受到ShadowDOM的影响而失效?所以开启该沙箱的用户需要明白自己在做什么,且可能需要对子应用内部代码做出一定的调整。

2. Scoped CSS

因为ShadowDOM存在着上述的一些问题,qiankun贴心的为用户提供了另一种更加无脑简便的样式隔离方式,那就是Scoped CSS。通过配置sandbox.experimentalStyleIsolation: true,Scoped样式沙箱会被开启。

在这种模式下,qiankun会遍历子应用中所有的CSS选择器,通过对选择器前缀添加一个固定的带有该子应用标识的属性选择器的方式来限制其生效范围,从而避免子应用间、主应用与子应用的样式相互污染。(源码地址github.com/umijs/qiank…

export const QiankunCSSRewriteAttr = 'data-qiankun';
// 在子应用的DOM树最外层进行一次包裹
function createElement(
  appContent: string,
  strictStyleIsolation: boolean,
  scopedCSS: boolean,
  appInstanceId: string,
): HTMLElement {
  // 包裹节点
  const containerElement = document.createElement('div');
  containerElement.innerHTML = appContent;
  // 子应用最外层节点
  const appElement = containerElement.firstChild as HTMLElement;
  // 。。。。。。
  // 当开启了Scoped CSS沙箱时
  if (scopedCSS) {
    // 为外层节点添加qiankun自定义属性,其值设定为子应用id标识
    const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr);
    if (!attr) {
      appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId);
    }
		// 获取子应用中全部样式并进行处理
    const styleNodes = appElement.querySelectorAll('style') || [];
    forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
      css.process(appElement!, stylesheetElement, appInstanceId);
    });
  }
  return appElement;
}

qiankun首先对子应用最外层的包裹节点(一般为div节点)添加一个属性名为data-qiankun,值为appInstanceId的属性。接着遍历处理子应用中的所有样式(源码地址github.com/umijs/qiank…

export const process = (
  appWrapper: HTMLElement,
  stylesheetElement: HTMLStyleElement | HTMLLinkElement,
  appName: string,
): void => {
  // lazy singleton pattern
  if (!processor) {
    processor = new ScopedCSS();
  }
	// !!!注意,对于link标签引入的外联样式不支持。qiankun在初期解析使用的import-html-entry在解析html模版时会将所有外联样式拉取并转换为style标签包裹的内联样式,所以这里不再处理link的外联样式。
  if (stylesheetElement.tagName === 'LINK') {
    console.warn('Feature: sandbox.experimentalStyleIsolation is not support for link element yet.');
  }
  const mountDOM = appWrapper;
  if (!mountDOM) {
    return;
  }
	// 获取包裹元素标签
  const tag = (mountDOM.tagName || '').toLowerCase();
  if (tag && stylesheetElement.tagName === 'STYLE') {
    // 生成属性选择器前缀,准备将其添加在选择器前(如div[data-qiankun=app1])
    const prefix = `${tag}[${QiankunCSSRewriteAttr}="${appName}"]`;
    processor.process(stylesheetElement, prefix);
  }
};
// 。。。。。。
process(styleNode: HTMLStyleElement, prefix: string = '') {
  if (styleNode.textContent !== '') {
    // 获取相关css规则rules
    const textNode = document.createTextNode(styleNode.textContent || '');
    this.swapNode.appendChild(textNode);
    const sheet = this.swapNode.sheet as any; // type is missing
    const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
    // 重写这些CSS规则,将前缀添加进去
    const css = this.rewrite(rules, prefix);
    // 用重写后的CSS规则覆盖之前的规则
    styleNode.textContent = css;
    // 标志符,代表该节点已经处理过
    (styleNode as any)[ScopedCSS.ModifiedTag] = true;
    return;
  }
	// 监听节点变化
  const mutator = new MutationObserver((mutations) => {
    for (let i = 0; i < mutations.length; i += 1) {
      const mutation = mutations[i];
      // 忽略已经处理过的节点
      if (ScopedCSS.ModifiedTag in styleNode) {
        return;
      }
      // 如果新增了未处理过的子节点(代表了用户新注入了一些属性),那么会再次重写所有的CSS规则以确保新增的CSS不会污染子应用外部
      if (mutation.type === 'childList') {
        const sheet = styleNode.sheet as any;
        const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
        const css = this.rewrite(rules, prefix);
        styleNode.textContent = css;
        (styleNode as any)[ScopedCSS.ModifiedTag] = true;
      }
    }
	});
  // 注册监听
  mutator.observe(styleNode, { childList: true });
}
// 具体CSS规则重写方式
private rewrite(rules: CSSRule[], prefix: string = '') {
  // 。。。。。。
  // 这里省略其实现方式,整体实现思路简单但步骤很繁琐,主要就是对字符串的正则判断和替换修改。
  // 1. 对于根选择器(html/body/:root等),直接将其替换为prefix
  // 2. 对于其它选择器,将prefix放在最前面( selector1 selector2, selector3 =》 prefix selector1 selector2,prefix selector3)
}

可以看到,qiankun通过为子应用的外层包裹元素注入属性并将子应用全部样式的作用范围都限制在该包裹元素下(通过添加指定的属性选择器作为前缀)实现了scoped样式沙箱隔离。需要注意的是,如果用户在运行时对内联样式进行修改,qiankun是可以侦测到并帮助用户限制其作用范围,但如果用户在运行时引入了新的外联样式或者自行创建了新的内联标签,那么qiankun并不会做出反应,相关的CSS规则还是可能会污染全局样式。

五、通信方式

对于微前端来说,除了应用间的隔离外,应用间的通信也是非常重要的部分。这里,single-spa提供了从主应用向子应用传递customProps的方式实现了最基础的参数传递。但是真实的开发场景需要的信息传递是非常复杂的,静态的预设参数传递只能起到很小的作用,我们还需要一种更加强大的通信机制来帮助我们开发应用。

这里,qiankun在框架内部预先设计实现了完善的发布订阅模式,降低了开发者的上手门槛。我们首先来看一下qiankun中的通信是如何进行的。

// ------------------主应用------------------
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
// 在当前应用监听全局状态,有变更触发 callback
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
// 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
actions.setGlobalState(state);
// 移除当前应用的状态监听,微应用 umount 时会默认调用
actions.offGlobalStateChange();
// ------------------子应用------------------
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });
  props.setGlobalState(state);
}

接下来,让我们一起来看一下它是如何实现的。(源码地址github.com/umijs/qiank…

import { cloneDeep } from 'lodash';
import type { OnGlobalStateChangeCallback, MicroAppStateActions } from './interfaces';
// 全局状态
let globalState: Record<string, any> = {};
// 缓存相关的订阅者
const deps: Record<string, OnGlobalStateChangeCallback> = {};
// 触发全局监听
function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {
  Object.keys(deps).forEach((id: string) => {
    if (deps[id] instanceof Function) {
      // 依次通知订阅者
      deps[id](cloneDeep(state), cloneDeep(prevState));
    }
  });
}
// 初始化
export function initGlobalState(state: Record<string, any> = {}) {
  if (state === globalState) {
    console.warn('[qiankun] state has not changed!');
  } else {
    const prevGlobalState = cloneDeep(globalState);
    globalState = cloneDeep(state);
    emitGlobal(globalState, prevGlobalState);
  }
  // 返回相关方法,形成闭包存储相关状态
  return getMicroAppStateActions(`global-${+new Date()}`, true);
}
export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {
  return {
    /**
     * onGlobalStateChange 全局依赖监听
     *
     * 收集 setState 时所需要触发的依赖
     *
     * 限制条件:每个子应用只有一个激活状态的全局监听,新监听覆盖旧监听,若只是监听部分属性,请使用 onGlobalStateChange
     *
     * 这么设计是为了减少全局监听滥用导致的内存爆炸
     *
     * 依赖数据结构为:
     * {
     *   {id}: callback
     * }
     *
     * @param callback
     * @param fireImmediately 是否立即执行callback
     */
    onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
      if (!(callback instanceof Function)) {
        console.error('[qiankun] callback must be function!');
        return;
      }
      if (deps[id]) {
        console.warn(`[qiankun] '${id}' global listener already exists before this, new listener will overwrite it.`);
      }
      / 注册订阅
      deps[id] = callback;
      if (fireImmediately) {
        const cloneState = cloneDeep(globalState);
        callback(cloneState, cloneState);
      }
    },
    /**
     * setGlobalState 更新 store 数据
     *
     * 1. 对输入 state 的第一层属性做校验,只有初始化时声明过的第一层(bucket)属性才会被更改
     * 2. 修改 store 并触发全局监听
     *
     * @param state
     */
    setGlobalState(state: Record<string, any> = {}) {
      if (state === globalState) {
        console.warn('[qiankun] state has not changed!');
        return false;
      }
      const changeKeys: string[] = [];
      const prevGlobalState = cloneDeep(globalState);
      globalState = cloneDeep(
        Object.keys(state).reduce((_globalState, changeKey) => {
          if (isMaster || _globalState.hasOwnProperty(changeKey)) {
            changeKeys.push(changeKey);
            return Object.assign(_globalState, { [changeKey]: state[changeKey] });
          }
          console.warn(`[qiankun] '${changeKey}' not declared when init state!`);
          return _globalState;
        }, globalState),
      );
      if (changeKeys.length === 0) {
        console.warn('[qiankun] state has not changed!');
        return false;
      }
      // 触发全局监听
      emitGlobal(globalState, prevGlobalState);
      return true;
    },
    // 注销该应用下的依赖
    offGlobalStateChange() {
      delete deps[id];
      return true;
    },
  };
}

可以看到在initGlobalState函数的执行中完成了一个发布订阅模式的创建工作,并返回了相关的订阅/发布/注销方法。接着qiankun将这些返回的方法通过生命周期函数mount传递给子应用,这样子应用就能够拿到并使用全局状态了,从而应用间的通信就得以实现了。此外offGlobalStateChange会在子应用unmount时自动调用以解除该子应用的订阅,避免内存泄露。(第三节子应用加载中的代码已经提到,源码参见github.com/umijs/qiank…

六、结语

qiankun在single-spa的基础上进行了二次封装,分别从子应用加载方式、应用间沙箱隔离、应用间通信这三个方面着手,通过自己的方式降低了用户的使用门槛,简便了微前端项目的开发改造成本,从而成为目前为止最为流行的微前端框架。

优化点 single-spa qiankun
子应用加载方式 用户自行编码配置子应用加载方式 用户只需配置子应用入口URL
应用间沙箱隔离 无隔离机制 内置了三种JS沙箱和两种CSS沙箱
应用间通信 主应用通过customProps向子应用传递静态参数 内置了一整套基于发布订阅的通信模式

本文通过对于qiankun源码的粗略解读,希望读者可以获取到自己所需的知识,得到些许的进步。编码的路程漫长且艰辛,诸位共同努力!

更多关于微前端框架qiankun剖析的资料请关注我们其它相关文章!

(0)

相关推荐

  • Qiankun原理详解JS沙箱是如何做隔离

    目录 前言 复习一下沙箱 SanpshotSandbox LegacySandbox ProxySandbox 隔离原理 XXX is undefined 总结 前言 相信大家也知道 qiankun 有 SnapshotSandbox, LegacySandbox 和 ProxySandbox 这些沙箱,而它们又可以分为单例和多例两种模式,网上也有很多文章对其进行介绍. 但这些文章的关注点都是沙箱的环境恢复做的事,那 JS 的隔离到底是怎么做到的呢? 换个问法,当我写 window.a = 1

  • 微前端qiankun沙箱实现源码解读

    目录 前言 LegacySandbox单实例沙箱 ProxySandbox多实例沙箱 SapshotSandbox 快照沙箱 结束语 前言 上篇我们介绍了微前端实现沙箱的几种方式,没看过的可以下看下JS沙箱这篇内容,扫盲一下.接下来我们通过源 码详细分析下qiankun沙箱实现,我们clone下qiankun代码,代码主要在sandbox文件夹下,目录结构为 ├── common.ts ├── index.ts // 入口文件 ├── legacy │ └── sandbox.ts // 代理沙

  • Qiankun Sentry 监控异常上报无法自动区分项目解决

    目录 前言 使用 Sentry 上报异常 解决方案 失败的方案一 不通用的方案二 合理.优雅的方案三 7.x 版本解决方案 结束语 前言 最近项目组决定将前端异常监控由 Fundebug 切换为 Sentry.整个切换过程可以说非常简单,部署一个后台服务,然后将 Sentry SDK 集成到前端应用中就完事儿了.在之后的使用过程中,小编遇到了一个问题.由于我们的项目采用的是基于 qiankun 的微前端架构,在应用使用过程中,常常会出现发生异常应用和上报应用不匹配的情况. 为了解决这个问题,小编

  • Vue qiankun微前端实现详解

    目录 引言 What:微前端是什么 Why:为什么选择微前端 微前端能做到什么 为什么不使用iFrame How:微前端实践 在主应用中注册微应用 在子应用导出相应的生命周期钩子 结尾 引言 前端时间有个契机,让我们团队开始进行微前端的相关实践. 最近正好有些成果了,来一个阶段性的总结,也方便后续进一步的开发. 可能第一次听说微前端的同学都会不明觉厉,那么ta到底是个啥?本章会从以下3个角度阐述我的理解: What:微前端是什么 Why:为什么选择微前端 How:微前端实践 What:微前端是什

  • qiankun 找不到入口问题彻底解决

    目录 前言 为什么要找生命周期 如何找入口 兜底找入口 微应用的 Webpack 配置 主应用的兜底逻辑 总结 前言 嗨害嗨,好久不见,我是海怪. 有一阵子没写文章了,今天来更一期关于 qiankun 找不到生命周期的问题. 刚开始给项目接入 qiankun 的时候,时不时就会报 Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry: 开发

  • 微前端qiankun改造日渐庞大的项目教程

    项目背景 很多小伙伴在工作中都碰到过和我一样的场景,手上的某个项目越来越大,眼看着每次build时间越来越长,吐了.在杭州某独角兽我碰到了这样的一个项目,他叫运营后台,听名字就知道,他的主要用户是运营人员.问题就是随着公司业务的越来越多,这个运营后台承担的已经不是某一块业务了,而是所有业务的运营操作的中后台都在这上面.你可以这样理解,这个系统的每个一级菜单都是一块独立的业务,相互之间没有任何瓜葛:按常规的理解,这应该是单独的每一个project比较合理,但是正因为他的用户又都是公司的同一群人,他

  • 微前端框架qiankun源码剖析之下篇

    目录 引言 四.沙箱隔离 4.1 JS隔离 1. Snapshot沙箱 2. Legacy沙箱 3. Proxy沙箱 4.2 CSS隔离 1. ShadowDOM 2. Scoped CSS 五.通信方式 六.结语 引言 承接上文  微前端框架qiankun源码剖析之上篇 注意: 受篇幅限制,本文中所粘贴的代码都是经过作者删减梳理后的,只为讲述qiankun框架原理而展示,并非完整源码.如果需要阅读相关源码可以自行打开文中链接. 四.沙箱隔离 在基于single-spa开发的微前端应用中,子应用

  • 微前端架构ModuleFederationPlugin源码解析

    目录 序言 背景 MF 基本介绍 应用场景 微前端架构 服务化的 library 和 components ModuleFederationPlugin 源码解析 入口源码 Exposes Remotes Shared 小结 总结 序言 本文是 Webpack ModuleFederationPlugin(后面简称 MF) 源码解析 文章中的第一篇,在此系列文章中,我将带领大家抽丝剥茧.一步步地去解析 MF 源码.当然为了帮助大家理解,可能中间也会涉及到 Webpack 源码中的其它实现,我会根

  • go zero微服务框架logx日志组件剖析

    目录 addTenant api 和 rpc 的实现 logx 日志组件剖析 Go-zero 中 logx 是如何使用的? Logx 基本的数据结构 Logx 的默认接口实现 Logx 自定义存储日志位置 和 实现自定义接口的方式 自定义存储日志位置 实现自定义接口 addTenant api 和 rpc 的实现 上一篇我们说到咱们还剩下 addTenant 功能还未实现,不知道有没有兄弟感兴趣去实验一波的,本篇文章进行简要补充 根据上一篇文章分析,其实我们只需要执行如下几步即可: 编写 ten

  • Django动态随机生成温度前端实时动态展示源码示例

    目录 随机生成温度 前端动态实时 一.django APScheduler定时任务 简介 安装 使用步骤 基础组件 二.dwebsocket 简介 安装 使用方法 属性和方法 为了模拟随机的温度显示,在models中的表中的数据 views 路由 VUE中的代码 随机生成温度 前端动态实时 一.django APScheduler定时任务 简介 APScheduler的全称是Advanced Python Scheduler. 它是一个轻量级的 Python 定时任务调度框架. APSchedu

  • vue3.x源码剖析之数据响应式的深入讲解

    目录 前言 什么是数据响应式 数据响应式的大体流程 vue2.x数据响应式和3.x响应式对比 大致流程图 实现依赖收集 代码仓库 结尾 前言 如果错过了秋枫和冬雪,那么春天的樱花一定会盛开吧.最近一直在准备自己的考试,考完试了,终于可以继续研究源码和写文章了,哈哈哈.学过vue的都知道,数据响应式在vue框架中极其重要,写代码也好,面试也罢,数据响应式都是核心的内容.在vue3的官网文档中,作者说如果想让数据更加响应式的话,可以把数据放在reactive里面,官方文档在讲述这里的时候一笔带过,笔

  • golang高并发系统限流策略漏桶和令牌桶算法源码剖析

    目录 前言 漏桶算法 样例 源码实现 令牌桶算法 样例 源码剖析 Limit类型 Limiter结构体 Reservation结构体 Limiter消费token limiter归还Token 总结 前言 今天与大家聊一聊高并发系统中的限流技术,限流又称为流量控制,是指限制到达系统的并发请求数,当达到限制条件则可以拒绝请求,可以起到保护下游服务,防止服务过载等作用.常用的限流策略有漏桶算法.令牌桶算法.滑动窗口:下文主要与大家一起分析一下漏桶算法和令牌桶算法,滑动窗口就不在这里这介绍了.好啦,废

  • 非常实用的js验证框架实现源码 附原理方法

    本文为大家分享一个很实用的js验证框架实现源码,供大家参考,具体内容如下 关键方法和原理: function check(thisInput) 方法中的 if (!eval(scriptCode)) { return false; } 调用示例: 复制代码 代码如下: <input type="text" class="text_field percentCheck" name="progress_payment_two" id="

  • cocos2dx骨骼动画Armature源码剖析(二)

    上篇文章从总体上介绍了cocos2dx自带的骨骼动画,这篇文章介绍一下导出的配置数据各个字段的含义(也解释了DragonBone导出的xml数据每个字段的含义). skeleton节点 <skeleton name="Dragon" frameRate="24" version="2.2"> name:flash文件名字. frameRate:flash帧率. version:dragonbones版本号. armatures节点 首

  • cocos2dx骨骼动画Armature源码剖析(一)

    cocos2dx从编辑器(cocostudio或flash插件dragonBones)得到xml或json数据,调用类似如下所示代码就可以展示出动画效果 ArmatureDataManager::getInstance()->addArmatureFileInfoAsync( "armature/Dragon.png", "armature/Dragon.plist", "armature/Dragon.xml", this, schedu

随机推荐