当前位置: 首页 > news >正文

微前端 qiankun@2.10.5 源码分析(二)

微前端 qiankun@2.10.5 源码分析(二)

我们继续上一节的内容。

loadApp 方法

找到 src/loader.ts 文件的第 244 行:

export async function loadApp<T extends ObjectType>(app: LoadableApp<T>,configuration: FrameworkConfiguration = {},lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {...// 根据入口文件获取应用信息const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry(entry, importEntryOpts);// 在执行应用入口文件的之前先加载其它的资源文件// 比如:http://127.0.0.1:7101/static/js/chunk-vendors.js 文件await getExternalScripts();...// 创建当前应用元素,并且替换入口文件的 head 元素为 qiankun-headconst appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);...// 创建 css scoped,跟 vue scoped 的一样const scopedCSS = isEnableScopedCSS(sandbox);let initialAppWrapperElement: HTMLElement | null = createElement(appContent,strictStyleIsolation,scopedCSS,appInstanceId,);const initialContainer = 'container' in app ? app.container : undefined;// 获取渲染器,也就是在第一步中执行的 render 方法const render = getRender(appInstanceId, appContent, legacyRender);// 第一次加载设置应用可见区域 dom 结构// 确保每次应用加载前容器 dom 结构已经设置完毕// 将子应用的 initialAppWrapperElement 元素插入挂载节点 initialContainerrender({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');// 创建一个 initialAppWrapperElement 元素的获取器const initialAppWrapperGetter = getAppWrapperGetter(appInstanceId,!!legacyRender,strictStyleIsolation,scopedCSS,() => initialAppWrapperElement,);let global = globalContext;let mountSandbox = () => Promise.resolve();let unmountSandbox = () => Promise.resolve();const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose;// enable speedy mode by defaultconst speedySandbox = typeof sandbox === 'object' ? sandbox.speedy !== false : true;let sandboxContainer;if (sandbox) {// 创建沙盒sandboxContainer = createSandboxContainer(appInstanceId,// FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518initialAppWrapperGetter,scopedCSS,useLooseSandbox,excludeAssetFilter,global,speedySandbox,);// 用沙箱的代理对象作为接下来使用的全局对象global = sandboxContainer.instance.proxy as typeof window;mountSandbox = sandboxContainer.mount;unmountSandbox = sandboxContainer.unmount;}const {beforeUnmount = [],afterUnmount = [],afterMount = [],beforeMount = [],beforeLoad = [],} = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat(v1 ?? [], v2 ?? []));// 调用 beforeLoad 生命周期await execHooksChain(toArray(beforeLoad), app, global);// 获取子应用模块信息const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {scopedGlobalVariables: speedySandbox ? cachedGlobals : [],});// 获取子应用模块信息导出的生命周期const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(scriptExports,appName,global,sandboxContainer?.instance?.latestSetProp,);// 全局状态const { onGlobalStateChange, setGlobalState, offGlobalStateChange }: Record<string, CallableFunction> =getMicroAppStateActions(appInstanceId);// 返回 spa 需要的钩子信息const parcelConfig: ParcelConfigObject = {name: appInstanceId,bootstrap, // bootstrap 钩子信息mount: [ // mount 钩子信息...],unmount: [ // unmount 钩子信息...],};// update 钩子信息if (typeof update === 'function') {parcelConfig.update = update;}return parcelConfig;};return parcelConfigGetter;
}

代码有点多,loadApp 算是 qiankun 框架最重要的一个方法了,不要慌,我们一步一步的来!

importEntry 方法

loadApp 方法中,使用了 importEntry 方法去根据子应用入口加载子应用信息:

export async function loadApp<T extends ObjectType>(app: LoadableApp<T>,configuration: FrameworkConfiguration = {},lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {...// 根据入口文件获取应用信息const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry(entry, importEntryOpts);// 在执行应用入口文件的之前先加载其它的资源文件// 比如:http://127.0.0.1:7101/static/js/chunk-vendors.js 文件await getExternalScripts();...
}

importEntry 方法是 import-html-entry 库中提供的方法:

import-html-entry

以 html 文件为应用的清单文件,加载里面的(css、js),获取入口文件的导出内容。

Treats the index html as manifest and loads the assets(css,js), get the exports from entry script.

<!-- subApp/index.html --><!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>test</title>
</head>
<body><!-- mark the entry script with entry attribute -->
<script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js" entry></script>
<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
</body>
</html>
import importHTML from 'import-html-entry';importHTML('./subApp/index.html').then(res => {console.log(res.template);res.execScripts().then(exports => {const mobx = exports;const { observable } = mobx;observable({name: 'kuitos'})})
});

更多 import-html-entry 库的内容,小伙伴们自己去看官网哦!

我们可以来测试一下,比如我们在第一步中注册的子应用信息:

{name: 'vue',entry: '//localhost:7101',container: '#subapp-viewport',loader,activeRule: '/vue',
}

vue 子应用的入口是 //localhost:7101,我们首先用 fetch 直接访问一下入口文件:

在这里插入图片描述

ok,可以看到,这是一个很普通的 vue 项目的入口文件,接着我们用 import-html-entry 库中提供的 importEntry 方法去测试一下:

import {importEntry} from "import-html-entry";
;(async ()=>{const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry("//localhost:7101");console.log("template", template);const externalScripts = await getExternalScripts();console.log("externalScripts", externalScripts);const module = await execScripts();console.log("module", module);console.log("assetPublicPath", assetPublicPath);console.log("assetPublicPath", assetPublicPath);
})()

我们运行看效果:

在这里插入图片描述

console.log("template", template) 的结果:

<html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Vue App</title><!-- prefetch/preload link /static/js/about.js replaced by import-html-entry --><!-- prefetch/preload link /static/js/app.js replaced by import-html-entry --><!-- prefetch/preload link /static/js/chunk-vendors.js replaced by import-html-entry --></head><body><div id="app"></div><!--  script http://localhost:7101/static/js/chunk-vendors.js replaced by import-html-entry --><!--  script http://localhost:7101/static/js/app.js replaced by import-html-entry --></body>
</html>

可以看到,我们的 js 文件都被 import-html-entry 框架给注释掉了,所以 template 返回的是一个被处理过后的入口模版文件,里面的 js、css 资源文件都被剔除了。

console.log("externalScripts", externalScripts); 的结果:

返回了原模版文件中两个 js 文件:

<script type="text/javascript" src="/static/js/chunk-vendors.js"></script>
<script type="text/javascript" src="/static/js/app.js"></script>

的文本内容了:

在这里插入图片描述

console.log("module", module); 结果:

在这里插入图片描述

返回的是 vue 项目入口文件 examples/vue/src/main.js 导出的几个生命周期方法:

...
export async function bootstrap() {console.log('[vue] vue app bootstraped');
}export async function mount(props) {console.log('[vue] props from main framework', props);storeTest(props);render(props);
}export async function unmount() {instance.$destroy();instance.$el.innerHTML = '';instance = null;router = null;
}

console.log("assetPublicPath", assetPublicPath); 返回的是入口文件的公共路径 publishPath

http://localhost:7101/

ok,到这里我们已经获取到子应用的信息了,我们继续分析 loadApp 方法。

接下来看看 qiankun 是如何做到子应用样式隔离的。

scoped css

export async function loadApp<T extends ObjectType>(app: LoadableApp<T>,configuration: FrameworkConfiguration = {},lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {...// 根据入口文件获取应用信息const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry(entry, importEntryOpts);// 在执行应用入口文件的之前先加载其它的资源文件// 比如:http://127.0.0.1:7101/static/js/chunk-vendors.js 文件await getExternalScripts();...
}  // 获取子应用模版节点const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;const scopedCSS = isEnableScopedCSS(sandbox);// 创建 css scoped,跟 vue scoped 的一样let initialAppWrapperElement: HTMLElement | null = createElement(appContent,strictStyleIsolation,scopedCSS,appInstanceId,);// 获取挂载节点const initialContainer = 'container' in app ? app.container : undefined;const legacyRender = 'render' in app ? app.render : undefined;// 获取渲染器,也就是在第一步中执行的 render 方法const render = getRender(appInstanceId, appContent, legacyRender);// 第一次加载设置应用可见区域 dom 结构// 确保每次应用加载前容器 dom 结构已经设置完毕// 将子应用的 initialAppWrapperElement 元素插入挂载节点 initialContainerrender({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');const initialAppWrapperGetter = getAppWrapperGetter(appInstanceId,!!legacyRender,strictStyleIsolation,scopedCSS,() => initialAppWrapperElement,);...
}

可以看到,获取子应用的入口文件后,首先调用了 getDefaultTplWrapper 方法创建了一个子应用模版节点:

// 获取子应用模版节点
const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);

src/utils.ts 文件中找到该方法:

export function getDefaultTplWrapper(name: string, sandboxOpts: FrameworkConfiguration['sandbox']) {return (tpl: string) => {let tplWithSimulatedHead: string;// 替换入口模版文件中的 <head> 标签为 <qiankun-head>if (tpl.indexOf('<head>') !== -1) {tplWithSimulatedHead = tpl.replace('<head>', `<${qiankunHeadTagName}>`).replace('</head>', `</${qiankunHeadTagName}>`);} else {tplWithSimulatedHead = `<${qiankunHeadTagName}></${qiankunHeadTagName}>${tpl}`;}// 创建模版入口元素 div,将子应用的信息设置到该节点的属性中return `<div id="${getWrapperId(name,)}" data-name="${name}" data-version="${version}" data-sandbox-cfg=${JSON.stringify(sandboxOpts,)}>${tplWithSimulatedHead}</div>`;};
}

可以看到,替换了原来的 head 元素,然后在入口模版元素外包裹了一个 div 元素,最后将应用的基本信息设置到该节点的属性中。

获取到 appContent 节点后,然后就进行 css 样式隔离:

const scopedCSS = isEnableScopedCSS(sandbox);
// 创建 css scoped,跟 vue scoped 的一样
let initialAppWrapperElement: HTMLElement | null = createElement(appContent,strictStyleIsolation,scopedCSS,appInstanceId,
);

我们重点看一下 qiankun 是如何做到子应用样式隔离。

首先找到 src/loader.ts 第 67 行的 createElement 方法:

function createElement(appContent: string,strictStyleIsolation: boolean,scopedCSS: boolean,appInstanceId: string,
): HTMLElement {const containerElement = document.createElement('div');containerElement.innerHTML = appContent;// appElement 节点const appElement = containerElement.firstChild as HTMLElement;// 如果设置了强行样式隔离,就利用 ShadowDOM 进行样式隔离  if (strictStyleIsolation) {// 判断是否支持 ShadowDOM 节点if (!supportShadowDOM) {console.warn('[qiankun]: As current browser not support shadow dom, your strictStyleIsolation configuration will be ignored!',);} else {// 利用 ShadowDOM 进行样式隔离 const { innerHTML } = appElement;appElement.innerHTML = '';let shadow: ShadowRoot;if (appElement.attachShadow) {shadow = appElement.attachShadow({ mode: 'open' });} else {// createShadowRoot was proposed in initial spec, which has then been deprecatedshadow = (appElement as any).createShadowRoot();}shadow.innerHTML = innerHTML;}}// 对入口文件中的 <style> 标签中的样式进行深度遍历,全部加上 scopedif (scopedCSS) {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;
}

ok,有小伙伴可以要问了,为什么需要样式隔离干什么呢?

我们来测试一下不加样式隔离的场景。

我们修改一下主应用的 examples/main/render/VueRender.js

import Vue from 'vue/dist/vue.esm';function vueRender({ loading }) {return new Vue({template: `<div id="subapp-container"><!-- 主应用中的样式测试隔离元素 --><h1 class="test-class">test-class</h1><h4 v-if="loading" class="subapp-loading">Loading...</h4><!-- 子应用挂载节点 --><div id="subapp-viewport"></div></div>`,el: '#subapp-container',data() {return {loading,};},});
}let app = null;export default function render({ loading }) {if (!app) {app = vueRender({ loading });} else {app.loading = loading;}
}

接着我们修改一下子应用 react16 的入口文件 examples/react16/public/index.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"/><link href="%PUBLIC_URL%/favicon.ico" rel="icon"/><meta content="width=device-width, initial-scale=1" name="viewport"/><meta content="#000000" name="theme-color"/><metacontent="Web site created using create-react-app"name="description"/><link href="logo192.png" rel="apple-touch-icon"/><link href="%PUBLIC_URL%/manifest.json" rel="manifest"/><title>React App</title><!-- 样式隔离测试样式 --><style>.test-class {color: red;}</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

可以看到,我们在子应用中添加了一个测试样式:

<style>.test-class {color: red;}
</style>

保存运行看效果:
在这里插入图片描述

可以看到,如果我们没有进行子应用样式隔离,子应用中的样式会污染主应用中的样式。

那在 qiankun 中如何启用样式隔离呢?

我们只需要在启动应用的时候传给 qiankun 就可以了,我们修改一下 examples/main/index.js 文件开启样式隔离:

/*** Step4 启动应用*/
start({sandbox: {strictStyleIsolation: true, // 开启子应用样式隔离,默认 false 关闭}
});

运行看效果:
在这里插入图片描述

可以看到,当开启了样式隔离后,子应用中的样式就不会污染主应用了。

那么为什么 qiankun 默认关闭样式隔离呢?我想可以能是考虑以下原因:

  1. ShadowDOM 兼容性不太好。
  2. 一般子应用中都会有样式命名规范,比如 vue 的 scoped、BEM 命名规范等,所以一般不用考虑。
  3. 子应用切换后节点都会被移除,所以不会引起子应用样式相互污染。

扩展:

qiankun 还可以对入口模版的 <style> 标签中的模版做深度遍历,对每个元素加上 scoped,从而来做到样式隔离。

对应的源码为:

function createElement(appContent: string,strictStyleIsolation: boolean,scopedCSS: boolean,appInstanceId: string,
): HTMLElement {...// 对入口文件中的 <style> 标签中的样式进行深度遍历,全部加上 scopedif (scopedCSS) {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;
}

那么如何开启 scopedCSS 呢?也是在启动 qiankun 的时候。

我们修改一下 examples/main/index.js 入口的 start 方法:

/*** Step4 启动应用*/
start({sandbox: {experimentalStyleIsolation: true, // 开启 scopedCSS(还在实验中的属性,不推荐使用!!!)}
});

运行看效果:
在这里插入图片描述

可以看到,当我们启用了 scopedCSS 后,qiankun 会给在每一个样式加上一个 div[data-qiankun="react16"] 元素,将当前元素作为该元素的后代样式。

具体源码就不分析了,大概就是利用 css 元素深度遍历,然后添加 scoped 元素,最后利用 MutationObserver 监听节点的变化,给每一个节点都加上 scoped 元素。

了解一下原理就行了,目前该属性还在试验中,不推荐使用!!!

ok,介绍完样式隔离,下面就到了最重要的应用沙盒隔离了。

sandbox 沙盒

why:为什么需要沙盒隔离?

因为我们的应用都运行在一个主应用中,我们会用的全局变量 window 中的任何东西,也会对它进行各种改造,所以为了避免对全局变量的污染,qiankun 会为每一个应用创建一个 sanbox 环境,这样就不会污染全局变量了。

ok,了解为什么需要 sanbox 后,我们继续分析我们的源码。

回到 src/loader.ts 文件的 loadApp 方法:

export async function loadApp<T extends ObjectType>(app: LoadableApp<T>,configuration: FrameworkConfiguration = {},lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {...// 根据入口文件获取应用信息const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry(entry, importEntryOpts);// 在执行应用入口文件的之前先加载其它的资源文件// 比如:http://127.0.0.1:7101/static/js/chunk-vendors.js 文件await getExternalScripts();...
}  // 获取子应用模版节点const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;const scopedCSS = isEnableScopedCSS(sandbox);// 创建 css scoped,跟 vue scoped 的一样let initialAppWrapperElement: HTMLElement | null = createElement(appContent,strictStyleIsolation,scopedCSS,appInstanceId,);// 获取挂载节点const initialContainer = 'container' in app ? app.container : undefined;const legacyRender = 'render' in app ? app.render : undefined;// 获取渲染器,也就是在第一步中执行的 render 方法const render = getRender(appInstanceId, appContent, legacyRender);// 第一次加载设置应用可见区域 dom 结构// 确保每次应用加载前容器 dom 结构已经设置完毕// 将子应用的 initialAppWrapperElement 元素插入挂载节点 initialContainerrender({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');...let sandboxContainer;// 如果开启了沙盒,就给每一个应用创建一个沙盒环境(默认开启)if (sandbox) {sandboxContainer = createSandboxContainer(appInstanceId,// FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518initialAppWrapperGetter,scopedCSS,useLooseSandbox,excludeAssetFilter,global,speedySandbox,);// 用沙箱的代理对象作为接下来使用的全局对象global = sandboxContainer.instance.proxy as typeof window;mountSandbox = sandboxContainer.mount;unmountSandbox = sandboxContainer.unmount;}...
}

可以看到,当应用开启了沙盒后,qiankun 会利用 createSandboxContainer 方法给每一个应用创建一个沙盒容器 sandboxContainer

找到 src/sandbox/index.ts 文件中的createSandboxContainer 方法:

export function createSandboxContainer(appName: string,elementGetter: () => HTMLElement | ShadowRoot,scopedCSS: boolean,useLooseSandbox?: boolean,excludeAssetFilter?: (url: string) => boolean,globalContext?: typeof window,speedySandBox?: boolean,
) {let sandbox: SandBox;// 兼容性处理if (window.Proxy) {sandbox = useLooseSandbox? new LegacySandbox(appName, globalContext): new ProxySandbox(appName, globalContext, { speedy: !!speedySandBox });} else {sandbox = new SnapshotSandbox(appName);}// some side effect could be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phaseconst bootstrappingFreers = patchAtBootstrapping(appName,elementGetter,sandbox,scopedCSS,excludeAssetFilter,speedySandBox,);// mounting freers are one-off and should be re-init at every mounting timelet mountingFreers: Freer[] = [];let sideEffectsRebuilders: Rebuilder[] = [];return {instance: sandbox,/*** 沙箱被 mount* 可能是从 bootstrap 状态进入的 mount* 也可能是从 unmount 之后再次唤醒进入 mount*/async mount() {/* ------------------------------------------ 因为有上下文依赖(window),以下代码执行顺序不能变 ------------------------------------------ *//* ------------------------------------------ 1. 启动/恢复 沙箱------------------------------------------ */sandbox.active();const sideEffectsRebuildersAtBootstrapping = sideEffectsRebuilders.slice(0, bootstrappingFreers.length);const sideEffectsRebuildersAtMounting = sideEffectsRebuilders.slice(bootstrappingFreers.length);// must rebuild the side effects which added at bootstrapping firstly to recovery to nature stateif (sideEffectsRebuildersAtBootstrapping.length) {sideEffectsRebuildersAtBootstrapping.forEach((rebuild) => rebuild());}/* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/// render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter, speedySandBox);/* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/// 存在 rebuilder 则表明有些副作用需要重建if (sideEffectsRebuildersAtMounting.length) {sideEffectsRebuildersAtMounting.forEach((rebuild) => rebuild());}// clean up rebuilderssideEffectsRebuilders = [];},/*** 恢复 global 状态,使其能回到应用加载之前的状态*/async unmount() {// record the rebuilders of window side effects (event listeners or timers)// note that the frees of mounting phase are one-off as it will be re-init at next mountingsideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map((free) => free());sandbox.inactive();},};
}

可以看到,主要就是创建了一个 sandbox 沙盒对象,然后返回了 mountunmount 方法给 single-spa 调用,当子应用渲染的时候会调用 mount 方法,当子应用销毁的时候会调用 unmount 方法:

  • mount 方法:会调用 sandbox.active() 方法启用沙盒,会创建并收集当前子应用的一些副作用,比如 setTimeoutsetIntervaladdEventListener 等。
  • unmount 方法:会调用 sandbox.inactive() 方法关闭沙盒,移除当前子应用的一些副作用,比如 setTimeoutsetIntervaladdEventListener 等。

可以看到,主要是为了在切换子应用的时候开启和关闭沙盒,清除一些副作用,来防止内存泄漏。

因为大多数浏览器是支持 window.Proxy 的,所以我们就直接分析这里的 ProxySandbox 对象了:

export function createSandboxContainer(appName: string,elementGetter: () => HTMLElement | ShadowRoot,scopedCSS: boolean,useLooseSandbox?: boolean,excludeAssetFilter?: (url: string) => boolean,globalContext?: typeof window,speedySandBox?: boolean,
) {
// 是否支持 window.Proxy
if (window.Proxy) {sandbox = useLooseSandbox? new LegacySandbox(appName, globalContext): new ProxySandbox(appName, globalContext, { speedy: !!speedySandBox });} else {sandbox = new SnapshotSandbox(appName);}...
}

找到 src/sandbox/proxySandbox.ts 文件:

// 伪造一个 window 对象
function createFakeWindow(globalContext: Window, speedy: boolean) {const fakeWindow = {} as FakeWindow;// 获取 window 所有不可以配置或者删除的属性Object.getOwnPropertyNames(globalContext).filter((p) => {const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);return !descriptor?.configurable;}).forEach((p) => {const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');...// 将这些属性都赋值给 fakeWindowrawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));// 可以通过 fakeWindow 获取到的一些属性if (hasGetter) propertiesWithGetter.set(p, true);}});// 返回伪造的 window 对象return {fakeWindow,propertiesWithGetter,};
}let activeSandboxCount = 0;/*** 基于 Proxy 实现的沙箱*/
export default class ProxySandbox implements SandBox {...// 激活沙盒active() {if (!this.sandboxRunning) activeSandboxCount++;this.sandboxRunning = true;}// 关闭沙盒inactive() {...this.sandboxRunning = false;}constructor(name: string, globalContext = window, opts?: { speedy: boolean }) {...// 伪造一个 window 对象 fakeWindowconst { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext, !!speedy);...// 创建代理对象 proxy 去代理 fakeWindow 对象const proxy = new Proxy(fakeWindow, {// 触发 fakeWindow 对象的 get 方法set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {if (this.sandboxRunning) {this.registerRunningApp(name, proxy){if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) {const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);const { writable, configurable, enumerable, set } = descriptor!;if (writable || set) {Object.defineProperty(target, p, { configurable, enumerable, writable: true, value });}} else {target[p] = value;}...return true;}// 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误return true;},// 触发 fakeWindow 对象的 set 方法get: (target: FakeWindow, p: PropertyKey): any => {this.registerRunningApp(name, proxy);if (p === Symbol.unscopables) return unscopables;// avoid who using window.window or window.self to escape the sandbox environment to touch the real window// see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13if (p === 'window' || p === 'self') {return proxy;}// hijack globalWindow accessing with globalThis keywordif (p === 'globalThis' || (inTest && p === mockGlobalThis)) {return proxy;}...return getTargetValue(boundTarget, value);},// 触发 fakeWindow 对象的 has 方法has(target: FakeWindow, p: string | number | symbol): boolean {// property in cachedGlobalObjects must return true to avoid escape from get trapreturn p in cachedGlobalObjects || p in target || p in globalContext;},...
}

这里简化了很多代码,主要就是创建了一个伪装的 window 对象 fakeWindow,然后对 fakeWindow 对象的getsethas 等方法进行代理,最后返回这个 fakeWindow 对象的代理对象 proxy,这样每个子应用都有一个自己的 window 对象了。

ok,我们继续回到 src/loader.ts 文件的第 321 行:

export async function loadApp<T extends ObjectType>(app: LoadableApp<T>,configuration: FrameworkConfiguration = {},lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {...// 根据入口文件获取应用信息const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry(entry, importEntryOpts);// 在执行应用入口文件的之前先加载其它的资源文件// 比如:http://127.0.0.1:7101/static/js/chunk-vendors.js 文件await getExternalScripts();...
}  // 获取子应用模版节点const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;const scopedCSS = isEnableScopedCSS(sandbox);// 创建 css scoped,跟 vue scoped 的一样let initialAppWrapperElement: HTMLElement | null = createElement(appContent,strictStyleIsolation,scopedCSS,appInstanceId,);// 获取挂载节点const initialContainer = 'container' in app ? app.container : undefined;const legacyRender = 'render' in app ? app.render : undefined;// 获取渲染器,也就是在第一步中执行的 render 方法const render = getRender(appInstanceId, appContent, legacyRender);// 第一次加载设置应用可见区域 dom 结构// 确保每次应用加载前容器 dom 结构已经设置完毕// 将子应用的 initialAppWrapperElement 元素插入挂载节点 initialContainerrender({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');...let sandboxContainer;// 如果开启了沙盒,就给每一个应用创建一个沙盒环境(默认开启)if (sandbox) {sandboxContainer = createSandboxContainer(appInstanceId,// FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518initialAppWrapperGetter,scopedCSS,useLooseSandbox,excludeAssetFilter,global,speedySandbox,);// 用沙箱的代理对象作为接下来使用的全局对象global = sandboxContainer.instance.proxy as typeof window;// 返回沙盒容器的 mount 方法用来开启沙盒mountSandbox = sandboxContainer.mount;// 返回沙盒容器的 unmount 方法用来关闭沙盒unmountSandbox = sandboxContainer.unmount;}...
}

可以看到,接下来将使用沙箱的代理对象 global 作为全局对象 window,并且返回了开启沙盒和关闭沙盒的方法给 single-spa 调用。

ok,我们接着往下分析 loadApp 方法。

找到 src/loader.ts 文件的第 337 行:

export async function loadApp<T extends ObjectType>(app: LoadableApp<T>,configuration: FrameworkConfiguration = {},lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {...// 根据入口文件获取应用信息const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry(entry, importEntryOpts);// 在执行应用入口文件的之前先加载其它的资源文件// 比如:http://127.0.0.1:7101/static/js/chunk-vendors.js 文件await getExternalScripts();...
}  // 获取子应用模版节点const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;const scopedCSS = isEnableScopedCSS(sandbox);// 创建 css scoped,跟 vue scoped 的一样let initialAppWrapperElement: HTMLElement | null = createElement(appContent,strictStyleIsolation,scopedCSS,appInstanceId,);// 获取挂载节点const initialContainer = 'container' in app ? app.container : undefined;const legacyRender = 'render' in app ? app.render : undefined;// 获取渲染器,也就是在第一步中执行的 render 方法const render = getRender(appInstanceId, appContent, legacyRender);// 第一次加载设置应用可见区域 dom 结构// 确保每次应用加载前容器 dom 结构已经设置完毕// 将子应用的 initialAppWrapperElement 元素插入挂载节点 initialContainerrender({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');...let sandboxContainer;// 如果开启了沙盒,就给每一个应用创建一个沙盒环境(默认开启)if (sandbox) {sandboxContainer = createSandboxContainer(appInstanceId,// FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518initialAppWrapperGetter,scopedCSS,useLooseSandbox,excludeAssetFilter,global,speedySandbox,);// 用沙箱的代理对象作为接下来使用的全局对象global = sandboxContainer.instance.proxy as typeof window;// 返回沙盒容器的 mount 方法用来开启沙盒mountSandbox = sandboxContainer.mount;// 返回沙盒容器的 unmount 方法用来关闭沙盒unmountSandbox = sandboxContainer.unmount;}// 获取生命周期const {beforeUnmount = [],afterUnmount = [],afterMount = [],beforeMount = [],beforeLoad = [],} = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat(v1 ?? [], v2 ?? []));// 调用 beforeLoad 生命周期 //在 beforeLoad 方法里主要给 window 的代理对象 fakeWindow 设置一些变量// 比如:global.__POWERED_BY_QIANKUN__ = true;await execHooksChain(toArray(beforeLoad), app, global);// 执行当前应用的入口文件const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {scopedGlobalVariables: speedySandbox ? cachedGlobals : [],});// 获取应用导出的 bootstrap、mount 等方法const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(scriptExports,appName,global,sandboxContainer?.instance?.latestSetProp,);...const parcelConfigGetter: ParcelConfigObjectGetter = (remountContainer = initialContainer) => {let appWrapperElement: HTMLElement | null;let appWrapperGetter: ReturnType<typeof getAppWrapperGetter>;const parcelConfig: ParcelConfigObject = {name: appInstanceId,bootstrap, // 返回 bootstrap 方法// 返回 mount 方法mount: [// 如果设置了单个应用运行,则需要等待上一个应用结束                             async () => {if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {return prevAppUnmountedDeferred.promise;}return undefined;},// initial wrapper element before app mount/remountasync () => {appWrapperElement = initialAppWrapperElement;appWrapperGetter = getAppWrapperGetter(appInstanceId,!!legacyRender,strictStyleIsolation,scopedCSS,() => appWrapperElement,);},// 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕async () => {const useNewContainer = remountContainer !== initialContainer;if (useNewContainer || !appWrapperElement) {// element will be destroyed after unmounted, we need to recreate it if it not exist// or we try to remount into a new containerappWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceId);syncAppWrapperElement2Sandbox(appWrapperElement);}render({ element: appWrapperElement, loading: true, container: remountContainer }, 'mounting');},// 开启沙盒mountSandbox,// 调用 beforeMount 生命周期async () => execHooksChain(toArray(beforeMount), app, global),// 调用 mount 生命周期,开始渲染子应用async (props) => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }),// 将子应用状态修改为 mountedasync () => render({ element: appWrapperElement, loading: false, container: remountContainer }, 'mounted'),// 触发 afterMount 生命周期async () => execHooksChain(toArray(afterMount), app, global),],unmount: [// 触发 beforeUnmount 生命周期async () => execHooksChain(toArray(beforeUnmount), app, global),// 调用 unmount 生命周期,开始卸载子应用async (props) => unmount({ ...props, container: appWrapperGetter() }),// 关闭沙盒unmountSandbox,// 调用 afterUnmount 生命周期async () => execHooksChain(toArray(afterUnmount), app, global),async () => {// 将子应用状态修改为 unmountedrender({ element: null, loading: false, container: remountContainer }, 'unmounted');offGlobalStateChange(appInstanceId);// for gcappWrapperElement = null;syncAppWrapperElement2Sandbox(appWrapperElement);},async () => {if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {prevAppUnmountedDeferred.resolve();}},],};// 触发子应用的 update 方法if (typeof update === 'function') {parcelConfig.update = update;}return parcelConfig;};return parcelConfigGetter;
}

ok,可能有些童鞋要问了:“沙箱对象 sandbox(也就是我们当前 loadApp 方法中的 global 对象),到底是怎么被子应用使用的呢?”

我们可以重点看到这么一段代码:

// 执行当前应用的入口文件
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {scopedGlobalVariables: speedySandbox ? cachedGlobals : [],
});

其实重点是 execScripts方法,那么它在执行子应用入口文件的时候,到底了什么呢?

其实原理很简单,就是用到了 with(){} 语句加 eval,我们来模拟一下 execScripts 方法的操作:

// 创建一个 fakeWindow
const fakeWindow = {name: "FakeWindow"
};
// 给 fakeWindow 设置代理对象
const proxy = new Proxy(fakeWindow, {get(target, p){if("window" === p || "self" === p){return target;}return target[p];},set(target, p, value){target[p] = value;}
});
window.proxy = proxy;
// 根据子应用入口文件封装执行的代码
const functionWrappedCode = `(function(){;(function(window, self, globalThis){with(window){// 子应用入口文件代码--startwindow.mount = ()=>{console.log("mount");console.log(window);}window.unmount= ()=>{console.log("unmount");console.log(window);}// 子应用入口文件代码--end}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);})
`;
// 调用 eval 执行封装过后的代码
(0, eval)(functionWrappedCode).call(window);// 执行 mount 方法
proxy.mount();
// 执行 unmount 方法
proxy.unmount();

可以看到,假设我们子应用的入口文件代码为:

 window.mount = ()=>{console.log("mount");console.log(window);}window.unmount= ()=>{console.log("unmount");console.log(window);}

我们在各自的子应用中随意用 window 对象,这是一个很正常的操作,经过我们 with 封装后,我们执行看效果:
在这里插入图片描述

可以看到,经过处理后,我们在子应用中用到的 window 已经被我们替换成了 fakeWindow,这样每一个子应用就都有一个自己的全局变量 window 了,这样就不会污染全局对象,这就是一个沙盒。

ok,分析完 loadApp 方法后,我们回到最初的 registerMicroApps 方法中。

找到 src/apis.ts 文件的第 80 行:

export function registerMicroApps<T extends ObjectType>(apps: Array<RegistrableApp<T>>,lifeCycles?: FrameworkLifeCycles<T>,
) {// 过滤未注册过的应用,防止多次注册const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name));microApps = [...microApps, ...unregisteredApps];// 遍历每一个未注册的应用unregisteredApps.forEach((app) => {const { name, activeRule, loader = noop, props, ...appConfig } = app;// 注册应用registerApplication({name,app: async () => {// 修改页面状态为 loadingloader(true);// 等待 start 方法的调用await frameworkStartedDefer.promise;// 加载当前子应用,获取子应用的 mount 方法const { mount, ...otherMicroAppConfigs } = (await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles))();return {mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],...otherMicroAppConfigs,};},activeWhen: activeRule,customProps: props,});});
}

single-spa

可以看到,当 loadApp 方法执行完成后,会返回应用的 mountbootstrapunmount 等方法,接下来会将这些方法传给 single-sparegisterApplication 方法,最后就是 single-spa 的事情了。

single-spa 会监听当前路由的变化,通过每个应用提供的的 activeWhen 来匹配出需要渲染的应用,接着调用该应用的 mount 方法对其进行渲染。

ok, qiankun 框架的源码到这我们就算是分析完了。

总结

整个源码分析下来我们会发现,要写出这么牛逼的框架,除了需要很扎实的 js 基础外,还需要有很强的架构意识,真的由衷佩服作者大大,请收下我的膝盖!!!

下一个 wujie 见!!!

相关文章:

微前端 qiankun@2.10.5 源码分析(二)

微前端 qiankun2.10.5 源码分析&#xff08;二&#xff09; 我们继续上一节的内容。 loadApp 方法 找到 src/loader.ts 文件的第 244 行&#xff1a; export async function loadApp<T extends ObjectType>(app: LoadableApp<T>,configuration: FrameworkConfi…...

08异步请求:何种场景下应该使用异步请求?

异步在计算机科学中早就是一个比较常用的词汇,从操作系统的特性( 并发、共享、虚拟、异步)开始,异步就在处理并发操作中起到很大的作用,不仅如此,在软件层面,异步同样也是解决并发请求的一个关键过程,它可以将瞬时激增的请求进行更加灵活的处理,通过异步请求,客户端可…...

【深度学习 | Transformer】Transformers 教程:pipeline一键预测

文章目录 一、前言二、Computer vision2.1 Image classification2.2 Object detection2.3 Image segmentation2.4 Depth estimation 三、NLP3.1 Text classification3.2 Token classification3.3 Question answering3.4 Summarization3.5 Translation3.6 Language modeling3.6.…...

HTMLCSS

1、HTML 1.1 介绍 HTML 是一门语言&#xff0c;所有的网页都是用HTML 这门语言编写出来的&#xff0c;也就是HTML是用来写网页的&#xff0c;像京东&#xff0c;12306等网站有很多网页。 这些都是网页展示出来的效果。而HTML也有专业的解释 HTML(HyperText Markup Language)…...

【安装Nginx】

Linux上安装Nginx 文章目录 Linux上安装NginxUbuntuCentOS查看已安装的软件 Ubuntu 在 Ubuntu 上安装 Nginx 非常简单。只需按照以下步骤操作&#xff1a; 打开终端&#xff0c;更新软件包索引&#xff1a; sudo apt update安装 Nginx&#xff1a; sudo apt install nginx安…...

VSCode作业1:猜数字游戏和简单计数器(包含完整代码)

目录 猜数字游戏 一、使用‘random’函数获取随机数 二、 分情况讨论输入值大小情况 三、HTML代码 四、CSS样式及运行效果 简单计数器&#xff08;计时器&#xff09; 一、使用‘setInterval’函数实现计数效果 二、使用’clearInterval‘函数实现暂停计数和重新计数效果 …...

NANK OE骨传导开放式蓝牙耳机发布,极致体验拉满!

近日&#xff0c;中国专业音频品牌NANK南卡发布了全新一代——骨传导开放式蓝牙耳机NANK OE&#xff0c;耳机采用了传统真无线和骨传导的结合方式&#xff0c;带来更加舒适的佩戴体验和音质升级&#xff0c;同时还支持单双耳自由切换&#xff0c;全新的设计收获了市场的喜爱和认…...

看完这篇文章你就彻底懂啦{保姆级讲解}-----(I.MX6U驱动GPIO中断《包括时钟讲解》) 2023.5.9

目录 前言整体文件结构源码分析&#xff08;保姆级讲解&#xff09;中断初始化部分初始化GIC控制器初始化中断向量表设置中断向量表偏移 系统时钟初始化部分使能所有的时钟部分led初始化部分beep初始化部分key初始化部分按键中断初始化部分按键中断服务函数部分 while循环部分 …...

MySql -- 事务

目录 1.概念 2.事务的运用场景 3.事务的四大特点 4.执行事务带来的问题 4.1 脏读 4.2 不可重复度 4.3 幻读 5. MySQL中事务的隔离级别 1.概念 事务就是把若干个独立操作打包成一个整体而诞生的一种功能. 2.事务的运用场景 比如&#xff1a;A——>B 转账500 A的余额-500…...

关于大模型对未来影响的一点看法

人们总是高估了未来一到两年的变化&#xff0c;低估了未来十年的变革。 ---比尔盖茨 近来OpenAI的GPT技术可以说在全球都带来了巨大的影响&#xff0c;也让大家看到了什么叫大力出奇迹。chatGPT和GPT4的能力给了大家很大的震撼&#xff0c;其流畅自如、逻辑清晰、出众的能力&am…...

Android - 约束布局 ConstraintLayout

一、概念 解决布局嵌套过多的问题&#xff0c;采用方向约束的方式对控件进行定位。 二、位置约束 2.1 位置 至少要保证水平和垂直方向都至少有一个约束才能确定控件的位置。 layout_constraintLeft_toLeftOf我的左边&#xff0c;与XXX左边对齐。layout_constraintLeft_toRight…...

Addictive Multiplicative in NN

特征交叉是特征工程中的重要环节&#xff0c;在以表格型&#xff08;或结构化&#xff09;数据为输入的建模中起到了很关键的作用。 特征交互的作用&#xff0c;一是尽可能挖掘对目标有效的模式、特征&#xff0c;二是具有较好的可解释性&#xff0c;三是能够将对数据的洞见引…...

LeetCode 1206. 实现跳表

不使用任何库函数&#xff0c;设计一个跳表。 跳表是在 O(log(n)) 时间内完成增加、删除、搜索操作的数据结构。跳表相比于树堆与红黑树&#xff0c;其功能与性能相当&#xff0c;并且跳表的代码长度相较下更短&#xff0c;其设计思想与链表相似。 例如&#xff0c;一个跳表包…...

离散数学_九章:关系(2)

9.2 n元关系及其应用 1、n元关系&#xff0c;关系的域&#xff0c;关系的阶2、数据库和关系 1. 数据库 2. 主键 3. 复合主键 3、n元关系的运算 1. 选择运算 (Select) 2. 投影运算 (Project) 3. 连接运算 (Join) n元关系&#xff1a;两个以上集合的元素间的关系 1、n元关系…...

[ubuntu][原创]通过apt方式去安装libnccl库

ubuntu18.04版本安装流程&#xff1a; wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-ubuntu1804.pin sudo mv cuda-ubuntu1804.pin /etc/apt/preferences.d/cuda-repository-pin-600 sudo apt-key adv --fetch-keys https://develo…...

YonLinker连接集成平台构建新一代产业互联根基

近日&#xff0c;由用友公司主办的“2023用友BIP技术大会“在用友产业园&#xff08;北京&#xff09;盛大召开&#xff0c;用友介绍了更懂企业业务的用友BIP-iuap平台&#xff0c;并发布了全面数智化能力体系&#xff0c;助力企业升级数智化底座&#xff0c;加强加速数智化推进…...

泛型的详解

泛型的理解和好处 首先我们先来看看泛型的好处 1)编译时&#xff0c;检查添加元素的类型&#xff0c;提高了安全性 2)减少了类型转换的次数&#xff0c;提高效率[说明] 不使用泛型 Dog -> Object -> Dog//放入到ArrayList 会先转成Object&#xff0c;在取出时&#x…...

用科技创造未来!流辰信息技术助您实现高效办公

随着社会的迅猛发展&#xff0c;科技的力量无处不见。它正在悄悄地改变整个社会&#xff0c;让人类变得进步和文明&#xff0c;让生活变得便捷和高效。在办公自动化强劲发展的今天&#xff0c;流辰信息技术让通信业、电网、汽车、物流等领域的企业实现了高效办公&#xff0c;数…...

基于R语言APSIM模型

随着数字农业和智慧农业的发展&#xff0c;基于过程的农业生产系统模型在模拟作物对气候变化的响应与适应、农田管理优化、作物品种和株型筛选、农田固碳和温室气体排放等领域扮演着越来越重要的作用。 APSIM (Agricultural Production Systems sIMulator)模型是世界知名的作物…...

块状链表实现BigString大字符串操作(golang)

前言 块状链表是介于链表和数组之间的数据结构&#xff0c;能够在 O ( n ) O(\sqrt{n}) O(n ​)时间内完成插入、删除、访问操作。 数据结构如图所示。假设最大容量为 n n n, 则它有一个长度为 s n s\sqrt{n} sn ​的链表。链表中每个结点是一个长度为 2 n 2 \times \sqrt{…...

项目问题记录(持续更新)

1.在 yarn install的时候报 error achrinza/node-ipc9.2.2: The engine "node" is incompatible with this module. Expected version "8 || 10 || 12 || 14 || 16 || 17". Got "20.1.0" error Found incompatible module.需要执行 yarn config…...

Linux的进程

目录 一、进程占用的内存资源 二、进程的系统环境 三、进程一直在切换 四、父进程和子进程 五、进程状态 六、查看进程 1.ps -ef 列出所有进程 2.ps -lax 列出所有进程 3.ps aux列出所有进程 4.树形列出所有进程 七、作业&#xff08;用来查看管理进程&#xff09; …...

与其焦虑被 AI 取代或猜测前端是否已死, 不如看看 vertical-align 扎实你的基础!!!

与其焦虑被 AI 取代或猜测前端是否已死, 不如看看 vertical-align 扎实你的基础!!! vertical-align 设置 display 值为 inline, inline-block 和 table-cell 的元素竖直对齐方式. 从 line-height: normal 究竟是多高说起 我们先来看一段代码, 分析一下为什么第二行的行高, 也就…...

路由、交换机、集线器、DNS服务器、广域网/局域网、端口、MTU

前言&#xff1a;网络名词术语解析(自行阅读扫盲)&#xff0c;推荐大家去读户根勤的《网络是怎样连接的》 路由(route)&#xff1a; 数据包从源地址到目的地址所经过的路径&#xff0c;由一系列路由节点组成。某个路由节点为数据包选择投递方向的选路过程。 路由器工作原理 路…...

在全志V851S开发板上进行屏幕触摸适配

1.修改屏幕驱动 从ft6236 &#xff08;删掉&#xff0c;不要保留&#xff09;&#xff0c;改为下面的 路径&#xff1a;/home/wells/tina-v853-open/tina-v853-open/device/config/chips/v851s/configs/lizard/board.dts&#xff08;注意路径&#xff0c;要设置为自己的实际路…...

字符串拷贝时的内存重叠问题

字符串拷贝时的内存重叠问题 1.什么是内存重叠 拷贝的目的地址在源地址的范围内&#xff0c;有重叠。 如在写程序的过程中&#xff0c;我们用到的strcpy这个拷贝函数&#xff0c;在这个函数中我们定义一个目的地址&#xff0c;一个源地址&#xff0c;在拷贝的过程中如果内存重…...

告别PPT手残党!这6款AI神器,让你秒变PPT王者!

如果你是一个PPT手残党&#xff0c;每每制作PPT总是让你焦头烂额&#xff0c;那么你一定需要这篇幽默拉风的推广文案&#xff01; 我向你保证&#xff0c;这篇文案将帮助你发现6款AI自动生成PPT的神器&#xff0c;让你告别PPT手残党的身份&#xff0c;成为一名PPT王者。 无论…...

JVM配置与优化

参考&#xff1a; JVM内存分区及作用&#xff08;JDK8&#xff09; https://blog.csdn.net/BigBug_500/article/details/104734957 java 进程占用系统内存过高分析 https://blog.csdn.net/fxh13579/article/details/104754340 Java之jvm和线程的内存 https://blog.csdn.ne…...

电力系统储能调峰、调频模型研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

C++基础之类、对象一(类的定义,作用域、this指针)

目录 面向对象的编程 类的引入 简介 类的定义 简介 访问限定符 命名规则 封装 简介 类的作用域 类的大小及存储模型 this指针 简介 面向对象的编程 C与C语言不同&#xff0c;C是面向对象的编程&#xff0c;那么什么是面向对象的编程呢&#xff1f; C语言编程&#xff0c;规定…...

wordpress点击后出现浮窗/百度app打开

文章目录一、基本概念&#xff1a;①字母表&#xff1a;②字母表上的运算&#xff1a;③符号串&#xff1a;④符号串上的运算&#xff1a;二、文法的定义&#xff1a;①文法的形式化定义&#xff1a;②产生式的简写&#xff1a;③符号约定&#xff1a;三、语言的定义&#xff1…...

赛迪建设网站/推广赚钱平台

十年内可以攻破癌症、糖尿病治愈难题&#xff1f;随着现代医疗技术和信息技术的融合发展&#xff0c;精准医疗的时代已经到来&#xff0c;这为许多特大疾病的治疗提供了新方向。这一领域也引来了国际巨头的关注。近日&#xff0c;英特尔在京推出了“英特尔精准医疗伙伴计划&…...

wordpress term id/苏州网站建设书生商友

系统装的RED HAT LINUX 9 装为服务器类别 只选装了English语言支持 装好之后 用 SSH SECURE SHELL 连接到系统,发现打开有些文档里有乱码 ,而在系统本身却没有,于是修改/etc/systemconfig/i18n这个文件,在最后加入一行LC_ALLPOSIX,重启系统,再也没有乱码了 .转载于:https://bl…...

python 做网站缺点/网站优化推广seo

前言 本篇文章继续我们的微软挖掘系列算法总结&#xff0c;前几篇文章已经将相关的主要算法做了详细的介绍&#xff0c;我为了展示方便&#xff0c;特地的整理了一个目录提纲篇&#xff1a;大数据时代&#xff1a;深入浅出微软数据挖掘算法总结连载&#xff0c; 有兴趣的童鞋可…...

苏州公司网站建设价格/如何优化网站快速排名

单例模式顾名思义就是只含有一个实例&#xff0c;一个实例复用&#xff0c;达到减少创建对象的开销以及大大节省资源的效果&#xff0c;是java23种设计模式最简单应用最多的设计模式&#xff1b;单例模式的实现方法主要分为饿汉式和懒汉式两大类&#xff0c; 饿汉式 public …...

专做药材的网站有哪些/免费b站动漫推广网站2023

1、 IP 地址: 网络之间互连的协议&#xff0c;是由4个字节(32位二进制)组成的逻辑上的地址。 将32位二进制进行分组&#xff0c;分成4组&#xff0c;每组8位(1个字节)。【ip地址通常使用十进制表示】ip地址分成四组之后&#xff0c;在逻辑上&#xff0c;分成网络号和主机号 2…...