基于 React+Antd 实现的 chrome 插件 TodoList demo
前言
前段时间了解了一下有关如何开发一个 chrome 插件,但是网上很多都是使用javascript开发,没有了我们平时开发项目的 ui 组件库,或者像vue,react开发框架等,使得开发起来有点困难。从今天开始实现一个小小的 todolist 插件 demo,使用我们常用的 React 框架搭配 Antd 组件库。
既然我们使用了 React框架 等一些浏览器不能直接去编译执行的代码,因此我还需要一个强大的打包工具 webpack,来帮我们完成这些操作。
于是本篇文章就不仅仅只是一个插件的开发,更是带你了解如何搭建一个适合开发 chrome 插件的脚手架。下面是我暂时列出的需要考虑的配置:
一、chrome 插件
每个应用(扩展)都应该包含下面的文件:
- 一个 manifest 文件
- 一个或多个 html 文件(除非这个应用是一个皮肤)
- 可选的一个或多个 javascript 文件
- 可选的任何需要的其他文件,例如图片
在开发应用(扩展)时,需要把这些文件都放到同一个目录下。发布应用(扩展)时,这个目录全部打包到一个应用(扩展)名是.crx 的压缩文件中。如果使用 Chrome Developer Dashboard,上传应用(扩展),可以自动生成.crx 文件。
那么如何使用webpack进行打包呢?接着往下看
二、基本配置
开发环境(development)和生产环境(production) 的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,则需关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,每个环境编写彼此独立的 webpack 配置。因此这边需要设置三个配置文件
2.1. webpack.common 公用配置
2.1.1. api 或者插件引入
/* eslint-disable global-require */
const path = require('path') // node 中的path api
const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin') // 在webpack中拷贝文件和文件夹
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
clean-webpack-plugin
默认 webpack 打包后的 dist 文件夹下的 js 文件并不会被自动删除,
如果重新打包,会生成新的文件,旧的文件仍然会存在。
使用此插件 webpack 打包后的 dist 目录中的所有文件将被删除一次。
const HtmlWebpackPlugin = require('html-webpack-plugin')
html-webpack-plugin该插件将为你生成一个 HTML5 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
mini-css-extract-plugin
将 css 单独打包成一个文件的插件,它为每个包含 css 的 js 文件都创建一个 css 文件。它支持 css 和 sourceMaps 的按需加载。
和 extract-text-webpack-plugin 相比:
- 异步加载
- 无重复编译,性能有所提升
- 用法简单
- 支持 css 分割
const rootDir = path.resolve(__dirname, '..')
const wrapperClassName = `chrome-extension-base-class${Math.floor(Math.random() * 10000)}`
const postCssPlugins = [
require('autoprefixer'), // autoprefixer可以自动在样式中添加浏览器厂商前缀,避免手动处理样式兼容问题
// 全局添加css wrapperClassName前缀
require('postcss-plugin-namespace')(`.${wrapperClassName}`, {
ignore: ['#chrome-extension-content-base-element'],
}),
]
举例:
postcss([ require('postcss-plugin-namespace')('.insert-selector') ])
input:
.foo {
/* Input example */
}
output:
.insert-selector .foo {
/* Output example */
}
2.1.2. 配置入口,出口
目录结构:
首先我们要清楚最终可以放到扩展应用中的文件dist目录:
不需要经过 webpack 打包的文件,称为静态资源文件,静态资源放在 public 目录下,这些文件会直接放到 dist 目录下。 比如 icons 和 images 中的图片。 manifest.json 也需要直接放到 dist 目录下。
而 html 目录下放着的是 react 项目的 html 模板文件,由于 chrome extension 是多入口的结构,我们会有多个 html 模板。
src 下的文件按照功能模板划分,以 popup 为例,popup 文件夹下的 jsx 文件最终会生成 dist/html/popup.html, dist/js/popup.js, dist/css/popup.css。
background 和 content-scripts 也是类似的功能。chrome 下封装 chrome extension 的 API。view 目录下则放一些 popup,background 和 content-scripts 之外的扩展页面。
webpack 如何将源码打包为 chrome extension 需要的项目模式?
多入口打包
不难发现,chrome extension 的本质是一些 html + js 代码集合,chrome 浏览器会按照 manifest.json 中的配置,加载并运行这些代码,不同的 js 代码作用域以及生命周期完全不同。
因此,源码中 src 下的 background,content-scripts,popup 需要独立打包,他们之间的使用 chrome 目录下封装的 API 进行通讯。
在 webpack.config.js 中进行多入口文件的配置。我这里配置了 4 个入口,popup,background,contentScripts 这三个肯定是不同的入口。
还有一个 demo 入口是为了拓展新页面,因为我们在写 chrome extension 的时候经常会有一些特殊的页面来实现比较复杂的功能,这些页面的路径类似于 chrome-extension:/XXX/xxx.html,它也是一个独立的页面。
module.exports = {
entry: {
popup: './src/popup', // 插件入口
background: './src/background', // 插件配置管理入口
contentScripts: './src/content-scripts',
demo: './src/view/demo',
},
output: {
path: path.resolve(rootDir, './dist/js'), // 编译完后输出的文件地址
filename: '[name].js',
},
2.1.3. 配置 module
module: {
// rules: js/jsx; css; less; scss sass; png|svg|jpg|gif|jpeg;
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
modules: false,
useBuiltIns: 'usage',
corejs: {
version: 3,
proposals: true,
},
},
],
'@babel/preset-react',
],
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties'],
[
'babel-plugin-import',
{
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
},
],
],
},
},
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
ident: 'postcss',
plugins: postCssPlugins,
},
},
},
],
},
{
test: /\.less$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
ident: 'postcss',
plugins: postCssPlugins,
},
},
},
{
loader: 'less-loader',
options: {
lessOptions: {
modifyVars: {
'primary-color': '#722ed1',
},
javascriptEnabled: true,
},
},
},
],
},
{
test: /\.(scss|sass)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
ident: 'postcss',
plugins: postCssPlugins,
},
},
},
{
loader: 'sass-loader',
},
],
},
{
test: /\.(png|svg|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: 'static/[name]-[hash].[ext]',
},
},
],
},
],
},
2.1.4. 配置支持的扩展
resolve: {
extensions: ['.js', '.jsx'],
},
2.1.5. 配置 plugin
介绍下使用到的插件:
-
mini-css-extract-plugin插件将 css 打包成一个单独的包,支持按需引用 -
clean-webpack-plugin将打包生成的 dist 目录删掉 (webpack 默认是不会删掉的) -
copy-webpack-plugin将 public 的文件复制到 dist 目录下面,展示如下代码👇🏻 -
Html-webpack-plugin:插件的基本作用就是生成 html 文件。
原理如下:
- 将 webpack 中
entry配置的相关入口 chunk 和extract-text-webpack-plugin抽取的 css 样式 - 插入到该插件提供的
template或者templateContent配置项指定的内容基础上生成一个 html 文件, - 具体插入方式是将样式
link插入到head元素中,script插入到head或者body中。 - popup 支持配置一个 html,因此 popup 有一个 html 模板,使用 HtmlWebpackPlugin 将 public 目录下的 popup.html 加载进来
- 然后将打包后的 popup 模块作为一个 js 文件附上去。demo 模块也是一样的处理流程。
plugins: [
new MiniCssExtractPlugin({
filename: '../css/[name].css',
}),
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(rootDir, 'public/icons'),
to: path.resolve(rootDir, 'dist/icons'),
},
{
from: path.resolve(rootDir, 'public/images'),
to: path.resolve(rootDir, 'dist/images'),
},
{
from: path.resolve(rootDir, 'public/manifest.json'),
to: path.resolve(rootDir, 'dist/manifest.json'),
},
],
}),
new HtmlWebpackPlugin({
template: path.resolve(rootDir, 'public/html/popup.html'),
filename: path.resolve(rootDir, 'dist/html/popup.html'),
chunks: ['popup'],
}),
new HtmlWebpackPlugin({
template: path.resolve(rootDir, 'public/html/view.html'),
filename: path.resolve(rootDir, 'dist/html/view.html'),
chunks: ['demo'],
}),
new webpack.DefinePlugin({
WRAPPER_CLASS_NAME: `'${wrapperClassName}'`,
}),
],
}
- 开发环境(development)和生产环境(production)的构建目标差异很大。
- 在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。
- 而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。
- 由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
2.2. webpack.dev 开发环境
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const reloadServer = require('./ReloadServer')
const CompilerEmitPlugin = require('./plugins/CompilerEmitPlugin')
module.exports = () =>
merge(commonConfig, {
mode: 'development',
entry: {
popup: './src/popup',
background: ['./src/background', './src/reload/Background'],
contentScripts: ['./src/content-scripts', './src/reload/ContentScript'],
demo: './src/view/demo',
},
devtool: 'source-map',
plugins: [new CompilerEmitPlugin()],
devServer: {
lazy: false,
// 将 bundle 写到磁盘而不是内存
writeToDisk: true,
before(app) {
reloadServer(app)
},
},
})
2.3. webpack.prod 生产环境
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
module.exports = () =>
merge(commonConfig, {
mode: 'production',
devtool: 'source-map',
})
三、页面布局
src/popup 插件页面
import React, { Component, useState } from 'react'
import { Form, Input, Button, Checkbox, Rate, Radio, List, Avatar, Modal, Select } from 'antd'
import { DownloadOutlined, DeleteOutlined } from '@ant-design/icons'
import './Popup.scss'
import { go } from '../chrome'
const options = [
{ label: '待完成', value: 'Apple' },
{ label: '已完成', value: 'Pear' },
{ label: '全部', value: 'Orange' },
]
const data = [
{
title: 'Ant 1',
},
{
title: 'Ant 2',
},
{
title: 'Ant 3',
},
{
title: 'Ant 4',
},
]
const Popup = () => {
const [radioValue, setRadioValue] = useState('Apple')
const [isModalVisible, setIsModalVisible] = useState(false)
const onChange = (e) => {
console.log('radio3 checked', e.target.value)
setRadioValue(e.target.value)
}
const onChangeCheckbox = (e) => {
console.log(e)
}
const onHnadleAdd = (e) => {
setIsModalVisible(true)
}
const handleOk = () => {
setIsModalVisible(false)
}
const handleCancel = () => {
setIsModalVisible(false)
}
return (
<div className={`${WRAPPER_CLASS_NAME}`}>
<Radio.Group
className="basic-radio"
options={options}
onChange={onChange}
value={radioValue}
optionType="button"
/>
<List
itemLayout="horizontal"
dataSource={data}
className="basic-list"
renderItem={(item) => (
<Checkbox onChange={onChangeCheckbox} className="basic-list-checkout">
<List.Item>
<List.Item.Meta
avatar={<Avatar src="https://joeschmoe.io/api/v1/random" />}
title={<a href="https://ant.design">{item.title}</a>}
/>
</List.Item>
</Checkbox>
)}
/>
<Modal
getContainer={false}
title="Basic Modal"
visible={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
>
<Form
name="wrap"
labelCol={{ flex: '110px' }}
labelAlign="left"
labelWrap
wrapperCol={{ flex: 1 }}
colon={false}
>
<Form.Item label="Todo 标题" name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item label="Todo 内容" name="password" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</Modal>
<div className="basic-bottom">
<Button type="default" shape="round" icon={<DeleteOutlined />} size="large">
删除
</Button>
<Button
type="primary"
shape="round"
icon={<DownloadOutlined />}
size="large"
onClick={onHnadleAdd}
>
添加
</Button>
</div>
</div>
)
}
export default Popup
到这里我们已经可以实现代码的打包,使用 antd 进行 popoup 页面展示,那么如何与服务器端进行交互呢? 怎样真正实现 Todo 小项目?
一个应用(扩展)其实是压缩在一起的一组文件,包括 HTML,CSS,Javascript 脚本,图片文件,还有其它任何需要的文件。 应用(扩展)本质上来说就是 web 页面,它们可以使用所有的浏览器提供的 API,从 XMLHttpRequest 到 JSON 到 HTML5 全都有。
应用(扩展)可以与 Web 页面交互,或者通过 content script 或 cross-origin XMLHttpRequests 与服务器交互。应用(扩展)还可以访问浏览器提供的内部功能,例如标签或书签等。
待我先研究一波,哈哈哈
是按照大哥的文章学习的:感兴趣的可以看下,写的很好哇。我也是跟着学习 Thanks♪(・ω・)ノ
我是婧大,一名前端小学崽,希望和你一起学习一起进步。🙆🙆🙆
加油!wx:lj18379991972 欢迎👏🏻一起交流学习
文章肯定有写的不好的地方,可以评论区指正❤。