Skip to content

Howto dev ru

den_po edited this page Jun 12, 2021 · 5 revisions

Vivaldi internals

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 {

Если в одном модуле объявлено несколько классов, может возникнуть путаница, поэтому используйте рефакторинг чтобы однозначно определить, в каком классе используется каждая такая переменная.

Hooks API

addStyle

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 секции страницы.

hookClass

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
})

hookModule

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 - возвращает значение по умолчанию для указанного параметра.

require

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.

insertWatcher

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.

onUIReady

vivaldi.jdhooks.onUIReady(callback)

Устаревший метод, используется для модов, основной инструмент которых - обработчики событий DOM. Колбэк вызывается после инициализации UI и его монтирования в DOM.

vivaldi.jdhooks.onUIReady(() => {
	document.addEventListener("mouseup", event => {

React

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. В противном случае скорее всего компонент не заметит изменения и не будет перерисован.

Modules

В то время, как split.js способен разделить бандлы на отдельные файлы и отформатировать код, он не может анализировать экспорт модулей. Для быстрого анализа можно воспользоваться модом dev-dumpVivaldi.js, который добавляет в консоль функцию vivaldi.jdhooks.dumpVivaldi(). Результат её выполнения - экспорт модулей, отсортированный по категориям. Например, для просмотра функций, работающих с настройками, можно найти и раскрыть в выводе группу objects, а в ней vivaldiSettings.

Clone this wiki locally