chrome扩展训练营 | 框架升级:Vue 开发实现一图一诗(新开 tab 页打开自己的内容)

1,117 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

王志远,微医前端技术部

系列文目录

小节主题文档期待产出补充
学习起始篇juejin.cn/post/714860…扩展能做到什么/如何学习扩展/扩展基础组成概念
开发调试发布juejin.cn/post/714861…熟悉谷歌扩展的开发流程/调试流程/发布流程
谷歌扩展开发能力详解:数据流处理能力、UI 能力、浏览器特性(历史/书签/下载/网络请求/等等)juejin.cn/post/714710…扩展能力知识体系学习,根据文档实现所有相关 demo
框架升级:Vue 开发实现一图一诗(新开 tab 页打开自己的内容)本文使用 Vue 开发扩展
chrome 扩展脚手架实现:vue2+elementui 版本实现 vue 开发扩展脚手架
落地:debug-plugin 集成谷歌扩展原调试插件重构为谷歌扩展并集成进项目

前言

​ 实战篇啦,经过前面系列文章,我们对 chrome 扩展的开发已经有了大致了解(不了解?👆),最最基础一点,chrome 扩展其实就是 html/css/js 三大件,那么,在经历了 vue/react/angular 框架的日常后,我们真的需要一行行的去实现原始的三大件吗?答案肯定是,不需要。

​ 掘金的浏览器扩展相信很多小伙伴都见过,即新开 tab 页时会打开掘金的页面作为默认显示页。这个功能是谷歌扩展的 over_page 配置,其实我们也在谷歌扩展开发能力详解:数据流处理能力、UI 能力、浏览器特性(历史/书签/下载/网络请求/等等)一文中见过,这次我们就来结合 vue 实现一个自己的“一图一诗”吧!

效果如下

知识结构

  • Vue 的扩展开发环境搭建

    • 脚手架创建
    • vue 项目搭建
    • 支持 chrome 扩展:改造/加载插件/热更新
  • Chrome 扩展的 Tab 页面 Overide 功能

    • Ajax 请求第三方接口
    • Vue 的热更新开发

Vue 的扩展开发环境搭建

脚手架创建

这里提前说下下一篇文章的内容,我实现了一个创建【Vue 的扩展开发环境】的脚手架,如果大家懒得跟着后文一步步实现,而是只想看看思路,可以使用如下命令进行安装创建

npm install vue-chrome-cli -g
vue-chrome-cli init test

也可以通过我实现的 vscode 插件【weiyi-tools】进行图形化操作一步安装执行

vue 脚手架环境搭建

npm install -g @vue/cli // 如果没有安装过 vue 脚手架
vue create my-extension
cd my-extension
npm run serve

这时会自动打开如下界面,属于正常 vue 思路,不加赘述

支持 chrome 扩展

我们先来看下结果,最终目录结构如下

.
|-- README.md
|-- babel.config.js
|-- jsconfig.json
|-- package-lock.json
|-- package.json
|-- public
|   |-- favicon.ico
|   |-- index.html
|-- src
|   |-- content-script
|   |   |-- App.vue
|   |   |-- content-script.css
|   |   |-- cs-init.js
|   |   |-- element-variables.scss
|   |   |-- index.js
|   |-- manifest.json
|   |-- options
|   |   |-- App
|   |   |-- index.html
|   |   |-- index.js
|   |-- popup
|       |-- App
|       |-- index.html
|       |-- index.js
|-- vue.config.js

补充:一个个建目录太麻烦,推荐个人开发的 vscode 插件 weiyi-tools,安装好后

  1. 复制上面目录结构内容
  2. 在侧边栏自己想要的空目录处右键
  3. 找到【选中目录工具箱 - 根据 tree 结果生成目录】,点击

效果如下

2022-06-24 14.40.09

对 vue 脚手架项目的改造

基础操作
  1. 删除无用文件:main.jsApp.vuecomponents以及文件夹下的HelloWorld.vuelogo.png文件
  2. 配置扩展图标:从阿里巴巴矢量图标库 下载自己想要的图标文件,建议用 16、48、128 这三种大小的图标,将下载的图标放到public下面。

