场景在我们的腾讯文档项目中,我们常用的顶部工具栏会根据编辑权限、屏幕宽度、设备等场景配置相应的显示内容。对于我们的菜单或者底栏,需要在特定的时间段出现红点来引导用户。子菜单栏在一定的交互行为下显示不同的内容项。切换为英文时,菜单栏内容变为中文等,以上场景会导致我们的业务代码大概率会写类似下面,大量的条件判断等,如果条件需要以后要更改或添加,需要在大量的条件判断中添加逻辑,工具栏的显示效果可能会根据不同的业务场景改变其外观,如图标、排序、样式等。那么每次第三方接入或者操作需要额外配置的时候,开发者都需要对代码进行改动,这样显然不利于维护,效率低下。constPcToolbarButtonConfig=['undo','redo','format','clear-format'];constPcFullToolbarButtonConfig=['undo','redo'];constPcShortToolbarButtonConfig=['undo'];constPcShortToolbarButtonReadonlyConfig=['撤销','重做','格式化'];switch(true){casewindow.innerWidth<1440&&window.innerWidth>=855:if(isMore)null;返回canEdit?PcToolbarButtonConfig:PcToolbarButtonReadonlyConfig;casewindow.innerWidth<855:if(isMore)null;返回canEdit?PcShortToolbarButtonConfig:PcShortToolbarButtonReadonlyConfig;}返回canEdit?自定义配置,从官网可以看到,它使用了一个package.json,定义了一个contributes属性,当传入一个对象时,编辑器的右上角会出现一个新的可点击的功能图标。"contributes":{"menus":{"editor/title":[{"when":"resourceLangId==markdown","command":"markdown.showPreview","alt":"markdown.showPreviewToSide","group":"navigation"}]}}如下图,这个图标的位置也是由配置项决定的。注意里面有一个when条件语句。其实打开的文件是markdown的时候,如果条件判断为真,就会出现图标。由于Vscode支持使用插件加载配置文件来配置编辑器的显示功能,所以我们也可以利用这个思路来配置我们的工具栏。下面详细讲解一下Vscode的插件机制,利用它的思想实现一个属于我们自己的腾讯文档UI可配置的机制。Vscode提供了一个ExtensionsRegistry实例,它有两个关键方法:ExtensionsRegistry.registerExtensionPointExtensionsRegistry.getExtensionPoints首先使用ExtensionsRegistry.registerExtensionPoint注册一个配置项菜单,当注册成功后,使用setHandler设置一个回调来处理配置参数,将稍后阅读,然后使用ExtensionsRegistry.getExtensionPoints将插件下的所有package.jsonScan一次,配合刚才的setHandler回调,解析出所有contribution中的menu参数,存入MenuRegistry中。ExtensionsRegistry.registerExtensionPoint<{[loc:string]:schema.IUserFriendlyMenuItem[]}>({extensionPoint:'menus',jsonSchema:schema.menusContribution}).setHandler(extensions=>{for(constextensionofextensions){const{value}=extension;for(constcommandofvalue){MenuRegistry.addCommands(command);}}})如果你想自定义一个配置项让Vscode成功扫描解析成功,那么你可以按照上面的思路,结合如下图配置,首先使用ExtensionsRegistry.registerExtensionPoint({extensionPoint:'toolbars'}).setHandler(callback)注册配置项toolbars并设置回调函数解析参数,然后使用ExtensionsRegistry.getExtensionPoints扫描contributions中的toolbars参数被解析后存放在ToolbarRegistry中,然后ToolbarRegistry解析出来的参数用于渲染或者更新Vscode的视图。这里会提供jsonSchema来验证扫描的参数是否符合规范。如果不指定,会有警告提示,非法配置参数将被忽略。因此,在将配置文件开放给第三方开发者时,我们也可以自由定制。我们无需担心配置参数严重影响代码。exportnamespacejsonSchema{exportfunctionisValidCommand(command:IUserFriendlyCommand,collector:ExtensionMessageCollector):boolean{if(!command){collector.error(localize('nonempty',"expectednon-emptyvalue."));返回假;}if(typeofcommand.command!=='string'){collector.error(localize('requirestring',"属性`{0}`是必需的,必须是`string`类型",'command'));返回假;}}ExpressionParser当然,上述所有操作的本质都是解析一个JSON配置文件并提供校验,所以其实还有更多的“隐藏”功能。上面我们提到,我们的腾讯文档顶部工具栏会根据编辑权限、屏幕宽度、设备等场景配置相应的显示内容。在业务代码中,我们会有很多条件判断逻辑:switch(true){casewindow.innerWidth<1440&&window.innerWidth>=855:if(isMore)null;返回canEdit?PcToolbarButtonConfig:PcToolbarButtonReadonlyConfig;casewindow.innerWidth<855:if(isMore)null;返回canEdit?fig;}我们可以使用上面的插件机制来避免这个问题,比如改写成这样的形式可以更直观:"contributes":{"toolbars":[{"command":"undo","component":"Redobutton","icon":"undo","when":"canEdit==true&&window.innerWidth<1080&&window.innerWidth>=855"},{"command":"redo","icon":"redo","when":"platform==pc&&window.innerWidth<855&&isMore==true"}]}当我们工具栏的UI视图需要自定义的时候,我们只需要改变我们的配置文件一点点就可以达到目的,第三方开发者也可以根据自己的需要定制自己的工具栏、菜单和底部栏。这个方案的本质是用"when":"canEdit==true&&window.innerWidth<1080&&window.innerWidth>=855"来代替各种复杂的条件语句,Vscode的插件机制就是用这个方案来实现绑定配置文件到UI视图。该方案的本质是将配置参数when:xxx解析为布尔值。为了达到这个目的,Vscode内部实现了一个简单的表达式解析器。目前支持以下表达式:支持变量支持常量:布尔值、数字、字符串支持正则表达式支持全等(===)、不等(!==)支持和(&&)、或(||)Vscode只实现上面这个简单的表达式解析就很好的支持了上万插件的配置,也就是说上面的解析器在正常情况下是够用的,也是Vscode鼓励我们使用的规范。如果我们自己实现一个复杂的解析器,可以考虑支持下面的表达式。不支持加(+)、减(-)、乘(*)、除(/)、余(%)运算不支持大于(>)、小于(<)、大于等于(>)=),小于等于(<=)等比较运算不支持非(!)等逻辑运算不支持括号()注意不支持大于和小于,所以我们只是“when”:"canEdit==true&&window.innerWidth<1080&&window.innerWidth>=855"这种写法我们不支持,需要自己扩展。该部分在腾讯文档的插件机制中得到支持。这里简单介绍一下,我们可以封装一个反序列化方法来解析“when”:“canEdit==true||platform==pc&&window.innerWidth>=1080”这个字符串涉及到==,&&,>=的解析三个表达式,使用indexOf和split进行分词,一般分为三部分,key,type和value,特殊情况canEdit==true,只要有key和value即可。privatestaticdeserialize(serializedOne:string,strict:boolean):ContextKeyExpression{if(serializedOne.indexOf('>=')>=0){letpieces=serializedOne.split('>=');}返回ContextKeyGreaterOrEqualsExpr.create(pieces[0].trim(),this._deserializeValue(pieces[1],strict));}if(serializedOne.indexOf('<')>=0){letpieces=serializedOne.split('<');返回ContextKeyLessExpr.create(pieces[0].trim(),this._deserializeValue(pieces[1],strict));}returnContextKeyDefinedExpr.create(serializedOne);}最后when会被解析成这个树结构,type是预先定义表达式的转义,如下表所示:ContextKeyTypeContextKeyTypeFalse0Regex7True1NotRegex8Defined2Or9Not3Greater10Equals4Less11NotEquals5GreaterOrEquals12And6LessOrEquals13分词规则也很简单。以下面生成树的思路为例,遵循我们常用表达式的一些语法规范和优先级规则,先切||两边的所有表达式,然后遍历两边的表达式切割&&表达式,切割所有||和&&,然后处理子节点的!=、==和>=符号。我们在切割整个when配置项的时候,将这个树结构和上面的ContextKey-Type映射表结合起来,转换成下面的JS对象,里面存放了重要的规则类,如ContextKeyOrExpr、ContextKeyAndExpr、ContextKeyEqualsExpr、ContextKeyGreaterOrEqualsExpr。JS对象存储在MenuRegistry中,MenuRegistry中存储的key和value可以根据类型操作规则进行检索比较,只需遍历MenuRegistry即可返回一个布尔值。当:{ContextKeyOrExpr:{expr:[{ContextKeyDefinedExpr:{key:"canEdit",type:2}},{ContextKeyAndExpr:{expr:[{ContextKeyEqualsExpr:{key:"platform",type:4,value:"pc",},ContextKeyGreaterOrEqualsExpr:{key:"window.innerWidth",type:12,value:"1080",}}],type:6}}],type:9}}策略模式但是要注意的是key是“window.innerWidth”,canEdit和“platform”都是字符串,不是真正的可以用来判断的值。这些键有的只会在运行时取值,有的只会在一定范围内取值。我们还需要对这些密钥进行转换。我们借鉴了Vscode的做法。在Vscode中,它会将这部分逻辑交给一个叫做context的对象来处理。它提供了两个关键接口setValue和getValue方法,简单实现如下。classContext{privatereadonly_values=newMap
