-
Notifications
You must be signed in to change notification settings - Fork 102
Tutorial: Adding a feature to HyperCRX
HyperCRX的特性注入机制在论文中有详细介绍:HyperCRX: A Browser Extension for Insights into GitHub Projects and Developers
在这篇教程中,你将通过一个教学案例学会如何为HyperCRX开发一个叫做colorful-calendar的新特性。这个特性用于改变GitHub用户Profile页面的日历图格子的颜色,例如从绿色改成紫色:
废话不多说,让我们开始吧!
准备好开发环境Node.js、Yarn(npm install --global yarn
)、配置Yarn源(yarn config set registry https://registry.npm.taobao.org/
);Fork仓库到自己的账号下并clone到本地;如何运行项目(看仓库README);使用Chrome多账户加载开发版HyperCRX或禁用商店版HyperCRX;开发前先创建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;
if (level === 0) {
root.style.setProperty(`--color-calendar-graph-day-bg`, color);
} else {
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;
}
保存index.scss文件后将index.tsx中的import ./index.scss;
取消注释。样式优化后的效果如上图右边所示。整个实现过程花费了约3个小时,最终效果如下:
刚刚,我们实现了利用颜色选择器改变日历格子颜色,但是自定义的颜色会在页面刷新后失效。因此,我们要利用chrome.storage.local API对用户的设置进行持久化。API的使用较为简单,唯一需要注意的是,storage.local的各个方法都是异步的。我们利用该API增加两个操作:在初始化时读取颜色配置、在改变颜色时更新颜色配置。代码做如下更新:
let colors = ['#ebedf0', '#ffedf9', '#ffc3eb', '#ff3ebf', '#c70085'];
const changeLevelColor = async (level: number, color: string) => {
const root = document.documentElement;
if (level === 0) {
root.style.setProperty(`--color-calendar-graph-day-bg`, color);
} else {
root.style.setProperty(`--color-calendar-graph-day-L${level}-bg`, color);
}
// Save to storage
const newColors = [...colors];
newColors[level] = color;
await chrome.storage.local.set({
calendar_level_colors: newColors,
});
};
const init = async (): Promise<void> => {
// Load colors from storage
colors =
(await chrome.storage.local.get('calendar_level_colors'))[
'calendar_level_colors'
] || colors;
for (let i = 0; i < colors.length; i++) {
changeLevelColor(i, colors[i]);
replaceLegendToColorPicker(i, colors[i]);
}
};
推送分支到origin;向upstream提PR;描述好截图录屏等;等待reviewer意见;成功合入后及时将本地master与upstream的master同步;……
在这篇Tutorial中,我们通过渐进式地完成一个demo特性colorful-calendar来熟悉HyperCRX的开发流程。