Chrome扩展框架-Vue项目(用vue配合webpack实现浏览器扩展,包含热更新,无需重复加载浏览器插件)

导读:本篇文章讲解 Chrome扩展框架-Vue项目(用vue配合webpack实现浏览器扩展,包含热更新,无需重复加载浏览器插件),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

Chrome扩展框架系列文章

本人是一个web前端开发工程师,主要是vue框架,整理了一些Chrome扩展,今后也会一直更新,有好建议的同学欢迎评论区分享讨论 ;-)

git:此处
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

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!