接下来我们就可以用 vue 去搭建对应页面的开发啦!这里的搭建需要分为两类:具有 dom 相关显示的,如popupoptionscontent_script;以及纯逻辑的,如 background。

  1. 锁定依赖:为避免版本问题费时费力,建议直接复制如下 package.json 内容,安装依赖
{
  "name": "hello-chrome-vue",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "build-watch": "vue-cli-service build-watch"
  },
  "dependencies": {
    "core-js": "^3.8.3",
    "element-ui": "^2.15.10",
    "vue": "^2.6.14",
    "vue-router": "^3.5.1"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-router": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "copy-webpack-plugin": "^4.6.0",
    "sass": "^1.55.0",
    "sass-loader": "^13.1.0",
    "vue-cli-plugin-chrome-ext": "0.0.5",
    "vue-template-compiler": "^2.6.14"
  },
  "browserslist": ["> 1%", "last 2 versions", "not dead"]
}
实现 vue 开发对应页面:popupoptionscontent_script

popupoptionscontent_script这三类分别代表着弹出层/选项页和注入页面的脚本;

src文件夹下面创建 popupoptionscontent_script文件夹,其内容均为独立的 vue 结构,目录结构如下

|-- App
|-- |-- app.vue
|-- index.html
|-- index.js

index.js

import Vue from "vue";
import AppComponent from "./App/App.vue";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";

Vue.use(ElementUI);
Vue.component("app-component", AppComponent);

new Vue({
  el: "#app",
  render: (createElement) => {
    return createElement(AppComponent);
  },
});

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <title>popup</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but test-dir doesn't work properly without JavaScript
        enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

App/app.vue

<template>
  <div class="main_app">
    <h1>Hello popup</h1>
    <el-divider></el-divider>
  </div>
</template>

<script>
export default {
  name: "app",
  mounted() {},
};
</script>

<style>
.main_app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
对 content_script 的兼容:提前生成挂载点

这里将 content_script 归类在具有 dom 的类别里,是因为我们常常用它去操作或插入一些网页 dom,所以看起来它是纯逻辑,但我们可以做下兼容从而使得也用 vue 开发,即在引入前先创建一个挂载点 dom。我们可以在 content_script 下创建一个 init.js,内容如下

const contentScriptDiv = document.createElement("div");
contentScriptDiv.id = "contentScript";
document.body.appendChild(contentScriptDiv);

然后在 manifest.json 中配置即可(此处点出方便理解,在下面的 manifest 文件中其实是已经有了的)

"content_scripts": [
    {
      "matches": ["*://*/*"],
      "css": ["content-script/content-script.css"],
      "js": ["content-script/init.js", "js/content-script.js"],
      "run_at": "document_end"
    }
  ]
纯逻辑文件支持:background.js

先在 vue.config.js 中配置

configureWebpack: {
		entry: {
			background: "./src/background/main.js"
		},
		output: {
			filename: "js/[name].js"
		}
	},

然后在 src/background 下新建一个 main.js,实现自己的逻辑即可

新增文件配置
  • manifest.json

我们先配置 src/plugins/manifest.json 文件,我们在前面已经说过,这个是 Chrome 插件必须的文件。

{
  "manifest_version": 2,
  "name": "hello-chrome-vue",
  "description": "vue 开发 chrome",
  "version": "0.0.1",
  "options_page": "options.html",
  "browser_action": {
    "default_popup": "popup.html"
  },
  "content_scripts": [
    {
      "matches": ["*://*/*"],
      "css": ["content-script/content-script.css"],
      "js": ["content-script/cs-init.js", "js/content-script.js"],
      "run_at": "document_end"
    }
  ]
}

  • 在根目录下创建vue.config.jsvue配置文件,vue.config.js

vue.config.js 文件作为 vue 项目打包,运行等的基本配置,主要功能是打包成为我们 Chrome 插件所需要的项目目录。

配置vue.config.js内容如下,以做到

  1. "popup", "options", "content-script"多页应用打包
  2. img / content-script.css content-script/init.js 直接复制
  3. background/main.js 打包后直接复制
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");

