由于业务需要,最近在做chrome
插件,于是寻找构建插件社区,找到了plasmo
,应该是社区中比较优秀的构建chrome
插件的脚手架,但最终还是没有用,原生插件非常简单,具体可以参考写个自己的chrome插件,为满足当下业务需要,于是基于webpack5
搭建一个构建插件的基本脚手架,本文是一篇搭建chrome
插件的实践总结,希望看完在项目中有所帮助。
插件基本能力
- 使用
webpack5
搭建,支持react18.x
- 支持
tsx
、jsx
构建函数组件 - 支持
tailwindcss
- 支持预览
tabs
页面预览
前置
在使用webpack5
搭建插件,可以参考笔者在之前写的webpack5系列笔记中有部分搭建案例,chrome插件
本质上是运行在chrome浏览器的网页,因此本质上也是用网页来展现的,只是一个插件的必须满足根目录必须mainifest.json
,我们具体以下面一张图来重新回顾下插件的基本要素
插件核心
- base chrome plugin
- 关键文件
manifest.json
在这个文件中,构建一个插件manifest.json
是必不可少的,其中注意一点manifest_version
是3
版本,因为好多新特性2
版本并不支持,同时当我们需要操作chrome
的api
时,在permissions
开放操作权限
json
{
"name": "base chrome plugin chrome", //插件的名称
"description": "a simple chrome plugin", // 插件描述
"version": "1.0.0", // 当前插件版本
"manifest_version": 3, // chrome插件必填版本3
"action": {
"default_icon": "assets/imgs/icon.png", // 插件icon
"default_title": "demo chrome plugin",
"default_popup": "popup.html" // popup页面
},
"background": {
"service_worker": "./src/background/index.js",
"type": "module"
},
"icons":
{
"16": "assets/imgs/icon.png"
},
"content_scripts": [
{
"matches": [
"<all_urls>" // 插件在任何页面可用
],
"exclude_matches": [ // 排除插件在部分页面中不可用
],
"js": [
"src/content/index.js" // 匹配的页面中加载content.js
],
"css": [
"assets/css/index.css" //匹配的页面加载css
],
"run_at": "document_end",
"all_frames": false
}
],
"host_permissions": [
],
"permissions": [
"tabs", // chrome api操作的权限
"contextMenus",
"notifications",
"webRequest",
"activeTab"
],
"omnibox": { "keyword" : "demo" },
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+Y",
"mac": "Command+Shift+Y"
},
"description": "Opens tabs.html"
}
}
}
webpack.config.js
这是插件的基础配置文件,主要讲解几个关键的配置
entry
我们看到entry
主要是background
、popup
、content
、set
的几个文件,这是打包的入口文件,在webpack
入口文件,会根据entry
找到自己依赖的模块,从而进行加载
js
// webpack.confg.js
const path = require("path");
const resolvePath = (dir) => {
return path.resolve(__dirname, dir);
};
module.exports = (env) => {
return {
entry: {
background: resolvePath("src/background/index"),
popup: resolvePath("src/pages/popup/index"),
content: resolvePath("src/pages/content/index"),
set: resolvePath("src/pages/tabs/set/index"),
}
}
}
output
output
会根据entry
的文件,输出到指定path
的文件夹中,其中publicPatch
指定绝对路径访问打包后的资源
js
module.exports = (env) => {
const PluginFileAssetsName = `dist/${env.mode}/${fileName}`;
return {
...
output: {
publicPath: "/",
path: path.join(__dirname, PluginFileAssetsName),
filename: "src/[name]/index.js",
},
}
}
plugins
在这个配置中,主要做了一下几件事情
- 注册
env
变量,让.env.xx
中的变量在其他文件中能被访问 - 使用
html-webpack-plugin
插件生成popup
、set
页面 - 使用
copy-webpack-plugin
插件将引入的资源复制到指定输出的文件夹中
js
module.exports = (env) => {
return {
...,
plugins: [
new Dotenv({
path: path.resolve(process.cwd(), `.env.${env.mode}`),
}), // 读取本地.env本地
new Html({
filename: "popup.html",
template: "./public/index.html",
chunks: ["popup"], // 打包后只会包含popup与content,避免将其他js引入
hash: false,
minify: {
removeComments: true,
collapseWhitespace: true,
minifyCSS: true,
},
title: "test popup",
}),
new Html({
filename: "set.html",
template: "./public/index.html",
chunks: ["set"],
hash: false,
title: "set",
minify: {
removeComments: true,
collapseWhitespace: true,
minifyCSS: true,
},
}),
new MiniCssExtractPlugin({
filename: "css/[name].css",
chunkFilename: "css/[name].css",
}),
new copyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, "manifest.json"),
to: path.join(__dirname, `${PluginFileAssetsName}/`),
},
{
from: path.join(__dirname, "src/assets/imgs"),
to: path.join(__dirname, `${PluginFileAssetsName}/assets/imgs`),
},
{
from: path.join(__dirname, "src/assets/css"),
to: path.join(__dirname, `${PluginFileAssetsName}/assets/css`),
},
],
}),
new CleanWebpackPlugin(),
],
}
}
- 支持
jsx
与tsx
支持jsx
与tsx
主要是利用babel-loader
,在rules
这个配置中,同时我们也对.tsx,.ts
单独使用ts-loader
去加载
js
module.exports = {
rules: [
{
test: /\.(js|jsx|tsx)$/i,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
],
},
{
test: /\.(tsx|ts)$/i,
use: [
{
loader: "ts-loader",
},
],
},
]
}
同时,在tsconfig.json
中我们也需要设置
json
{
"compilerOptions": {
...
"target": "ES5",
"jsx": "react-jsx",
"paths": {
"@public/*": ["public/*"],
"@comp/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@src/*": ["src/*"],
"@assets": ["src/assets/*"]
},
"baseUrl": "."
},
}
其中你看到有设置通用路径别名,不过,除了这里设置,我们同时也需要设置resolve.alains
js
module.exports = {
...
resolve: {
extensions: [".tsx", ".ts", ".js", ".jsx"],
alias: {
"@public": resolvePath("public/"),
"@utils": resolvePath("src/utils/"),
"@src": resolvePath("src/"),
"@comp": resolvePath("src/components/"),
"@assets": resolvePath("src/assets/"),
}
}
}
这样你就可以在tsx,jsx
文件中构建我们的组件了。
- 支持tailwindcss 参考官网tailwindcss,不过注意在
postcss.config.js
中设置
js
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
},
};
其实我们已经完成了一个插件,当我们执行pnpm run build:local
时,就会打包成一个插件的最终输出文件了
我们打开chrome浏览器
,扩展程序
>打开开发者模式
>加载已解压的扩展程序
这个文件夹即可
安装插件后,访问任何一个网站,我们在content
写入的内容就生效了
不知道你有没有好奇,当我们使用插件时,我们必须修改代码,然后执行打包,然后重新加载插件看效果,这显然与我们实际开发有些繁琐,因此,你可以执行将content
变成一个多页页面,这样可以就可以高效的开发了,不过你需要稍修改下manifest.json
的exclude_matches
,主要为了插件不在当前访问本地开发页面生效
json
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"exclude_matches": [
"http://localhost:8080/*"
],
"js": [
"src/content/index.js"
],
"css": [
"assets/css/index.css"
],
"run_at": "document_end",
"all_frames": false
}
],
至于tab
页面,我们也是直接访问/set.html
就可以了
因此一个插件的基础就基本完成了。
总结
- 理解一个插件的基本要素,关键是
manifest.json
这个文件必不可少 webpack5
支持构建react18.x
,支持tsx
、jsx
构建组件- 如何让项目支持
tailwindcss
- 预览当前构建的页面比如
set
页面,需要在exclude_matches
字段中排出当前指定的端口域名 - 本文示例code example