-
Notifications
You must be signed in to change notification settings - Fork 0
Description
VSCode 作为当今大热的代码编辑器,其依赖前端技术与 electron 搭建成桌面软件,本质上为 web 应用。探索其源码,对开发 web 应用有较大帮助。其中,主题服务的实现,包含了自定义的事件机制监听改动,依赖注入获取全局服务对象的应用,动态修改组件样式与全局样式的方法,值得探讨。
VSCode 没有使用现成的前端框架,而是自己实现了各种基础类,甚至其各种 UI 元素。在页面布局中,像是标题栏,状态栏,侧边栏等各个部分(Part)被分在 src/vs/workbench/browser/parts 文件夹下。这些部分都带有主题样式动态修改的需求。
举个栗子
这里我们以 statusbar (状态栏) 为例:
export class StatusBarPart extends Part implements IStatusBarService, ISerializableView {
// 其他私有属性
// 负责插入 styleSheet 的元素
private styleElement: HTMLStyleElement
// 在构造函数中,利用依赖注入的方式将主题服务绑定到私有属性 themeService 上,并通过 super 传递给所继承的类(即 Part)
constructor(
id: string,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
// 其他参数
) {
super(id, { hasTitle: false }, themeService, storageService)
// 其他初始化方法
}
// 更新每个组件样式的函数
protected updateStyles(): void {
super.updateStyles();
// 获取该组件的容器
const container = this.getContainer();
// 通过 style 属性更改元素样式颜色
const backgroundColor = this.getColor('该部件的颜色id')
container.style.backgroundColor = backgroundColor;
// 省略其他同样通过 style 属性更改样式的操作
if (!this.styleElement) {
// 创建一个 styleSheet 元素
this.styleElement = createStyleSheet(container)
}
// 直接改变 styleSheet 元素的 innerHTML 属性插入 css 样式。可见主要是为了改变伪类的样式
this.styleElement.innerHTML = `.monaco-workbench .part.statusbar > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor}; }`
}
}关于 Typescript 的依赖注入,能避免手动创建各个类所需的依赖项,具体细节以后会专门探讨。
这里我们说说常用的利用 js 来改变已有元素样式的方法:
// 一般先通过 document.querySelector 之类的方法拿到已有元素
let ele = document.querySelector('.app');
// 然后利用 style 属性来更改目标样式
ele.style.backgroundColor = '#e5e5e5'但这种方法有个缺陷,不能改变伪类的样式。
什么是伪类?简单的说就是带冒号的。常见的伪类:
- :hover 控制悬浮样式
- :focus 控制获取焦点后的样式
- :before / :after 前置与后置元素
伪类的样式无法通过 style 属性更改,一般只能利用 css 样式来变更。要动态的往页面中插入 css 样式表,来看下 updateStyles 方法中的 createStyleSheet 方法的实现:
位置 src/vs/base/browser/dom.ts
export function createStyleSheet(container: HTMLElement = document.getElementByTagName('head')[0]): HTMLStyleElement {
let style = document.createElement('style');
style.type = 'text/css';
style.media = 'screen';
container.appendChild('style');
return style;
}可见,该方法单纯向目标元素末尾,加入一个 <style /> 元素并返回。将返回的 style 元素作为组件的私有变量,所属组件通过改变其 innerHTML 属性达到动态变更 css 内容的目的。
深入一点
现在来分析一下主题变更的过程。沿着 StatusBarPart 的继承关系往上找,可以找到 Themable 类:
位置:src/vs/workbench/common/theme
export class Themable extends Disposable {
protected theme: ITheme;
constructor(protected themeService: IThemeService) {
super();
this.theme = themeSercice.getTheme()
// 绑定当主题服务变更主题时,执行 onThemeChange 函数
this._register(this.themeService.onThemeChange(theme => this.onThemeChange(theme)));
}
// 主题变更时
protected onThemeChange(theme: ITheme): void {
this.theme = theme;
this.updateStyles();
}
protected updateStyles(): void { /* 由具体组件实现 */ }
// 根据 id 返回具体颜色信息
protected getColor(id: string, modify?: (color: Color, theme: ITheme) => Color): string | null {
let color = this.theme.getColor(id);
if (color && modify) {
color = modify(color, this.theme);
}
return color ? color.toString() : null;
}
}Themable 类拥有一个 theme 对象,由 themeService.getTheme 获取,每次更换主题时,重新设置 theme 对象,并且各个组件执行 updateStyles 更新样式。
可见继承自 Themeable 的类都应该实现 updateStyles 方法,用于每次更新主题时调用,变更样式。
Themeable 类也提供了 getColor 方法,从属性 theme 中,按指定 id 获取颜色信息。
关注一下构造函数中的事件绑定:
this._register(this.themeService.onThemeChange(theme => this.onThemeChange(theme)));这里分为两部分,第一部分是括号里的:
this.themeService.onThemeChange(theme => this.onThemeChange(theme))themeService 对象的 onThemeChange 方法返回的其实是自定义的 Emitter 类的 event 属性,为一个自定义的 Event 实例。该 Event 实例接受一个回调函数 linstener: (e: T) => any ,返回一个带有 dispose 方法的对象。回调函数会被插入 Emitter 实例的回调函数链表中,当 Emitter 实例执行 fire 方法时,链表中的回调函数便会相继执行。
再看 this._register 方法,由于 Themable 类继承自 Disposable 类,_register 方法由 Disposable 类实现。
Disposable 是 VSCode 中定义的可手动执行清理的类,主要用于清理一些中间数据。内部维护一个 _toDispose 数组,_register 方法便是将带有 dispose 方法的对象加入该数组中。当 Disposable 类的实例执行 dispose 方法(也由 Disposable 类实现)时,_toDispose 数组中的各个项便会依次执行自己的 dispose 方法。
其主要是进行一些垃圾清理的工作。
关于 VSCode 的事件机制实现,会在以后专门探讨。
看下 ThemeService
ThemeService 与 Theme 类的具体实现:
位置:src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts
// Theme 类
class StandaloneTheme implements IStandaloneTheme {
public readonly id: string;
public readonly themeName: string; // 主题名
private themeData: IStandaloneThemeData; // 具体的颜色数据
private colors: { [colorId: string]: Color } | null; // 颜色映射
private _tokenTheme: TokenTheme | null; // 用于代码着色
public getColor(colorId: ColorIdentifier, useDefault?: boolean): Color | undefined {} // 获取指定 id 的颜色
// other func
}
// ThemeService 类
class StandaloneThemeServiceImpl implements IStandaloneThemeService {
private _knownThemes: Map<string, StandaloneTheme>; // 已知主题
private _theme: IStandaloneTheme; // 当前主题
private _styleElement: HTMLStyleElement; // 主题的全局样式
private _onThemeChange: Emitter<IStandaloneTheme>; // 主题更改时的事件发射器
// others
constructor() {
this._onThemeChange = new Emitter<IStandaloneTheme>();
this._knownThemes = new Map<string, StandaloneTheme>();
this._styleElement = dom.createStyleSheet();
// others
}
// 获取主题改动时的事件
public get onThemeChange(): Event<IStandaloneTheme> {
return this._onThemeChange.event; // 这里直接返回 Event 类型的实例
}
// 定义主题
public defineTheme(themeName: string, themeData: IStandaloneThemeData): void {
// 省略前面验证数据有效性的操作
// 设置为已知主题或进行更新
this._knowThemes.set(themeName, new StandaloneTheme(themeName, themeData));
// 若定义的为基础主题,其他继承该基础主题的主题进行基础更新
if (isBuiltinTheme(themeName)) {
this._knowThemes.forEach(theme => {
if (theme.base === themeName) {
theme.notifyBaseUpdated();
}
})
}
// 若定义的为当前使用的主题,则刷新一下主题
if (this._theme && this._theme.themeName === themeName) {
this.setTheme(themeName)
}
}
// 获取当前主题
public getTheme(): IStandaloneTheme {
return this._theme;
}
// 设置主题
public setTheme(themeName: string): string {
let theme: StandaloneTheme;
if (this._knownThemes.has(themeName)) {
theme = this._knownThemes.get(themeName)!;
} else {
// 若设置的为非已知主题,则取默认主题
theme = this._knownThemes.get(VS_THEME_NAME)!;
}
if (this._theme === theme) {
return theme.id; // 重复设置已用主题的话,仅返回主题id
}
this._theme = theme;
// 以下是一些全局的样式设置
let cssRules: string[] = []; // 最终样式表
let hasRule: { [rule: string]: boolean; } = {}; // 确保不添加重复样式
let ruleCollector: ICssStyleCollector = {
addRule: (rule: string) => {
if (!hasRule[rule]) {
cssRules.push(rule);
hasRule[rule] = true;
}
}
};
// 辅助转换主题到css样式的方法
themingRegistry.getThemingParticipants().forEach(p => p(theme, ruleCollector, this.environment));
// 省略代码高亮相关样式的添加 ...
// 插入样式到页面中
this._styleElement.innerHTML = cssRule.join('\n');
// 设置颜色映射
TokenizationRegistry.setColorMap(colorMap);
// 触发主题更改的事件,参数为新主题对象
this._onThemeChange.fire(theme);
return theme.id
}
}