-
Notifications
You must be signed in to change notification settings - Fork 102
Tutorial: Adding a feature to HyperCRX
As discussed in #548 and its related issues, HyperCRX adopts a specifically designed mechanism to load all of its features. The feature loading mechanism is well explained in our latest paper HyperCRX: A Browser Extension for Insights into GitHub Projects and Developers which was accepted by ICSE 2024. So in this tutorial, we will not delve into the details of the feature loading mechanism. If you have any questions about this mechanism, please read the relevant source code and the paper aforenamed.
在这篇教程中,你将通过一个教学案例学会如何为HyperCRX开发一个叫做colorful-calendar的新特性。这个特性用于改变GitHub用户Profile页面的日历图格子的颜色,例如从绿色改成紫色:
废话不多说,让我们开始吧!
准备好开发环境Node.js Yarn 配置NPM源;如何运行项目;使用Chrome X-lab账号;开发前先创建Git分之;……
在HyperCRX的项目中,src/pages/ContentScripts/features
目录包含了所有特性的源码。在该目录下,每个特性都对应着一个目录,并且目录名就是特性的名称。因此,我们要为colorful-calendar这个特性创建一个同名的新目录。
创建目录后,我们要在特性目录中创建一个名为index.tsx的新文件,所有特性的目录下都有index.tsx文件,这个文件是特性的入口文件。我们将下面代码填入文件中(对此段代码的解释会在后文中展开):
import features from '../../../../feature-manager';
import * as pageDetect from 'github-url-detection';
const featureId = features.getFeatureID(import.meta.url);
const init = async (): Promise<void> => {
console.log('init colorful-calendar');
};
const restore = async () => {
console.log('restore colorful-calendar');
};
features.add(featureId, {
asLongAs: [pageDetect.isUserProfile],
awaitDomReady: false,
init,
restore,
});
在创建了特性目录和index.tsx文件后,我们在src/pages/ContentScript/index.ts
中引入该特性,如下所示:
// 省略其他已有特性的 import
// ...
import './features/colorful-calendar';
在此文件中引入新特性后,需要杀掉开发进程并重新运行yarn run start
来生成新的特性列表,因为feature-loader.cjs只会在项目初次构建时运行一次。之后,在浏览器chrome://extensions/页面中点击按钮重载HyperCRX扩展程序,然后打开HyperCRX的选项页面,你将发现colorful-calendar已经出现在特性列表中并处于启用状态:
请再访问你的GitHub主页,并打开Chrome DevTools,如果你发现控制台中输出了“init colorful-calendar”,那么恭喜你成功迈出了第一步!
温馨提示:刚刚你已经收获了初步的成功,请及时打一个Commit,细粒度的Commit对软件开发是大有裨益的。
HyperCRX是一款为GitHub量身打造的浏览器扩展,所谓量身打造,就是通过分析GitHub页面DOM元素,寻找突破口,然后通过浏览器扩展Content Script能力操纵宿主DOM达到目的。为了改变日历格子的颜色,我们先要了解日历格子对应的DOM元素。如下图所示,点击Chrome DevTools的Inspect按钮检视日历格子,定位其在DOM树中的位置,发现它是用div元素实现的。在右侧属性面板中,我们可以轻易发现和颜色有关的CSS样式属性,通过与属性面板进行交互,可以确定var(--color-calendar-graph-day-L1/2/3/4-bg)
是控制格子颜色的CSS Variables。没错,我们已经找到了突破口。
在步骤1中,我们在新建的index.tsx中写了一些代码,现在我通过代码注释的方式对这段代码做些解释:
import features from '../../../../feature-manager'; // 导入特性管理器模块
import * as pageDetect from 'github-url-detection'; // 导入第三方的GitHub页面检测模块
const featureId = features.getFeatureID(import.meta.url); // 通过特性管理器的getFeatureID方法获取当前特性的ID
const init = async (): Promise<void> => { // 该特性的初始化工作都在这里进行
console.log('init colorful-calendar');
};
const restore = async () => { // 在GitHub的restoration visit后运行,对于此特性可以不需要在该函数中写内容,详见论文
console.log('restore colorful-calendar');
};
features.add(featureId, { // 调用特性管理器的add方法添加特性,第一个参数是ID,第二个参数是meta信息配置对象
asLongAs: [pageDetect.isUserProfile], // 表示“只有当前页面是用户Profile页面时才运行该特性”
awaitDomReady: false, // 是否等待DOM加载完毕,如无特殊情况,都置为false
init, // 指明初始化函数,"init,"是"init: init,"的简写,这是ES6的特性
restore,
});
所以,init函数是我们要写代码的地方。浏览器扩展赋予我们Content Script的能力,该能力允许我们直接访问宿主页面的DOM并进行操作。下面代码通过改变我们在步骤2中确认的相关CSS Variables的值实现了改变日历格子颜色的目的:
const init = async (): Promise<void> => {
const root = document.documentElement;
root.style.setProperty('--color-calendar-graph-day-L1-bg', '#ffedf9');
root.style.setProperty('--color-calendar-graph-day-L2-bg', '#ffc3eb');
root.style.setProperty('--color-calendar-graph-day-L3-bg', '#ff3ebf');
root.style.setProperty('--color-calendar-graph-day-L4-bg', '#c70085');
};
保存代码,Webpack增量编译,页面刷新后,我们就可以看到日历格子的颜色被改变了:
怎么样,很酷吧~😎不要忘了做个Commit!
该步骤属于功能设计环节。对于复杂功能,可以尝试使用专业工具如Figma进行设计;对于如color-calendar这样的简单功能,语雀画板甚至是截图后涂鸦进行表达都是合适的。关键是能正确表达出你的设计,并且和其他人在Issue中交流达成一致后再开始用代码实现。
HyperCRX是为GitHub量身打造的浏览器扩展,因此设计功能时,需要考虑和GitHub原生界面自然融洽。通过什么方式让用户自定义格子颜色呢?我脑子里很快就有了主意,于是我利用截图软件和涂鸦功能表达了我的设计,如下图所示:
一图胜千言,我相信不需要额外的文字解释,大家都能get到这个功能设计。
antd的ColorPicker让我们无需从头实现颜色选择器组件,而只需要关注如何让5个ColorPicker替换掉日历右下角代表5个level的格子。
“替换”意味着要操作DOM,那么先要利用Inspect工具检索5个格子对应的DOM元素。如下图所示,5个格子对应着5个div元素,每个div都有id,因此可以轻松利用id进行元素定位。
下面是index.tsx代码(包含了必要的注释):
import features from '../../../../feature-manager';
import waitFor from '../../../../helpers/wait-for';
import React from 'react';
import { render } from 'react-dom';
import { ColorPicker } from 'antd';
import $ from 'jquery';
import * as pageDetect from 'github-url-detection';
// import './index.scss'; // 需要引入自定义的样式来覆盖antd ColorPicker的默认样式,后面展开说明
const featureId = features.getFeatureID(import.meta.url);
const CALENDAR_LEVEL_COLORS = [ '#ebedf0', '#ffedf9', '#ffc3eb', '#ff3ebf', '#c70085' ];
const changeLevelColor = (level: number, color: string) => {
const root = document.documentElement;
root.style.setProperty(`--color-calendar-graph-day-L${level}-bg`, color);
};
const replaceLegendToColorPicker = async (level: number, defaultColor: string) => {
const legendSelector = `#contribution-graph-legend-level-${level}`; // 选择器selector是用于定位DOM元素的字符串
await waitFor(() => $(legendSelector).length > 0); // init函数运行的时候,页面中某些元素不一定已经加载完毕,经过测试,日历图加载时机比较靠后,因此需要waitFor一下,不然后面的操作都是无用的
const $legend = $(legendSelector);
const container = $('<div></div>');
render(
<ColorPicker defaultValue={defaultColor} size="small" onChange={(color, hex) => changeLevelColor(level, hex)} />, // 选择新颜色后会调用changeLevelColor改变格子颜色
container[0]
); // 将React组件渲染为真实的DOM元素
$legend.replaceWith(container); // 使用jQuery的replaceWith方法将图例格子替换为ColorPicker
};
const init = async (): Promise<void> => {
for (let i = 0; i < CALENDAR_LEVEL_COLORS.length; i++) {
changeLevelColor(i, CALENDAR_LEVEL_COLORS[i]); // 初始化时就按照给定的颜色改变日历格子的颜色
await replaceLegendToColorPicker(i, CALENDAR_LEVEL_COLORS[i]);
}
};
const restore = async () => {
console.log('restore colorful-calendar');
};
features.add(featureId, {
asLongAs: [pageDetect.isUserProfile],
awaitDomReady: false,
init,
restore,
});
上面的代码保存后会得到如下图左边所示的结果:每个ColorPicker都很大,和GitHub原生的小格子风格迥异,总之看着不美观。这是因为antd是一套设计,它有自己的一套设计理念和风格,其暴露给开发者用于调整样式的API是有限的,所以无法通过API达到让ColorPicker变得很小的目的。
只能用点黑科技,即CSS样式覆盖:使用DevTools Inspect工具检查antd ColorPicker DOM树上各级元素的样式信息,然后引入自定义的CSS样式覆盖掉需要修改的样式。下面的index.scss文件中的覆盖样式是一个半小时反复检查和尝试的结果:
.ant-color-picker-trigger {
min-width: 10px !important;
padding: 0 !important;
margin-right: 4px;
border: none !important;
}
.ant-color-picker-color-block {
width: 10px !important;
min-width: 10px !important;
height: 10px !important;
}
.ant-color-picker-color-block-inner {
width: 10px !important;
min-width: 10px !important;
height: 10px !important;
border-radius: 3px !important;
}
样式优化后的效果如上图右边所示。整个实现过程花费了约3个小时,最终效果如下:
xxx
推送分支到origin;向upstream提PR;描述好截图录屏等;等待reviewer意见;成功合入后及时将本地master与upstream的master同步;……
……