微前端无界机制浅析

简介

随着项目的发展,前端SPA应用的规模不断加大、业务代码耦合、编译慢,导致日常的维护难度日益增加。同时前端技术的发展迅猛,导致功能扩展吃力,重构成本高,稳定性低。

为了能够将前端模块解耦,通过相关技术调研,最终选择了无界微前端框架作为物流客服系统解耦支持。为了更好的使用无界微前端框架,我们对其运行机制进行了相关了解,以下是对无界运行机制的一些认识。

基本用法

主应用配置

import WujieVue from 'wujie-vue2';const { setupApp, preloadApp, bus } = WujieVue;
/*设置缓存*/
setupApp({
});
/*预加载*/
preloadApp({name: 'vue2'
})
<WujieVue width="100%" height="100%" name="vue2" :url="vue2Url" :sync="true" :alive="true"></WujieVue

具体实践详细介绍参考:

京东(JD.COM)-正品低价、品质保障、配送及时、轻松购物!

快速上手 | 无界

无界源码解析

1 源码包目录结构

packages 包里包含无界框架核心代码wujie-core和对应不同技术栈应用包

examples 使用案例,main-xxx对应该技术栈主应用的使用案例,其他代表子应用的使用案例

2 wujie-vue2组件

该组件默认配置了相关参数,简化了无界使用时的一些配置项,作为一个全局组件被主引用使用

这里使用wujie-vue2示例,其他wujie-react,wujie-vue3大家可自行阅读,基本作用和wujie-vue2相同都是用来简化无界配置,方便快速使用

