微前端 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 默认关闭样式隔离呢?我想可以能是考虑以下原因:
- ShadowDOM 兼容性不太好。
- 一般子应用中都会有样式命名规范,比如 vue 的 scoped、BEM 命名规范等,所以一般不用考虑。
- 子应用切换后节点都会被移除,所以不会引起子应用样式相互污染。
扩展:
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
沙盒对象,然后返回了 mount
跟 unmount
方法给 single-spa 调用,当子应用渲染的时候会调用 mount
方法,当子应用销毁的时候会调用 unmount
方法:
- mount 方法:会调用
sandbox.active()
方法启用沙盒,会创建并收集当前子应用的一些副作用,比如setTimeout
、setInterval
、addEventListener
等。 - unmount 方法:会调用
sandbox.inactive()
方法关闭沙盒,移除当前子应用的一些副作用,比如setTimeout
、setInterval
、addEventListener
等。
可以看到,主要是为了在切换子应用的时候开启和关闭沙盒,清除一些副作用,来防止内存泄漏。
因为大多数浏览器是支持 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
对象的get
、set
、has
等方法进行代理,最后返回这个 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
方法执行完成后,会返回应用的 mount
、bootstrap
、unmount
等方法,接下来会将这些方法传给 single-spa
的 registerApplication
方法,最后就是 single-spa
的事情了。
single-spa
会监听当前路由的变化,通过每个应用提供的的 activeWhen
来匹配出需要渲染的应用,接着调用该应用的 mount
方法对其进行渲染。
ok, qiankun 框架的源码到这我们就算是分析完了。
总结
整个源码分析下来我们会发现,要写出这么牛逼的框架,除了需要很扎实的 js 基础外,还需要有很强的架构意识,真的由衷佩服作者大大,请收下我的膝盖!!!
下一个 wujie 见!!!
相关文章:
微前端 qiankun@2.10.5 源码分析(二)
微前端 qiankun2.10.5 源码分析(二) 我们继续上一节的内容。 loadApp 方法 找到 src/loader.ts 文件的第 244 行: 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 是一门语言,所有的网页都是用HTML 这门语言编写出来的,也就是HTML是用来写网页的,像京东,12306等网站有很多网页。 这些都是网页展示出来的效果。而HTML也有专业的解释 HTML(HyperText Markup Language)…...
【安装Nginx】
Linux上安装Nginx 文章目录 Linux上安装NginxUbuntuCentOS查看已安装的软件 Ubuntu 在 Ubuntu 上安装 Nginx 非常简单。只需按照以下步骤操作: 打开终端,更新软件包索引: sudo apt update安装 Nginx: sudo apt install nginx安…...
VSCode作业1:猜数字游戏和简单计数器(包含完整代码)
目录 猜数字游戏 一、使用‘random’函数获取随机数 二、 分情况讨论输入值大小情况 三、HTML代码 四、CSS样式及运行效果 简单计数器(计时器) 一、使用‘setInterval’函数实现计数效果 二、使用’clearInterval‘函数实现暂停计数和重新计数效果 …...
NANK OE骨传导开放式蓝牙耳机发布,极致体验拉满!
近日,中国专业音频品牌NANK南卡发布了全新一代——骨传导开放式蓝牙耳机NANK OE,耳机采用了传统真无线和骨传导的结合方式,带来更加舒适的佩戴体验和音质升级,同时还支持单双耳自由切换,全新的设计收获了市场的喜爱和认…...
看完这篇文章你就彻底懂啦{保姆级讲解}-----(I.MX6U驱动GPIO中断《包括时钟讲解》) 2023.5.9
目录 前言整体文件结构源码分析(保姆级讲解)中断初始化部分初始化GIC控制器初始化中断向量表设置中断向量表偏移 系统时钟初始化部分使能所有的时钟部分led初始化部分beep初始化部分key初始化部分按键中断初始化部分按键中断服务函数部分 while循环部分 …...
MySql -- 事务
目录 1.概念 2.事务的运用场景 3.事务的四大特点 4.执行事务带来的问题 4.1 脏读 4.2 不可重复度 4.3 幻读 5. MySQL中事务的隔离级别 1.概念 事务就是把若干个独立操作打包成一个整体而诞生的一种功能. 2.事务的运用场景 比如:A——>B 转账500 A的余额-500…...
关于大模型对未来影响的一点看法
人们总是高估了未来一到两年的变化,低估了未来十年的变革。 ---比尔盖茨 近来OpenAI的GPT技术可以说在全球都带来了巨大的影响,也让大家看到了什么叫大力出奇迹。chatGPT和GPT4的能力给了大家很大的震撼,其流畅自如、逻辑清晰、出众的能力&am…...
Android - 约束布局 ConstraintLayout
一、概念 解决布局嵌套过多的问题,采用方向约束的方式对控件进行定位。 二、位置约束 2.1 位置 至少要保证水平和垂直方向都至少有一个约束才能确定控件的位置。 layout_constraintLeft_toLeftOf我的左边,与XXX左边对齐。layout_constraintLeft_toRight…...
Addictive Multiplicative in NN
特征交叉是特征工程中的重要环节,在以表格型(或结构化)数据为输入的建模中起到了很关键的作用。 特征交互的作用,一是尽可能挖掘对目标有效的模式、特征,二是具有较好的可解释性,三是能够将对数据的洞见引…...
LeetCode 1206. 实现跳表
不使用任何库函数,设计一个跳表。 跳表是在 O(log(n)) 时间内完成增加、删除、搜索操作的数据结构。跳表相比于树堆与红黑树,其功能与性能相当,并且跳表的代码长度相较下更短,其设计思想与链表相似。 例如,一个跳表包…...
离散数学_九章:关系(2)
9.2 n元关系及其应用 1、n元关系,关系的域,关系的阶2、数据库和关系 1. 数据库 2. 主键 3. 复合主键 3、n元关系的运算 1. 选择运算 (Select) 2. 投影运算 (Project) 3. 连接运算 (Join) n元关系:两个以上集合的元素间的关系 1、n元关系…...
[ubuntu][原创]通过apt方式去安装libnccl库
ubuntu18.04版本安装流程: 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连接集成平台构建新一代产业互联根基
近日,由用友公司主办的“2023用友BIP技术大会“在用友产业园(北京)盛大召开,用友介绍了更懂企业业务的用友BIP-iuap平台,并发布了全面数智化能力体系,助力企业升级数智化底座,加强加速数智化推进…...
泛型的详解
泛型的理解和好处 首先我们先来看看泛型的好处 1)编译时,检查添加元素的类型,提高了安全性 2)减少了类型转换的次数,提高效率[说明] 不使用泛型 Dog -> Object -> Dog//放入到ArrayList 会先转成Object,在取出时&#x…...
用科技创造未来!流辰信息技术助您实现高效办公
随着社会的迅猛发展,科技的力量无处不见。它正在悄悄地改变整个社会,让人类变得进步和文明,让生活变得便捷和高效。在办公自动化强劲发展的今天,流辰信息技术让通信业、电网、汽车、物流等领域的企业实现了高效办公,数…...
基于R语言APSIM模型
随着数字农业和智慧农业的发展,基于过程的农业生产系统模型在模拟作物对气候变化的响应与适应、农田管理优化、作物品种和株型筛选、农田固碳和温室气体排放等领域扮演着越来越重要的作用。 APSIM (Agricultural Production Systems sIMulator)模型是世界知名的作物…...
块状链表实现BigString大字符串操作(golang)
前言 块状链表是介于链表和数组之间的数据结构,能够在 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.树形列出所有进程 七、作业(用来查看管理进程) …...
与其焦虑被 AI 取代或猜测前端是否已死, 不如看看 vertical-align 扎实你的基础!!!
与其焦虑被 AI 取代或猜测前端是否已死, 不如看看 vertical-align 扎实你的基础!!! vertical-align 设置 display 值为 inline, inline-block 和 table-cell 的元素竖直对齐方式. 从 line-height: normal 究竟是多高说起 我们先来看一段代码, 分析一下为什么第二行的行高, 也就…...
路由、交换机、集线器、DNS服务器、广域网/局域网、端口、MTU
前言:网络名词术语解析(自行阅读扫盲),推荐大家去读户根勤的《网络是怎样连接的》 路由(route): 数据包从源地址到目的地址所经过的路径,由一系列路由节点组成。某个路由节点为数据包选择投递方向的选路过程。 路由器工作原理 路…...
在全志V851S开发板上进行屏幕触摸适配
1.修改屏幕驱动 从ft6236 (删掉,不要保留),改为下面的 路径:/home/wells/tina-v853-open/tina-v853-open/device/config/chips/v851s/configs/lizard/board.dts(注意路径,要设置为自己的实际路…...
字符串拷贝时的内存重叠问题
字符串拷贝时的内存重叠问题 1.什么是内存重叠 拷贝的目的地址在源地址的范围内,有重叠。 如在写程序的过程中,我们用到的strcpy这个拷贝函数,在这个函数中我们定义一个目的地址,一个源地址,在拷贝的过程中如果内存重…...
告别PPT手残党!这6款AI神器,让你秒变PPT王者!
如果你是一个PPT手残党,每每制作PPT总是让你焦头烂额,那么你一定需要这篇幽默拉风的推广文案! 我向你保证,这篇文案将帮助你发现6款AI自动生成PPT的神器,让你告别PPT手残党的身份,成为一名PPT王者。 无论…...
JVM配置与优化
参考: JVM内存分区及作用(JDK8) 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代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
C++基础之类、对象一(类的定义,作用域、this指针)
目录 面向对象的编程 类的引入 简介 类的定义 简介 访问限定符 命名规则 封装 简介 类的作用域 类的大小及存储模型 this指针 简介 面向对象的编程 C与C语言不同,C是面向对象的编程,那么什么是面向对象的编程呢? C语言编程,规定…...
wordpress点击后出现浮窗/百度app打开
文章目录一、基本概念:①字母表:②字母表上的运算:③符号串:④符号串上的运算:二、文法的定义:①文法的形式化定义:②产生式的简写:③符号约定:三、语言的定义࿱…...
赛迪建设网站/推广赚钱平台
十年内可以攻破癌症、糖尿病治愈难题?随着现代医疗技术和信息技术的融合发展,精准医疗的时代已经到来,这为许多特大疾病的治疗提供了新方向。这一领域也引来了国际巨头的关注。近日,英特尔在京推出了“英特尔精准医疗伙伴计划&…...
wordpress term id/苏州网站建设书生商友
系统装的RED HAT LINUX 9 装为服务器类别 只选装了English语言支持 装好之后 用 SSH SECURE SHELL 连接到系统,发现打开有些文档里有乱码 ,而在系统本身却没有,于是修改/etc/systemconfig/i18n这个文件,在最后加入一行LC_ALLPOSIX,重启系统,再也没有乱码了 .转载于:https://bl…...
python 做网站缺点/网站优化推广seo
前言 本篇文章继续我们的微软挖掘系列算法总结,前几篇文章已经将相关的主要算法做了详细的介绍,我为了展示方便,特地的整理了一个目录提纲篇:大数据时代:深入浅出微软数据挖掘算法总结连载, 有兴趣的童鞋可…...
苏州公司网站建设价格/如何优化网站快速排名
单例模式顾名思义就是只含有一个实例,一个实例复用,达到减少创建对象的开销以及大大节省资源的效果,是java23种设计模式最简单应用最多的设计模式;单例模式的实现方法主要分为饿汉式和懒汉式两大类, 饿汉式 public …...
专做药材的网站有哪些/免费b站动漫推广网站2023
1、 IP 地址: 网络之间互连的协议,是由4个字节(32位二进制)组成的逻辑上的地址。 将32位二进制进行分组,分成4组,每组8位(1个字节)。【ip地址通常使用十进制表示】ip地址分成四组之后,在逻辑上,分成网络号和主机号 2…...