源码分析 @angular/cdk 之 Portal
@angular/material 是 Angular 官方根据 Material Design 设计语言提供的 UI 库,开发人员在开发 UI 库时发现很多 UI 组件有着共同的逻辑,所以他们把这些共同逻辑抽出来单独做一个包 @angular/cdk,这个包与 Material Design 设计语言无关,可以被任何人按照其他设计语言构建其他风格的 UI 库。学习 @angular/material 或 @angular/cdk 这些包的源码,主要是为了学习大牛们是如何高效使用 TypeScript 语言的;学习他们如何把 RxJS 这个包使用的这么出神入化;最主要是为了学习他们是怎么应用 Angular 框架提供的技术。只有深入研究这些大牛们写的代码,才能更快提高自己的代码质量,这是一件事半功倍的事情。
Portal 是什么
最近在学习 React 时,发现 React 提供了 Portals 技术,该技术主要用来把子节点动态的显示到父节点外的 DOM 节点上,该技术的一个经典用例应该就是 Dialog 了。设想一下在设计 Dialog 时所需要的主要功能点:当点击一个 button 时,一般需要在 body 标签前动态挂载一个组件视图;该 dialog 组件视图需要共享数据。由此看出,Portal 核心就是在任意一个 DOM 节点内动态生成一个视图,该 视图却可以置于框架上下文环境之外。那 Angular 中有没有类似相关技术来解决这个问题呢?
Angular Portal 就是用来在任意一个 DOM 节点内动态生成一个视图,该视图既可以是一个组件视图,也可以是一个模板视图,并且生成的视图可以挂载在任意一个 DOM 节点,甚至该节点可以置于 Angular 上下文环境之外,也同样可以与该视图共享数据。该 Portal 技术主要就涉及两个简单对象:PortalOutlet 和 Portal。从字面意思就可知道,PortalOutlet 应该就是把某一个 DOM 节点包装成一个挂载容器供 Portal 来挂载,等同于 插头-插线板 模式的 插线板;Portal 应该就是把组件视图或者模板视图包装成一个 Portal 挂载到 PortalOutlet 上,等同于 插头-插线板 模式的 插头。这与 @angular/router 中 Router 和 RouterOutlet 设计思想很类似,在写路由时,router-outlet 就是个挂载点,Angular 会把由 Router 包装的组件挂载到 router-outlet 上,所以这个设计思想不是个新东西。
如何使用 Portal
Portal<T> 只是一个抽象泛型类,而 ComponentPortal<T> 和 TemplatePortal<T> 才是包装组件或模板对应的 Portal 具体类,查看两个类的构造函数的主要依赖,都基本是依赖于:该组件或模板对象;视图容器即挂载点,是通过 ViewContainerRef 包装的对象;如果是组件视图还得依赖 injector,模板视图得依赖 context 变量。这些依赖对象也进一步暴露了其设计思想。
抽象类 BasePortalOutlet 是 PortalOutlet 的基本实现,同时包含了三个重要方法:attach 表示把 Portal 挂载到 PortalOutlet 上,并定义了两个抽象方法,来具体实现挂载组件视图还是模板视图:
abstract attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;
abstract attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;
detach 表示从 PortalOutlet 中拆卸出该 Portal,而 PortalOutlet 中可以挂载多个 Portal,dispose 表示整体并永久销毁 PortalOutlet。其中,还有一个重要类 DomPortalOutlet 是 BasePortalOutlet 的子类,可以在 Angular 上下文之外 创建一个 PortalOutlet,并把 Portal 挂载到该 PortalOutlet 上,比如将 body 最后子元素 div 包装为一个 PortalOutlet,然后将组件视图或模板视图挂载到该挂载点上。这里的的难点就是如果该挂载点在 Angular 上下文之外,那挂载点内的 Portal 如何与 Angular 上下文内的组件共享数据。 DomPortalOutlet 还实现了上面的两个抽象方法:attachComponentPortal 和 attachTemplatePortal,如果对代码细节感兴趣可接着看下文。
现在已经知道了 @angular/cdk/portal 中最重要的两个核心,即 Portal 和 PortalOutlet,接下来写一个 demo 看看如何使用 Portal 和 PortalOutlet 来在 Angular 上下文之外 创建一个 ComponentPortal 和 TemplatePortal。
Demo 关键功能包括:在 Angular 上下文内 挂载 TemplatePortal/ComponentPortal;在 Angular 上下文外 挂载 TemplatePortal/ComponentPortal;在 Angular 上下文外 共享数据。接下来让我们逐一实现每个功能点。
Angular 上下文内挂载 Portal
在 Angular 上下文内挂载 Portal 比较简单,首先需要做的第一步就是实例化出一个挂载容器 PortalOutlet,可以通过实例化 DomPortalOutlet 得到该挂载容器。查看 DomPortalOutlet 的构造依赖主要包括:挂载的元素节点 Element,可以通过 @ViewChild DOM 查询得到该组件内的某一个 DOM 元素;组件工厂解析器 ComponentFactoryResolver,可以通过当前组件构造注入拿到,该解析器是为了当 Portal 是 ComponentPortal 时解析出对应的 Component;当前程序对象 ApplicationRef,主要用来挂载组件视图;注入器 Injector,这个很重要,如果是在 Angular 上下文外挂载组件视图,可以用 Injector 来和组件视图共享数据。
第二步就是使用 ComponentPortal 和 TemplatePortal 包装对应的组件和模板,需要留意的是 TemplatePortal 还必须依赖 ViewContainerRef 对象来调用 createEmbeddedView() 来创建嵌入视图。
第三步就是调用 PortalOutlet 的 attach() 方法挂载 Portal,进而根据 Portal 是 ComponentPortal 还是 TemplatePortal 分别调用 attachComponentPortal() 和 attachTemplatePortal() 方法。
通过以上三步,就可以知道该如何设计代码:
@Component({selector: 'portal-dialog',template: `<p>Component Portal<p>`
})
export class DialogComponent {}@Component({selector: 'app-root',template: `<h2>Open a ComponentPortal Inside Angular Context</h2><button (click)="openComponentPortalInsideAngularContext()">Open a ComponentPortal Inside Angular Context</button><div #_openComponentPortalInsideAngularContext></div><h2>Open a TemplatePortal Inside Angular Context</h2><button (click)="openTemplatePortalInsideAngularContext()">Open a TemplatePortal Inside Angular Context</button><div #_openTemplatePortalInsideAngularContext></div><ng-template #_templatePortalInsideAngularContext><p>Template Portal Inside Angular Context</p></ng-template>`,
})
export class AppComponent {private _appRef: ApplicationRef;constructor(private _componentFactoryResolver: ComponentFactoryResolver,private _injector: Injector,@Inject(DOCUMENT) private _document) {}@ViewChild('_openComponentPortalInsideAngularContext', {read: ViewContainerRef}) _openComponentPortalInsideAngularContext: ViewContainerRef;openComponentPortalInsideAngularContext() {if (!this._appRef) {this._appRef = this._injector.get(ApplicationRef);}// instantiate a DomPortalOutletconst portalOutlet = new DomPortalOutlet(this._openComponentPortalInsideAngularContext.element.nativeElement, this._componentFactoryResolver, this._appRef, this._injector);// instantiate a ComponentPortal<DialogComponent>const componentPortal = new ComponentPortal(DialogComponent);// attach a ComponentPortal to a DomPortalOutletportalOutlet.attach(componentPortal);}@ViewChild('_templatePortalInsideAngularContext', {read: TemplateRef}) _templatePortalInsideAngularContext: TemplateRef<any>;@ViewChild('_openTemplatePortalInsideAngularContext', {read: ViewContainerRef}) _openTemplatePortalInsideAngularContext: ViewContainerRef;openTemplatePortalInsideAngularContext() {if (!this._appRef) {this._appRef = this._injector.get(ApplicationRef);}// instantiate a DomPortalOutletconst portalOutlet = new DomPortalOutlet(this._openTemplatePortalInsideAngularContext.element.nativeElement, this._componentFactoryResolver, this._appRef, this._injector);// instantiate a TemplatePortal<>const templatePortal = new TemplatePortal(this._templatePortalInsideAngularContext, this._openTemplatePortalInsideAngularContext);// attach a TemplatePortal to a DomPortalOutletportalOutlet.attach(templatePortal);}
}
查阅上面设计的代码,发现没有什么太多新的东西。通过 @ViewChild DOM 查询到模板对象和视图容器对象,注意该装饰器的第二个参数 {read:},用来指定具体查询哪种标识如 TemplateRef 还是 ViewContainerRef。当然,最重要的技术点还是 attach() 方法的实现,该方法的源码解析可以接着看下文。
完整代码可见 demo。
Angular 上下文外挂载 Portal
从上文可知道,如果想要把 Portal 挂载到 Angular 上下文外,关键是 PortalOutlet 的依赖 outletElement 得处于 Angular 上下文之外。这个 HTMLElement 可以通过 _document.body.appendChild(element) 来手动创建:
let container = this._document.createElement('div');
container.classList.add('component-portal');
container = this._document.body.appendChild(container);
有了处于 Angular 上下文之外的一个 Element,后面的设计步骤就和上文完全一样:实例化一个处于 Angular 上下文之外的 PortalOutlet,然后挂载 ComponentPortal 和 TemplatePortal:
@Component({selector: 'app-root',template: `<h2>Open a ComponentPortal Outside Angular Context</h2><button (click)="openComponentPortalOutSideAngularContext()">Open a ComponentPortal Outside Angular Context</button><h2>Open a TemplatePortal Outside Angular Context</h2><button (click)="openTemplatePortalOutSideAngularContext()">Open a TemplatePortal Outside Angular Context</button><ng-template #_templatePortalOutsideAngularContext><p>Template Portal Outside Angular Context</p></ng-template>`,
})
export class AppComponent {...openComponentPortalOutSideAngularContext() {let container = this._document.createElement('div');container.classList.add('component-portal');container = this._document.body.appendChild(container);if (!this._appRef) {this._appRef = this._injector.get(ApplicationRef);}// instantiate a DomPortalOutletconst portalOutlet = new DomPortalOutlet(container, this._componentFactoryResolver, this._appRef, this._injector);// instantiate a ComponentPortal<DialogComponent>const componentPortal = new ComponentPortal(DialogComponent);// attach a ComponentPortal to a DomPortalOutletportalOutlet.attach(componentPortal);
}@ViewChild('_templatePortalOutsideAngularContext', {read: TemplateRef}) _template: TemplateRef<any>;
@ViewChild('_templatePortalOutsideAngularContext', {read: ViewContainerRef}) _viewContainerRef: ViewContainerRef;
openTemplatePortalOutSideAngularContext() {let container = this._document.createElement('div');container.classList.add('template-portal');container = this._document.body.appendChild(container);if (!this._appRef) {this._appRef = this._injector.get(ApplicationRef);}// instantiate a DomPortalOutletconst portalOutlet = new DomPortalOutlet(container, this._componentFactoryResolver, this._appRef, this._injector);// instantiate a TemplatePortal<>const templatePortal = new TemplatePortal(this._template, this._viewContainerRef);// attach a TemplatePortal to a DomPortalOutletportalOutlet.attach(templatePortal);
}...
通过上面代码,就可以在 Angular 上下文之外创建一个视图,这个技术对创建 Dialog 会非常有用。
完整代码可见 demo。
Angular 上下文外共享数据
最难点还是如何与处于 Angular 上下文外的 Portal 共享数据,这个问题需要根据 ComponentPortal 还是 TemplatePortal 分别处理。其中,如果是 TemplatePortal,解决方法却很简单,注意观察 TemplatePortal 的构造依赖,发现存在第三个可选参数 context,难道是用来向 TemplatePortal 里传送共享数据的?没错,的确如此。可以查看 DomPortalOutlet.attachTemplatePortal() 的 75 行,就是把 portal.context 传给组件视图内作为共享数据使用,既然如此,TemplatePortal 共享数据问题就很好解决了:
@Component({selector: 'app-root',template: `<h2>Open a TemplatePortal Outside Angular Context with Sharing Data</h2><button (click)="openTemplatePortalOutSideAngularContextWithSharingData()">Open a TemplatePortal Outside Angular Context with Sharing Data</button><input [value]="sharingTemplateData" (change)="setTemplateSharingData($event.target.value)"/><ng-template #_templatePortalOutsideAngularContextWithSharingData let-name="name"><p>Template Portal Outside Angular Context, the Sharing Data is {{name}}</p></ng-template>`,
})
export class AppComponent {
sharingTemplateData: string = 'lx1035';
@ViewChild('_templatePortalOutsideAngularContextWithSharingData', {read: TemplateRef}) _templateWithSharingData: TemplateRef<any>;
@ViewChild('_templatePortalOutsideAngularContextWithSharingData', {read: ViewContainerRef}) _viewContainerRefWithSharingData: ViewContainerRef;
setTemplateSharingData(value) {this.sharingTemplateData = value;
}
openTemplatePortalOutSideAngularContextWithSharingData() {let container = this._document.createElement('div');container.classList.add('template-portal-with-sharing-data');container = this._document.body.appendChild(container);if (!this._appRef) {this._appRef = this._injector.get(ApplicationRef);}// instantiate a DomPortalOutletconst portalOutlet = new DomPortalOutlet(container, this._componentFactoryResolver, this._appRef, this._injector);// instantiate a TemplatePortal<DialogComponentWithSharingData>const templatePortal = new TemplatePortal(this._templateWithSharingData, this._viewContainerRefWithSharingData, {name: this.sharingTemplateData}); // <--- key point// attach a TemplatePortal to a DomPortalOutletportalOutlet.attach(templatePortal);
}...
那 ComponentPortal 呢?查看 ComponentPortal 的第三个构造依赖 Injector,它依赖的是注入器。TemplatePortal 的第三个参数 context 解决了共享数据问题,那 ComponentPortal 可不可以通过第三个参数注入器解决共享数据问题?没错,完全可以。可以构造一个自定义的 Injector,把共享数据存储到 Injector 里,然后 ComponentPortal 从 Injector 中取出该共享数据。查看 Portal 的源码包,官方还很人性的提供了一个 PortalInjector 类供开发者实例化一个自定义注入器。现在思路已经有了,看看代码具体实现:
let DATA = new InjectionToken<any>('Sharing Data with Component Portal');@Component({selector: 'portal-dialog-sharing-data',template: `<p>Component Portal Sharing Data is: {{data}}<p>`
})
export class DialogComponentWithSharingData {constructor(@Inject(DATA) public data: any) {} // <--- key point
}@Component({selector: 'app-root',template: `<h2>Open a ComponentPortal Outside Angular Context with Sharing Data</h2><button (click)="openComponentPortalOutSideAngularContextWithSharingData()">Open a ComponentPortal Outside Angular Context with Sharing Data</button><input [value]="sharingComponentData" (change)="setComponentSharingData($event.target.value)"/>`,
})
export class AppComponent {...sharingComponentData: string = 'lx1036';
setComponentSharingData(value) {this.sharingComponentData = value;
}
openComponentPortalOutSideAngularContextWithSharingData() {let container = this._document.createElement('div');container.classList.add('component-portal-with-sharing-data');container = this._document.body.appendChild(container);if (!this._appRef) {this._appRef = this._injector.get(ApplicationRef);}// Sharing data by Injector(Dependency Injection)const map = new WeakMap();map.set(DATA, this.sharingComponentData); // <--- key pointconst injector = new PortalInjector(this._injector, map);// instantiate a DomPortalOutletconst portalOutlet = new DomPortalOutlet(container, this._componentFactoryResolver, this._appRef, injector); // <--- key point// instantiate a ComponentPortal<DialogComponentWithSharingData>const componentPortal = new ComponentPortal(DialogComponentWithSharingData);// attach a ComponentPortal to a DomPortalOutletportalOutlet.attach(componentPortal);
}
通过 Injector 就可以实现 ComponentPortal 与 AppComponent 共享数据了,该技术对于 Dialog 实现尤其重要,设想对于 Dialog 弹出框,需要在 Dialog 中展示来自于外部组件的数据依赖,同时 Dialog 还需要把数据传回给外部组件。Angular Material 官方就在 @angular/cdk/portal 基础上构造一个 @angular/cdk/overlay 包,专门处理类似覆盖层组件的共同问题,这些类似覆盖层组件如 Dialog, Tooltip, SnackBar 等等。
完整代码可见 demo。
解析 attach() 源码
不管是 ComponentPortal 还是 TemplatePortal,PortalOutlet 都会调用 attach() 方法把 Portal 挂载进来,具体挂载过程是怎样的?查看 BasePortalOutlet 的 attach() 的源码实现:
/** Attaches a portal. */
attach(portal: Portal<any>): any {...if (portal instanceof ComponentPortal) {this._attachedPortal = portal;return this.attachComponentPortal(portal);} else if (portal instanceof TemplatePortal) {this._attachedPortal = portal;return this.attachTemplatePortal(portal);}...
}
attach() 主要逻辑就是根据 Portal 类型分别调用 attachComponentPortal 和 attachTemplatePortal 方法。下面将分别查看两个方法的实现。
attachComponentPortal()
还是以 DomPortalOutlet 类为例,如果挂载的是组件视图,就会调用 attachComponentPortal() 方法,第一步就是通过组件工厂解析器 ComponentFactoryResolver 解析出组件工厂对象:
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {let componentFactory = this._componentFactoryResolver.resolveComponentFactory(portal.component);let componentRef: ComponentRef<T>;...
然后如果 ComponentPortal 定义了 ViewContainerRef,就调用 ViewContainerRef.createComponent 创建组件视图,并依次插入到该视图容器中,最后设置 ComponentPortal 销毁回调:
if (portal.viewContainerRef) {componentRef = portal.viewContainerRef.createComponent(componentFactory,portal.viewContainerRef.length,portal.injector || portal.viewContainerRef.parentInjector);this.setDisposeFn(() => componentRef.destroy());
}
如果 ComponentPortal 没有定义 ViewContainerRef,就用上文的组件工厂 ComponentFactory 来创建组件视图,但还不够,还需要把组件视图挂载到组件树上,并设置 ComponentPortal 销毁回调,回调包括需要从组件树中拆卸出该视图,并销毁该组件:
else {componentRef = componentFactory.create(portal.injector || this._defaultInjector);this._appRef.attachView(componentRef.hostView);this.setDisposeFn(() => {this._appRef.detachView(componentRef.hostView);componentRef.destroy();});
}
需要注意的是 this._appRef.attachView(componentRef.hostView);,当把组件视图挂载到组件树时会自动触发变更检测(change detection)。
目前组件视图只是挂载到视图容器里,最后还需要在 DOM 中渲染出来:
this.outletElement.appendChild(this._getComponentRootNode(componentRef));
这里需要了解的是,视图容器 ViewContainerRef、视图 ViewRef、组件视图 ComponentRef.hostView、嵌入视图 EmbeddedViewRef 的关系。组件视图和嵌入视图都是视图对象的具体形态,而视图是需要挂载到视图容器内才能正常工作,视图容器内可以挂载多个视图,而所谓的视图容器就是包装任意一个 DOM 元素所生成的对象。视图容器可以通过 @ViewChild 或者当前组件构造注入获得,如果是通过 @ViewChild 查询拿到当前组件模板内某个元素如 div,那 Angular 就会根据这个 div 元素生成一个视图容器;如果是当前组件构造注入获得,那就根据当前组件挂载点如 app-root 生成视图容器。所有的视图都会依次作为子节点挂载到容器内。
attachTemplatePortal()
根据上文的类似设计,挂载 TemplatePortal 的源码 就很简单了。在构造 TemplatePortal 必须依赖 ViewContainerRef,所以可以直接创建嵌入视图 EmbeddedViewRef,然后手动强制执行变更检测。不像上文 this._appRef.attachView(componentRef.hostView); 会检测整个组件树,这里 viewRef.detectChanges(); 只检测该组件及其子组件:
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {let viewContainer = portal.viewContainerRef;let viewRef = viewContainer.createEmbeddedView(portal.templateRef, portal.context);viewRef.detectChanges();
最后在 DOM 渲染出视图:
viewRef.rootNodes.forEach(rootNode => this.outletElement.appendChild(rootNode));
现在,就可以理解了如何把 Portal 挂载到 PortalOutlet 容器内的具体过程,它并不复杂。
Portal 快捷指令
让我们重新回顾下 Portal 技术要解决的问题以及如何实现:Portal 是为了解决可以在 Angular 框架执行上下文之外动态创建子视图,首先需要先实例化出 PortalOutlet 对象,然后实例化出一个 ComponentPortal 或 TemplatePortal,最后把 Portal 挂载到 PortalOutlet 上。整个过程非常简单,但是难道 @angular/cdk/portal 没有提供什么快捷方式,避免让开发者写大量重复代码么?有。@angular/cdk/portal 提供了两个指令:CdkPortal 和 CdkPortalOutlet。该两个指令会隐藏所有实现细节,开发者只需要简单调用就行,使用方式可以查看官方 demo。
demo 实践过程中,发现两个问题:组件视图都会多产生一个 p 标签;AppComponent 模板中挂载点作为 ViewContainerRef 时,挂载点还不能为 ng-template 和 ng-container,和印象中有出入。有时间在查找,谁知道原因,也可留言帮助解答,先谢了。
源码分析 @angular/cdk 之 Portal相关推荐
- 源码分析 @angular/cdk 之 Portal 1
@angular/material 是 Angular 官方根据 Material Design 设计语言提供的 UI 库,开发人员在开发 UI 库时发现很多 UI 组件有着共同的逻辑,所以他们把这些 ...
- qiankun 2.x 运行时沙箱 源码分析
当学习成为了习惯,知识也就变成了常识.感谢各位的 点赞.收藏和评论. 新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn 文章已收录到 github,欢迎 Watch 和 Star. 简介 ...
- PostgreSQL源码分析
PostgreSQL源码结构 PostgreSQL的使用形态 PostgreSQL采用C/S(客户机/服务器)模式结构.应用层通过INET或者Unix Socket利用既定的协议与数据库服务器进行通信 ...
- 【转】ABP源码分析三十七:ABP.Web.Api Script Proxy API
ABP提供Script Proxy WebApi为所有的Dynamic WebApi生成访问这些WebApi的JQuery代理,AngularJs代理以及TypeScriptor代理.这些个代理就是j ...
- k8s kube-proxy源码分析
service service简介 service是为了给一组pod提供负载均衡功能的服务. Service的类型 a. clusterIP:此类型服务只能在集群内部访问,比如在pod内部或者在wor ...
- UnityStandardAsset工程、源码分析_5_赛车游戏[AI控制]_AI机制
上一章地址: UnityStandardAsset工程.源码分析_4_赛车游戏[玩家控制]_摄像机控制 前几章我们已经将赛车游戏的绝大多数机制分析过了,而Unity还提供了不同的操控模式--AI控制. ...
- UnityStandardAsset工程、源码分析_4_赛车游戏[玩家控制]_摄像机控制
上一章地址:UnityStandardAsset工程.源码分析_3_赛车游戏[玩家控制]_特效.声效 经过前几章的分析,我们已经大致地了解了车辆控制相关的脚本.现在还有最后一个与玩家体验息息相关的部分 ...
- PG数据库内核源码分析——UPDATE
PG中UPDATE源码分析 本文主要描述SQL中UPDATE语句的源码分析,代码为PG13.3版本. 整体流程分析 以 update dtea set id = 1;这条最简单的Update语句进行源 ...
- 微前端框架 之 qiankun 从入门到源码分析
当学习成为了习惯,知识也就变成了常识.感谢各位的 点赞.收藏和评论. 新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn 文章已收录到 github,欢迎 Watch 和 Star. 简介 ...
最新文章
- Learn Jenkins the hard way (0) - Jenkins的罪与罚
- VTK:可视化之BoxClipStructuredPoints
- oracle查询表字段sql语句怎么写,oracle联合查询并更新一个表字段的sql语句
- 第3章 Python 数字图像处理(DIP) - 灰度变换与空间滤波8 - 直方图处理 - 直方图均衡化(全局直方图均衡化)
- DataPipeline | PayPal庞姬桦:大数据在小微企业贷款上的运用
- 超级详细的Spring Boot 注解总结
- 云图说 | 快速创建一个kubernetes集群
- VB 汉字字符串转换成拼音
- (cljs/run-at (JSVM. :all) 一次说白DataType、Record和Protocol) 1
- [转]如何撰写学术论文
- CRM——销售与客户
- Linux系统,Hadoop,R语言,RHadoop的安装
- linux退出热键_LINUX常用快捷键
- 使用screw生成数据库文档
- 自定义QT标题栏和背景·边框
- Python基础知识(二):序列结构---字符串、 列表、 元组、 字典、 集合
- 泰克TBS1000X示波器仪器的使用
- 【工具封装】Python 字典列表按中文姓名首字母排序
- 伺服驱动器原理学习笔记
- 反编译+混淆的攻守战
热门文章
- 计算机无法打开策略,windows电脑本地计算机策略打不开该怎么解决?
- tf.keras.losses.Huber 损失函数 示例
- 二十六、二叉树--查找指定节点
- 对服务器文件夹写,服务器文件夹写入权限设置
- 简述java的线程_JAVA线程简述
- p20华为云电脑白屏_永别了电脑,华为大举动:华为云电脑,重新定义个人电脑...
- mysql事务的4大特性
- maven打jar包,导入本地jar
- 【Prometheus】Exporter详解
- VS2012/13本地发布网站详细步骤(可带数据库)