腾小云导读
Visual Studio Code「VSCode」是 Microsoft 在2015年推出的、针对于编写现代 Web 和云运用的跨渠道源代码编辑器,受到广大开发者热捧。腾讯文档向 VSCode 奉献了一些中心代码,首要涉及到 VSCode 装备化的部分,为其显著增强了装备化和插件化才能。作者期望将其间堆集的经历共享出来,奉献给开源社区,为广大开发爱好者供给参阅。本文详细解读源代码。欢迎阅读!
1 项目布景
2 腾讯文档奉献源码剖析
3 腾讯文档给 VSCode 带来了什么
4 总结
01、项目布景
腾讯文档在完善自己的装备化体系,在完善的进程探究了多种完成计划,剖析了许多产品如大名鼎鼎的 VSCode 的完成办法。近期腾讯文档向 VSCode 奉献了 400 多行中心代码,首要涉及到 VSCode 装备化的部分。这增强了其插件化才能,供给更多的匹配接口。腾讯文档团队整理了部分代码结构和弥补功用单测,期望把将这些堆集的经历奉献给开源社区,与广大开发爱好者共同进步。公众号回复「VSCode」获取源代码。
合入腾讯文档代码的是微软 VSCode 团队现首要负责人之一Alexdima(VScode 前身 Monaco Editor 的负责人),他和Erich Gamma ( VSCode 之父) 来自同一团队。
腾讯文档团队给 VSCode 的装备化奉献了什么功用?相信大部分的开发者都运用过 VSCode,所以对装备化应该不生疏。因为运用者许多,任何编辑器其实都不能做到面面俱到去满足一切的运用者。假如什么用户的需求都要满足,就需求把一切的功用都塞进去。这不光臃肿,还欠好保护。下面一起来看看咱们怎样处理。
02、腾讯文档奉献源码剖析
咱们需求将装备化丰富和拓展,减轻编辑器本身的包袱,把部分内容移交给用户/合作方去定制。例如:能够在 VSCode 的设置面板选择编辑器的色彩,更换它的主题布景。
也能够在快捷键面板里边绑定或许解绑此快捷键,更换字体大小和改动悬浮信息等,这些其实都离不开背后完成的一套装备化体系。
上面的举例,都是有默许的装备。能够经过面板去更改,当然还有些躲藏的装备无需在面板改动也能完成装备。例如:缩小 VSCode 的界面大小,某些功用就会自动躲藏,这种也是归于装备化。
咱们除了经过面板可视化操作,还能够经过插件来装备界面,VSCode 中插件的中心就是一个装备文件 package.json,里边供给了装备点。只需按要求编写正确的装备点就能够改动 VSCode 的视图状况。里边最首要的字段就是 contributes 字段:
字段 |
解析 |
contributes.configuration | 插件有哪些可供用户装备的选项,供给的界面需与面切布景色彩棉棒相似 |
contributes.configurationDefaults | 掩盖 vscode 默许的一些编辑器装备 |
contributes.commands | 向 vscode 的命令体系注册一些可供用户调用的命令 |
contributes.menus | 扩展菜单 |
这是更换编辑器部分方位色彩的装备参数。里边的代码思路其实是挖了一个「洞」给第三方,然后支撑参数的填入。
{
"colors": {
"activityBar.background":"#333842",
"activityBar.foreground":"#D7DAE0",
"activityBarBadge.background":"#528BFF"
}
}
下面代码为示例。把装备文件的色彩读取出来,然后生成一个新的色彩规矩,到达控制面板布景色彩的功用。
constcolor = theme.getColor(registerColor("activityBar.background"));
if(color) {
collector.addRule(
`.monaco-workbench .activitybar > .content > .home-bar > .home-bar-icon-badge { background-color:${color}}`
);
}
上面这个最基本的功用在代码里边完成是毫无难度的,只需求挖空一个装备点即可,可是实践会更杂乱。假如此刻用户想在此功用基础上持续做装备,例如编辑器在 Win 体系的时分色彩变深,在 Mac 体系的时分色彩变浅。
if(color) {
if(isMacintosh) {
color = darken(color);
}
if(isWindows) {
color = lighter(color);
}
collector.addRule(
`.monaco-workbench .activitybar > .content > .home-bar > .home-bar-icon-badge { background-color:${color}}`
);
}
这些操作对于对开发人员而言难度虽不是很大,只需在代码里边刺进几段条件判别的代码。可是假如用户又要求更改的话,能够更改为在分辨率大于 855 的时分使色彩变深,在分辨率小于或等于 855 的时分使色彩变浅,而且遇到 Linux 体系也会色彩变深。此刻可能再变更代码来满足客户的需求,需求持续加如下的代码。这样做会添加开发人员的任务量。编辑器用户量不止上千万,用户需求十分多样,必然难以招架。
if(color) {
if(isMacintosh ||window.innerWidth >855) {
color = darken(color);
}
if(isLinux) {
color = darken(color);
}
if(isWindows ||window.innerWidth <=855) {
color = lighter(color);
}
collector.addRule(
`.monaco-workbench .activitybar > .content > .home-bar > .home-bar-icon-badge { background-color:${color}}`
);
}
这时就需咱们自行定制标准。供给变暗和变深的接口,不负责写规矩,而是需用户供给。详细调整代码如下:
classColor{
color = theme.getColor(registerColor("activityBar.background"));
@If(isLinux)
@If(isMacintosh ||window.innerWidth >855)
darken() {
returndarken(this.color);
}
@If(userRule1)
@If(userRule2)
@If(userRule3)
@If([isWindows,window.innerWidth <=855].includes(true))
lighter() {
returnlighter(this.color);
}
}
上面只是列出伪代码,并非很简略。只供给朴实的 darken 和 lighter,不与频频的条件表达式耦合,所以可能会做判别条件的前置化。那么后续开发人员只需叠加装修器即可,而且动态保存一个装修器 @If(userRule) 作为装备文件的洞口。再供给官方装备文档给用户写相似的 package.json 文件填写对应的参数,这样压力就会转移到运用者(用户)或许接入者身上。
这种写法看似夸姣,但会呈现许多致命问题,darken 和 lighter 在履行前现已被带条件表达式装修,后边假如二次履行 darken 和 lighter 办法则不会再履行装修器中条件表达式的判别,实质上这两个函数的 descriptor.value 现已被覆写,但逻辑从根本上发生了改动。
exportconstIf =(condition:boolean) =>{
console.log(condition);
return(target:any, name?:any, descriptor?:any) =>{
constfunc = descriptor?.value;
if(typeoffunc ==="function") {
descriptor.value =function(...args:any){
returncondition && func.apply(this, args);
};
}
};
};
正常状况下客户端侧 isLinux,isMacintosh 和 isWindows 是不会发生改动的,可是 window.innerWidth 在客户端却是有可能持续发生改变。所以一般状况下对待客户端环境常常改变的值或许需求经过效果域判别的值,我不主张写成上面装修器露出接口的计划。假如这是一个比较固定的装备值,这种计划合作 webpack 的 DefinePlugin 会有意外的收获。
newwebpack.DefinePlugin({
isLinux: JSON.stringify(true),
VERSION: JSON.stringify("5fa3b9"),
BROWSER_SUPPORTS_HTML5: true,
TWO:"1+1",
"typeof window": JSON.stringify("object"),
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
});
可是许多时分是需求在程序运行的时分进行装备化的,上述的大部分内容都是静态的装备(俗话说是写死的),比方 if (window.innerWidth > 855) 这个装备参数:
左面 window.innerWidth 在运行时是改变的,右边 855 代码是写死的,所以一般把这一整段留出一个缺口来进行外部的装备化,会选用 json 去描述这份装备。
在 VSCode 等运用中,许多当地没有 json 文件进行装备,因为大部分状况它会供给可视化界面用来修改装备。其实实质是改动了 json 的装备文件来到达意图的,例如上面的 if(isMacintosh || window.innerWidth > 855) 就被刺进到外面的 json 文件中了。
//if(isMacintosh ||window.innerWidth >855) ...
//if(isWindows ||window.innerWidth <=855) ...
//↓
{
"darken": {"when":"isMacintosh || window.innerWidth > 855"},
"lighter": {"when":"isWindows || window.innerWidth <= 855"}
}
一般需求接入方或许运用者写成上面相似的文件,然后经过服务器装备体系,下发到客户端。然后把奉献点放入装修器中的缺口,再履行对应的装备逻辑。大致如下:
@If(JSON.darken)
darken() {
returndarken(this.color);
}
@If(JSON.lighter)
lighter() {
returnlighter(this.color);
}
JSON.darken 和 JSON.lighter 分别是对应 JSON 文件中的装备项,实践在代码运行时承受的字符串参数是:
@If("isMacintosh || window.innerWidth > 855")
darken() {
returndarken(this.color);
}
@If("isWindows || window.innerWidth <= 855")
lighter() {
returnlighter(this.color);
}
这是大部分装备化绕不开的问题,简略的装备只需求传承好字符串语义即可,可是杂乱的装备化可能是带有条件表达式,代码语法等。这是VSCode 官方插件的装备代码,均是装备表达式。
实质上这些字符串终究是要解析为布尔值,作为开关去发动 darken 或许 lighter 接口的。所以这儿需求支付一些代价去完成表达式解析器,和字符串转义解说引擎的。
“window.innerWidth”=>window.innerWidth “isWindows”=>isWindows “isMacintosh || window.innerWidth > 855″=>true/false |
这个进程中还需求完成校验函数,假如识别到是不合法的字符串则不答应解析,避免不合法发动装备接口。
“isMacintosh || window.innerWidth > 855″=>合法装备参数 “isMacintosh |&&| window.innerWidth > 855″=>不合法装备参数 “isMacintosh \\// window.innerWidth > 855″=>不合法装备参数 |
这种引擎的完成规划其实还有一种更暴力的处理计划,就是把读取的装备字符串完全交给 eval 去处理。这当然能够很快去完成,可是仍是上面提到的问题,这个操作假如承受了一段不合法的字符串,就会很容易履行一些不合法的脚本,肯定不是一个最优的计划。
eval("window.innerWidth > 855"); // true或许 false
"darken": {"when":"isMacintosh || window.innerWidth > 855"},
"lighter": {"when":"isWindows || window.innerWidth <= 855"}
}
下面介绍处理计划。先读取 json 文件,定位到要害词when: xxx (VSCode 现在只能露出 when 对外匹配,腾讯文档实践还没开源的代码是能够完成露出更多的键值规矩给运用方去匹配),不管是后端装备体系读取仍是前端装备体系读取,解题思路均一致。
将读取条件表达式字符串“isMacintosh || window.innerWidth > 855“,按照表达式的优先级拆解成几个部分,放入下面的 contextMatchesRules 去匹配预埋的效果域回来布尔值,( VSCode 只做到按对应的键值去解析,腾讯文档能够做到对整个 JSON 装备表的键值扫描解析)。
context.set("isMacNative", isMacintosh && !isWeb);
context.set("isEdge", _userAgent.indexOf("Edg/") >=0);
context.set("isFirefox", _userAgent.indexOf("Firefox") >=0);
context.set("isChrome", _userAgent.indexOf("Chrome") >=0);
context.set("isSafari", _userAgent.indexOf("Safari") >=0);
context.set("isIPad", _userAgent.indexOf("iPad") >=0);
context.set(window.innerWidth, () => window.innerWidth);
contextMatchesRules(context, ["isMacintosh"]);// true
contextMatchesRules(context, ["isEdge"]);// false
contextMatchesRules(context, ["window.innerWidth",">=","800"]);// true
VSCode 只是完成了很简略的表达式解析就支撑起了上万个插件的装备。因为 VSCode 有完善的文档,足够大的体量去定制标准,对开发人员能做到了强束缚。上面这些解析器其实在有束缚的状况下,不会被乱添加规矩,正常状况是够用的。可是能用或许够用不代表好用。开源项目和商业化项目对用户侧的束缚和标准不会相同。
03、腾讯文档给 VSCode带来了什么
腾讯文档把整个解析器完成完好化,并完善了 VSCode 的解析器,赋予其更多的装备功用,后续还会持续推进并完善整个解析器,因为现在 VSCode 这方面还不是最完好的。
咱们的装备解析器支撑下面一切的办法。
-
支撑变量
-
支撑常量:布尔值、数字、字符串
-
支撑正则
-
支撑全等 in 和 typeof
-
支撑全等 =、不等 !
-
支撑与 &&、或 ||
-
支撑大于 >、小于 <、大于等于 >=、小于等于 **<=**的比较运算
-
支撑非 ! 等逻辑运算
咱们接下来再详细叙述下思路。运用下面这个杂乱的比如来归纳不同的状况:
"when": "canEdit == true || platform == pc && window.innerWidth >= 1080"
封装一个 deserialize 办法去解析“when”: “canEdit == true || platform == pc && window.innerWidth >= 1080” 这段字符串,里边涉及了 ==,&&,>= 三个表达式的解析。
运用 indexOf 和 split 进行分词,一般切割成三部分,key、type 和 value,特殊状况 canEdit == true,只需有 key 和 value 即可。依据优先级,先拆解 || 再拆解 && 这两个表达式。
const_deserializeOrExpression: ContextKeyExpression |undefined= (
serialized:string,
strict:boolean
) => {
// 先解 ||
letpieces = serialized.split("||");
// 再解 &&
returnContextKeyOrExpr.create(pieces.map((p) =>_deserializeAndExpression(p, strict)));
};
const_deserializeAndExpression: ContextKeyExpression | undefinedn = (
serialized:string,
strict:boolean
) => {
letpieces = serialized.split("&&");
returnContextKeyAndExpr.create(pieces.map((p) =>_deserializeOne(p, strict)));
};
再拆解其他表达式。这儿代码解析的次序十分重要,比方有些时分需求添加 !== 这种表达式的解析,那么一定留意先解析 == 再解析 !==,不然会拆解有误,代码的解析次序也决议表达式的履行优先级,因为大部分都是字符串比对,所以一般无需比对类型,特殊状况在运用大于和小于号的时分,假如呈现 5 < ‘6’ 也是判别履行成功的。
const_deserializeOne: ContextKeyExpression = (
serializedOne:string,
strict:boolean
) => {
serializedOne = serializedOne.trim();
if(serializedOne.indexOf("!=") >=0) {
letpieces = serializedOne.split("!=");
returnContextKeyNotEqualsExpr.create(
pieces[0].trim(),
this._deserializeValue(pieces[1], strict)
);
}
if(serializedOne.indexOf("==") >=0) {
letpieces = serializedOne.split("==");
returnContextKeyEqualsExpr.create(
pieces[0].trim(),
this._deserializeValue(pieces[1], strict)
);
}
if(serializedOne.indexOf("=~") >=0) {
letpieces = serializedOne.split("=~");
returnContextKeyRegexExpr.create(
pieces[0].trim(),
this._deserializeRegexValue(pieces[1], strict)
);
}
if(serializedOne.indexOf(" in ") >=0) {
letpieces = serializedOne.split(" in ");
returnContextKeyInExpr.create(pieces[0].trim(), pieces[1].trim());
}
if(serializedOne.indexOf(">=") >=0) {
constpieces = serializedOne.split(">=");
returnContextKeyGreaterEqualsExpr.create(pieces[0].trim(), pieces[1].trim());
}
if(serializedOne.indexOf(">") >=0) {
constpieces = serializedOne.split(">");
returnContextKeyGreaterExpr.create(pieces[0].trim(), pieces[1].trim());
}
if(serializedOne.indexOf("<=") >=0) {
constpieces = serializedOne.split("<=");
returnContextKeySmallerEqualsExpr.create(pieces[0].trim(), pieces[1].trim());
}
if(serializedOne.indexOf("<") >=0) {
constpieces = serializedOne.split("<");
returnContextKeySmallerExpr.create(pieces[0].trim(), pieces[1].trim());
}
if(/^\!\s*/.test(serializedOne)) {
returnContextKeyNotExpr.create(serializedOne.substr(1).trim());
}
returnContextKeyDefinedExpr.create(serializedOne);
};
终究 when 会被解析为下面的树结构,type 是预先界说对表达式的转义,如下表所示:
ContextKey | Type | ContextKey | Type |
False | 0 | Regex | 7 |
True | 1 | NotRegex | 8 |
Defined | 2 | Or | 9 |
Not | 3 | Greater | 10 |
Equals | 4 | Less | 11 |
NotEquals | 5 | GreaterOrEquals | 12 |
And | 6 | LessOrEquals | 13 |
这儿留了一个很有意思的 Defined 接口,它不归于任何的表达式语法,后续能够这样运用:
exportclassRawContextKey<T>extendsContextKeyDefinedExpr {
privatereadonly _defaultValue: T |undefined;
constructor(key:string, defaultValue: T |undefined) {
super(key);
this._defaultValue = defaultValue;
}
publictoNegated(): ContextKeyExpression {
returnContextKeyExpr.not(this.key);
}
publicisEqualTo(value:string): ContextKeyExpression {
returnContextKeyExpr.equals(this.key, value);
}
publicnotEqualsTo(value:string): ContextKeyExpression {
returnContextKeyExpr.notEquals(this.key, value);
}
}
constExtension =newRawContextKey<string>('resourceExtname',undefined);
Extension.isEqualTo("abc");
constExtensionContext =newMaps();
ExtensionContext.setValue("resourceExtname","abc");
console.log(contextMatchesRules(ExtensionContext, Extension.isEqualTo("abc")));
在任何当地创建一个 ExtensionContext 效果域,再树立键值对来运用 isEqualTo 进行等值比对。
条件表达式分词规矩用一张图来表明,以下面这颗树生成的思路为例,遵从常用表达式的一些语法标准和优先级规矩,优先切割 || 两头的表达式,然后遍历两头的表达式往下去切割 && 表达式,切完一切的 || 和 && 两头的表达式后,再处理子节点的 !=、== 和 >= 等符号。
当切割完好个 when 装备项,将这个树结构结合上面的 ContextKey-Type 映射表,转换出下面的 JS 目标,上面存储着 ContextKeyOrExpr**,**ContextKeyAndExprContextKeyEqualsExpr 和 ContextKeyGreaterOrEqualsExpr 这些重要的规矩类,再将该 JS 目标存储到 MenuRegistry 里边,后边只需遍历 MenuRegistry 就能够把里边存着的 key 和 value ,依据 type 运算规矩取出来进行比对,并回来布尔值。
when: {
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
}
}
上面提到,“window.innerWidth”,canEdit 和“platform”这些是字符串,不是真正可用于判别的值。这些 key 有些是运行时才会得到值,有些是在某个效果域下才会得到值。所以需求将这些 key 进行转化,借鉴Vscode 的做法,在 Vscode 中,它会将这部分逻辑交给一个叫 context 的目标进行处理,它供给两个要害的接口 setValue 和 getValue 办法,简略的完成如下。
exportclassMaps{
protectedreadonly_values =newMap<string, any>();
publicgetvalues(){
returnthis._values;
}
publicgetValue(key:string): any{
if(this._values.has(key)) {
letvalue=this._values.get(key);
// 履行获取最新的值,并回来
if(typeofvalue=="function") {
value=value();
}
returnvalue;
}
}
publicremoveValue(key:string): boolean{
if(keyinthis._values) {
this._values.delete(key);
returntrue;
}
returnfalse;
}
publicsetValue(key:string,value: any){
this._values.set(key,value);
}
}
它实质是保护着一份 Map 目标,需求把“window.innerWidth”,canEdit 和“platform”这些值绑定进去,从而让 key 能够转化对应的变量或许常量。
这儿留意的是 getValue 里边有一段代码是判别是否是函数,假如是函数则履行获取最新的值。这个当地十分要害,因为去搜集 window.innerWidth 这些的值,很可能是实时改变的。需求在判别的时分触发这个回调获取真正最新的值,确保条件表达式解析终究成果的正确性。当然假如是 platform 或许 isMacintosh 这些在运行的时分一般不会变,直接写入即可,不需求每次都触发回调来获取最新的值。
const context =newContext();
context.setValue("platform","pc");
context.setValue("window.innerWidth",()=>window.innerWidth);
context.setValue(
"canEdit",
window.SpreadsheetApp.sheetStatus.rangesStatus.status.canEdit
);
当然有些常量或许全局的固定变量,需求事先预埋,比方字符串“true“ 对应就是 true,字符串“false“对应就是 false:
context.setValue(JSON.stringify(true), true);
context.setValue(JSON.stringify(false), false);
假如要交给第三方装备,就需求提前在这儿规定好 key 值绑定的变量和常量,输出一份装备文档就能够让第三方运用这些要害 key 来进行个性化装备。
那么终究只需封装上面比如用到的 contextMatchesRules 办法,先读取 json 装备文件为目标,遍历出每一个 when,并相关 context 终究得出一个布尔值,这个布尔值来之不易,生成的终究成果其实是一个带布尔值的战略树,这棵树的前后终究节点的意图都是为了求出布尔值,假如是服务端下发的动态装备,实质是 0 和 1 的战略树即可。
完成一个强大的装备体系还能确保整体的质量和功能是很不容易的,上图是实践项目中的一个改造比如,左面的表达式搜集会转化成右边表达式装备,左面一切的 if 会到装备表里边转嫁给接入方或许可视化装备界面,之后每逢变化装备表的信息,都能够合作效果域搜集得到全信的战略树来渲染视图或许更新视图。
04
总结
腾讯文档团队一路走来遇到许多问题、逐个击破,终究才奉献出这个计划。后续期望能输出更多代码回馈开源社区,也期望有更多志同道合的开发者们一起去探究和遨游技术开发常识,终究也期望这篇文章能给到大家一些启发。公众后回复「VSCode」获取源代码。
以上是本次共享全部内容,欢迎大家在谈论区共享交流。假如觉得内容有用,欢迎转发~
-End-
原创作者|姚嘉隆
技术责编|姚嘉隆
最近微信改版啦,有粉丝反应收不到小云的文章。
请重视「腾讯云开发者」并点亮星标,
周一三晚8点 和小云一起涨(领)技(福)术(利)!
开源无国界。开发者群体对于创新和创造的热爱,让「更早更多地参与开源奉献」成为趋势。
-
你怎样了解开源精力?怎样看待当下的开源现状?
-
假如只能给其他开发者引荐一个开源项目,你会引荐什么?
欢迎在公众号谈论区聊一聊你的观点。快来加腾小云的微信(微信号yun_assistant,统一处理时间9:00-18:00),在3月17日前将你的谈论记录截图发送给小云,可领取腾讯云「开发者春季限定红包封面」一个,数量有限先到先得。咱们还将选取点赞量最高的1位朋友,送出腾讯QQ公仔1个。3月22日正午12点开奖。快约请你的开发者朋友们一起来参与吧!
阅读原文