# portalTemplate - 门户模板扩展

# 简介

SuccBI中内置了几个常见的门户模板,但是无法满足个性化的门户需求,因此需要开发一些个性化的门户模板。门户模板扩展可以决定门户的整体结构和布局,并支持配置额外的属性。已安装的门户模板,可以在新建门户应用时选用,或者在门户设计器中即时切换。门户模板扩展属于前端扩展,支持的开发语言为typescript、javascript、css、less。

# 开发步骤

# 安装开发环境

扩展开发环境的安装可参考文档:扩展开发

# 设计好门户的平面效果

开发门户模板前需要让设计师设计好门户的平面效果,确定门户的整体结构和布局。

# 选择合适的模板新建扩展

使用vscode新建扩展,在扩展的开发环境中执行命令succ create extension,然后选择开发语言,选择扩展点(portalTemplate),然后可以选择3种基本模板来新建扩展:

  1. 经典门户模板(classic):顶部模块选项卡,左侧资源树
  2. 磁贴门户模板(colorBlock):类似于windows开始菜单
  3. 空白门户模板(empty):门户的整体布局可以自由设计

根据设计好的门户效果,选择合适的模板来创建扩展,生成的扩展代码里会自动继承对应的类,减少开发工作量。更多扩展开发命令,可参考文档SuccIDE

# 门户模板扩展的代码结构

创建好扩展后,会自动生成一个扩展的目录结构:

  • images 用于存放模板需要的图片资源
  • main.ts 模板的入口脚本文件,如果改名需同步更改package.json中的配置
  • main.less 模板的样式文件,在main.ts中引用,如果改名需同步更改main.ts里的引用
  • package.json 扩展的配置文件,包括扩展的信息和扩展点的信息,不同的扩展点的配置也不同。该文件的详细结构可参考api中的extension-types.d.ts
  • thumbnail.png 扩展的缩略图,在package.json中引用

