使用 Flutter 開(kāi)發(fā) Chrome 插件【又來(lái)?yè)屒岸孙埻肓恕?/font>

前言
Flutter3.0推出后,對(duì)多平臺(tái)支持更好且更穩(wěn)定,今天我們將探索一種將Flutter應(yīng)用作為Chrome擴(kuò)展程序的獨(dú)特運(yùn)行方式。您可以使用帶有--csp標(biāo)志的HTML渲染器生成Flutter Web構(gòu)建,并且可以將其用作 chrome擴(kuò)展。想了解更多信息,請(qǐng)繼續(xù)。

構(gòu)建chrome擴(kuò)展程序
今天,我們將使用一個(gè)生成二維碼的例子看一下使用Flutter來(lái)構(gòu)建chrome擴(kuò)展程序。

讓我們從創(chuàng)建一個(gè)新的 Flutter 項(xiàng)目開(kāi)始。

項(xiàng)目創(chuàng)建
您可以使用與創(chuàng)建任何基本 Flutter 項(xiàng)目完全相同的 Flutter 命令:

flutter create qr_code_extension
這里,qr_code_extension 是 Flutter 項(xiàng)目的名稱。這里的Flutter版本僅供參考,我目前使用的是 Flutter 3.0.1 版。

創(chuàng)建項(xiàng)目后,使用您喜歡的 IDE 打開(kāi)它,我這里使用 VS Code打開(kāi)。

如何將這個(gè)基本的 Flutter Web 應(yīng)用程序作為 chrome 擴(kuò)展程序運(yùn)行?
僅需要三步即可完成…

1、刪除不支持的js腳本
導(dǎo)航到 web/index.html 文件并刪除所有 <script>...</script> 標(biāo)記:



然后只在 <body> 中插入以下<script>標(biāo)簽:

<script src="main.dart.js" type="application/javascript"></script>
2、設(shè)置擴(kuò)展程序尺寸
擴(kuò)展程序具有固定的尺寸,因此您需要在 HTML 中明確指定擴(kuò)展視圖的寬度和高度值。

只需將起始 <html> 標(biāo)記替換為以下內(nèi)容:

<html style="height: 600px; width: 350px">
這會(huì)將擴(kuò)展視圖的高度和寬度分別設(shè)置為 600 像素和 350 像素。

3、在 manifest.json 中進(jìn)行一些更改
導(dǎo)航到 web/manifest.json 文件并將整個(gè)內(nèi)容替換為以下內(nèi)容:

{
    "name": "QR Code Extension",
    "description": "QR Code Extension",
    "version": "1.0.0",
    "content_security_policy": {
        "extension_pages": "script-src 'self' ; object-src 'self'"
    },
    "action": {
        "default_popup": "index.html",
        "default_icon": "icons/Icon-192.png"
    },
    "manifest_version": 3
}
構(gòu)建擴(kuò)展程序
完成所需的更改后,您就可以將其作為 Chrome 擴(kuò)展程序構(gòu)建和運(yùn)行了。

默認(rèn)情況下,當(dāng)您使用以下命令運(yùn)行 Flutter Web 構(gòu)建時(shí):

flutter build web
它為移動(dòng)瀏覽器使用 HTML 渲染器,為桌面瀏覽器使用 CanvasKit 渲染器。

為了提供一點(diǎn)上下文,F(xiàn)lutter web 支持兩種類型的渲染器(參考文檔: https://docs.flutter.dev/development/platform-integration/web/renderers):

HTML 渲染器
使用 HTML 元素、CSS、Canvas 元素和 SVG 元素的組合。此渲染器具有較小的下載大小。

CanvasKit 渲染器
此渲染器具有更快的性能和更高的小部件密度(支持像素級(jí)別的操作),但下載大小增加了約 2MB。

但是為了將其用作擴(kuò)展,您必須僅使用 HTML 渲染器專門生成構(gòu)建。可以使用以下命令完成:

flutter build web --web-renderer html
暫時(shí)不要運(yùn)行該命令!

最后,您必須使用 --csp 標(biāo)志來(lái)禁用在生成的輸出中動(dòng)態(tài)生成代碼,這是滿足 CSP 限制所必需的。

運(yùn)行此命令:

flutter build web --web-renderer html --csp
您將在 Flutter 項(xiàng)目根目錄中的 build/web 文件夾中找到生成的文件。

擴(kuò)展程序運(yùn)行
要安裝和使用此擴(kuò)展程序,請(qǐng)?jiān)?Chrome 瀏覽器打開(kāi)以下 URL:

