-
Notifications
You must be signed in to change notification settings - Fork 20
Howto dev ru
Vivaldi построен поверх React - библиотеки для разработки пользовательских интерфейсов. Исходный код Vivaldi состоит из большого числа модулей, которые с помощью Webpack упаковываются в несколько js-файлов. VivaldiHooks предоставляет средства для подмены React-компонентов и того, что экспортируют модули, обеспечивая при этом разработчиков модов возможностью легко менять не только внешний вид, но и поведение, спрятанное глубоко внутри.
Для помощи в исследовании внутреннего устройства браузера существует проект bundleutils, а точнее - скрипт split.js
. Для его настройки нужно установить Node.js, а в папке со скриптом выполнить команду
npm install
Для запуска нужно скопировать в эту же папку оригинальные js файлы Vivaldi, а затем выполнить команду
node split.js
Результатом будут папки с отдельными модулями, большинство из которых будут иметь осмысленные названия. Внутри же файлов аргументы метода require (минифицированные имена чаще всего - n
или a
) - номера модулей - дополняются комментариями с их названиями, если таковые найдены. Пример:
var i = a(0 /* React_wrapper */),
s = a(10 /* PageActions */),
n = a(2 /* _getLocalizedMessage */),
r = a(126 /* settings_keywordFound */),
o = a(97),
Если вы пользуетесь редактором исходных кодов с поддержкой рефакторинга, удобно сразу переименовывать такие переменные в имена модулей - понимать такой модуль будет гораздо легче
}), d(this, "openPrivacyPage", e => {
PageActions.a.openURL(u), e.preventDefault()
}), this.state = {
Также в случае, если какой-то модуль экспортирует svg изображение, само изображение будет сохранено отдельно.
Итак, имена файлов содержат имена, с помощью которых моды могут использовать или модифицировать модули. Например, наличие файла jsout_name/React.js
значит, что мод может обратиться к модулю "React":
const ReactModule = vivaldi.jdhooks.require("React")
//...
return ReactModule.createElement("div", {}, "Text")
или модифицировать его
vivaldi.hookModule("React", //..
Немного сложнее с React-классами.
class n extends React.PureComponent {
render() {
const {
children: e,
filter: t
} = this.props;
return t ? Array.isArray(e) ? e.map((e, a) => React.createElement(React.Fragment, {
key: a,
__source: {
fileName: "C:\\new-bot\\new-builder\\workers\\ow32\\build\\vivaldi\\vivapp\\src\\components\\settings\\SettingsSearchCategoryChild.jsx",
lineNumber: 18
}
}, Object(s.a)(e, t))) : Object(s.a)(e, t) : e
}
}
VivaldiHooks использует значение fileName
для обращения к классу по имени. Именем класса будет часть строки после "components" и до расширения с заменой всех \\
или /
на _
. В данном случае именем класса будет "settings_SettingsSearchCategoryChild". Стоит отметить, что не всегда fileName
находится внутри определения класса, гораздо чаще строка хранится в отдельной переменной, определённой до определения класса. Пример:
var i = a(117 /* common_Favicon */),
s = a(0 /* React_wrapper */),
n = a.n(s),
r = a(13 /* _VivaldiIcons */),
o = "C:\\new-bot\\new-builder\\workers\\ow32\\build\\vivaldi\\vivapp\\src\\components\\tabs\\LoadingFavicon.jsx";
class l extends n.a.PureComponent {
Если в одном модуле объявлено несколько классов, может возникнуть путаница, поэтому используйте рефакторинг чтобы однозначно определить, в каком классе используется каждая такая переменная.
vivaldi.jdhooks.addStyle
(style, /*optional*/
description)
Используется для добавления наборов стилей непосредственно из js мода.
vivaldi.jdhooks.addStyle(`
.quick-command:not([data-selected]) .quick-command-close-tab { display: none }
.quick-command[data-selected] .quick-command-close-tab { background-color: rgba(0, 0, 0, .15)}
`, "qc-close-tab.js")
Параметр description
информационный, его значение будет добавлено в одноимённый атрибут стиля в head секции страницы.
vivaldi.jdhooks.hookClass
(className, /*class => newClass*/
callback)
Основной метод для подмены поведения React-классов. Колбэк получает аргументом старый (или подменённый другим модом) класс, должен вернуть либо его, либо новый.
vivaldi.jdhooks.hookClass("find-in-page_FindInPage", cls => {
class newFindInPage extends cls {
constructor(...e) {
super(...e)
this.populateWithPageSelection = () => { this.focusFindInPageInput() }
}
}
return newFindInPage
})
vivaldi.jdhooks.hookModule
(moduleName, /*(moduleInfo, exports) => newExports*/
callback)
Вызывает колбэк после загрузки и инициализации модуля с именем moduleName, позволяет переопределить экспорт указанного модуля, изменив этим его поведение. Колбэк должен вернуть новое значение экспорта. Пример - добавление нового значения по умолчанию для кастомного параметра настроек:
vivaldi.jdhooks.hookModule("vivaldiSettings", (moduleInfo, exports) => {
let oldGetDefault = exports.getDefault
exports.getDefault = name => {
switch (name) {
case "BOOKMARK_BUTTON_POSITION": return "value"
default: return oldGetDefault(name)
}
}
return exports
})
`hookModule` не рекомендуется использовать и он будет удалён в ближайшее время
Модуль vivaldiSettings
экспортирует объект, имеющий множество методов, один из которых - getDefault
- возвращает значение по умолчанию для указанного параметра.
vivaldi.jdhooks.require
(moduleId, exportName)
Возвращает экспорт указанного модуля. Идентификатором модуля может быть как номер модуля (может быть полезно при отладке), так и строка с именем. Главный метод для переиспользования компонентов Vivaldi.
const UrlFieldActions = vivaldi.jdhooks.require('_UrlFieldActions')
UrlFieldActions.go(["https://vivaldi.com"], {
inCurrent: false,
addTypedHistory: true,
addTypedSearchHistory: false,
enableSearch: true
})
Иногда модуль экспортирует несколько сущностей (например несколько модулей, объединенных webpack-плагином ModuleConcatenationPlugin
). Для указания, какая из них требуется, используется параметр exportName
. Значение по умолчанию "default".
Если модуль экспортирует не объект, параметр игнорируется и require
возвращает экспорты как есть.
Если exportName
= "*", require
возвращает экспорты как есть.
Если exportName
совпадает с именем члена экспортов модуля, возвращается его значение.
Если exportName
= "default" и модуль экспортирует объект с единственным членом, возвращается значение члена.
Внимание: require
можно вызывать только из колбэков VivaldiHook API или из функций, вызываемых этими колбэками. Это связано с тем, что require вызывает принудительную инициализацию зависимых модулей, так что некоторые модули могут загрузиться раньше, чем какой-нибудь мод успел загрузиться и вызвать hookModule.
vivaldi.jdhooks.insertWatcher
(class, /*{settings:[], prefs:[]}*/
params)
Расширяет класс class
так, что он начинает следить за изменениями настроек (settings/prefs). Настройки, доступные из модуля vivaldiSettings
, будут сохраняться в this.state.jdVivaldiSettings.{keyname}
, настройки, доступные из PrefsCache
- в this.state.jdPrefs["{keyname}"]
. Пример:
return vivaldi.jdhooks.insertWatcher(downloadTabWebpageContent,
{
settings: ["SHOW_DOWNLOADTAB_FOR_NEW_DOWNLOADS"],
prefs: ["vivaldi.tabs.visible", "vivaldi.clock.mode"]
})
Внутри класса:
const val1 = this.state.jdVivaldiSettings.SHOW_DOWNLOADTAB_FOR_NEW_DOWNLOADS
const val2 = this.state.jdPrefs["vivaldi.tabs.visible"]
Не передавайте результат этой функции напрямую в React.createElement directly, сохраняйте его вне метода render
, а лучше вне перехваченного класса. Используйте модуль _PrefKeys
для доступа к именам prefs.
vivaldi.jdhooks.onUIReady
(callback)
Устаревший метод, используется для модов, основной инструмент которых - обработчики событий DOM. Колбэк вызывается после инициализации UI и его монтирования в DOM.
vivaldi.jdhooks.onUIReady(() => {
document.addEventListener("mouseup", event => {
React оперирует виртуальным DOM, созданные компоненты могут не существовать в реальном DOM какое-то время после отрисовки (рендеринга). Ознакомьтесь с описанием жизненного цикла React-компонентов.
DOM в React-приложении - это набор вложенных друг в друга элементов, созданных вызовом React.createElement.
render() {
return React.createElement(settings_SettingsSearchCategoryChild, { filter: this.props.filter },
React.createElement("h2", null, "Hooks"),
React.createElement("div", { className: "setting-group unlimited" },
React.createElement("div", { className: "setting-single" },
React.createElement("label", null,
React.createElement("input", {
type: "checkbox",
checked: this.state.defaultLoad,
onChange: this.toggleDefaultLoad.bind(this)
}),
React.createElement("span", null, "Startup mode for new items")
)
),
React.createElement("div", {
className: "setting-group unlimited pad-top"
},
Самый наглядный способ модификации React-класса - изменение результата вызова render
оригинального класса. Для этого модифицируется значение .props.children
результата. В целях ознакомления рекомендую перехватить какой-нибудь класс и переопределить его метод render таким образом и посмотреть вывод в консоли:
render()
{
let r = super.render() //call original render
console.log("new render", {this: this, rendered: r})
return r
}
Если в классе, переопределённом вызовом hookClass, вы реализуете методы componentDidMount
или componentWillUnmount
, проверяйте существование этих методов в родительском классе и вызывайте их, если они существуют. Это поможет избежать ошибок, если два мода будут реализовывать эти методы для одного и того же перехваченного класса.
componentWillUnmount() {
//do some work
//...
if (super.componentWillUnmount) super.componentWillUnmount()
}
Иногда методы в исходных классах определяются не статически, как элементы класса, а динамически - во время выполнения, как элементы созданного объекта. Переопределять такие методы можно также только во время выполнения. Пример части исходного класса (не очень хорошо отформатировано - результат работы js-beautify):
}), fe(this, "attachEventHandlers", () => {
q.a.addChangeListener(this._onWebpageviewStoreChanged)
}), fe(this, "removeEventHandlers", () => {
q.a.removeChangeListener(this._onWebpageviewStoreChanged)
}), fe(this, "attachWebviewHandlers", () => {
Object.keys(_e).forEach(e => {
fe
в данном случае - обёртка над Object.defineProperty
. Пример перехвата метода, определённого таким образом:
class newClass extends oldClass {
constructor(...e) {
super(...e)
const old_handleLoadStop = this.handleLoadStop
this.handleLoadStop = e => {
//do some work
//...
return old_handleLoadStop(e)
}
Если новая реализация render зависит от значений, от которых не зависит оригинальная (например, вы хотите отображать в компоненте текущее время), стоит внедрять эти значения в состояние компонента и изменять состояние с помощью вызова this.setState
. В противном случае скорее всего компонент не заметит изменения и не будет перерисован.
В то время, как split.js
способен разделить бандлы на отдельные файлы и отформатировать код, он не может анализировать экспорт модулей. Для быстрого анализа можно воспользоваться модом dev-dumpVivaldi.js
, который добавляет в консоль функцию vivaldi.jdhooks.dumpVivaldi()
. Результат её выполнения - экспорт модулей, отсортированный по категориям. Например, для просмотра функций, работающих с настройками, можно найти и раскрыть в выводе группу objects
, а в ней vivaldiSettings
.