// Generate pages object
const pagesObj = {};

const chromeName = ["popup", "options", "content-script"];

chromeName.forEach((name) => {
  pagesObj[name] = {
    entry: `src/${name}/index.js`,
    template: "public/index.html",
    filename: `${name}.html`,
  };
});
let commonPlugins = [
  { from: "src/content-script/init.js", to: "content-script/init.js" },
  {
    from: "src/content-script/content-script.css",
    to: "content-script/content-script.css",
  },
  {
    from: "src/manifest.json",
    to: "manifest.json",
    transform: (content) => {
      const jsonContent = JSON.parse(content);
      // jsonContent.version = version;

      if (process.env.NODE_ENV !== "production") {
        jsonContent["content_security_policy"] =
          "script-src 'self' 'unsafe-eval'; object-src 'self'";
      }

      return JSON.stringify(jsonContent, null, 2);
    },
  },
];
module.exports = {
  pages: pagesObj,
  configureWebpack: {
    entry: {
      background: "./src/background/main.js",
    },
    output: {
      filename: "js/[name].js",
    },
    plugins: [CopyWebpackPlugin(commonPlugins)],
  },
};
打包
npm run build

可以看到打印台已经完成打包

加载插件

Chrome 插件中加载已解压的扩展程序 ,选择我们的 dist 文件,即可发现我们的插件已经成功安装完 ☹️

之前写过,复用一下,不了解这步的同学可见文章:juejin.cn/post/714861…

优化:热更新

我们会发现,尽管介入了 vue+elementui,我们的开发体量瞬间小了数个重量级,但开发过程还是【写代码 - 扩展管理页点击扩展更新- 刷新页面 】,这也太麻烦了叭!!

【表情】

能不能我本地修改完代码自动自动帮我做这些呢?你想到了什么?没错,就是热更新;热更新一般指 app 有小规模更新,以直接打补丁的形式更新 app,想象一下,如果有小的更新就让用户重新下载完整包,估计没几天这个 app 就没人用了。web 端其实最开始热更新了,主要因为 web 是 B/S(浏览器+服务端),浏览器主要和服务器通过 http 协议通信,服务端响应 http 请求之后,本次 http 连接就结束了,后面出现了 websocket+EventSource 才解决了这个问题。

那谷歌扩展呢?大可不必这么麻烦,扩展机制提供了如下接口

  1. background:一直运行在后台的逻辑
  2. getPackageDirectoryEntry:获取所有扩展包文件实例及地址
  3. tabs.query:获取当前打开的 tab 页面
  4. runtime.reload:刷新页面

这样思路就变的很清晰了,我们在 background.js 中实现【获取所有扩展包文件地址 - 生成对应时间戳 - 每一秒查询一次时间戳是否更改 - 更改了则获取当前页并进行刷新】,这样就实现啦!

【表情】

效果如下(注意,这个一定要在开发环境下才开启,不然太影响性能了)

具体代码如下

utils 文件夹下创建 hotReload.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) => {
    // 文件没有改动则循环监听 watchChanges 方法
    if (!lastTimestamp || lastTimestamp === timestamp) {
      setTimeout(() => watchChanges(dir, timestamp), 1000); // retry after 1s
    } else {
      // 强制刷新页面
      reload();
    }
  });
};

const hotReload = () => {
  window.chrome.management.getSelf((self) => {
    if (self.installType === "development") {
      // 获取插件目录,监听文件变化
      window.chrome.runtime.getPackageDirectoryEntry((dir) =>
        watchChanges(dir)
      );
    }
  });
};

export default hotReload;

background/main.js 中引入

import hotReload from "@/utils/hotReload";
hotReload();

console.log("hello world background");

至此,我们的 chrome 开发 vue 环境也就搭建完成啦!!准备开始开发我们的项目吧

Chrome 扩展的 Tab 页面 Overide 功能

有了上面的框架,实现项目变得异常的简单,效果在文章开头已经分享过了,我们来梳理下思路

  1. 配置项目模版并覆盖默认页配置

  2. 配置请求

  3. 开发调试上线

配置项目模版并覆盖默认页配置