import Vue from "vue";
import { bus, preloadApp, startApp as rawStartApp, destroyApp, setupApp } from "wujie";const wujieVueOptions = {name: "WujieVue",props: {/*传入配置参数*/},data() {return {startAppQueue: Promise.resolve(),};},mounted() {bus.$onAll(this.handleEmit);this.execStartApp();},methods: {handleEmit(event, ...args) {this.$emit(event, ...args);},async startApp() {try {// $props 是vue 2.2版本才有的属性,所以这里直接全部写一遍await rawStartApp({name: this.name,url: this.url,el: this.$refs.wujie,loading: this.loading,alive: this.alive,fetch: this.fetch,props: this.props,attrs: this.attrs,replace: this.replace,sync: this.sync,prefix: this.prefix,fiber: this.fiber,degrade: this.degrade,plugins: this.plugins,beforeLoad: this.beforeLoad,beforeMount: this.beforeMount,afterMount: this.afterMount,beforeUnmount: this.beforeUnmount,afterUnmount: this.afterUnmount,activated: this.activated,deactivated: this.deactivated,loadError: this.loadError,});} catch (error) {console.log(error);}},execStartApp() {this.startAppQueue = this.startAppQueue.then(this.startApp);},destroy() {destroyApp(this.name);},},beforeDestroy() {bus.$offAll(this.handleEmit);},render(c) {return c("div", {style: {width: this.width,height: this.height,},ref: "wujie",});},
};const WujieVue = Vue.extend(wujieVueOptions);WujieVue.setupApp = setupApp;
WujieVue.preloadApp = preloadApp;
WujieVue.bus = bus;
WujieVue.destroyApp = destroyApp;
WujieVue.install = function (Vue) {Vue.component("WujieVue", WujieVue);
};
export default WujieVue;

3 入口defineWujieWebComponent和StartApp

首先从入口文件index看起,defineWujieWebComponent

import { defineWujieWebComponent } from "./shadow";
// 定义webComponent容器
defineWujieWebComponent();// 定义webComponent  存在shadow.ts 文件中
export function defineWujieWebComponent() {class WujieApp extends HTMLElement {connectedCallback(){if (this.shadowRoot) return;const shadowRoot = this.attachShadow({ mode: "open" });const sandbox = getWujieById(this.getAttribute(WUJIE_DATA_ID));patchElementEffect(shadowRoot, sandbox.iframe.contentWindow);sandbox.shadowRoot = shadowRoot;}disconnectedCallback() {const sandbox = getWujieById(this.getAttribute(WUJIE_DATA_ID));sandbox?.unmount();}}customElements?.define("wujie-app", WujieApp);
}

startApp方法

startApp(options) {const newSandbox = new WuJie({ name, url, attrs, degradeAttrs, fiber, degrade, plugins, lifecycles });const { template, getExternalScripts, getExternalStyleSheets } = await importHTML({url,html,opts: {fetch: fetch || window.fetch,plugins: newSandbox.plugins,loadError: newSandbox.lifecycles.loadError,fiber,},});const processedHtml = await processCssLoader(newSandbox, template, getExternalStyleSheets);await newSandbox.active({ url, sync, prefix, template: processedHtml, el, props, alive, fetch, replace });await newSandbox.start(getExternalScripts);return newSandbox.destroy;

4 实例化

4-1, wujie (sandbox.ts)
// wujie
class wujie {constructor(options) {/** iframeGenerator在 iframe.ts中**/this.iframe = iframeGenerator(this, attrs, mainHostPath, appHostPath, appRoutePath);if (this.degrade) { // 降级模式const { proxyDocument, proxyLocation } = localGenerator(this.iframe, urlElement, mainHostPath, appHostPath);this.proxyDocument = proxyDocument;this.proxyLocation = proxyLocation;} else {           // 非降级模式const { proxyWindow, proxyDocument, proxyLocation } = proxyGenerator();this.proxy = proxyWindow;this.proxyDocument = proxyDocument;this.proxyLocation = proxyLocation;}this.provide.location = this.proxyLocation;addSandboxCacheWithWujie(this.id, this);}
}

4-2.非降级Proxygenerator

非降级模式window、document、location代理

window代理拦截,修改this指向

export function proxyGenerator(iframe: HTMLIFrameElement,urlElement: HTMLAnchorElement,mainHostPath: string,appHostPath: string
): {proxyWindow: Window;proxyDocument: Object;proxyLocation: Object;
} {const proxyWindow = new Proxy(iframe.contentWindow, {get: (target: Window, p: PropertyKey): any => {// location进行劫持/*xxx*/// 修正this指针指向return getTargetValue(target, p);},set: (target: Window, p: PropertyKey, value: any) => {checkProxyFunction(value);target[p] = value;return true;},/**其他方法属性**/});// proxy documentconst proxyDocument = new Proxy({},{get: function (_fakeDocument, propKey) {const document = window.document;const { shadowRoot, proxyLocation } = iframe.contentWindow.__WUJIE;const rawCreateElement = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__;const rawCreateTextNode = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__;// need fix/* 包括元素创建,元素选择操作等createElement,createTextNode, documentURI,URL,querySelector,querySelectorAlldocumentElement,scrollingElement ,forms,images,links等等*/// from shadowRootif (propKey === "getElementById") {return new Proxy(shadowRoot.querySelector, {// case document.querySelector.callapply(target, ctx, args) {if (ctx !== iframe.contentDocument) {return ctx[propKey]?.apply(ctx, args);}return target.call(shadowRoot, `[id="${args[0]}"]`);},});}},});// proxy locationconst proxyLocation = new Proxy({},{get: function (_fakeLocation, propKey) {const location = iframe.contentWindow.location;if (propKey === "host" || propKey === "hostname" || propKey === "protocol" || propKey === "port" ||propKey === "origin") {return urlElement[propKey];}/** 拦截相关propKey, 返回对应lication内容propKey =="href","reload","replace"**/return getTargetValue(location, propKey);},set: function (_fakeLocation, propKey, value) {// 如果是跳转链接的话重开一个iframeif (propKey === "href") {return locationHrefSet(iframe, value, appHostPath);}iframe.contentWindow.location[propKey] = value;return true;}});return { proxyWindow, proxyDocument, proxyLocation };
}

4-3,降级模式localGenerator

export function localGenerator(
){// 代理 documentObject.defineProperties(proxyDocument, {createElement: {get: () => {return function (...args) {const element = rawCreateElement.apply(iframe.contentDocument, args);patchElementEffect(element, iframe.contentWindow);return element;};},},});// 普通处理const {modifyLocalProperties,modifyProperties,ownerProperties,shadowProperties,shadowMethods,documentProperties,documentMethods,} = documentProxyProperties;modifyProperties.filter((key) => !modifyLocalProperties.includes(key)).concat(ownerProperties, shadowProperties, shadowMethods, documentProperties, documentMethods).forEach((key) => {Object.defineProperty(proxyDocument, key, {get: () => {const value = sandbox.document?.[key];return isCallable(value) ? value.bind(sandbox.document) : value;},});});// 代理 locationconst proxyLocation = {};const location = iframe.contentWindow.location;const locationKeys = Object.keys(location);const constantKey = ["host", "hostname", "port", "protocol", "port"];constantKey.forEach((key) => {proxyLocation[key] = urlElement[key];});Object.defineProperties(proxyLocation, {href: {get: () => location.href.replace(mainHostPath, appHostPath),set: (value) => {locationHrefSet(iframe, value, appHostPath);},},reload: {get() {warn(WUJIE_TIPS_RELOAD_DISABLED);return () => null;},},});return { proxyDocument, proxyLocation };
}

实例化化主要是建立起js运行时的沙箱iframe, 通过非降级模式下proxy和降级模式下对document,location,window等全局操作属性的拦截修改将其和对应的js沙箱操作关联起来

5 importHTML入口文件解析

importHtml方法(entry.ts)

export default function importHTML(params: {url: string;html?: string;opts: ImportEntryOpts;
}): Promise<htmlParseResult> {/*xxxx*/const getHtmlParseResult = (url, html, htmlLoader) =>(html? Promise.resolve(html): fetch(url).then( /** 使用fetch Api 加载子应用入口**/(response) => response.text(),(e) => {embedHTMLCache[url] = null;loadError?.(url, e);return Promise.reject(e);})).then((html) => {const assetPublicPath = getPublicPath(url);const { template, scripts, styles } = processTpl(htmlLoader(html), assetPublicPath);return {template: template,assetPublicPath,getExternalScripts: () =>getExternalScripts(scripts.filter((script) => !script.src || !isMatchUrl(script.src, jsExcludes)).map((script) => ({ ...script, ignore: script.src && isMatchUrl(script.src, jsIgnores) })),fetch,loadError,fiber),getExternalStyleSheets: () =>getExternalStyleSheets(styles.filter((style) => !style.src || !isMatchUrl(style.src, cssExcludes)).map((style) => ({ ...style, ignore: style.src && isMatchUrl(style.src, cssIgnores) })),fetch,loadError),};});if (opts?.plugins.some((plugin) => plugin.htmlLoader)) {return getHtmlParseResult(url, html, htmlLoader);// 没有html-loader可以做缓存} else {return embedHTMLCache[url] || (embedHTMLCache[url] = getHtmlParseResult(url, html, htmlLoader));}
}

importHTML结构如图:

注意点: 通过Fetch url加载子应用资源,这里也是需要子应用支持跨域设置的原因

6 CssLoader和样式加载优化

export async function processCssLoader(sandbox: Wujie,template: string,getExternalStyleSheets: () => StyleResultList
): Promise<string> {const curUrl = getCurUrl(sandbox.proxyLocation);/** css-loader */const composeCssLoader = compose(sandbox.plugins.map((plugin) => plugin.cssLoader));const processedCssList: StyleResultList = getExternalStyleSheets().map(({ src, ignore, contentPromise }) => ({src,ignore,contentPromise: contentPromise.then((content) => composeCssLoader(content, src, curUrl)),}));const embedHTML = await getEmbedHTML(template, processedCssList);return sandbox.replace ? sandbox.replace(embedHTML) : embedHTML;
}

7 子应用active

active方法主要用于做 子应用激活, 同步路由,动态修改iframe的fetch, 准备shadow, 准备子应用注入

7-1, active方法(sandbox.ts)

public async active(options){/**  options的检查  **/// 处理子应用自定义fetch// TODO fetch检验合法性const iframeWindow = this.iframe.contentWindow;iframeWindow.fetch = iframeFetch;this.fetch = iframeFetch;// 处理子应用路由同步if (this.execFlag && this.alive) {// 当保活模式下子应用重新激活时,只需要将子应用路径同步回主应用syncUrlToWindow(iframeWindow);} else {// 先将url同步回iframe,然后再同步回浏览器urlsyncUrlToIframe(iframeWindow);syncUrlToWindow(iframeWindow);}// inject templatethis.template = template ?? this.template;/* 降级处理 */if (this.degrade) {return;}if (this.shadowRoot) {this.el = renderElementToContainer(this.shadowRoot.host, el);if (this.alive) return;} else {// 预执行无容器,暂时插入iframe内部触发Web Component的connect// rawDocumentQuerySelector.call(iframeWindow.document, "body") 相当于Document.prototype.querySelector('body')const iframeBody = rawDocumentQuerySelector.call(iframeWindow.document, "body") as HTMLElement;this.el = renderElementToContainer(createWujieWebComponent(this.id), el ?? iframeBody);}await renderTemplateToShadowRoot(this.shadowRoot, iframeWindow, this.template);this.patchCssRules();// inject shadowRoot to appthis.provide.shadowRoot = this.shadowRoot;}

7-2,createWujieWebComponent, renderElementToContainer, renderTemplateToShadowRoot

// createWujieWebComponent
export function createWujieWebComponent(id: string): HTMLElement {const contentElement = window.document.createElement("wujie-app");contentElement.setAttribute(WUJIE_DATA_ID, id);contentElement.classList.add(WUJIE_IFRAME_CLASS);return contentElement;
}/*** 将准备好的内容插入容器*/
export function renderElementToContainer(element: Element | ChildNode,selectorOrElement: string | HTMLElement
): HTMLElement {const container = getContainer(selectorOrElement);if (container && !container.contains(element)) {// 有 loading 无需清理,已经清理过了if (!container.querySelector(`div[${LOADING_DATA_FLAG}]`)) {// 清除内容clearChild(container);}// 插入元素if (element) {//  rawElementAppendChild = HTMLElement.prototype.appendChild;rawElementAppendChild.call(container, element);}}return container;
}
/*** 将template渲染到shadowRoot*/
export async function renderTemplateToShadowRoot(shadowRoot: ShadowRoot,iframeWindow: Window,template: string
): Promise<void> {const html = renderTemplateToHtml(iframeWindow, template);// 处理 css-before-loader 和 css-after-loaderconst processedHtml = await processCssLoaderForTemplate(iframeWindow.__WUJIE, html);// change ownerDocumentshadowRoot.appendChild(processedHtml);const shade = document.createElement("div");shade.setAttribute("style", WUJIE_SHADE_STYLE);processedHtml.insertBefore(shade, processedHtml.firstChild);shadowRoot.head = shadowRoot.querySelector("head");shadowRoot.body = shadowRoot.querySelector("body");// 修复 html parentNodeObject.defineProperty(shadowRoot.firstChild, "parentNode", {enumerable: true,configurable: true,get: () => iframeWindow.document,});patchRenderEffect(shadowRoot, iframeWindow.__WUJIE.id, false);
}
/*** 将template渲染成html元素*/
function renderTemplateToHtml(iframeWindow: Window, template: string): HTMLHtmlElement {const sandbox = iframeWindow.__WUJIE;const { head, body, alive, execFlag } = sandbox;const document = iframeWindow.document;let html = document.createElement("html");html.innerHTML = template;// 组件多次渲染,head和body必须一直使用同一个来应对被缓存的场景if (!alive && execFlag) {html = replaceHeadAndBody(html, head, body);} else {sandbox.head = html.querySelector("head");sandbox.body = html.querySelector("body");}const ElementIterator = document.createTreeWalker(html, NodeFilter.SHOW_ELEMENT, null, false);let nextElement = ElementIterator.currentNode as HTMLElement;while (nextElement) {patchElementEffect(nextElement, iframeWindow);const relativeAttr = relativeElementTagAttrMap[nextElement.tagName];const url = nextElement[relativeAttr];if (relativeAttr) nextElement.setAttribute(relativeAttr, getAbsolutePath(url, nextElement.baseURI || ""));nextElement = ElementIterator.nextNode() as HTMLElement;}if (!html.querySelector("head")) {const head = document.createElement("head");html.appendChild(head);}if (!html.querySelector("body")) {const body = document.createElement("body");html.appendChild(body);}return html;
}
/*
// 保存原型方法
// 子应用的Document.prototype已经被改写了
export const rawElementAppendChild = HTMLElement.prototype.appendChild;
export const rawElementRemoveChild = HTMLElement.prototype.removeChild;
export const rawHeadInsertBefore = HTMLHeadElement.prototype.insertBefore;
export const rawBodyInsertBefore = HTMLBodyElement.prototype.insertBefore;
export const rawAddEventListener = Node.prototype.addEventListener;
export const rawRemoveEventListener = Node.prototype.removeEventListener;
export const rawWindowAddEventListener = window.addEventListener;
export const rawWindowRemoveEventListener = window.removeEventListener;
export const rawAppendChild = Node.prototype.appendChild;
export const rawDocumentQuerySelector = window.__POWERED_BY_WUJIE__? window.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__: Document.prototype.querySelector;
*/

8 子应用启动执行start

start 开始执行子应用,运行js,执行无界js插件列表

public async start(getExternalScripts: () => ScriptResultList): Promise<void> {this.execFlag = true;// 执行脚本const scriptResultList = await getExternalScripts();const iframeWindow = this.iframe.contentWindow;// 标志位,执行代码前设置iframeWindow.__POWERED_BY_WUJIE__ = true;// 用户自定义代码前 const beforeScriptResultList: ScriptObjectLoader[] = getPresetLoaders("jsBeforeLoaders", this.plugins);// 用户自定义代码后const afterScriptResultList: ScriptObjectLoader[] = getPresetLoaders("jsAfterLoaders", this.plugins);// 同步代码const syncScriptResultList: ScriptResultList = [];// async代码无需保证顺序,所以不用放入执行队列const asyncScriptResultList: ScriptResultList = [];// defer代码需要保证顺序并且DOMContentLoaded前完成,这里统一放置同步脚本后执行const deferScriptResultList: ScriptResultList = [];scriptResultList.forEach((scriptResult) => {if (scriptResult.defer) deferScriptResultList.push(scriptResult);else if (scriptResult.async) asyncScriptResultList.push(scriptResult);else syncScriptResultList.push(scriptResult);});// 插入代码前beforeScriptResultList.forEach((beforeScriptResult) => {this.execQueue.push(() =>this.fiber? requestIdleCallback(() => insertScriptToIframe(beforeScriptResult, iframeWindow)): insertScriptToIframe(beforeScriptResult, iframeWindow));});// 同步代码syncScriptResultList.concat(deferScriptResultList).forEach((scriptResult) => {/**xxxxx**/});// 异步代码asyncScriptResultList.forEach((scriptResult) => {scriptResult.contentPromise.then((content) => {this.fiber? requestIdleCallback(() => insertScriptToIframe({ ...scriptResult, content }, iframeWindow)): insertScriptToIframe({ ...scriptResult, content }, iframeWindow);});});//框架主动调用mount方法this.execQueue.push(this.fiber ? () => requestIdleCallback(() => this.mount()) : () => this.mount());//触发 DOMContentLoaded 事件const domContentLoadedTrigger = () => {eventTrigger(iframeWindow.document, "DOMContentLoaded");eventTrigger(iframeWindow, "DOMContentLoaded");this.execQueue.shift()?.();};this.execQueue.push(this.fiber ? () => requestIdleCallback(domContentLoadedTrigger) : domContentLoadedTrigger);// 插入代码后afterScriptResultList.forEach((afterScriptResult) => {/**xxxxx**/});//触发 loaded 事件const domLoadedTrigger = () => {eventTrigger(iframeWindow.document, "readystatechange");eventTrigger(iframeWindow, "load");this.execQueue.shift()?.();};this.execQueue.push(this.fiber ? () => requestIdleCallback(domLoadedTrigger) : domLoadedTrigger);// 由于没有办法准确定位是哪个代码做了mount,保活、重建模式提前关闭loadingif (this.alive || !isFunction(this.iframe.contentWindow.__WUJIE_UNMOUNT)) removeLoading(this.el);this.execQueue.shift()();// 所有的execQueue队列执行完毕,start才算结束,保证串行的执行子应用return new Promise((resolve) => {this.execQueue.push(() => {resolve();this.execQueue.shift()?.();});});}

// getExternalScripts
export function getExternalScripts(scripts: ScriptObject[],fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response> = defaultFetch,loadError: loadErrorHandler,fiber: boolean
): ScriptResultList {// module should be requested in iframereturn scripts.map((script) => {const { src, async, defer, module, ignore } = script;let contentPromise = null;// asyncif ((async || defer) && src && !module) {contentPromise = new Promise((resolve, reject) =>fiber? requestIdleCallback(() => fetchAssets(src, scriptCache, fetch, false, loadError).then(resolve, reject)): fetchAssets(src, scriptCache, fetch, false, loadError).then(resolve, reject));// module || ignore} else if ((module && src) || ignore) {contentPromise = Promise.resolve("");// inline} else if (!src) {contentPromise = Promise.resolve(script.content);// outline} else {contentPromise = fetchAssets(src, scriptCache, fetch, false, loadError);}return { ...script, contentPromise };});
}// 加载assets资源 
// 如果存在缓存则从缓存中获取
const fetchAssets = (src: string,cache: Object,fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response>,cssFlag?: boolean,loadError?: loadErrorHandler
) =>cache[src] ||(cache[src] = fetch(src).then((response) => {/**status > 400按error处理**/return response.text();})}));// insertScriptToIframe
export function insertScriptToIframe(scriptResult: ScriptObject | ScriptObjectLoader,iframeWindow: Window,rawElement?: HTMLScriptElement
) {const { src, module, content, crossorigin, crossoriginType, async, callback, onload } =scriptResult as ScriptObjectLoader;const scriptElement = iframeWindow.document.createElement("script");const nextScriptElement = iframeWindow.document.createElement("script");const { replace, plugins, proxyLocation } = iframeWindow.__WUJIE;const jsLoader = getJsLoader({ plugins, replace });let code = jsLoader(content, src, getCurUrl(proxyLocation));// 内联脚本处理if (content) {// patch locationif (!iframeWindow.__WUJIE.degrade && !module) {code = `(function(window, self, global, location) {${code}
}).bind(window.__WUJIE.proxy)(window.__WUJIE.proxy,window.__WUJIE.proxy,window.__WUJIE.proxy,window.__WUJIE.proxyLocation,
);`;}} else {// 外联自动触发onloadonload && (scriptElement.onload = onload as (this: GlobalEventHandlers, ev: Event) => any);src && scriptElement.setAttribute("src", src);crossorigin && scriptElement.setAttribute("crossorigin", crossoriginType);}// esm 模块加载module && scriptElement.setAttribute("type", "module");scriptElement.textContent = code || "";// 执行script队列检测nextScriptElement.textContent ="if(window.__WUJIE.execQueue && window.__WUJIE.execQueue.length){ window.__WUJIE.execQueue.shift()()}";const container = rawDocumentQuerySelector.call(iframeWindow.document, "head");if (/^<!DOCTYPE html/i.test(code)) {error(WUJIE_TIPS_SCRIPT_ERROR_REQUESTED, scriptResult);return !async && container.appendChild(nextScriptElement);}container.appendChild(scriptElement);// 调用回调callback?.(iframeWindow);// 执行 hooksexecHooks(plugins, "appendOrInsertElementHook", scriptElement, iframeWindow, rawElement);// 外联转内联调用手动触发onloadcontent && onload?.();// async脚本不在执行队列,无需next操作!async && container.appendChild(nextScriptElement);
}

9 子应用销毁

/** 销毁子应用 */public destroy() {this.bus.$clear();// thi.xxx = null;// 清除 domif (this.el) {clearChild(this.el);this.el = null;}// 清除 iframe 沙箱if (this.iframe) {this.iframe.parentNode?.removeChild(this.iframe);}// 删除缓存deleteWujieById(this.id);}

主应用,无界,子应用之间的关系

主应用创建自定义元素和创建iframe元素

无界将子应用解析后的html,css加入到自定义元素,进行元素和样式隔离

同时建立iframe代理,将iframe和自定义元素shadowDom进行关联,

将子应用中的js放入iframe执行,iframe中js执行的结果被代理到修改shadowDom结构和数据

文章转载自:京东云技术团队

原文链接:https://www.cnblogs.com/jingdongkeji/p/17812320.html

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/628059.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用Qt连接scrcpy-server控制手机

Qt连接scrcpy-server 测试环境如何启动scrcpy-server1. 连接设备2. 推送scrcpy-server到手机上3. 建立Adb隧道连接4. 启动服务5. 关闭服务 使用QTcpServer与scrcpy-server建立连接建立连接并视频推流完整流程1. 开启视频推流过程2. 关闭视频推流过程 视频流的解码1. 数据包协议…

python每日学17:控制推导逻辑的子表达式不要超过两个

背景&#xff1a;今天放假在家&#xff0c;《python学习手册》不在身边&#xff0c;所以今天学习《Effective Python: 编写高质量Python代码的90个有效方法》第28条《控制推导逻辑的子表达式不要超过两个》&#xff0c;这本书已经是第二版了&#xff0c;第一版是《编写高质量py…

shell脚本,ADB

Linux命令行命令是系统内置的命令或用户自定义的脚本&#xff08;shell 脚本&#xff0c;.sh扩展名结尾&#xff09;&#xff0c;可以通过终端输入命令来执行。这些命令通常存储在Linux系统的/bin、/usr/bin、/sbin、/usr/sbin等目录下&#xff0c;也可以在$PATH环境变量中指定…

NVMe系统内存结构 - Meta Data

NVMe系统内存结构 - Meta Data 1 为什么需要数据保护2 Meta Data定义3 Meta Data传输方式4 常见Meta Data使用场景4.1 不带数据保护信息4.2 带数据保护信息“数据写”流程4.3 带数据保护信息“数据读”流程4.4 SSD内部加入数据保护信息4.5 SSD内部根据数据保护信息验证数据 本文…

如何在你的网站接入QQ登录?

文章目录 准备阶段申请QQ登录的权限创建应用最后上传qqlogin.php代码 准备阶段 国内服务器和备案域名需要你有张独一无二本人的身份证你正面手持身份证的图片一张100px*100px的网站图标 申请QQ登录的权限 首先访问qq互联&#xff0c;点击我直接访问 登陆完成后我们点击面的…

bash shell基础命令(一)

1.shell启动 shell提供了对Linux系统的交互式访问&#xff0c;通常在用户登录终端时启动。系统启动的shell程序取决于用户账户的配置。 /etc/passwd/文件包含了所有用户的基本信息配置&#xff0c; $ cat /etc/passwd root:x:0:0:root:/root:/bin/bash ...例如上述root账户信…

Python新年文字烟花简单代码

简单的Python新年烟花代码示例&#xff1a; import random import timedef create_firework():colors [红色, 橙色, 黄色, 绿色, 蓝色, 紫色]flashes [爆裂, 闪光, 旋转, 流星, 喷射]color random.choice(colors)flash random.choice(flashes)print(f"发射一枚{color…

redis之单线程和多线程

目录 1、redis的发展史 2、redis为什么选择单线程&#xff1f; 3、主线程和Io线程是怎么协作完成请求处理的&#xff1f; 4、IO多路复用 5、开启redis多线程 1、redis的发展史 Redis4.0之前是用的单线程&#xff0c;4.0以后逐渐支持多线程 Redis4.0之前一直采用单线程的主…

GUI编程(函数解析以及使用)

1.介绍 AWT&#xff08;Abstract Window Toolkit&#xff09;和Swing 是 Java 提供的用于创建图形用户界面&#xff08;GUI&#xff09;的类库。 AWT&#xff1a;AWT 是 Java 最早提供的 GUI 类库&#xff0c;它基于本地平台的窗口系统&#xff0c;使用操作系统的原生组件进行…

17.蒙特卡洛强化学习之批量式策略评估

文章目录 1. 是什么2. 为什么直接估计Q而不是V2. 根据多条完整轨迹计算Q(s,a)3. 初访法和每访法哪个更好&#xff1f; 1. 是什么 智能体利用完整轨迹数据估计出 Q π ( s , a ) Q_\pi(s,a) Qπ​(s,a)的过程 2. 为什么直接估计Q而不是V 因为策略评估的目的在于改进现有策略 …

Springboot中时间格式化

时间格式化方式 JsonFromat方式全局配置方式格式化工具方式 JsonFromat方式 前端传参或后端响应 yyyy-MM-dd HH:mm:ss 格式&#xff0c;直接属性字段上加注解 JsonFromat JsonFromat(pattern "yyyy-MM-dd HH:mm:ss", timezone "GMT8") private Date fi…

文件的创建时间可以修改吗,怎么改?

文件的创建时间可以修改吗&#xff0c;怎么改&#xff1f;文件的创建时间是由操作系统自动生成并记录的&#xff0c;通常情况下无法直接修改。创建时间是文件的属性之一&#xff0c;它反映了文件在文件系统中的生成时间。一旦文件被创建&#xff0c;其创建时间就被确定下来&…

pod配置资源管理

secret 保存密码&#xff0c;token&#xff0c;敏感的k8s资源 configmap(重点) 1.2加入的新特征 这类数据可以存放在镜像当中&#xff0c;但是放在secret当中可以更方便的控制&#xff0c;减少暴露的风险&#xff0c;保存加密的信息 secret的类型 docker-registry 存储docke…

Vulnhub-tr0ll-1

一、信息收集 端口收集 PORT STATE SERVICE VERSION 21/tcp open ftp vsftpd 3.0.2 | ftp-anon: Anonymous FTP login allowed (FTP code 230) |_-rwxrwxrwx 1 1000 0 8068 Aug 09 2014 lol.pcap [NSE: writeable] | ftp-syst: | STAT: | FTP …

Java使用Quartz做定时任务并持久化

一、导包 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId></dependency> 二、创建定时任务 创建一个定时任务如下&#xff0c;需要传入定时任务执行的class、定时任务名字…

【PostgreSQL】函数与操作符-比较函数和操作符

PostgreSQL函数与操作符-比较函数和操作符 PostgreSQL中的比较函数和操作符 PostgreSQL中的比较函数和操作符用于比较两个表达式的值。它们的作用是确定表达式的值是否满足某种条件&#xff0c;例如等于、大于、小于等。 比较函数是一个接受两个参数的函数&#xff0c;返回一…

leetcode-2719统计证书数目

题目链接 2719. 统计整数数目 - 力扣&#xff08;LeetCode&#xff09; 解题思路 题目实际上求的是[num1,...num2]中&#xff0c;数位和在[min_sum,...max_sum]的数的个数。对于这种区间[l,...r]的问题&#xff0c;我们可以考虑转化为求[1,...r]和[1,...l-1]的答案&#xff…

分布式搜索——Elasticsearch

Elasticsearch 文章目录 Elasticsearch简介ELK技术栈Elasticsearch和Lucene 倒排索引正向索引倒排索引正向和倒排 ES概念文档和字段索引和映射Mysql与Elasticsearch 安装ES、Kibana安装单点ES创建网络拉取镜像运行 部署kibana拉取镜像部署 安装Ik插件扩展词词典停用词词典 索引…

Linux 内核大转变:是否将迈入现代 C++ 的时代?

Linux开发者 H. Peter Anvin 在邮件列表中重启了关于 Linux内核C代码转换为C的讨论&#xff0c;并陈述了自己的观点。说之前先看一下这个话题的历史背景。 早在2018年4月1日&#xff0c;Andrew Pinski提议将 Linux 内核源码转为 C&#xff0c;在文中写道之所以引入是由于以下优…

centos7配置时间同步网络时间

centos7配置时间同步网络时间 1、安装 NTP 工具。 sudo yum install -y ntp2启动 NTP 服务。 sudo systemctl start ntpd3、将 NTP 服务设置为开机自启动。 sudo systemctl enable ntpd4、验证 date