本文译者为 360 奇舞团前端开发工程师
原文标题:Simplify Chrome Extension Development: Add React without CRA
原文作者:Kristian Ivanov
原文地址:
https://dev.to/k_ivanow/simplify-chrome-extension-development-add-react-without-cra-5h4c
简介
我们都喜欢 React,对吧(除了 Primeagen)。它通过提供一种简单的方法来处理状态,以及访问一个巨大的 npm 模块和组件库,极大地简化了复杂 UI 的构建。它甚至提供了 Create React App (CRA),只需一个命令就可以快速引导并完成初始设置。
我是 Create React App 的粉丝。它非常适合大多数 web 应用程序,在看到许多从 CRA 弹出的应用程序或使用 gulp 构建/打包的应用后(有时我会觉得自己老了),即使不使用 TypeScript,我也逐渐欣赏它的简单性,以及它在我启动任何新项目时所带来的便利。
然而,在构建像 Chrome 扩展这样的项目时,CRA并不适用,生成的代码过于臃肿。此外,在扩展中,UI 可能只是项目的一部分,大部分逻辑被封装在后台/服务工作线程或内容脚本中。这意味着将大量冗余代码与 React 的使用方式绑定并不总是最佳选择。
另外,如果你开始时只是简单地在 UI 侧写了一些 HTML 和 Tailwind 代码来测试你的想法,然后为扩展的其他部分编写了一堆代码,然后决定让 UI 更易于维护和扩展,并将 React 添加到其中,这该怎么办?
在本指南中,我们将使用 TypeScript、Webpack 和 Tailwind 构建一个 Chrome 扩展,然后以更轻量的方式引入 React,而不使用 CRA。
设置
我最近想开始一个非常简单的 Chrome 扩展,探索一些潜在的 Mutation Observer 用例来定制页面。同时,我也想借此机会尝试 TypeScript,因为我已经有一段时间没有使用它了。
1 设置项目
首先,我们创建一个新的项目目录并用 npm 初始化它:
mkdir ts-chrome-extension
cd ts-chrome-extension
npm init -y
2 添加 TypeScript
安装 TypeScript:
npm install --save-dev typescript
安装完成后,我们需要创建一个 TypeScript 配置文件。该文件将告诉 TypeScript 编译器如何工作。创建 tsconfig.json
文件:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es2015"],
"module": "esnext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
有关 tsconfig.json
文件的更多细节,请查看官方文档。
3 添加基本文件
创建文件夹和基本文件:
mkdir -p src/scripts && mkdir src/popup && touch src/manifest.json src/scripts/{background.ts,contentScript.ts} src/popup/{popup.html,popup.ts}
在此时,我们的文件夹结构应如下所示:
├── package.json
├── package-lock.json
├── popup.ts
├── src
│ ├── manifest.json
│ ├── popup
│ │ ├── popup.html
│ │ └── popup.ts
│ └── scripts
│ ├── background.ts
│ └── contentScript.ts
└── tsconfig.json
4 清单文件
清单文件是 Chrome 扩展的基础。它告诉 Chrome 扩展的功能以及在哪里找到其脚本。我们创建了清单文件,现在填充它:
{
"manifest_version": 3,
"name": "TypeScript Chrome Extension",
"version": "1.0",
"description": "A Chrome extension built using TypeScript.",
"action": {
"default_popup": "popup.html"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["contentScript.js"]
}
],
"permissions": ["storage"]
}
说明:
Action and Popup:
"action"
键告诉 Chrome 弹出 HTML 的位置。用户点击扩展图标时,弹出窗口将加载。Service Worker:
"service_worker"
在"background"
键中,用于后台任务。Content Scripts:这些脚本允许扩展与网页进行交互。
Permissions:
"storage"
权限使扩展能够通过 Chrome 的存储 API 存储用户数据。
有关 Chrome 扩展的 manifest.json
的完整文档,在这里找到。
5 弹出窗口文件
由于我们还未使用 React,弹出窗口将简单地处理基本的 DOM 交互。
5.1 popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popup</title>
</head>
<body>
<div>
<h1>Hello, Chrome Extension!</h1>
<button id="alertButton">Click me</button>
</div>
<script src="popup.js"></script>
</body>
</html>
这是一个简单的 HTML 页面,包含一个按钮和对 popup.js
的脚本引用(将从 popup.ts
打包而来)。
5.2 popup.ts
该文件将处理弹出窗口的基本交互:
document.addEventListener('DOMContentLoaded', () => {
const alertButton = document.getElementById('alertButton');
if (alertButton) {
alertButton.addEventListener('click', () => {
alert('Button clicked!');
});
}
});
这个简单的脚本在 DOM 加载后等待,然后为按钮添加点击监听器,以触发警报。在我们添加 React 之前,此结构为弹出功能奠定了基础。
6 编译 TypeScript
接下来,我们将 TypeScript 文件编译为 JavaScript。运行以下命令:
npx tsc
这将会把 TypeScript 文件转换为 JavaScript。popup.ts
、background.ts
和 contentScript.ts
将被编译为 popup.js
、background.js
和 contentScript.js
。但由于我们很快会添加 Webpack 来管理打包,因此这只是一个临时步骤。
7 添加 Webpack 进行打包
现在我们有了基本结构,我们需要 Webpack 来处理将所有脚本打包成 Chrome 可加载的优化文件。它可以处理图标的复制、将 ts 转换为 js、自动注入脚本或样式文件等等。
7.1 安装 Webpack 和必要的加载器
npm install --save-dev webpack webpack-cli ts-loader html-webpack-plugin mini-css-extract-plugin css-loader postcss postcss-loader copy-webpack-plugin
webpack: 核心打包工具。
ts-loader: 为 Webpack 转换 TypeScript。
html-webpack-plugin: 帮助生成带有注入脚本标签的 HTML 文件。
CSS and PostCSS Loaders: 用于处理 CSS(我们稍后将与 Tailwind 一起使用)。
CopyWebpackPlugin: 确保将
manifest.json
和任何其他资产(如图标)从src
文件夹复制到dist
文件夹。
7.2 Webpack 配置
接下来,我们需要配置 Webpack 来打包所有内容。创建 webpack.config.js
在项目根目录下:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: {
popup: './src/popup/popup.ts',
background: './src/scripts/background.ts',
contentScript: './src/scripts/contentScript.ts',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
new HtmlWebpackPlugin({
filename: 'popup.html',
template: 'src/popup/popup.html',
chunks: ['popup'],
}),
new CopyWebpackPlugin({
patterns: [
{ from: 'src/manifest.json', to: 'manifest.json' },
// { from: 'src/icons', to: 'icons' }, // 复制任何其他资产
],
}),
],
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? false : 'inline-source-map', // 在生产中禁用源映射
};
};
说明:
Entry Points: 我们指定了
popup.ts
、background.ts
和contentScript.ts
的入口点。Output: 打包文件将输出到
dist
文件夹。Loaders: 使用
ts-loader
处理 TypeScript,并使用css-loader
和postcss-loader
处理 CSS(稍后我们将引入 Tailwind CSS)。HtmlWebpackPlugin: 此插件自动将打包后的
popup.js
文件注入到popup.html
文件中,确保我们的脚本正确链接。
7.3 使用 Webpack 打包扩展
让我们通过运行以下命令来打包所有内容:
npx webpack --mode development
这将把打包后的文件输出到 dist
目录中。
如果你在图标部分遇到问题,可以选择暂时删除它或放置一些占位的 .png
文件。
测试弹出窗口
点击按钮会弹出警报,可能会弹出两次。为什么会这样?
Webpack 可以注入脚本标签。检查一下你在 dist
文件夹中的 popup.js
,你会发现 popup.js
被注入了两次(一次来自 HTML 文件本身,一次来自 Webpack)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popup</title>
<script defer src="popup.js"></script>
</head>
<body>
<div>
<h1>Hello, Chrome Extension!</h1>
<button id="alertButton">Click me</button>
</div>
<script src="popup.js"></script>
</body>
</html>
要解决这个问题,只需删除 src/popup
文件夹中的 <script>
标签,确保它看起来像这样:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popup</title>
</head>
<body>
<div>
<h1>Hello, Chrome Extension!</h1>
<button id="alertButton">Click me</button>
</div>
</body>
</html>
不用担心,Webpack 仍会在内部添加它。
8 添加 Tailwind 进行样式设计
Tailwind CSS 是一个以实用优先的 CSS 框架,让你可以在不为每个组件编写自定义 CSS 的情况下为用户界面进行样式设计。
多年来,人们对它越来越喜欢。虽然自己添加一些基本样式会更简单,但当你知道会在这个项目上花费时间时,Tailwind 使得后期扩展更容易,并能保持一致性,以便其他人加入时使用(相同的类名总是意味着相同的样式,而如果是自己实现的,font-size-small 可能会有多种含义)。
8.1 安装 Tailwind
npm install tailwindcss postcss postcss-loader autoprefixer
8.2 配置 Tailwind 和 PostCSS
接下来,我们需要初始化 Tailwind 和配置 PostCSS。通过以下命令初始化 Tailwind:
npx tailwindcss init
这将在项目根目录创建一个 tailwind.config.js
文件。打开该文件并修改,以包含 Tailwind 应该应用样式的路径(在我们的案例中,位于 src
文件夹内):
module.exports = {
content: ['./src/**/*.{ts,tsx,html}'],
theme: {
extend: {},
},
plugins: [],
};
content
键告诉 Tailwind 在你的文件中查找类。
接下来,在根目录中创建 postcss.config.js
文件,并配置它以使用 Tailwind 和 Autoprefixer:
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
8.3 创建 Tailwind CSS 入口文件
在 src/styles
中创建一个名为 tailwind.css
的新文件。我们将在这里导入基本的 Tailwind 样式:
@tailwind base;
@tailwind components;
@tailwind utilities;
此文件将包括所有的 Tailwind 实用类。
8.4 修改 Webpack 配置以处理 CSS
我们需要确保 Webpack 知道如何使用 PostCSS 处理 CSS 文件。我们之前已经安装了 postcss-loader
和 css-loader
,现在只需确保它们正确配置。
将以下配置更改为:
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
}
这样可以确保 Webpack 在我们在弹出窗口中包含 Tailwind CSS 时进行处理。接下来,我们将继续实现这个功能。
8.5 在弹出窗口中导入 Tailwind
要开始使用 Tailwind,我们需要在 popup.ts
文件顶部添加以下导入语句:
import '../styles/tailwind.css';
8.6 添加简单的 Tailwind 样式
更新 popup.html
文件,使用一些 Tailwind 类进行基本样式设置。这样,我们的 popup.html
应该看起来像这样:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popup</title>
</head>
<body class="bg-gray-100 p-4 w-80">
<div class="flex flex-col items-center">
<h1 class="text-xl font-bold mb-4">Hello, Chrome Extension!</h1>
<button id="alertButton" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Click me
</button>
</div>
<script src="popup.js"></script>
</body>
</html>
这为弹出窗口添加了背景色、居中内容和样式化的按钮。运行 npx webpack --mode development
重新构建它,看看效果。
9 为弹出窗口添加 React
现在我们有了一个功能性的扩展,包含 Tailwind 样式和 TypeScript 的语法优点。为了应对更复杂的 UI,比如加载和卸载数据、处理选项卡等,我们将添加 React。
9.1 安装 React 和 ReactDOM
npm install react react-dom @types/react @types/react-dom
9.2 更新 Webpack 以支持 React
我们需要更新 Webpack,以便它可以处理 .tsx
文件(使用 TypeScript 的 React)。记住,这正是我们设定的主要目标。 为此,请修改 webpack.config.js
文件,将 React 添加为入口点:
entry: {
popup: './src/popup/popup.tsx', // 更新为指向新的 React 组件
...
},
resolve: {
extensions: ['.ts', '.tsx', '.js'], // 添加 .tsx 以解析 React 文件
},
module: {
rules: [
{
test: /\.tsx?$/, // 同时处理 .ts 和 .tsx 文件
...
9.3 将 popup.ts 转换为 React 组件
现在我们将把 popup.ts
转换为一个 React 组件。将 popup.ts
重命名为 popup.tsx
,并更新为一个基本的 React 组件:
import React from 'react';
import { createRoot } from 'react-dom/client';
import '../styles/tailwind.css';
const Popup = () => {
const handleClick = () => {
alert('Button clicked!');
};
return (
<div className="flex flex-col items-center bg-gray-100 p-4">
<h1 className="text-xl font-bold mb-4">Hello, React Chrome Extension!</h1>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={handleClick}
>
Click me
</button>
</div>
);
};
const container = document.getElementById('root');
const root = createRoot(container as HTMLDivElement);
root.render(<Popup />);
这个简单的 React 组件实现了原始 popup.ts
的功能,但使用了 React 的 onClick
事件来处理按钮点击。
9.4 更新 popup.html
我们不需要对 popup.html
进行太多更改,只需确保它有合适的 div
来挂载我们的 React 组件。以下是更新后的 popup.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popup</title>
</head>
<body class="bg-gray-100 w-80">
<div id="root"></div>
</body>
</html>
9.5 更新 tsconfig.json
我们需要在 tsconfig.json
中添加 "jsx": "react"
和 "moduleResolution": "node"
。文件应如下所示:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es2015"],
"module": "esnext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"jsx": "react",
"moduleResolution": "node"
},
"include": ["src/**/*"]
}
通过这些步骤,我们为弹出窗口成功添加了 React!
9.6 使用 Webpack 重新构建
完成更改后,运行 Webpack 来重新构建扩展:
npx webpack --mode development
现在,当你在 Chrome 中重新加载扩展并打开弹出窗口时,你应该会看到一个由 React 驱动、使用 Tailwind CSS 样式的用户界面。
结论 为什么不使用 Create-React-App?
Create-React-App 对如何构建应用做出了很多假设,将所有内容打包成一个单页应用并动态注入 JS。这种方式非常适合 React 网络应用,但不太适合 Chrome 扩展, 因为Chrome 扩展使用单独的 HTML 页面来处理弹出窗口、后台脚本,有时甚至还有选项页面。
不使用 Create-React-App 的最佳部分是什么? 控制和学习。
你可以决定包含哪些内容,打包的方式,以及你的扩展是如何精简还是丰富的。 我还认为,尝试在没有库、框架或引导工具的情况下看看有多困难是个好主意。这有助于更深入地了解所有间接使用的、隐藏在你面前的技术。 如果你看到这里,我感谢你,并希望这些内容对你有用!