Как создать расширение для браузера? Создание расширения для Google Chrome.

Сложность: Опытный

Вступление

Всем привет. Месяц назад, пока бороздил просторы интернета, понял, что у меня есть проблема, я хотел проверить текущую цену Биткойна, но каждый раз заходить на сайт валюты мне было некомфортно. Итак, я решил сделать расширение для Google Chrome с помощью Flutter. И хочу рассказать как я это сделал.

Расширение будет достаточно простым, будет лишь функционал проверки состояния Биткойна. Вам не понадобится дополнительная установка каких-либо плагинов. Мы напишем его с помощью встроенных средств web.

Ваш Flutter SDK должен быть обновлен до последней версии. Если нет, у вас могут быть некоторые проблемы, поскольку веб-поддержка — довольно новая возможность во Flutter. А также убедитесь, что на вашем устройстве уже установлен Google Chrome.

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

Содержание

Раздел 1: Создание проекта

Создаем новый Flutter-проект, я его назвал "chrome_extension":

Прежде всего, нам нужно создать наше приложение для веб. Измените свое устройство на Chrome из нижней панели:

После того как мы это сделали, наше приложение работает в Chrome без ошибок:

Теперь перейдите к файлу index.html. Очистите тегbody. Затем создайте новый тег script внутри тега body и укажите атрибуты type и src.

Затем вам нужно указать высоту и ширину внутри тега html. Если нет, расширение будет создано с нулевой высотой и нулевой шириной. В итоге ваш код файла index.html должен быть похож на этот:

<!DOCTYPE html>
<html style="height: 650px; width: 300px;">
<head>
  <!--
	   Если вы обслуживаете свое веб-приложение по пути, отличному от корневого, измените
     href ниже, чтобы обращаться по нужному пути.

     Больше подробностей:
     * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

     $FLUTTER_BASE_HREF — это заполнитель для базового href, 
		 который в дальнейшем будет заменен значением.
     Аргумент `--base-href`, при использовании команды "flutter build".
  -->
  <base href="$FLUTTER_BASE_HREF">

  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="A new Flutter project.">

  <!-- iOS мета-теги и иконки -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="chrome_extension">
  <link rel="apple-touch-icon" href="icons/Icon-192.png">

  <!-- Favicon -->
  <link rel="icon" type="image/png" href="favicon.png"/>

  <title>chrome_extension</title>
  <link rel="manifest" href="manifest.json">
</head>
<body>
  <script type="application/javascript" src="main.dart.js"></script>
</body>
</html>

Следующее, что нужно сделать, это перейти к файлу manifest.json. Здесь вы указываете то, как хотите чтобы ваше расширение себя вело. Мы можем удалить все это и создать снова. Потому что нам нужны не все из них:

{
    "name": "First Chrome Extension Name",
    "short_name": "First Chrome Extension Short Name",
    "description": "Chrome Extension description",
    "version": "1.0",
    "action": {
        "default_popup": "index.html",
        "default_icon": "/icons/Icon-192.png"
    },
    "manifest_version": 3
}

В файле manifest.json есть много полей. Вы можете прочитать больше по этой ссылке.

Теперь все установлено. Нам нужно собрать версию нашего проекта. Скопируйте это и вставьте в свой терминал:

flutter build web --web-renderer html --csp

Если вы хотите знать, для чего мы пишем этот код, вы можете узнать больше, написав:

flutter build web -h

Теперь нам нужно загрузить наше расширение в Chrome. Откройте Chrome и перейдите к своим расширениям: chrome://extensions/

Убедитесь, что вы включили режим разработчика в верхнем правом углу.

Нажмите на кнопку «Загрузить распакованное» и добавьте папку web в папку build.

После этих действий расширение появилось:

Исходный код полного приложения доступен на Github.

Раздел 2: Биткойн-трекер

Пришло время создавать настоящее расширение. Мы будем использовать nomics.com API для текущих цен на валюту. Вы можете получить ключ API по этой ссылке.

Нам нужно получить данные из API. Этот класс CryptoApi получает данные и возвращает их как List<Currency>.

import 'dart:convert';
import 'package:extension_example/model/currency.dart';
import 'package:http/http.dart' as http;

class CryptoApi {
  static const _key = ''; // Сюда ваш API ключ

  static Future<List<Currency>> getCurrencies() async {
    final url =
        "https://api.nomics.com/v1/currencies/ticker?key=$_key&per-page=100";

    final response = await http.get(Uri.parse(url));
    final parsed = jsonDecode(response.body).cast<Map<String, dynamic>>();

    return parsed;
  }
}

Затем создай модель валюты (Currency):

class Currency {
  final String id;
  final String name;
  final String currency;
  final String logoUrl;
  final double price;
  final double oneDayChange;
  final double marketCap;
  final int rank;

  Currency({
    required this.id,
    required this.name,
    required this.currency,
    required this.logoUrl,
    required this.price,
    required this.oneDayChange,
    required this.marketCap,
    required this.rank,
  });

  factory Currency.fromJson(Map<String, dynamic> json) {
    return Currency(
      id: json['id'],
      name: json['name'],
      currency: json['currency'],
      logoUrl: json['logo_url'],
      price: double.parse(json['price']),
      oneDayChange: double.parse(json['1d']['price_change_pct']),
      marketCap: double.parse(json['market_cap']),
      rank: int.parse(json['rank']),
    );
  }
}

Далее займемся файлом main.dart и отобразим наш список:

import 'package:extension_example/api/crypto_api.dart';
import 'package:extension_example/model/currency.dart';
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        theme: ThemeData.dark(),
        themeMode: ThemeMode.dark,
        debugShowCheckedModeBanner: false,
        title: 'Crypto Tracker',
        home: const Currencies());
  }
}

