Chrome扩展框架系列文章
本人是一个web前端开发工程师,主要是vue框架,整理了一些Chrome扩展,今后也会一直更新,有好建议的同学欢迎评论区分享讨论 ;-)
文章目录
前言
之前写Chrome插件的时候,老是会遇到数据回显以及事件绑定的繁琐操作,代码量不多还好,随着需求的变动,项目到后期就显得很凌乱了,不好追溯问题,懒人创造天下,就想着能不能通过Vue来实现双向绑定问题,并且通过webpack打包。
(涵盖background,devtools pannel,popup,options,content_script)
一、Chrome插件是什么
Chrome 插件,如果是翻译过来,应该是Chrome扩展,是谷歌提供给用户对谷歌浏览器的功能进行扩展。用户可以通过扩展程序来根据个人的需求和喜好定制开发一些chrome功能。这些程序开发是基于html,javascript及css等技术。
二、Vue对于Chrome扩展有什么优势
- 轻量级框架,几十KB
- 视图以及数据分离
- 双向绑定以及组件化
- 是单页面web,符合Chrome扩展需要单个html文件或者js,该项目结构更加清晰明朗
三、Chrome扩展都有什么功能页面
首先我们要介绍Chrome扩展的配置文件 manifest.json,他主要用于向浏览器报告这个插件配置了什么功能,并且将该功能页告知浏览器。
{
"manifest_version": 2,
"name": "penk-ext",
"description": "penk extension by Vue",
"version": "1.0.1",
"options_page": "options.html",
"homepage_url": "https://www.baidu.com",
"icons": {
"16": "images/logo.png",
"48": "images/logo.png",
"128": "images/logo.png"
},
"devtools_page": "devtools.html",
"web_accessible_resources": ["devtools.json", "fonts/element-icons.woff", "fonts/element-icons.ttf"],
"background": {
"scripts": ["background/hot-reload.js", "background/index.js"]
},
"browser_action": {
"default_popup": "popup.html"
},
"content_scripts": [{
"matches": [
"https://www.baidu.com/*",
"https://www.zhihu.com/*",
"https://aline.mshengedu.com/yunkaiadmin/*"
],
"js": [
"js/content.js"
],
"css": [
"css/content.css"
],
"run_at": "document_end"
}],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}
1.options_page页面
所谓options页,就是插件的设置页面,右键图标有一个“选项”菜单:
"options_page": "options.html"
2.popup页面
popup页面,左键图标就会弹出来的页面,主要是用来做交互的,用来配置页可以~
"browser_action": {
"default_popup": "popup.html"
}
3.devtools_page页面
devtools_page本身并没有展示什么,他主要是为了加载penal面板,打开方式就是打开它的控制台,F12。
"devtools_page": "devtools.html"
4.background脚本
background这个是后台页面,他只能配置html,或者JS,只有一个能生效,这边只配置了JS,因为还需要引入一个热加载hot-reload.js。
"background": {
"scripts": ["background/hot-reload.js", "background/index.js"]
}
5.content_scripts脚本
content_scripts脚本主要是在目标页面注入脚本,这边通过JS新增了一个标签,并且显示在目标页上
"content_scripts": [{
"matches": [
"https://www.baidu.com/*",
"https://www.zhihu.com/*"
],
"js": [
"js/content.js"
],
"css": [
"css/content.css"
],
"run_at": "document_end"
}]
四、项目结构
文件名 | 文件作用 |
---|---|
src\background | 存放background脚本,其中包含着 hot-reload.js,主要用来热加载Chrome扩展,避免开发的时候重复加载问题;index.js文件,主要用来读取Chrome扩展文件夹中文件,其中包含webpack打包携带的devtools下的panel信息~,用于动态生成panel,避免了重复修改扩展配置文件manifest.json。 |
src\components | 存放通用组件的地方,这里主要存放了,hello.vue,上面的很多截图都已经用过啦~ |
src\content | 存放的是content_script脚本,这里比较特殊,因为不是使用html,所以只能用src\content\index.js中通过JS创建一个标签,然后再把Vue挂载上去即可。 |
src\devtools | 这里主要存放devtools_page的代码,其中,index.html 没什么用;index.json是webpack打包生成的一个临时文件,用于记录有多少个panel面板文件;主要是index.js 用于引入文件,下文讲解;后面的panel文件夹,是Vue项目,后期webpack会将根据文件名他们打包成一个个penal.html。 |
src\options | 存放的是options_page页面,是Vue项目 |
src\popup | 存放的是popup页面,是Vue项目 |
src\static | 存放公共静态资源 |
src\manifest.development.json | 开发环境下的,会将其打包成Chrome扩展的配置文件manifest.json |
src\manifest.production.json | 生产环境下的,会将其打包成Chrome扩展的配置文件manifest.json |
vue.config.js | Vue的配置文件,里面包含了webpack的打包配置~ |
package.json | 开发环境使用npm run build-watch,会实时更新Chrome扩展,如果是目录变动之类的就需要重新run一次。 |
五、主要文件讲解
1.background/hot-reload.js
主要用于热加载,来源于网上
https://github.com/xpl/crx-hotreload/edit/master/hot-reload.js
可以打开背景页,会发现他不断的轮训文件夹,如果有变动就重新加载~
// 代码来源:https://github.com/xpl/crx-hotreload/edit/master/hot-reload.js
const filesInDirectory = dir => new Promise(resolve =>
dir.createReader().readEntries(entries =>
Promise.all(entries.filter(e => e.name[0] !== '.').map(e =>
e.isDirectory ?
filesInDirectory(e) :
new Promise(resolve => e.file(resolve))
))
.then(files => [].concat(...files))
.then(resolve)
)
)
const timestampForFilesInDirectory = dir =>
filesInDirectory(dir).then(files =>
files.map(f => f.name + f.lastModifiedDate).join())
const reload = () => {
window.chrome.tabs.query({
active: true,
currentWindow: true
}, tabs => { // NB: see https://github.com/xpl/crx-hotreload/issues/5
if (tabs[0]) {
window.chrome.tabs.reload(tabs[0].id)
}
window.chrome.runtime.reload()
})
}
const watchChanges = (dir, lastTimestamp) => {
timestampForFilesInDirectory(dir).then(timestamp => {
if (!lastTimestamp || (lastTimestamp === timestamp)) {
setTimeout(() => watchChanges(dir, timestamp), 1000) // retry after 1s
console.log("插件文件夹没有变动,1秒后再检查...");
} else {
reload()
console.log("插件文件夹有变动...");
}
})
}
window.chrome.management.getSelf(self => {
if (self.installType === 'development') {
window.chrome.runtime.getPackageDirectoryEntry(dir => watchChanges(dir))
}
})
2.content/index.js
1.加载elementUI;
2.通过window.chrome.extension.getURL 加载扩展文件夹中的文件,这里主要加载字体,加载扩展内容也是需要放开权限的需要再扩展配置文件manifest.json,修改:“web_accessible_resources”: [ “fonts/element-icons.woff”, “fonts/element-icons.ttf”],
3.在页面中通过JS新建一个标签;
4.将Vue挂载在该标签上
/*
* @Author: Penk
* @LastEditors: Penk
* @LastEditTime: 2022-07-05 21:09:44
* @Desc:Vue的工作原理,是将一个指定的元素替换成vue示例,所以就算这里没有html也无妨,咋们,可以生成一个拥有ID选择器的元素,并将其VUE示例挂载在上面。
* @FilePath: \vue-chrome-ext\src\content\index.js
*/
import Vue from "vue";
import AppComponent from "./App/App.vue";
import 'element-ui/lib/theme-chalk/index.css';
// import $ from "jquery";
// 通过Chrome插件的API加载字体文件
(function insertElementIcons() {
let elementIcons = document.createElement('style')
elementIcons.type = 'text/css';
elementIcons.textContent = `
@font-face {
font-family: "element-icons";
src: url('${ window.chrome.extension.getURL("fonts/element-icons.woff")}') format('woff'),
url('${ window.chrome.extension.getURL("fonts/element-icons.ttf")}') format('truetype'); /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
}
`
document.head.appendChild(elementIcons);
})();
Vue.component("app-component", AppComponent);
// 部分引入
// import {
// Card,
// Button
// } from 'element-ui';
// 部分引入
// Vue.use(Card);
// Vue.use(Button);
// 全局引入
import ElementUI from 'element-ui';
Vue.use(ElementUI);
// 客优云弄个固定窗口存放按钮
function initBox() {
console.log("initBox...");
var contentBox = document.createElement("div");
contentBox.setAttribute("class", "penk");
contentBox.setAttribute("id", "penk");
document.body.appendChild(contentBox);
}
initBox();
setTimeout(() => {
new Vue({
el: "#penk",
render: createElement => {
return createElement(AppComponent);
}
});
}, 2000);
3.devtools/index.js
咋们最开始的目的,是为了运行打包命令,就可以可以直接将devtools,里面的xxx文件夹,挨个编译成对应的xxx.html,并且devtools也会将xxx.html加载进去面板中去,并且面板名称就是文件夹名xxx。
打包后的文件如下:
于是呼,遇到了如下问题:
1、我怎么知道那个哪个html是属于devtools里面的呢,强迫症的我又不喜欢根据前缀判断?
答:webpack(vue.config.js)打包前将devtools目录里面的文件夹名遍历一下,存储在一个数组中,再变成一个临时文件,就是devtools/index.json,然后将其打包成devtools.json,放在扩展文件夹中,然后运行devtools/index的时候,只要读取了该文件,就可以知道有什么panel面板页面了。
// Vue.config.js
// 配置devtools
function dealDevtools() {
// 读取devtools 目录文件
var ret = fs.readdirSync('src/devtools');
var devtools_panels = [];
// 文件夹则打包,并且用数组存储文件夹信息
ret.forEach(e => {
var isDir = fs.lstatSync('src/devtools/' + e).isDirectory();
if (isDir) {
pagesObj[e] = {
entry: `src/devtools/${e}/index.js`,
template: "public/index.html",
filename: `${e}.html`
};
devtools_panels.push(e);
}
})
// 文件夹信息写进JSON文件
var json = {
panels: [...devtools_panels]
}
const content = JSON.stringify(json, null, "\t");
// 生成临时文件devtools/index.json
fs.writeFileSync("src/devtools/index.json", content, function (err) {
if (err) {
return console.log(err);
}
});
// 将devtools/index.json文件打包到dist/devtools.json
plugins.push(
CopyWebpackPlugin([{
from: path.resolve("src/devtools/index.json"),
to: path.resolve("dist/devtools.json")
}])
)
}
2、devtools根据devtools.json文件,新增panels
// devtools/index.js
// 读取相对目录下的文件不用特殊权限,根据这个文件,自动配置panels
var url = 'devtools.json';
fetch(url).then(function (response) {
//response.status表示响应的http状态码
if (response.status === 200) {
//json是返回的response提供的一个方法,会把返回的json字符串反序列化成对象,也被包装成一个Promise了
response.json().then(data => {
console.log(data);
data.panels.forEach(panelName => {
window.chrome.devtools.panels.create(panelName, 'static/images/logo.png', `${panelName}.html`, function (panel) {
console.log('自定义面板创建成功!', panel); // 注意这个log一般看不到
});
});
});
} else {
return {}
}
});
4.vue.config.js
Vue的配置文件,里面有webpack打包的配置
注释很详细了,可以看注释~~~
/*
* @Author: Penk
* @LastEditors: Penk
* @LastEditTime: 2022-07-06 18:26:53
* @Desc:这个配置有点复杂,需要慢慢阅读了...
* 另外,有时候有问题的话,可以试试手动重新加载插件,有可能是修改了这个文件的问题~
* @FilePath: \vue-chrome-ext\vue.config.js
*/
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");
const fs = require("fs");
const ZipPlugin = require('zip-webpack-plugin');
// Generate pages object
const pagesObj = {};
let plugins = []
// 配置"popup", "options", "devtools"
dealPerfectProjectByVue();
// 配置静态文件
dealStatic();
// 配置manifest文件
dealManifest();
// 配置devtools
dealDevtools();
// 配置有完整vue项目的chrome扩展项,background没有html页面,只能特殊处理...
// 配置background
dealBackground();
// 开发环境将热加载文件复制到dist文件夹
if (process.env.NODE_ENV !== 'production') {
plugins.push(
CopyWebpackPlugin([{
from: path.resolve("src/background/hot-reload.js"),
to: path.resolve("dist/background")
}])
)
}
// 生产环境打包dist为zip
else if (process.env.NODE_ENV === 'production') {
plugins.push(
new ZipPlugin({
path: path.resolve("dist"),
filename: 'dist.zip',
})
)
}
module.exports = {
pages: pagesObj,
// // 生产环境是否生成 sourceMap 文件
productionSourceMap: true,
configureWebpack: {
resolve: {
alias: {
// 'src': '@', 默认已配置
'assets': '@/assets',
'common': '@/common',
'components': '@/components',
'api': '@/api',
'views': '@/views',
'plugins': '@/plugins',
'utils': '@/utils',
}
},
entry: {
'content': './src/content/index.js'
},
output: {
filename: 'js/[name].js'
},
plugins
},
css: {
extract: {
filename: 'css/[name].css'
}
},
chainWebpack: config => {
// 处理字体文件名,去除hash值
const fontsRule = config.module.rule('fonts')
// 清除已有的所有 loader。
// 如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。
fontsRule.uses.clear()
fontsRule.test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)
.use('url')
.loader('url-loader')
.options({
limit: 1000,
name: 'fonts/[name].[ext]'
})
// 查看打包组件大小情况
if (process.env.npm_config_report) {
// 在运行命令中添加 --report参数运行, 如:npm run build --report
config
.plugin('webpack-bundle-analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
}
}
};
// 配置manifest文件
function dealManifest() {
const manifest =
process.env.NODE_ENV === "production" ? [{
from: path.resolve("src/manifest.production.json"),
to: `${path.resolve("dist")}/manifest.json`
}] : [{
from: path.resolve("src/manifest.development.json"),
to: `${path.resolve("dist")}/manifest.json`
}];
plugins.push(CopyWebpackPlugin(manifest));
}
// 配置devtools
function dealDevtools() {
// 读取devtools 目录文件
var ret = fs.readdirSync('src/devtools');
var devtools_panels = [];
// 文件夹则打包
ret.forEach(e => {
var isDir = fs.lstatSync('src/devtools/' + e).isDirectory();
if (isDir) {
pagesObj[e] = {
entry: `src/devtools/${e}/index.js`,
template: "public/index.html",
filename: `${e}.html`
};
devtools_panels.push(e);
}
})
// 生成JSON文件
var json = {
panels: [...devtools_panels]
}
const content = JSON.stringify(json, null, "\t");
// 生成临时文件devtools/index.json
fs.writeFileSync("src/devtools/index.json", content, function (err) {
if (err) {
return console.log(err);
}
});
// 将devtools/index.json文件打包到dist/devtools.json
plugins.push(
CopyWebpackPlugin([{
from: path.resolve("src/devtools/index.json"),
to: path.resolve("dist/devtools.json")
}])
)
}
// 配置background
function dealBackground() {
plugins.push(
CopyWebpackPlugin([{
from: path.resolve("src/background/index.js"),
to: path.resolve("dist/background")
}])
)
}
// 配置静态文件
function dealStatic() {
// 引入图片
plugins.push(
CopyWebpackPlugin([{
from: path.resolve("src/static"),
to: path.resolve("dist/static")
}])
)
}
// 配置有完整vue项目的chrome扩展项,background没有html页面,只能特殊处理...
function dealPerfectProjectByVue() {
const chromeName = ["popup", "options", "devtools"];
chromeName.forEach(name => {
pagesObj[name] = {
entry: `src/${name}/index.js`,
template: "public/index.html",
filename: `${name}.html`
};
});
}
总结
这个Git项目以后会持续更新优化!
有错误的地方或者可以优化的地方,在评论区留言即可~
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/66371.html