chrome://extensions
此頁(yè)面列出了所有 Chrome 擴(kuò)展程序(如果您已有安裝的擴(kuò)展程序)。

啟用網(wǎng)頁(yè)右上角的開(kāi)發(fā)者模式切換。



單擊「加載已解壓的擴(kuò)展程序」。



選擇 /build/web 文件夾。



您將看到新的擴(kuò)展程序現(xiàn)已添加到該頁(yè)面。

該擴(kuò)展程序?qū)⒆詣?dòng)安裝,您可以像任何常規(guī)擴(kuò)展程序一樣通過(guò)單擊頂部欄中的擴(kuò)展程序圖標(biāo)來(lái)訪問(wèn)它(也可以固定它以便于訪問(wèn))。



QR碼生成擴(kuò)展程序?qū)嵺`
現(xiàn)在讓我們進(jìn)入一個(gè)實(shí)際的實(shí)現(xiàn),并使用 Flutter 構(gòu)建 QR 碼生成器擴(kuò)展程序。

首先,轉(zhuǎn)到 lib 目錄中的 main.dart 文件。將該頁(yè)面的全部?jī)?nèi)容替換為以下內(nèi)容:

import 'package:flutter/material.dart';

import 'qr_view.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Chrome Extension',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const QRView(),
    );
  }
}
這只是為了簡(jiǎn)化應(yīng)用程序的起點(diǎn),并刪除與演示計(jì)數(shù)器應(yīng)用程序相關(guān)的代碼。

接下來(lái),在名為 qr_view.dart 的 lib 文件夾中創(chuàng)建一個(gè)新文件。在這個(gè)文件中,我們將添加用于構(gòu)建 QR 碼擴(kuò)展 UI 的代碼和一些用于根據(jù) TextField 中存在的文本生成 QR 碼的邏輯。

為了在 UI 中呈現(xiàn) QR 碼,我們將使用名為 qr_flutter 的 Flutter 包。從 Flutter 根目錄運(yùn)行以下命令來(lái)安裝這個(gè)包:

flutter pub add qr_flutter





添加如下代碼到qr_view.dart文件:

import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';

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

  @override
  State<QRView> createState() => _QRViewState();
}

class _QRViewState extends State<QRView> {
  late final TextEditingController _textController;
  late final FocusNode _textFocus;
  String qrText = '';
  int qrColorIndex = 0;
  int qrBackgroundColorIndex = 0;

  @override
  void initState() {
    _textController = TextEditingController(text: qrText);
    _textFocus = FocusNode();

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: Add the UI code here
    return Scaffold();
  }
}
在上面的代碼中,我們導(dǎo)入了 qr_flutter 包,并為 TextField 小部件初始化了一些變量,并用于訪問(wèn)當(dāng)前選擇的 QR 碼背景和前景色。

現(xiàn)在,在 lib 目錄中創(chuàng)建另一個(gè)名為 color_list.dart 的文件,并添加顏色列表以顯示為 QR 碼背景和前景色選擇。

import 'package:flutter/material.dart';

const List<Color> qrBackgroundColors = [
  Colors.white,
  Colors.orange,
  Colors.blueGrey,
  Colors.red,
  Colors.greenAccent,
];

const List<Color> qrColors = [
  Colors.black,
  Colors.purple,
  Colors.white,
  Colors.green,
  Colors.blue,
];
最后,完成擴(kuò)展的用戶界面。

這是 QRView 小部件的完整代碼以及擴(kuò)展的 UI:

import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';

import 'color_list.dart';

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

  @override
  State<QRView> createState() => _QRViewState();
}

class _QRViewState extends State<QRView> {
  late final TextEditingController _textController;
  late final FocusNode _textFocus;
  String qrText = '';
  int qrColorIndex = 0;
  int qrBackgroundColorIndex = 0;

  @override
  void initState() {
    _textController = TextEditingController(text: qrText);
    _textFocus = FocusNode();

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Padding(
        padding: const EdgeInsets.symmetric(
          horizontal: 16.0,
          vertical: 24.0,
        ),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            ClipRRect(
              borderRadius: BorderRadius.circular(16),
              child: QrImage(
                data: qrText,
                padding: const EdgeInsets.all(16),
                backgroundColor: qrBackgroundColors[qrBackgroundColorIndex],
                foregroundColor: qrColors[qrColorIndex],
              ),
            ),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.symmetric(
                  horizontal: 24.0,
                  vertical: 16,
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    TextField(
                      controller: _textController,
                      focusNode: _textFocus,
                      decoration: InputDecoration(
                        labelText: 'QR Text',
                        labelStyle: const TextStyle(
                          color: Color(0xFF80919F),
                        ),
                        hintText: 'Enter text / URL',
                        hintStyle: const TextStyle(
                          color: Color(0xFF80919F),
                        ),
                        enabledBorder: OutlineInputBorder(
                          borderSide: const BorderSide(
                            color: Colors.black54,
                            width: 2,
                          ),
                          borderRadius: BorderRadius.circular(16),
                        ),
                        focusedBorder: OutlineInputBorder(
                          borderSide: const BorderSide(
                            color: Colors.black,
                            width: 2,
                          ),
                          borderRadius: BorderRadius.circular(16),
                        ),
                      ),
                      onChanged: (value) => setState(() {
                        qrText = value;
                      }),
                    ),
                    const SizedBox(height: 24),
                    const Text(
                      'Choose QR Color',
                      style: TextStyle(
                        color: Colors.black,
                        fontSize: 16,
                      ),
                    ),
                    Expanded(
                      child: ListView.separated(
                        shrinkWrap: true,
                        scrollDirection: Axis.horizontal,
                        separatorBuilder: (_, __) => const SizedBox(width: 8),
                        itemCount: qrColors.length,
                        itemBuilder: (context, index) {
                          return InkWell(
                            hoverColor: Colors.transparent,
                            splashColor: Colors.transparent,
                            highlightColor: Colors.transparent,
                            onTap: () => setState(() {
                              qrColorIndex = index;
                            }),
                            child: Stack(
                              alignment: Alignment.center,
                              children: [
                                CircleAvatar(
                                  radius: qrColorIndex == index ? 23 : 22,
                                  backgroundColor: qrColorIndex == index
                                      ? Colors.black
                                      : Colors.black26,
                                ),
                                CircleAvatar(
                                  radius: 20,
                                  backgroundColor: qrColors[index],
                                ),
                              ],
                            ),
                          );
                        },
                      ),
                    ),
                    const Text(
                      'Choose QR Background Color',
                      style: TextStyle(
                        color: Colors.black,
                        fontSize: 16,
                      ),
                    ),
                    Expanded(
                      child: ListView.separated(
                        shrinkWrap: true,
                        scrollDirection: Axis.horizontal,
                        separatorBuilder: (_, __) => const SizedBox(width: 8),
                        itemCount: qrBackgroundColors.length,
                        itemBuilder: (context, index) {
                          return InkWell(
                            hoverColor: Colors.transparent,
                            splashColor: Colors.transparent,
                            highlightColor: Colors.transparent,
                            onTap: () => setState(() {
                              qrBackgroundColorIndex = index;
                            }),
                            child: Stack(
                              alignment: Alignment.center,
                              children: [
                                CircleAvatar(
                                  radius:
                                      qrBackgroundColorIndex == index ? 23 : 22,
                                  backgroundColor:
                                      qrBackgroundColorIndex == index
                                          ? Colors.black
                                          : Colors.black26,
                                ),
                                CircleAvatar(
                                  radius: 20,
                                  backgroundColor: qrBackgroundColors[index],
                                ),
                              ],
                            ),
                          );
                        },
                      ),
                    ),
                    const SizedBox(height: 16),
                  ],
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}
轉(zhuǎn)到 web/index.html 文件,并將起始  標(biāo)記內(nèi)定義的尺寸更改為以下內(nèi)容:

<html style="height: 350px; width: 650px">
擴(kuò)展程序的視圖尺寸必須明確定義,上述尺寸應(yīng)正確適應(yīng)擴(kuò)展 UI。

至此,您已成功創(chuàng)建 QR 碼生成器擴(kuò)展。運(yùn)行相同的 flutter build 命令來(lái)重新生成 Flutter web 文件:

flutter build web --web-renderer html --csp
按照我們之前討論的相同步驟安裝擴(kuò)展。

哇喔!您的 QR 碼生成器擴(kuò)展程序已準(zhǔn)備好使用。??



局限性
盡管看起來(lái)令人興奮,但也存在一定的局限性。

以下是我在探索如何使用 Flutter 構(gòu)建 chrome 擴(kuò)展時(shí)遇到的一些限制(其中一些可能是可以修復(fù)的,這需要更多的探索)。

1. 僅限于 HTML 渲染器
另一個(gè)讓 QR 碼生成器擴(kuò)展更酷的附加功能是添加了“另存為圖像”按鈕。使用它,您可以將生成的二維碼保存為普通圖像文件。

但不幸的是,由于擴(kuò)展不支持 CanvasKit 渲染器,因此您不能在任何 RenderRepaintBoundary 對(duì)象上使用 toImage() 方法(這是將任何 Flutter 小部件轉(zhuǎn)換為圖像格式所必需的)。

2. 無(wú)法使用 Firebase(應(yīng)該可以)
如果您還記得,最初我們修改了 index.html 文件以刪除大部分 <script> 標(biāo)記。這是為了滿足內(nèi)容安全策略 (CSP) 規(guī)則。Manifest V3 不支持第三方 URL 的內(nèi)聯(lián)執(zhí)行。

但是我嘗試運(yùn)行一個(gè)較舊的項(xiàng)目,該項(xiàng)目不使用 Flutter 中的 Firebase 的僅 Dart 初始化(很可能應(yīng)該解決它,因?yàn)樵谶@種情況下您不需要在 <script> 標(biāo)記內(nèi)定義 Firebase 插件 )。

3. 小部件狀態(tài)丟失
這可能不被視為限制,而只是您在使用 Flutter 構(gòu)建擴(kuò)展時(shí)應(yīng)牢記的一點(diǎn)。每當(dāng)您在擴(kuò)展視圖之外單擊以將其關(guān)閉時(shí),您的所有應(yīng)用程序狀態(tài)都會(huì)丟失。

您應(yīng)該使用 Flutter 插件在本地存儲(chǔ)狀態(tài),例如 shared_preferences 插件(它工作正常,我已經(jīng)在擴(kuò)展中測(cè)試過(guò)它)。

最后,QR 碼生成器 Chrome 擴(kuò)展的 GitHub 代碼倉(cāng)庫(kù)可在以下鏈接中找到:

https://github.com/sbis04/flutter_qr_extension
參考
Chrome 擴(kuò)展文檔:https://developer.chrome.com/docs/extensions/

Flutter Web 文檔:https://docs.flutter.dev/get-started/web

Flutter Web 渲染器:https://docs.flutter.dev/development/platform-integration/web/renderers

原文:https://medium.com/flutter-community/building-a-chrome-extension-using-flutter-aeb100a6d6c


歡迎關(guān)注微信公眾號(hào) :前端開(kāi)發(fā)愛(ài)好者


添加好友備注【進(jìn)階學(xué)習(xí)】拉你進(jìn)技術(shù)交流群