class Currencies extends StatefulWidget {
  const Currencies({Key? key}) : super(key: key);

  @override
  _CurrenciesState createState() => _CurrenciesState();
}

class _CurrenciesState extends State<Currencies> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Crypto Tracker'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () {
              setState(() {});
              // Обновление данных используя setstate.
              // Это не сколько чисто, сколько быстро.
            },
          ),
        ],
      ),
      body: FutureBuilder<List<Currency>>(
        future: CryptoApi.getCurrencies(),
        builder: (context, AsyncSnapshot snapshot) {
          if (!snapshot.hasData) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }
          return ListView.builder(
            itemCount: snapshot.data.length,
            itemBuilder: (context, index) {
              Currency currency = Currency.fromJson(snapshot.data[index]);

              return ListTile(
                leading: Image.network(
                  currency.logoUrl,
                ),
                title: Text("${currency.name} (${currency.currency})"),
                subtitle: Text(currency.price.toStringAsFixed(2)),
                trailing: RichText(
                  text: TextSpan(
                    children: [
                      WidgetSpan(
                        child: currency.oneDayChange >= 0
                            ? const RotatedBox(
                                quarterTurns: 1,
                                child: Icon(
                                  Icons.arrow_back_ios,
                                  color: Colors.green,
                                ),
                              )
                            : const RotatedBox(
                                quarterTurns: 3,
                                child: Icon(
                                  Icons.arrow_back_ios,
                                  color: Colors.red,
                                ),
                              ),
                      ),
                      TextSpan(
                        text: " % " +
                            (currency.oneDayChange * 100).toStringAsFixed(2),
                        style: TextStyle(
                            color: currency.oneDayChange >= 0
                                ? Colors.green
                                : Colors.red),
                      ),
                    ],
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

Еще кое-что, файл manifest.json немного отличается от другого примера. Я загружаю несколько иконок для каждого размера экрана. Не смотря на то, что все они имеют одинаковый значок, Chrome говорит, что нам нужно указать их.

А еще я добавляю host_permission. Мы используем API в нашем проекте, поэтому мы должны дать разрешение на запрос данных из API. В противном случае доступ может быть заблокирован политикой CORS:

{
    "name": "Crypto Tracker",
    "short_name": "Crypto Tracker",
    "version": "1.0.0",
    "description": "There are 100 cryptocurrencies available. You can see current prices.",
    
    "host_permissions": [
        "<all_urls>"
    ],

    "content_security_policy": {
        "extention_pages": "script-src 'self' 'unsafe-eval'; object-src 'self'"
    },
    "action": {
        "default_popup":"index.html",
        "default_icon":"/icons/bitcoin.png"
    },

    "icons": {
        "16": "icons/bitcoin.png",
        "32": "icons/bitcoin.png",
        "64": "icons/bitcoin.png",
        "128": "icons/bitcoin.png"
    },
    "manifest_version": 3
}

Теперь мы должны создать веб-приложение с помощью следующей команды:

flutter build web --web-renderer html --csp

Затем мы откроем Chrome и перейдем к chrome://extensions/. Нажмите Загрузить распакованное и выберите маршрут ../build/web/. Этот маршрут был сгенерирован на предыдущем шаге.

Мы видим, что расширение успешно добавлено:

Заключение

В этой статье мы создали простое расширение для отслеживания актуальности валют Биткойна для браузера Google Chrome, надеюсь было полезно.

Исходный код: Github.

Комментарии (6)


  1. selkwind
    21.04.2022 14:53
    +1

    Интересно, а можно на Flutter сделать плагин для сохранения заполеннных форм на экране, как это делает Roboform?


    1. Legend5366 Автор
      22.04.2022 07:25

      Впервые слышу о такой программе, посмотрел что она делает, на самом деле очень удобно, и на 100% уверен что подобное можно реализовать с помощью данного фреймворка. Так что ответ — да.


  1. Vinni37
    22.04.2022 09:01
    +2

    Не силен в Flutter, но имеется какой ни какой опыт в разработке и публикации расширений как под MV2 так и под MV3. С вероятностью 99% расширение с "<all_urls>" и csp "unsafe-eval" не пройдет модерацию в сторе, без объяснения причин, хотя причины понятны. Так же в идеологии гугла (с чем я согласен), popup должна легкой и в нее должно выводиться информация из бэкэнда. У вас получается при каждом показе popup все с рендерится нуля идут обращения к API и т.д. ИМХО. Нужно разделять фронт и бэк


    1. Legend5366 Автор
      22.04.2022 15:12

      Спасибо за комментарий! И прошу сразу обратить внимание, что статью писал не я — это просто оформленный и оптимизированный мной перевод англоязычной статьи. Да и я с вами согласен, есть некоторые неточные моменты, но в любом случае, если мы говорим не про стор, а про личное использование собственноручно написанного плагина, вполне себе неплохое решение, ведь вы не будете в своем же плагине майнер пихать))))


      1. Vinni37
        22.04.2022 15:26
        +2

        Извиняюсь, не заметил что перевод. Тогда обстрактно об изложенном.
        По сути, текущая реализация сводится к тому что бы запихать spa в popup, и это не есть хорошо, это зло.
        Можно с тойже легкостью можно написать сатью "Vue.js: Создание расширения для Chrome" или другой фреймворк/библиотека на вкус и цвет. Но к созданию расширения это мало имеет отношения.


        1. Legend5366 Автор
          22.04.2022 17:36

          Ну, ничего другого не могу сказать, кроме как все работает, все по делу, разве что вы предложили советы по улучшению структуры кода, с чем и спасибо.