基于 React+Antd 实现的 chrome 插件 TodoList demo - 配置篇 【1】

·  阅读 94

基于 React+Antd 实现的 chrome 插件 TodoList demo

截屏2022-03-29 下午7.19.25.png

前言

前段时间了解了一下有关如何开发一个 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 配置。因此这边需要设置三个配置文件

截屏2022-03-29 下午7.05.28.png

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 模板。

截屏2022-03-29 下午7.11.55.png

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 之外的扩展页面。

截屏2022-03-29 下午7.12.17.png

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',
  })
复制代码

三、页面布局

截屏2022-03-29 下午4.04.20.png

截屏2022-03-29 下午4.04.27.png

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 欢迎👏🏻一起交流学习

文章肯定有写的不好的地方,可以评论区指正❤。

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改