Skip to content

Tutorial: Adding a feature to HyperCRX

Lam Tang edited this page Apr 29, 2024 · 13 revisions

HyperCRX的特性注入机制在论文中有详细介绍:HyperCRX: A Browser Extension for Insights into GitHub Projects and Developers

案例介绍

在这篇教程中,你将通过一个教学案例学会如何为HyperCRX开发一个叫做colorful-calendar的新特性。这个特性用于改变GitHub用户Profile页面的日历图格子的颜色,例如从绿色改成紫色:

image

手把手教程

废话不多说,让我们开始吧!

步骤0. 准备工作

准备好开发环境Node.js、Yarn(npm install --global yarn)、配置Yarn源(yarn config set registry https://registry.npm.taobao.org/);Fork仓库到自己的账号下并clone到本地;如何运行项目(看仓库README);使用Chrome多账户加载开发版HyperCRX或禁用商店版HyperCRX;开发前先创建Git分支;……

步骤1. 为新特性创建一个目录和一个index.tsx文件

在HyperCRX的项目中,src/pages/ContentScripts/features目录包含了所有特性的源码。在该目录下,每个特性都对应着一个目录,并且目录名就是特性的名称。因此,我们要为colorful-calendar这个特性创建一个同名的新目录。

image

创建目录后,我们要在特性目录中创建一个名为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已经出现在特性列表中并处于启用状态:

image

请再访问你的GitHub主页,并打开Chrome DevTools,如果你发现控制台中输出了“init colorful-calendar”,那么恭喜你成功迈出了第一步!

image

温馨提示:刚刚你已经收获了初步的成功,请及时打一个Commit,细粒度的Commit对软件开发是大有裨益的。

步骤2. 对GitHub用户Profile页面的日历元素进行逆向工程

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。没错,我们已经找到了突破口。

image

步骤3. 在index.tsx文件中操作DOM改变日历格子颜色

在步骤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增量编译,页面刷新后,我们就可以看到日历格子的颜色被改变了:

image

怎么样,很酷吧~😎不要忘了做个Commit!

步骤4. 结合GitHub界面设计颜色自定义功能

该步骤属于功能设计环节。对于复杂功能,可以尝试使用专业工具如Figma进行设计;对于如color-calendar这样的简单功能,语雀画板甚至是截图后涂鸦进行表达都是合适的。关键是能正确表达出你的设计,并且和其他人在Issue中交流达成一致后再开始用代码实现。

HyperCRX是为GitHub量身打造的浏览器扩展,因此设计功能时,需要考虑和GitHub原生界面自然融洽。通过什么方式让用户自定义格子颜色呢?我脑子里很快就有了主意,于是我利用截图软件和涂鸦功能表达了我的设计,如下图所示:

image

一图胜千言,我相信不需要额外的文字解释,大家都能get到这个功能设计。

步骤5. 实现颜色自定义功能

antd的ColorPicker让我们无需从头实现颜色选择器组件,而只需要关注如何让5个ColorPicker替换掉日历右下角代表5个level的格子。

“替换”意味着要操作DOM,那么先要利用Inspect工具检索5个格子对应的DOM元素。如下图所示,5个格子对应着5个div元素,每个div都有id,因此可以轻松利用id进行元素定位。

image

下面是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变得很小的目的。

image

只能用点黑科技,即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个小时,最终效果如下:

image

步骤6. 记住用户上次选择的颜色

刚刚,我们实现了利用颜色选择器改变日历格子颜色,但是自定义的颜色会在页面刷新后失效。因此,我们要利用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]);
  }
};

步骤7. 提交PR

推送分支到origin;向upstream提PR;描述好截图录屏等;等待reviewer意见;成功合入后及时将本地master与upstream的master同步;……

总结

在这篇Tutorial中,我们通过渐进式地完成一个demo特性colorful-calendar来熟悉HyperCRX的开发流程。