最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 用Vue实现一个谷歌浏览器搜索扩展

    正文概述 掘金(林景宜)   2021-03-10   741

    用Vue实现一个谷歌浏览器搜索扩展

    前言

    在平时的工作机中,一般安装了两个浏览器,一个最新版的 Chrome 专用于前端开发,一个 360极速浏览器 用于日常事务处理。但由于某些原因,工作机不再允许安装 360浏览器 ,但是 Chrome浏览器 中缺少了一个非常便捷的功能:工具栏和右键中的自定义引擎搜索,缺少了这个功能非常难受,感觉搜索的效率直线下降。

    Chrome 扩展市场中倒是有一些类似功能的插件,但是用起来总是不如意。恰逢疫情突然变重,过年没办法回老家,窝在武汉手写一个浏览器扩展 Rummage 来实现这些功能。

    最近刚刚谷歌扩展市场审核通过,所以可以直接安装:Rummage,访问不了的话可以下载 crx 拖到浏览器中:magee.lanzous.com/iVZ89moyubg 密码:2021,总结下开发的过程。

    产品设计

    分析下需求,核心需求主要是三个:

    1. 可自定义配置搜索引擎
    2. 在工具栏中可以输入内容并选择引擎后搜索
    3. 页面中选中文本后,右键菜单中选择引擎搜索指定文本

    锦上添花的需求如下:

    1. 自定义配置搜索引擎时,可多配置一些功能:新页面打开,无痕模式打开,默认搜索引擎可以被动态修改
    2. 显示搜索引擎的 Favicon,便于识别
    3. 工具栏中能保存并回显搜索记录
    4. 导入导出配置
    5. 实现国际化

    调研

    需求分析完了,但是现在有一个问题,我从来没开发过浏览器扩展,两眼一抹黑。搜了很多资料并看了官方文档后总结如下:

    扩展文档

    如果英文水平不错,可以直接官网文档:developer.chrome.com/docs/extens…。

    另外,有大佬之前详细的总结过的一篇博客——【干货】Chrome 插件(扩展)开发全攻略。

    UI 框架

    大致学会了扩展的开发流程,核心就是 manifest.json,通过这个配置文件指向需要的文件,另外还有很多专用的 API 用于和浏览器交互。

    所以在开发中,只要能在打包时生成对应文件,就可以开发和打包分开,也就可以引入体积较大的 UI框架 来敏捷开发。但是另一个问题来了,我对 Webpack 玩的不是很溜,自己配置一个太费功夫。

    功夫不负有心人,多番搜索发现了 Vue 的一个小众 UI框架 —— Quasar,这个框架不仅样式精美,它的 CLI 中自带一个浏览器扩展开发模式 Quasar Bex,可以自动生成一个与 src 层级并列的 src-bex 文件夹,不管是 manifest.json 还是其他文件都已经配置妥当,只需要按照自己的需要来开发就可以了。

    用Vue实现一个谷歌浏览器搜索扩展

    开发

    首先给插件起个名叫“翻查”,英文名 “Rummage”,其次扩展还需要个图标,在 Iconfont 上搜了一个挺好看的图标。开发过程中其实只需要三个部分:

    1. 点击扩展图标后的 Popup 弹出页(需先配置 manifest.json 中的 browser_action.default_popup
    2. 内置页面:右键选扩展图标后点击选项,新打开的扩展配置和说明页面(需先配置 manifest.json 中的 options_page
    3. 页面中点击右键后的菜单配置(需先配置 manifest.json 中的 backgroundpermissions

    Quasarbex 模式已经配置好,只需要将路由与 manifest.json 文件匹配即可。

    下面是 vue-router 的配置:

    const routes = [
      {
        path: '/',
        component: () => import('layouts/MainLayout.vue'),
        children: [
          { path: '', component: () => import('pages/Index.vue') },
          { path: 'options', component: () => import('pages/Options.vue') },
          { path: 'about', component: () => import('pages/about.vue') },
        ],
      },
      {
        path: '/popup',
        component: () => import('pages/Popup.vue'),
        children: [],
      },
      {
        path: '*',
        component: () => import('pages/Error404.vue'),
      },
    ];
    
    export default routes;
    

    对应的 manifest.json 配置:

    {
      // ...
      "options_page": "www/index.html#/options",
      "browser_action": {
        "default_title": "__MSG_ext_title__",
        "default_popup": "www/index.html#/popup"
      }
      // ...
    }
    

    浏览器端保存 Favicon

    前两种页面的开发就是很普通的 Vue 页面的开发方式,唯一多费了脑筋的地方是保存搜索引擎的 favicon 上。

    最后的实现步骤是如下方式:

    1. 先请求给定的链接,再用浏览器自带的 DOMParser API 来解析 DOM
    2. 判断页面的 <head /> 中是否有 rel 属性为 "shortcut icon""icon" <link/> 标签,假如有则保存 faviconurl
    3. 如果没有则将 URL 设为协议:域名/favicon
    4. 统一规范化 URL
    5. 用 Image 对象请求 URL 后,使用 Canvas 加载图片元素。
    6. 将图片导出成为 Base64 格式。

    具体的代码写的有点乱,如下所示:

    /**
     * @description: 获取指定url的favicon链接
     * @param {String} url
     * @return {String} url
     */
    const getFaviconUrl = async (url) => {
      if (!isValidHttpOrHttpsUrl(url)) {
        return null;
      }
    
      try {
        url = new URL(url).origin;
        let href = await getHref(url);
        let pathFormated = formatHref(href, url);
        return pathFormated;
      } catch (_) {
        return null;
      }
    };
    
    /**
     * @description: 校验url
     * @param {string} string
     * @return {*}
     */
    const isValidHttpOrHttpsUrl = (string) => {
      let url;
      try {
        url = new URL(string);
      } catch (_) {
        return false;
      }
    
      return url.protocol === 'http:' || url.protocol === 'https:';
    };
    
    /**
     * @description: 从 DOM 的 head 中 获取 link 指向的url
     * @param {String} url
     * @return {String} url
     */
    const getHref = async (url) => {
      try {
        let res = await fetch(url);
        let resText = await res.text();
        let resHtml = new DOMParser().parseFromString(resText, 'text/html');
        let linkHtml =
          resHtml.querySelector('link[rel="icon"]') ??
          resHtml.querySelector('link[rel="shortcut icon"]');
        let href = linkHtml.getAttribute('href');
        return href;
      } catch (_) {
        return null;
      }
    };
    
    /**
     * @description: 规范化 favicon 的 URL 链接
     * @param {String} rawHref
     * @param {String} url
     * @return {String} 返回 favicon 的完整链接
     */
    const formatHref = (rawHref, url) => {
      try {
        let urlObj = new URL(url);
        if (rawHref == null) {
          return `${urlObj.origin}/favicon.ico`;
        }
        // start with http or https
        if (rawHref.startsWith('http')) {
          return rawHref;
        }
        // start with //
        if (rawHref.startsWith('//')) {
          return `${urlObj.protocol}${rawHref}`;
        }
        // start with /
        if (rawHref.startsWith('/')) {
          return `${urlObj.origin}${rawHref}`;
        }
    
        // default, root path + /favicon.ico
      } catch (_) {
        return null;
      }
    };
    
    /**
     * @description: 将 IMG元素 转为 base64
     * @param {IMGElement} imgElement
     * @param {Number} width 宽
     * @param {Number} height 高
     * @return {String} base64
     */
    const getBase64Image = (imgElement, width, height) => {
      try {
        //width、height调用时传入具体像素值,控制大小 ,不传则默认图像大小
        let canvas = document.createElement('canvas');
        canvas.width = width
          ? width
          : imgElement.width <= 20
          ? imgElement.width
          : 20;
        canvas.height = height
          ? height
          : imgElement.height <= 20
          ? imgElement.height
          : 20;
    
        let ctx = canvas.getContext('2d');
        ctx.drawImage(imgElement, 0, 0, canvas.width, canvas.height);
        let dataURL = canvas.toDataURL('image/gif', 0.8);
        return dataURL;
      } catch (_) {
        return null;
      }
    };
    
    /**
     * @description: 将 图片URL 转为 base64
     * @param {String} imgUrl
     * @return {String} base64
     */
    const getBase64FromFaviconUrl = async (imgUrl) => {
      if (imgUrl == null) {
        return null;
      }
      try {
        let imgElement = new Image();
        imgElement.crossOrigin = '';
        imgElement.src = imgUrl;
        let imgPromise = new Promise((resolve, reject) => {
          if (imgUrl) {
            imgElement.onload = function () {
              resolve(getBase64Image(imgElement));
            };
            imgElement.onerror = function () {
              reject();
            };
          }
        });
        return await imgPromise;
      } catch (_) {
        return null;
      }
    };
    
    /**
     * @description: 传入链接,返回对应网站的base64
     * @param {String} url
     * @return {String} base64
     */
    const getBase64FromUrl = async (url) => {
      // debugger;
      try {
        let faviconUrl = await getFaviconUrl(url);
        let faviconBase64 = await getBase64FromFaviconUrl(faviconUrl);
        return faviconBase64;
      } catch (_) {
        return null;
      }
    };
    

    background.js 的模块化

    最后一种 background.js 的开发主要是为了能够配置右键菜单。都是对照着谷歌官方文档开发就可以,但是有一个地方比较特殊——模块化。

    如果想 importexport 一些公用方法,普通的 import 'XXXX' from 'XXXX.js';的模式是不生效的,得用如下的特殊方法:

    模块 js 导出:

    // func.js
    const funcA = () => {
      console.log('funcA函数执行');
    };
    const funcB = () => {
      console.log('funcB函数执行');
    };
    export { funcA, funcB };
    

    background.js 导入:

    // background.js
    (async () => {
      const funcURL = chrome.runtime.getURL('js/func.js');
      const funcMain = await import(funcURL);
    
      funcMain.funcA(); // output: funcA函数执行
      funcMain.funcB(); // output: funcB函数执行
    })();
    

    存储

    对于扩展来说,只是单纯的展示页面,可以用普通页面存储用到的 cookieslocalStorage。但是如果需要与浏览器交互,比如给右键配置菜单,就需要用扩展专用的负责存储的 APIchrome.storage.localchrome.storage.sync

    存储chrome.storage.localchrome.storage.syncwindow.localStorage
    总最大限制可无限大100KB5MB单条最大限制可无限大8KB5MB修改频率限制可无限大8KB1800 次/小时存储格式可直接存储对象可直接存储对象只可存储字符串数据同步方式手动导入导出自动跨设备同步手动导入导出事件支持支持其他页面修改才会触发可用位置可用于 content 和 background可用于 content 和 background只能用于插件自身页面

    写了一些工具方法,把的异步存储操作的回调变为了 async:

    const setStorageLocal = async (items) => {
      let result = await new Promise((resolve) => {
        chrome.storage.local.set(items, () => {
          // 通知保存完成。
          console.log('保存成功', items);
          resolve(items);
        });
      });
      return result;
    };
    
    const getStorageLocal = async (keys) => {
      return await new Promise((resolve) => {
        chrome.storage.local.get(keys, (items) => {
          console.log('获取成功', items);
          resolve(items);
        });
      });
    };
    
    const removeStorageLocal = async (keys) => {
      return await new Promise((resolve) => {
        chrome.storage.local.remove(keys, () => {
          console.log('删除成功', keys);
          resolve(keys);
        });
      });
    };
    
    const clearStorageLocal = async () => {
      return await new Promise((resolve) => {
        chrome.storage.local.clear(() => {
          console.log('清空成功');
          resolve(true);
        });
      });
    };
    

    发布

    Quasar cli 中自带的 bex 打包模式,将插件打包到 dist 中,有 Chrome 版,FireFox 版和未压缩版。

    用Vue实现一个谷歌浏览器搜索扩展

    发布到 Google 应用市场需要做一些准备工作,主要是需要以下准备内容:

    1. 如果没有谷歌 Web 的开发者账户,需要准备可支付 5 美元的信用卡来注册开发者。如果开发移动端 APP,注册谷歌 Play 开发者需要 25 美元,相比下页面端还是便宜。
    2. 扩展图标 128x128 像素,png 格式。
    3. 插件名字,根据你扩展的 i18n 支持的数量准备不同语言下的名字。
    4. 插件介绍,与上条类似。
    5. 屏幕截图,1280x800640x400 JPEG24PNG(无 alpha 透明层),每种语言不得多于 5 张,不得少于一张。
    6. 全球通用的屏幕截图:不区分语言,条件与上一条相同。
    7. 小型宣传图块,不区分语言,440x280JPEG24PNG(无 alpha 透明层)。
    8. 大型宣传图块,不区分语言,920x680JPEG24PNG(无 alpha 透明层)。
    9. 顶部宣传图块,不区分语言,1400x560JPEG24PNG(无 alpha 透明层)。
    10. manifest.json 中需要启用的权限,每种权限都需要说明用途。

    用Vue实现一个谷歌浏览器搜索扩展

    全都填完后就可以提交审核,大概需要一周的时间。发布到 EDGE 或者 FireFox 也是类似的步骤。


    前端记事本,不定期更新,欢迎关注!

    • 微信公众号: 林景宜的记事本
    • 博客:林景宜的记事本
    • 掘金专栏:林景宜的记事本
    • 知乎专栏: 林景宜的记事本
    • Github: MageeLin


    起源地下载网 » 用Vue实现一个谷歌浏览器搜索扩展

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元