复制如上模版(推荐使用vue-chrom-cli脚手架创建),复制 popup 目录改名为override-page;然后在 vue.config.js 中找到chromeName数组,添加 override-page 支持对应的打包。

const chromeName = ["popup", "options", "content-script", "override-page"];

在 manifest.json 中配置chrome_url_overrides覆盖默认页即可

{
  ...
  "chrome_url_overrides": {
    "newtab": "override-page.html"
  }
}

配置请求

因为需要【获取背景图片和获取古诗数据】,所以我们需要请求接口,这里我们使用 axios 并在 manifest.json 中配置权限

安装依赖
npm i axios@^0.21.0
配置权限
{
  ...
  "permissions": [
    "<all_urls>",
    "*://*/*"
  ]
}

开发调试上线

这里的开发没啥难度,就是日常页面开发,不加赘述了,show code

仓库地址:github.com/Sympath/tra…

app.vue

<template>
  <div id="app">
    <Page msg="Welcome to 王志远 Vue.js App" />
  </div>
</template>

<script>
import Page from "./components/Page.vue";

export default {
  name: "App",
  components: {
    Page,
  },
};
</script>

<style>
/* http://meyerweb.com/eric/tools/css/reset/ 
   v2.0 | 20110126
   License: none (public domain)
*/

html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
  display: block;
}
body {
  line-height: 1;
}
ol,
ul {
  list-style: none;
}
blockquote,
q {
  quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
  content: "";
  content: none;
}
table {
  border-collapse: collapse;
  border-spacing: 0;
}

#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
</style>

components/page.vue

<template>
  <div v-bind:style="styeImg">
    <div class="page-widget">
      <div class="poem-content">
        <div>{{ this.poemData.content }}</div>
        <div class="poem-content-sub">
          《{{ this.poemData.origin }}》{{ this.poemData.author }}
        </div>
        <p class="page-date">{{ weekDay }}</p>
        <p>hot refresh local:8080</p>
      </div>
    </div>
    <div class="page-author">
      <a href="http://82.157.62.28:49153/">by 王志远</a>
    </div>
  </div>
</template>

<script>
import axios from "axios";
export default {
  name: "Page",
  mounted() {},
  computed: {
    weekDay() {
      var week = new Date().getDay(),
        arr = ["日", "一", "二", "三", "四", "五", "六"],
        str = "星期" + arr[week];
      return str;
    },
  },
  created() {
    this.loadImage();
    this.loadPoem();
  },
  data() {
    return {
      styeImg: {
        backgroundImage: "",
        width: "100%",
        height: "100%",
        backgroundSize: "cover",
      },
      poemData: {},
    };
  },
  methods: {
    /**
     * 获取背景图片
     * https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1
     */
    loadImage() {
      axios
        .get("https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1")
        .then((response) => {
          this.styeImg.height = `${window.innerHeight}px`;
          this.styeImg.backgroundImage = `url(http://s.cn.bing.net${response.data.images[0].url})`;
        })
        .catch((error) => {
          console.log(error);
        });
    },
    /**
     * 获取古诗数据
     * https://v1.jinrishici.com/all.json
     */
    loadPoem() {
      axios
        .get("https://v1.jinrishici.com/all.json")
        .then((response) => {
          this.poemData = response.data;
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },
};
</script>

<style scoped>
p {
  font-size: 20px;
}
.page-widget {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translateX(-50%);
  transform: translate(-50%, -50%);
  line-height: 46px;
  font-size: 26px;
  color: white;
  background-color: rgba(0, 0, 0, 0.5);
  padding: 40px 20px;
  border-radius: 10px;
}
.poem-content-sub {
  font-size: 20px;
}
.page-author {
  font-size: 13px;
  color: white;
  position: absolute;
  right: 15px;
  bottom: 20px;
}
.page-author a {
  color: white;
  text-decoration: none;
}
</style>

执行调试命令

npm run build-watch

然后安装扩展指定 dist 目录即可。不了解这步的同学可见文章:juejin.cn/post/714861…

至此,我们就用 vue 开发了一个类似掘金的【打开新 tab 时默认显示自己页面】对扩展,是不是很酷!!!