Расширения Chrome для начинающих. Часть 2. Практика
В этой части мы создадим расширение таймера Pomodoro, используя API, о которых мы узнали в части 1. Стилизация будет выполняться с помощью CSS. Полный код этого проекта можно найти на GitHub. Посмотреть демо на Youtube.
Манифест и всплывающее окно
Как вы уже знаете, при создании расширений первым файлом, который вы создаете, является файл манифеста.
Создайте новый файл manifest.json
.
📦 Chrome-Extension-Series ┣ 🎨 icon.png ┣ 📄 manifest.json { "manifest_version": 3, "name": "Pomodoro Timer", "version": "1.0", "description": "Assists you to focus and get things done", "icons": { "16": "icon.png", "48": "icon.png", "128": "icon.png" }, "action": { "default_icon": "icon.png", "default_title": "Pomodoro Timer", "default_popup": "popup/popup.html" } }
Теперь у нас есть набор расширений, давайте создадим всплывающую страницу. Файл popup.html
помещается в папку popup
, чтобы добавить структуру нашему проекту.
📦 Chrome-Extension-Series ┣ 🎨 icon.png ┣ 📄 manifest.json ┣ 📂 popup ┃ ┣ 📄 popup.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./popup.css"> <title>Pomodoro Timer</title> </head> <body> <div class="header"> <img src="../icon.png"> </div> <h1>00:00</h1> <button>Start Timer</button> <button>Add Task</button> <div> <input type="text"> <input type="button" value="X"> </div> </body> <script src="popup.js"></script> </html>
В предыдущем коде определена структура нашей всплывающей страницы. Он связан с pop.css
для стиля и pop.js
для интерактивности.
Давайте добавим немного стиля и создадим файл popup.css
, с которым связан наш popup.html
.
📦 Chrome-Extension-Series ┣ 🎨 icon.png ┣ 📄 manifest.json ┣ 📂 popup ┣ 📄 popup.css body { height: 400px; width: 300px; } .header { display: flex; justify-content: center; height: 40px; }
Перезагрузите страницу расширения в браузере и щелкните всплывающее окно, отобразится popup.html
.
Функция списка задач
Функция списка задач позволит нам добавлять и удалять задачи.
Добавление задач
In popup.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./popup.css"> <title>Pomodoro Timer</title> </head> <body> <div class="header"> <img src="../icon.png"> </div> <h1>00:00</h1> <button>Start Timer</button> <button id="add-task-btn">Add Task</button> + <div id="task-container"> <input type="text"> <input type="button" value="X"> + </div> </body> <script src="popup.js"></script> </html>
In popup.js
📦 Chrome-Extension-Series ┣ 🎨 icon.png ┣ 📄 manifest.json ┣ 📂 popup ┣ 📄 popup.css ┣ 📄 popup.js const addTaskBtn = document.getElementById('add-task-btn') addTaskBtn.addEventListener('click', () => addTask()) function addTask() { const taskRow = document.createElement('div') // Create text input const text = document.createElement('input') text.type = 'text' text.placeholder = 'Enter a task..' // Create delete button const deleteBtn = document.createElement('input') deleteBtn.type = 'button' deleteBtn.value = 'X' // append input elements to taskRow taskRow.appendChild(text) taskRow.appendChild(deleteBtn) // append taskRow to taskContainer const taskContainer = document.getElementById('task-container') taskContainer.appendChild(taskRow) }
В предыдущем коде мы выбираем кнопку Add Task
через ее id
, добавляем прослушиватель событий click
и функцию обратного вызова, которая добавляет новую задачу в пользовательский интерфейс.
Удаление задач
In popup.js
- const addTaskBtn = document.getElementById('add-task-btn') addTaskBtn.addEventListener('click', () => addTask()) - function addTask() { const taskRow = document.createElement('div') // Create text input - const text = document.createElement('input') text.type = 'text' text.placeholder = 'Enter a task..' // Create delete button - const deleteBtn = document.createElement('input') deleteBtn.type = 'button' deleteBtn.value = 'X' // append input elements to taskRow taskRow.appendChild(text) taskRow.appendChild(deleteBtn) // append taskRow to taskContainer - const taskContainer = document.getElementById('task-container') taskContainer.appendChild(taskRow) } // array to store tasks let tasks = [] const addTaskBtn = document.getElementById('add-task-btn') addTaskBtn.addEventListener('click', () => addTask()) // render tasks function renderTask(taskNum) { const taskRow = document.createElement('div') // Create text input const text = document.createElement('input') text.type = 'text' text.placeholder = 'Enter a task..' //Set and track input values of tasks in the array text.value = tasks[taskNum] text.addEventListener('change', () => { tasks[tasksNum] = text.value }) // Create delete button const deleteBtn = document.createElement('input') deleteBtn.type = 'button' deleteBtn.value = 'X' // delete task deleteBtn.addEventListener('click', () => { deleteTask(taskNum) }) // append input elements to taskRow taskRow.appendChild(text) taskRow.appendChild(deleteBtn) // append taskRow to taskContainer const taskContainer = document.getElementById('task-container') taskContainer.appendChild(taskRow) } function addTask() { const tasksNum = tasks.length // add tasks to array tasks.push('') renderTask(tasksNum) } // delete and re-render tasks after mutation function deleteTask(tasksNum) { tasks.splice(tasksNum, 1) renderTasks() } function renderTasks() { const taskContainer = document.getElementById('task-container') taskContainer.textContent = '' tasks.forEach((taskText, tasksNum) => { renderTask(tasksNum) }) }
Мы внесли серьезные изменения в файл popup.js
в предыдущем коде. Давайте разбираться, что происходит:
- В основном, мы добавляем и удаляем задачи
- Массив (
tasks
) создается, чтобы мы могли хранить задачи - Функция
rendTask()
создает новую задачу и отображает ее в DOM (объектная модель документа) при нажатии кнопкиAdd Task
. - Функция
addTask()
является обработчиком событий для кнопкиAdd Task
. - Функция
deleteTask()
удаляет задачи при нажатии кнопки удаления задачи (X
). - Функция
renderTasks()
обновляет массив задач всякий раз, когда задача удаляется, т. е. повторно отображает пользовательский интерфейс.
Теперь, если мы проверим наше расширение, мы можем добавлять и удалять задачи, но данные не являются постоянными — нам нужно реализовать хранилище.
Хранение задач
Во-первых, мы установили необходимые разрешения для использования API хранилища в manifest.json
.
{ "manifest_version": 3, "name": "Pomodoro Timer", "version": "1.0", "description": "Assists you to focus and get things done", "icons": { "16": "icon.png", "48": "icon.png", "128": "icon.png" }, "action": { "default_icon": "icon.png", "default_title": "Pomodoro Timer", "default_popup": "popup/popup.html" }, + "permissions": ["storage"] }
In popup.js
// array to store tasks let tasks = [] const addTaskBtn = document.getElementById('add-task-btn') addTaskBtn.addEventListener('click', () => addTask()) // set default storage value for the tasks chrome.storage.sync.get(['tasks'], (res) => { tasks = res.tasks ? res.tasks : [] renderTasks() }) // save tasks function saveTasks() { chrome.storage.sync.set({ tasks: tasks, }) } // render tasks function renderTask(taskNum) { const taskRow = document.createElement('div') // Create text input const text = document.createElement('input') text.type = 'text' text.placeholder = 'Enter a task..' //Set and track input values of tasks in the array text.value = tasks[taskNum] text.addEventListener('change', () => { tasks[taskNum] = text.value // call saveTask whenever a value changes saveTasks() }) .... function addTask() { const tasksNum = tasks.length // add tasks to array tasks.push('') renderTask(tasksNum) saveTasks() } // delete and re-render tasks after mutation function deleteTask(tasksNum) { tasks.splice(tasksNum, 1) renderTasks() saveTasks() }
Мы используем API хранилища Chrome в предыдущем коде для хранения данных нашего расширения.
- Данные по умолчанию для расширения изначально устанавливаются в пустой массив, если в массиве задач нет задач для рендеринга.
- Функция
saveTasks()
сохраняет наш массивtask
в API хранилища. - В
renderTask()
всякий раз, когда задача добавляется или удаляется, она сохраняется черезsaveTasks()
, и то же самое касаетсяaddTask()
иdeleteTask()
.
Функция задачи завершена; мы можем удалять, добавлять и сохранять задачи.
Функция таймера
Функция таймера потребует от нас создания фонового сценария и использования сигналов тревоги и уведомлений, чтобы уведомлять пользователя, когда таймер истекает.
Таймер запуска и паузы
Давайте установим необходимые разрешения в manifest.json
.
{ "manifest_version": 3, "name": "Pomodoro Timer", "version": "1.0", "description": "Assists you to focus and get things done", "icons": { "16": "icon.png", "48": "icon.png", "128": "icon.png" }, "action": { "default_icon": "icon.png", "default_title": "Pomodoro Timer", "default_popup": "popup/popup.html" }, + "permissions": ["storage", "alarms", "notifications"], + "background": { "service_worker": "background.js" } }
Создайте файл background.js
для нашего фонового сценария.
📦 Chrome-Extension-Series ┣ 🎨 icon.png ┣ 📄 manifest.json ┣ 📂 popup ┣ 📄 popup.css ┣ 📄 popup.js ┣ 📄 background.js // create an alarm to notify user when time is up chrome.alarms.create("pomodoroTimer", { periodInMinutes: 1 / 60 }) // alarm listener chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === "pomodoroTimer") { chrome.storage.local.get(["timer", "isRunning"], (res) => { if (res.isRunning) { let timer = res.timer + 1 console.log(timer) chrome.storage.local.set({ timer, }) } }) } }) // storage to set and track timer variables on load chrome.storage.local.get(["timer", "isRunning"], (res) => { chrome.storage.local.set({ timer: "timer" in res ? res.timer : 0, isRunning: "isRunning" in res ? res.isRunning : false, }) })
In popup.js
// array to store tasks let tasks = [] // Start Timer Button + const startTimerBtn = document.getElementById("start-timer-btn"); startTimerBtn.addEventListener("click", () => { chrome.storage.local.get(["isRunning"], (res) => { chrome.storage.local.set({ isRunning: !res.isRunning, }, () => { startTimerBtn.textContent = !res.isRunning ? "Pause Timer" : "Start Timer" }) }) })
В предыдущем коде:
- Мы устанавливаем будильник, который срабатывает при нажатии кнопки
Start Timer
. - Переменные
timer
иisRunning
используются для отслеживания времени и состояния таймера, они хранятся как исходные данные приложения в хранилище. - Мы слушаем (
onAlarm.addListener
) тревогу и увеличиваемtimer
, когдаisRunning
равноtrue
, тогдаtimer
регистрируется в консоли. - Наконец, в
popup.js
мы прослушиваем событиеclick
на кнопкеStart Timer
и получаем текущее значениеisRunning
. Если текущее значение равноtrue,
, оно устанавливается наfalse,
и таймер приостанавливается; если этоfalse,
, то для сброса таймера установлено значение true.
Сбросить таймер
Теперь давайте поработаем над функцией сброса таймера. Создайте разметку для кнопки сброса popup.html.
<body> <div class="header"> <img src="../icon.png"> </div> <h1>00:00</h1> <button id="start-timer-btn">Start Timer</button> + <button id="reset-timer-btn">Reset Timer</button> <button id="add-task-btn">Add Task</button> <div id="task-container"> <input type="text"> <input type="button" value="X"> </div> </body>
In popup.js
// Reset Timer Button const resetTimerBtn = document.getElementById("reset-timer-btn") resetTimerBtn.addEventListener("click", () => { chrome.storage.local.set({ // reset variables timer: 0, isRunning: false }, () => { // reset start button text-content startTimerBtn.textContent = "Start Timer" }) })
В предыдущем коде мы сделали следующее:
- Выберите кнопку
Reset Timer
в DOM через ееid.
- Добавьте к кнопке прослушиватель событий
click
с функцией обратного вызова, которая сбрасывает переменныеtimer
иisRunning
в хранилище. - Наконец, текст кнопки
Start Timer
устанавливается на строку «Запуск таймера», исходя из предположения, что в настоящее время это «Таймер паузы».
Отображение времени во всплывающем окне
До сих пор мы регистрировали значения наших таймеров на консоли. Давайте отобразим его на всплывающей странице.
In popup.html
<body> <div class="header"> <img src="../icon.png"> </div> + <h1 id="time">00:00</h1> <button id="start-timer-btn">Start Timer</button> <button id="reset-timer-btn">Reset Timer</button> <button id="add-task-btn">Add Task</button> <div id="task-container"> <input type="text"> <input type="button" value="X"> </div> </body>
In popup.js
// array to store tasks let tasks = []; const time = document.getElementById("time"); // Update time every 1sec function updateTime() { chrome.storage.local.get(["timer"], (res) => { const time = document.getElementById("time") // get no. of minutes & secs const minutes = `${25 - Math.ceil(res.timer / 60)}`.padStart(2, "0"); let seconds = "00"; if (res.timer % 60 != 0) { seconds = `${60 -res.timer % 60}`.padStart(2, "0"); } // show minutes & secs on UI time.textContent = `${minutes}:${seconds}` }) } updateTime() setInterval(updateTime, 1000) // Start Timer Button
В предыдущем коде мы отображаем время на всплывающей странице и обновляем его при нажатии любой из кнопок, влияющих на таймер. Давайте лучше разберем код:
- В функции
updateTime()
делается немного математики; значение таймера берется из хранилища. - Минуты для таймера получаются (таймер должен отсчитывать от 25 минут) и сохраняются в переменной
minute
. '25 - res.timer/60- for example, if our timer value(
res.timer) was
120secs,
120/60=2, then '25 - 2 = 23,
т.е. на часах останется 23 минуты. - Чтобы получить секунды, мы делим значение таймера (
res.timer
) на60
. - Значения
minutes
иseconds
отображаются в пользовательском интерфейсе черезtime.textContent
. updateTime()
вызывается автоматически при загрузке всплывающего окна и вызывается каждые1sec
пользователемsetInterval.
Теперь время можно увидеть во всплывающем окне.
Отправка уведомления
Теперь давайте настроим уведомления, чтобы уведомлять пользователя, когда время истекло.
In background.js
// alarm listener chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === "pomodoroTimer") { chrome.storage.local.get(["timer", "isRunning"], (res) => { if (res.isRunning) { let timer = res.timer + 1 let isRunning = true if(timer === 25) { this.registration.showNotification('Pomodoro Timer', { body: "25 minutes has passed", icon: "icon.png" }) timer = 0 isRunning = false } chrome.storage.local.set({ timer, isRunning, }) } }) } })
В предыдущем коде оператор if
используется для проверки того, истек ли наш таймер на 25 минут, после чего регистрируется уведомление. Когда таймер истекает, значение timer
сбрасывается на 0
, а isRunning
на false
, чтобы приостановить таймер. Чтобы проверить эту функциональность, установите значение таймера по умолчанию на 10secs
(помните, что это только для целей тестирования, поэтому мы не ждем 25 минут). В операторе if
в приведенном выше коде измените значение таймера на 10secs
- if(timer === 10)
. Теперь перезапустите таймер, и после 10secs
вы увидите уведомление.
Повтор сеанса с открытым исходным кодом
OpenReplay – это пакет для воспроизведения сеансов с открытым исходным кодом, который позволяет вам видеть, что пользователи делают в вашем веб-приложении, помогая вам быстрее устранять неполадки. OpenReplay размещается на собственном сервере для полного контроля над вашими данными.
Начните получать удовольствие от отладки — начните использовать OpenReplay бесплатно.
Страница параметров
Теперь у нас есть базовая функциональность для расширения: мы можем запускать таймер, приостанавливать таймер, сбрасывать таймер, а также удалять и добавлять задачи. Теперь давайте сделаем расширение более настраиваемым, чтобы пользователи могли адаптировать его к своим потребностям. Некоторые пользователи могут захотеть сосредоточиться на более длительных или более коротких периодах времени. Мы должны создать страницу параметров, чтобы пользователь мог настроить расширение. Пользователь сможет установить максимальное время сеанса на 1 час (60 минут) и минимальное на 1 минуту.
Вариант настройки и хранения
Добавьте "options_pagefile in `manifest.json.`
{ "manifest_version": 3, "name": "Pomodoro Timer", "version": "1.0", "description": "Assists you to focus and get things done", "icons": { "16": "icon.png", "48": "icon.png", "128": "icon.png" }, "action": { "default_icon": "icon.png", "default_title": "Pomodoro Timer", "default_popup": "popup/popup.html" }, "permissions": ["storage", "alarms", "notifications"], "background": { "service_worker": "background.js" }, "options_page": "options/options.html" }
Файл options.html
помещается в папку, чтобы добавить структуру нашему проекту.
Создайте папку options
и добавьте в нее файлы options.html
и options.css
.
📦 Chrome-Extension-Series ┣ 🎨 icon.png ┣ 📄 manifest.json ┣ 📂 popup ┣ 📂 options ┃ ┣ 📄 options.css ┃ ┣ 📄 options.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> `<link rel="stylesheet" href="options.css"> <title>Pomodoro Timer Extension Options</title> </head> <body> <h1>Pomodoro Timer Options</h1> <input id="time-option" type="number" min="1" max="60" value="25"> <button id="save-btn">Save Options</button> </body> <script src="options.js"></script> </html>
В HTML у нас есть числовое поле input
с минимальным значением 1
(1 минута) и максимальным значением 60
(60 минут). Атрибут value
содержит значение таймера по умолчанию 25
(25 минут). Кнопка Save options
позволит нам сохранить параметры.
In options.js
// Add validation const timeOption = document.getElementById('time-option') timeOption.addEventListener('change', (event) => { const val = event.target.value if (val < 1 || val > 60) { timeOption.value = 25 } }) // Save Option const saveBtn = document.getElementById('save-btn') saveBtn.addEventListener('click', () => { chrome.storage.local.set({ timeOption: timeOption.value, timer: 0, isRunning: false, }) }) // Load Saved Option chrome.storage.local.get(['timeOption'], (res) => { timeOption.value = res.timeOption })
В предыдущем коде в option.js
:
- Мы проверяем значения, переданные в качестве параметров. Оно не должно быть меньше
1
и больше60
. - Мы сохраняем новую опцию, сбрасываем наш таймер и устанавливаем для параметра
isRunning
значениеfalse
при изменении настройки таймера.
Отображение сохраненной опции во всплывающем окне
Теперь давайте прочитаем сохраненный через фоновый скрипт и отобразим его на всплывающей странице.
In background.js
// create an alarm to notify user when time is up chrome.alarms.create("pomodoroTimer", { periodInMinutes: 1 / 60 }) // alarm listener chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === "pomodoroTimer") { chrome.storage.local.get(["timer", "isRunning", "timeOption"], (res) => { if (res.isRunning) { let timer = res.timer + 1 let isRunning = true // console.log(timer) if(timer === 60 * res.timeOption) { this.registration.showNotification('Pomodoro Timer', { - body: "25 minutes has passed", + body: `${res.timeOption} minutes has passed!`, icon: "icon.png" }) timer = 0 isRunning = false } chrome.storage.local.set({ timer, isRunning, }) } }) } }) // storage to set and track timer variables chrome.storage.local.get(["timer", "isRunning", "timeOption"], (res) => { chrome.storage.local.set({ timer: "timer" in res ? res.timer : 0, timeOption: "timeOption" in res ? res.timeOption : 25, isRunning: "isRunning" in res ? res.isRunning : false, }) })
In popup.js
// array to store tasks let tasks = []; const time = document.getElementById("time"); // Update time every 1sec function updateTime() { chrome.storage.local.get(["timer", "timeOption"], (res) => { const time = document.getElementById("time") // get no. of minutes & secs const minutes = `${ res.timeOption - Math.ceil(res.timer / 60)}`.padStart(2, "0"); let seconds = "00"; if (res.timer % 60 != 0) { seconds = `${60 -res.timer % 60}`.padStart(2, "0"); } // show minutes & secs on UI time.textContent = `${minutes}:${seconds}` }) }
Если вы протестируете расширение, установив свой параметр, вы увидите новое значение на странице popup
.
Стайлинг
Наконец, давайте стилизуем наше расширение. Скопируйте и вставьте код ниже.
In popup.css
body { height: 400px; width: 350px; background: hsla(238, 100%, 71%, 1); background: linear-gradient( 90deg, hsla(238, 100%, 71%, 1) 0%, hsla(295, 100%, 84%, 1) 100% ); background: -moz-linear-gradient( 90deg, hsla(238, 100%, 71%, 1) 0%, hsla(295, 100%, 84%, 1) 100% ); background: -webkit-linear-gradient( 90deg, hsla(238, 100%, 71%, 1) 0%, hsla(295, 100%, 84%, 1) 100% ); filter: progid: DXImageTransform.Microsoft.gradient( startColorstr="#696EFF", endColorstr="#F8ACFF", GradientType=1 ); } .header { display: flex; justify-content: center; height: 40px; background-color: whitesmoke; margin: -8px; padding: 5px; } #time { text-align: center; font-size: 50px; margin: 10px; font-weight: normal; color: whitesmoke; } #btn-container { display: flex; justify-content: space-evenly; } #btn-container > button { color: black; background-color: whitesmoke; border: none; outline: none; border-radius: 5px; padding: 8px; font-weight: bold; width: 100px; cursor: pointer; } #task-container { display: flex; flex-direction: column; align-items: center; padding: 20px; } .task-input { outline: none; border: none; border-radius: 4px; margin: 5px; padding: 5px 10px 5px 10px; width: 250px; } .task-delete { outline: none; border: none; height: 25px; width: 25px; border-radius: 4px; color: indianred; cursor: pointer; font-weight: 700; }
In options.css
body { background: hsla(238, 100%, 71%, 1) no-repeat; background: linear-gradient( 90deg, hsla(238, 100%, 71%, 1) 0%, hsla(295, 100%, 84%, 1) 100% ) no-repeat; background: -moz-linear-gradient( 90deg, hsla(238, 100%, 71%, 1) 0%, hsla(295, 100%, 84%, 1) 100% ) no-repeat; background: -webkit-linear-gradient( 90deg, hsla(238, 100%, 71%, 1) 0%, hsla(295, 100%, 84%, 1) 100% ) no-repeat; filter: progid: DXImageTransform.Microsoft.gradient( startColorstr="#696EFF", endColorstr="#F8ACFF", GradientType=1 ); } h1 { color: whitesmoke; text-align: center; font-size: 50px; margin: 10px; font-weight: normal; } h2 { font-weight: normal; color: whitesmoke; } #time-option { outline: none; border: none; width: 300px; border-radius: 4px; padding: 10px; } #save-btn { display: block; margin-top: 40px; border: none; outline: none; border-radius: 4px; padding: 10px; color: black; font-weight: bold; cursor: pointer; }
Заключение
Мы подошли к концу серии расширений Chrome для начинающих. Я надеюсь, что это помогло вам начать работу с основными концепциями создания расширений Chrome. Я рекомендую вам попробовать создать свои собственные проекты или даже добавить больше функций в этот проект для начала. Всего наилучшего в вашем обучении.
Первоначально опубликовано на blog.openreplay.com 21 декабря 2022 г.