main.ts 中就是扩展的实现脚本,里面会自动继承类BaseTemplatePage,并初始化了基本的代码结构。(如果创建扩展时选择的是经典门户,则会继承PortalTemplatePage

门户模板的实现类默认继承了Component类,Component类是系统内部所有UI控件的基类,有一些默认的基本规范需要注意:

  1. 应该在_init_default方法中初始化成员变量.
  2. 应该在_init方法中构造dom结构,this.domBase为控件最外层的dom,this.domParent为控件的父dom
  3. 需要实现dispose方法来销毁控件,dispose方法中最后要调用父类的dispose方法

生成的main.ts中也会有较详细的注释。

# 开发门户模板的渲染逻辑

我们把通过js来构造、修改html/css等导致用户界面发生变化的过程称为渲染。门户模板的基本实现思路就是数据驱动渲染。

渲染时需要用到的所有数据在miniapp-types.d.tsTemplatePageInfo接口中有详细定义。如果接口中定义的数据不能满足需求,不够个性化,可参考配置门户模板参数来配置额外的属性。

开发过程中可通过门户的数据对象TemplatePageDataBuilder来获取元数据,如获取门户背景:

// this 为 BaseTemplatePage
this.builder.settings.background;
// 或
this.builder.getProperty('background');

门户模板需要实现两个接口,一个全量渲染doRefresh()和一个增量渲染doRequestRender(undoItem: UndoItemInfo)

增量渲染用于设计器中,根据UndoItemInfo中记录的变化的属性来选择性的渲染UI,如ColorBlock模板中doRequestRender接口的实现:

public doRequestRender(undoItem: UndoItemInfo): Promise<void> {
	let comp = undoItem.c;
	// 判断是否是资源发生变化
	if (comp instanceof ResourceNodeBuilder) {
		if (undoItem.op === UndoOperationType.Modify) {
			// 修改了资源节点
		} else {
			// 增加或者删除了资源节点
		}
	} else {
		let settings = this.builder.settings;
		// undoItem.p中记录了变化的属性名,根据属性名决定ui如何更新
		switch (undoItem.p) {
			case 'logo':
				let logo = settings.logo;
				if (logo) {
					this.setLogoImg(this.domLogo, logo.logoImage);
					this.setLogoTitle(this.domTitle, logo.logoTitle);
				}
				break;
			case 'allowGotoHome':
				this.homeBtn.setVisible(settings.allowGotoHome);
				break;
			case 'allowLogout':
				this.logoutBtn.setVisible(settings.allowLogout);
				break;
			case 'background':
				this.setBackground(this.domBg, settings.background);
				break;
		}
		return Promise.resolve();
	}
}

如果需要获取资源节点的属性,则应该先获取对应资源节点的数据对象ResourceNodeBuilder

public doRequestRender(undoItem: UndoItemInfo): Promise<void> {
	let comp = undoItem.c;
	if (comp instanceof ResourceNodeBuilder) {
		// 获取节点用于渲染的数据
		comp.toData().then(data=>{

		});
	}
}

如需获取所有的资源(用来渲染列表、树等),可直接使用数据对象的fetchData接口。如ColorBlock模板中渲染磁贴的实现:

private refreshBlocks(): Promise<void> {
	return this.builder.fetchData().then(datas => {
		let blocks = this.blocks;
		blocks.forEach(block => block.dispose());
		blocks.clear();
		datas.forEach(data => {
			blocks.set(data.id, new ContentBlock({
				domParent: this.domContent,
				data: data
			}));
		});
	});
}

在开发门户模板时,往往只有一个页面对象是不够用的,页面内的部分子模块也需要定义为类,页面在渲染时,需要调用子对象的渲染方法,所以推荐所有子对象都实现ITemplatePagePartRenderer接口,该接口在templatepages.d.ts中有定义,同样也是主要实现两个方法,一个全量渲染一个增量渲染,让页面对象来调用,如经典门户模板中增量渲染的实现:

public doRequestRender(undoItem: UndoItemInfo): Promise<void> {
	return waitAnimationFrame().then(() => {
		/**
		 * 经典门户页面包括上方的标题栏headbar和下方模块页面modulePages,
		 * 所以增量更新时需要分别调用他们的updateRender方法,让子对象自己去处理自己的渲染。
		 */
		let proms: Array<Promise<void>> = [this.headbar.updateRender(undoItem), this.modulePages.updateRender(undoItem)];
		return Promise.all(proms).then(() => {
			let settings = this.builder.settings;
			let p = undoItem.p;
			switch (p) {
				case 'background':
					this.setBackground(this.domBg, settings.background);
					break;
				case 'template':
					this.setTheme(settings.template.theme);
					break;
			}
			if (undoItem.c instanceof ResourceNodeBuilder || undoItem.items) {
				return this.navigate({ params: {} }, true).then(() => { });
			}
		});
	});
}

获取到数据后就需要根据数据来渲染ui,基类上提供了三个常用的工具方法,包括设置背景色(setBackground)、设置logo(setLogoImg)、设置标题(setLogoTitle)。

# 配置门户模板参数

扩展的信息和门户模板的扩展参数都需要配置在package.json文件中,该文件的结构在extension-meta-types.d.ts中有详细的定义(ExtensionInfo)。

如果门户模板想要一些额外的属性,并且可在属性栏中进行设置,那么可以在扩展中配置extraProperties属性,分为content,style,action三类,分别对应属性栏中的三个面板(内容、样式、交互),其中propertyName是直接记录在元数据中的属性名,propertyType表示该属性的类型,不同的类型的属性在属性栏中对应的控件不同,返回的数据结构也不同。常见的类型有:

propertyType 数据类型 说明
checkbox boolean 勾选框
edit string 文本输入框
numberInput number 数值输入框
spinner number 数值微调框
combobox string 下拉框
fill FillInfo 背景填充控件,包括颜色填充、渐变填充和图片填充。FillInfo需要使用基类上的setBackground方法设置到dom上,如this.setBackground(this.domBg, data.background);
icon IconInfo 图标编辑控件,包括大小和颜色。IconInfo可使用基类上的setIcon方法给dom设置图标,如this.setIcon(domIcon, data.icon)
colorButton string 颜色按钮,点击后弹出颜色选择面。返回的字符串为主题色,需要使用基类上的getConvertedColor方法转为css颜色,如dom.style.color = this.getConvertedColor(data.color)

更多的类型可参考属性栏ppteditor.d.ts

defaultValue表示该属性的默认值,值的类型需要和上面的类型对应。

配置的额外属性在属性栏中显示的标题,需要配置国际化,国际化key的格式为扩展名.propertyCaption.属性名,如: succ-portalTemplate-colorBlock.propertyCaption.background。关于如何配置国际化,可参考如何在代码中进行国际化

# 发布测试

扩展开发过程中可能需要边测试边开发,实时观察模板的效果。这时就需要使用命令succ publish to server将扩展发布到BI服务器上,发布后扩展就会立即生效,且会实时监听本地文件的变化,自动同步到服务器上。详情请参考扩展插件使用。发布后可打开小应用的设计器(新建或者编辑小应用)切换模板来查看效果。

# 示例

# 类windows磁贴模板的示例

创建门户模板扩展时如果选择磁贴门户模板,则生成的模板代码里会有一个较完整的门户模板实现:ColorBlockTemplate,可参考该类的实现。

# 常见问题解答

# 如何识别资源结构变化和样式属性变化

门户在增量渲染时通常需要知道是资源变化还是页面样式变化,因为资源变化需要做的处理往往较多,那么应该如何区分呢?

门户资源有单独的数据对象ResourceNodeBuilder,可以根据UndoItemInfo中记录的控件是否是该类的实例来判断:

public doRequestRender(undoItem: UndoItemInfo): Promise<void> {
	// 门户资源可批量操作,此时items为UndoItemInfo的数组
	let items = undoItem.items;
	if (items || undoItem.c instanceof ResourceNodeBuilder) {

	} else {

	}
}

# 如何快速跳转到定义该接口的地方

开发扩展时想看看接口的定义和注释来了解如何使用,如何快速跳转到定义该接口的地方?

在vscode的开发环境中,按住ctrl用鼠标点击变量、方法、类、接口等都可以直接跳转到定义的位置,也可使用ctrl + shift + O在当前文件中搜索,或者ctrl + T在工作区中搜索。

# 如何在门户中打开一个资源

首先获取需要打开的资源的数据,可通过数据对象ResourceNodeBuilder或者fetchData方法(参考开发门户模板的渲染逻辑),然后使用基类上的openFile方法获取打开的资源对应的Component对象(如Dashboard),需要自行决定显示位置并管理显示隐藏等逻辑。同一个id多次调用该方法返回的是同一个对象。销毁一个不再使用的对象,需要调用deleteViewer方法。

# 如何使用less

开发扩展时会自动将less编译成css放在同级目录下,ts中import编译后的css就行。

# 如何基于默认的模板进行定制开发

默认的模板是比较常见的经典布局,很多个性化的模板都可以直接基于默认的模板来修改。创建扩展时选择经典门户模板即可自动继承默认模板的实现

  • 如果不需要修改dom结构和交互行为,可不用更改ts脚本,通过覆盖css来调整样式。
  • 如果需要修改页面的某个部件,如左侧资源树,可重载对应的类(ResourcesTree),然后重写PortalTemplatePage中对应的create方法(createResourcesTree)。

# 如何配置模板默认的logo、标题、背景等

package.json中可配置模板初始化的默认数据(defaultPageInfo),这些数据会初始化到属性栏中。如果不想显示在属性栏中,可以用样式定制,或者在渲染时做默认的处理。

# 如何实现多主题

一个模板支持多个主题,不同主题一般只有配色不同,实现多主题需要做:

  • 在package.json中配置模板支持的主题信息。
  • 实现setTheme方法。
  • 用less或者css实现各主题的样式。

其中实现setTheme方法需要注意:

  1. 需要调用父类的setTheme方法。如果有子控件,需要调用子控件的setTheme方法,方便重载样式。
  2. 如果门户中当前有打开的资源(如dashboard),需要先设置该资源查看器的主题,由于设置查看器主题的方法setPreferredThemes是异步的,为了避免主题切换时不同步,需要先等待查看器切换主题完成再切换模板的主题。

如默认经典模板中的实现:

public setTheme(theme: string) {
	if (this.theme === theme || theme !== 'dark' && theme !== 'light') {
		return;
	}
	/**获取当前打开的资源查看器的方法 */
	let activeViewer = this.getActiveViewer();
	if (!activeViewer) {
		super.setTheme(theme);
		this.headbar.setTheme(theme);
		this.modulePages.setTheme(theme);
		this.storeTheme();
		return;
	}
	/**获取主题配置信息,用于设置查看器的主题 */
	this.builder.getTemplateThemeInfo(theme).then(info => {
		let preferredThemes = info && info.preferredThemes || [theme, 'default'];
		// 先等viewer切换theme完成,避免dashboard切换theme延迟的问题
		activeViewer.setPreferredThemes(preferredThemes).then(() => {
			super.setTheme(theme);
			this.headbar.setTheme(theme);
			this.modulePages.setTheme(theme);
			this.storeTheme();
		});
	});
}
是否有帮助?
0条评论
评论