В прошлый раз мы разобрали, как устроены браузеры и что нужно для создания веб-браузера. Теперь соберём свой браузер из готовых компонентов и пойдём по пути Кристофера Нолана: сделаем страницу, которая будет работать как браузер, и превратим эту страницу в отдельный браузер. Браузер внутри браузера внутри фреймворка — всё как мы любим. В итоге получится вот такое:
Что делаем
Сначала мы напишем HTML-страницу, которая будет работать как браузер, а потом возьмём фреймворк ElectronJS, чтобы превратить её в полноценное приложение. В Электроне много готовых компонентов и инструментов отладки для создания кроссплатформенных настольных приложений на HTML, CSS и JavaScript. Фреймворк объединяет движок браузера Chromium и библиотеку Node.js, так что, по сути, нужно только сделать интерфейс, а Electron сделает всё остальное. В итоге мы получим минимальную рабочую версию браузера.
Порядок работы будет такой:
- Установим Node.js и npm — его менеджер пакетов.
- Установим Electron.
- Создадим папку проекта.
- Добавим в неё файлы package.json, main.js, preload.js, index.html, style.css.
- Добавим в package.json описание проекта, а в раздел
scripts
— команду для запуска приложения. - Сделаем интерфейс на index.html и добавим стили в style.css.
- Напишем код для создания основного окна приложения и загрузки в него файла index.html.
- Пропишем в preload.js скрипт для безопасного взаимодействия между основным процессом и рендер-процессом. Этот файл позволяет передавать функции и данные из основного процесса, где работает main.js, в рендер-процесс, где выполняется код HTML и JavaScript.
Теперь сделаем всё это по очереди.
Устанавливаем Node.js, npm и ElectronJS
Для начала нужно установить Node.js — для этого заходим на официальный сайт Node.js и следуем инструкциям для своей операционной системы.
👉 Node.js — это система, которая исполняет JavaScript отдельно от вашего браузера. Можно сказать, что это самостоятельная среда для выполнения JavaScript. В состав Node.js ещё входит npm — менеджер пакетов, который поможет нам со сборкой. Пакет — это маленькая служебная программа, к которой можно обращаться из своей основной программы за помощью в каких-то делах. Если нам нужен пакет, которого ещё нет на компьютере, его можно установить командой npm install <имя пакета>
.
После установки Node.js ставим ElectronJS. Для этого открываем командную строку и пишем такую команду:
npm install electron --save-dev
Она установит Электрон и скачает все нужные файлы на компьютер, чтобы не выкачивать их каждый раз из Сети.
Создаём папку проекта и добавляем файлы
Далее создаём папку проекта, где будут лежать все наши файлы, и инициализируем npm-проект. Инициализация — это как бы старт проекта, когда мы говорим системе, что мы хотим начать делать что-то новое, а она в ответ создаёт и наполняет разные служебные файлы.
Один из таких файлов — package.json, в котором находятся метаданные о проекте и список зависимостей. Для инициализации проекта в терминале вводим команду:
npm init
После инициализации файл автоматически заполнится стартовыми данными, которые мы можем поправить при необходимости:
Теперь открываем в редакторе кода папку проекта и создаём в ней следующие файлы:
- main.js — основной файл, который будет запускать приложение;
- preload.js — файл для безопасного взаимодействия между основным процессом и рендер-процессом;
- index.html — главная HTML-страница;
- style.css — файл стилей.
Добавляем описание и команду
Открываем package.json и редактируем его — вводим название проекта, информацию об авторе и версиях. Сохраняем: файл package.json обновится, и система скажет нам об этом.
{
"name": "kodium",
"version": "1.0.0",
"main": "main.js",
"devDependencies": {
"electron": "^latest",
},
"scripts": {
"start": "electron main.js"
},
"description": "simple browser",
"author": "Kristina",
"license": "ISC"
}
Создаём разметку страницы
Наша задача здесь — наполнить HTML-страницу index.html всеми нужными элементами и подключёнными стилями и скриптами, чтобы можно было их позже написать и сразу использовать в проекте. Логически страница состоит из таких элементов:
- навигационная панель с полем для ввода адреса и кнопкой перехода;
- контейнер, в котором отображается загруженная новая страница, куда мы перешли из навигации.
Когда мы нажимаем кнопку, страница берёт адрес из поля ввода, загружает внутрь себя новую страницу и показывает её в основном контейнере. Получается страница внутри страницы: первая нужна для выбора адреса и навигации, а вторая — основной контент.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Браузер «Кодиум»</title>
<!-- Подключаем файл стилей -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Навигационная панель, содержащая поле ввода URL и кнопку для перехода -->
<div id="nav-bar">
<!-- Поле ввода для URL-адреса -->
<input type="text" id="url-bar" placeholder="Введите URL" />
<!-- Кнопка для перехода по введённому URL -->
<button id="go-button">Перейти</button>
</div>
<!-- Контейнер для отображения содержимого страницы -->
<div id="browser-content">
<!-- Содержимое будет подгружаться сюда -->
</div>
<!-- Скрипт для обработки нажатия кнопки и перехода по указанному URL -->
<script>
document.getElementById('go-button').addEventListener('click', () => {
// Получаем значение, введённое в поле URL
const url = document.getElementById('url-bar').value;
// Перенаправляем текущий документ на введённый URL
window.location.href = url;
});
</script>
</body>
</html>
Получилась простая форма для ввода URL в адресной строке — выглядит странно, потому что стилей пока нет:
Добавляем стили
Теперь переходим к стилям — открываем в той же папке проекта файл style.css и начинаем визуальную магию:
- устанавливаем отступы и поля;
- настраиваем шрифты;
- определяем, где какое должно быть выравнивание;
- говорим, как выглядит и ведёт себя кнопка перехода при наведении;
- приписываем, как располагать загруженную страницу внутри основного контейнера.
Почти всё из этого относится к панели навигации, потому что остальное (загруженная страница) отображается самостоятельно в контейнере и там уже применяются свои стили.
Запишем всё это на языке CSS:
/* Сброс отступов и полей, установка высоты на 100% для html и body */
body,
html {
margin: 0; /* Убираем отступы */
padding: 0; /* Убираем поля */
height: 100%; /* Высота на 100% от высоты окна */
font-family: Arial, sans-serif; /* Устанавливаем шрифт Arial для всего текста */
box-sizing: border-box; /* Используем border-box для всех элементов */
}
/* Поле ввода URL */
#url-bar {
width: 80%; /* Ширина поля ввода — 80% от ширины контейнера */
padding: 10px; /* Внутренние отступы */
border-radius: 64px; /* Скругляем углы */
}
/* Навигационная панель */
#nav-bar {
display: flex; /* Используем флексбокс для выравнивания содержимого */
align-items: center; /* Вертикальное выравнивание элементов по центру */
width: 100vw; /* Ширина на всю ширину окна */
height: 50px; /* Высота */
background: #6780ee; /* Цвет фона */
}
/* Кнопка перехода */
#go-button {
padding: 10px; /* Внутренние отступы */
background-color: #d6fb51; /* Цвет фона */
border-radius: 64px; /* Скругляем углы */
color: #1b2b4c; /* Цвет текста */
margin-left: auto; /* Вырыванием кнопку по правой часть панели */
}
/* Ховер для кнопки */
#go-button:hover {
cursor: pointer; /* Изменяем курсор на указатель */
background-color: #1b2b4c; /* Меняем цвет фона */
color: #f5f6f7; /* Меняем цвет текста */
}
/* Контейнер для отображения загруженной страницы */
#browser-content {
margin-top: 60px; /* Отступ сверху для размещения под панелью навигации */
padding: 10px; /* Внутренние отступы для контента */
}
Вот что получилось после стилизации:
Пишем скрипты
Чтобы проект заработал, нам осталось написать два скрипта — main.js
и preload.js
.
main.js
Скрипт main.js
отвечает за основной процесс, который создаёт окна приложения и управляет их поведением. А ещё он определяет, какие файлы должны быть загружены в рендер-процессе при сборке и запуске полноценного приложения.
// Импортируем модули из Electron
const { app, BrowserWindow, ipcMain } = require("electron");
// Импортируем модуль path для работы с файловыми путями
const path = require("path");
// Переменная для хранения ссылки на главное окно приложения
let mainWindow;
// Функция для создания главного окна приложения
function createWindow() {
// Создаём новое окно браузера с заданными параметрами
mainWindow = new BrowserWindow({
// Ширина окна
width: 800,
// Высота окна
height: 600,
webPreferences: {
// Подключаем preload.js для безопасного взаимодействия
preload: path.join(__dirname, "preload.js"),
// Включаем изоляцию контекста для безопасности
contextIsolation: true,
// Отключаем удалённый модуль для дополнительной безопасности
enableRemoteModule: false,
},
});
// Загружаем файл index.html в окно
mainWindow.loadFile("index.html");
// Отображаем окно, когда оно будет готово
mainWindow.once("ready-to-show", () => {
mainWindow.show();
});
// Обрабатываем событие открытия URL из рендер-процесса
ipcMain.on("open-url", (event, url) => {
// Загружаем указанный URL в текущее окно
mainWindow.loadURL(url);
});
}
// Событие "ready" возникает, когда Electron готов создавать окна
app.on("ready", createWindow);
// Закрываем приложение, если все окна закрыты, кроме macOS
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
// Завершаем работу приложения
app.quit();
}
});
// Восстанавливаем окно, если приложение активируется без открытых окон
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
// Создаём новое окно, если нет открытых окон
createWindow();
}
});
preload.js
С рендер-процессом связан файл preload.js
. Этот файл обеспечивает безопасное взаимодействие между основным процессом и тем, как всё это отрисовывается на экране. Иногда можно обойтись без этого файла, но так как у нас внутри Электрона будет показываться сложное визуальное содержимое разных сайтов, лучше сделаем и добавим в проект.
// Импортируем нужные модули из Electron
const { contextBridge, ipcRenderer } = require("electron");
// Используем contextBridge для безопасного взаимодействия между рендер-процессом и основным процессом
contextBridge.exposeInMainWorld("electronAPI", {
// Создаём функцию openURL, доступную в рендер-процессе, которая отправляет сообщение в основной процесс
openURL: (url) => ipcRenderer.send("open-url", url),
});
Запускаем браузер
Чтобы запустить браузер, запускаем терминал, переходим в папку проекта и вводим команду npm start
. Запустится приложение Electron, и мы увидим окно нашего браузера:
Вводим адрес в адресной строке и проверяем, что всё работает:
Что дальше
Мы сделали минимальную рабочую версию — очень простую, максимально минималистичную и с примитивной логикой. Полноценным браузером такое приложение назвать сложно: здесь нет нормальной навигации и инструментов для работы с вкладками или истории посещений. Но на основе этого прототипа в Electron можно дальше добавить всю необходимую функциональность. Это попробуем сделать в другой раз.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Браузер «Кодиум»</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="nav-bar">
<input type="text" id="url-bar" placeholder="Введите URL" />
<button id="go-button">Перейти</button>
</div>
<div id="browser-content">
</div>
<script>
document.getElementById('go-button').addEventListener('click', () => {
const url = document.getElementById('url-bar').value;
window.location.href = url;
});
</script>
</body>
</html>
body,
html {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial, sans-serif;
box-sizing: border-box;
}
#url-bar {
width: 80%;
padding: 10px;
border-radius: 64px;
}
#nav-bar {
display: flex;
align-items: center;
width: 100vw;
z-index: 1000;
height: 50px;
background: #6780ee;
}
#go-button {
padding: 10px;
background-color: #d6fb51;
border-radius: 64px;
color: #1b2b4c;
margin-left: auto;
}
#go-button:hover {
cursor: pointer;
background-color: #1b2b4c;
color: #f5f6f7;
}
#browser-content {
margin-top: 60px;
padding: 10px;
}
// Импортируем модули из Electron
const { app, BrowserWindow, ipcMain } = require("electron");
// Импортируем модуль path для работы с файловыми путями
const path = require("path");
// Переменная для хранения ссылки на главное окно приложения
let mainWindow;
// Функция для создания главного окна приложения
function createWindow() {
// Создаём новое окно браузера с заданными параметрами
mainWindow = new BrowserWindow({
// Ширина окна
width: 800,
// Высота окна
height: 600,
webPreferences: {
// Подключаем preload.js для безопасного взаимодействия
preload: path.join(__dirname, "preload.js"),
// Включаем изоляцию контекста для безопасности
contextIsolation: true,
// Отключаем удалённый модуль для дополнительной безопасности
enableRemoteModule: false,
},
});
// Загружаем файл index.html в окно
mainWindow.loadFile("index.html");
// Отображаем окно, когда оно будет готово
mainWindow.once("ready-to-show", () => {
mainWindow.show();
});
// Обрабатываем событие открытия URL из рендер-процесса
ipcMain.on("open-url", (event, url) => {
// Загружаем указанный URL в текущее окно
mainWindow.loadURL(url);
});
}
// Событие "ready" возникает, когда Electron готов создавать окна
app.on("ready", createWindow);
// Закрываем приложение, если все окна закрыты, кроме macOS
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
// Завершаем работу приложения
app.quit();
}
});
// Восстанавливаем окно, если приложение активируется без открытых окон
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
// Создаём новое окно, если нет открытых окон
createWindow();
}
});
// Импортируем нужные модули из Electron
const { contextBridge, ipcRenderer } = require("electron");
// Используем contextBridge для безопасного взаимодействия между рендер-процессом и основным процессом
contextBridge.exposeInMainWorld("electronAPI", {
// Создаём функцию openURL, доступную в рендер-процессе, которая отправляет сообщение в основной процесс
openURL: (url) => ipcRenderer.send("open-url", url),
});