持续创作,加速成长!这是我参与「掘金日新计划 · 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,安装好后
- 复制上面目录结构内容
- 在侧边栏自己想要的空目录处右键
- 找到【选中目录工具箱 - 根据 tree 结果生成目录】,点击
效果如下
对 vue 脚手架项目的改造
基础操作
- 删除无用文件:
main.js
、App.vue
、components
以及文件夹下的HelloWorld.vue
、logo.png
文件 - 配置扩展图标:从阿里巴巴矢量图标库 下载自己想要的图标文件,建议用 16、48、128 这三种大小的图标,将下载的图标放到
public
下面。
接下来我们就可以用 vue 去搭建对应页面的开发啦!这里的搭建需要分为两类:具有 dom 相关显示的,如popup
、options
、content_script
;以及纯逻辑的,如 background。
- 锁定依赖:为避免版本问题费时费力,建议直接复制如下 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 开发对应页面:popup
、options
、content_script
popup
、options
、content_script
这三类分别代表着弹出层/选项页和注入页面的脚本;
src
文件夹下面创建 popup
、options
、content_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.js
的vue
配置文件,vue.config.js
vue.config.js
文件作为 vue
项目打包,运行等的基本配置,主要功能是打包成为我们 Chrome
插件所需要的项目目录。
配置vue.config.js
内容如下,以做到
- "popup", "options", "content-script"多页应用打包
- img / content-script.css content-script/init.js 直接复制
- 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 才解决了这个问题。
那谷歌扩展呢?大可不必这么麻烦,扩展机制提供了如下接口
- background:一直运行在后台的逻辑
- getPackageDirectoryEntry:获取所有扩展包文件实例及地址
- tabs.query:获取当前打开的 tab 页面
- 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 功能
有了上面的框架,实现项目变得异常的简单,效果在文章开头已经分享过了,我们来梳理下思路
-
配置项目模版并覆盖默认页配置
-
配置请求
-
开发调试上线
配置项目模版并覆盖默认页配置
复制如上模版(推荐使用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
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 时默认显示自己页面】对扩展,是不是很酷!!!