使用Vue開發Chrome插件

愧怍 2021-09-18 14:46:48 阅读数:724

使用 vue chrome 插件

原文鏈接: 使用Vue開發Chrome插件 - 愧怍的小站 (kuizuo.cn)

前言

我當時學習開發Chrome插件的時候,還不會Vue,更別說Webpack了,所以使用的都是原生的html開發,效率就不提了,而這次就准備使用vue-cli來進行編寫一個某B站獲取視頻信息,評論的功能(原本是打算做自動回複的),順便鞏固下chrome開發(快一年沒碰脚本類相關技術了),順便寫套模板供自己後續編寫Chrome插件做鋪墊。

關於Chrome插件開發的基本知識就不贅述了,之前寫過一篇原生開發的Chrome插件開發 - 愧怍的小站,裏面有附帶相關文檔鏈接。

相關代碼開源github地址

環境搭建

Vue Web-Extension - A Web-Extension preset for VueJS (vue-web-extension.netlify.app)

npm install -g @vue/cli
npm install -g @vue/cli-init
vue create --preset kocal/vue-web-extension my-extension
cd my-extension
npm run server
複制代碼

會提供幾個選項,如Eslint,background.js,tab頁,axios,如下圖

image-20210916142751129

選擇完後,將會自動下載依賴,通過npm run server將會在根目錄生成dist文件夾,將該文件拖至Chrome插件管理便可安裝,由於使用了webpack,所以更改代碼將會熱更新,不用反複的編譯導入。

項目結構

├─src
| ├─App.vue
| ├─background.js
| ├─main.js
| ├─manifest.json
| ├─views
| | ├─About.vue
| | └Home.vue
| ├─store
| | └index.js
| ├─standalone
| | ├─App.vue
| | └main.js
| ├─router
| | └index.js
| ├─popup
| | ├─App.vue
| | └main.js
| ├─override
| | ├─App.vue
| | └main.js
| ├─options
| | ├─App.vue
| | └main.js
| ├─devtools
| | ├─App.vue
| | └main.js
| ├─content-scripts
| | └content-script.js
| ├─components
| | └HelloWorld.vue
| ├─assets
| | └logo.png
├─public
├─.browserslistrc
├─.eslintrc.js
├─.gitignore
├─babel.config.js
├─package.json
├─vue.config.js
├─yarn.lock
複制代碼

根據所選的頁面,並在src與vue.config.js中配置頁面信息編譯後dist目錄結構如下

├─devtools.html
├─favicon.ico
├─index.html
├─manifest.json
├─options.html
├─override.html
├─popup.html
├─_locales
├─js
├─icons
├─css
複制代碼

安裝組件庫

安裝elementUI

整體的開發和vue2開發基本上沒太大的區別,不過既然是用vue來開發的話,那肯定少不了組件庫了。

要導入Element-ui也十分簡單,Vue.use(ElementUI); Vue2中怎麼導入element,便怎麼導入。演示如下

image-20210916150154078

不過我沒有使用babel-plugin-component來按需引入,按需引入一個按鈕打包後大約1.6m,而全量引入則是5.5左右。至於為什麼不用,因為我需要在content-scripts.js中引入element組件,如果使用babel-plugin-component將無法按需導入組件以及樣式(應該是只支持vue文件按需引入,總之就是折騰了我一個晚上的時間)

安裝tailwindcss

不過官方提供了如何使用TailwindCSS,這裏就演示一下

在 Vue 3 和 Vite 安裝 Tailwind CSS - Tailwind CSS 中文文檔

推薦安裝低版本,最新版有兼容性問題

npm install [email protected]:@tailwindcss/postcss7-compat [email protected]^7 [email protected]^9
複制代碼

創建postcss.config.js文件

// postcss.config.js
module.exports = {
plugins: [
// ...
require('tailwindcss'),
require('autoprefixer'), // if you have installed `autoprefixer`
// ...
]
}
複制代碼

創建tailwind.config.js文件

// tailwind.config.js
module.exports = {
purge: {
// Specify the paths to all of the template files in your project
content: ['src/**/*.vue'],
// Whitelist selectors by using regular expression
whitelistPatterns: [
/-(leave|enter|appear)(|-(to|from|active))$/, // transitions
/data-v-.*/, // scoped css
],
}
// ...
}
複制代碼

在src/popup/App.vue中導入樣式,或在新建style.css在mian.js中import "../style.css";

<style>
/* purgecss start ignore */
@tailwind base;
@tailwind components;
/* purgecss end ignore */
@tailwind utilities;
</style>
複制代碼

從官方例子導入一個登陸錶單,效果如下

image-20210916152633247

項目搭建

頁面搭建

頁面搭建就沒什麼好說的了,因為使用的是element-ui,所以頁面很快就搭建完畢了,效果如圖

image-20210918115438700

懸浮窗

懸浮窗其實可有可無,不過之前寫Chrome插件的時候就寫了懸浮窗,所以vue版的也順帶寫一份。

要注意的是懸浮窗是內嵌到網頁的(且在document加載前載入,也就是"run_at": "document_start"),所以需要通過content-scripts.js才能操作頁面Dom元素,首先在配置清單manifest.json與vue.confing.js中匹配要添加的網站,以及注入的js代碼,如下

 "content_scripts": [
{
"matches": ["https://www.bilibili.com/video/*"],
"js": ["js/jquery.js", "js/content-script.js"],
"css": ["css/index.css"],
"run_at": "document_start"
},
{
"matches": ["https://www.bilibili.com/video/*"],
"js": ["js/jquery.js", "js/bilibili.js"],
"run_at": "document_end"
}
]
複制代碼
 contentScripts: {
entries: {
'content-script': ['src/content-scripts/content-script.js'],
bilibili: ['src/content-scripts/bilibili.js'],
},
},
複制代碼

由於是用Vue,但又要在js中生成組件,就使用document.createElement來進行創建元素,Vue組件如下(可拖拽)

image-20210917142340863

:::danger

如果使用babel-plugin-component按需引入,組件的樣式將無法載入,同時自定義組件如果編寫了style標簽,那麼也同樣無法載入,報錯:Cannot read properties of undefined (reading 'appendChild')

大致就是css-loader無法加載對應的css代碼,如果執意要寫css的話,直接在manifest.json中注入css即可

:::

完整代碼
// 注意,這裏引入的vue是運行時的模塊,因為content是插入到目標頁面,對組件的渲染需要運行時的vue, 而不是編譯環境的vue (我也不知道我在說啥,反正大概意思就是這樣)
import Vue from 'vue/dist/vue.esm.js';
import ElementUI, { Message } from 'element-ui';
Vue.use(ElementUI);
// 注意,必須設置了run_at=document_start此段代碼才會生效
document.addEventListener('DOMContentLoaded', function() {
console.log('vue-chrome擴展已載入');
insertFloat();
});
// 在target頁面中新建一個帶有id的dom元素,將vue對象掛載到這個dom上。
function insertFloat() {
let element = document.createElement('div');
let attr = document.createAttribute('id');
attr.value = 'appPlugin';
element.setAttributeNode(attr);
document.getElementsByTagName('body')[0].appendChild(element);
let link = document.createElement('link');
let linkAttr = document.createAttribute('rel');
linkAttr.value = 'stylesheet';
let linkHref = document.createAttribute('href');
linkHref.value = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css';
link.setAttributeNode(linkAttr);
link.setAttributeNode(linkHref);
document.getElementsByTagName('head')[0].appendChild(link);
let left = 0;
let top = 0;
let mx = 0;
let my = 0;
let onDrag = false;
var drag = {
inserted: function(el) {
(el.onmousedown = function(e) {
left = el.offsetLeft;
top = el.offsetTop;
mx = e.clientX;
my = e.clientY;
if (my - top > 40) return;
onDrag = true;
}),
(window.onmousemove = function(e) {
if (onDrag) {
let nx = e.clientX - mx + left;
let ny = e.clientY - my + top;
let width = el.clientWidth;
let height = el.clientHeight;
let bodyWidth = window.document.body.clientWidth;
let bodyHeight = window.document.body.clientHeight;
if (nx < 0) nx = 0;
if (ny < 0) ny = 0;
if (ny > bodyHeight - height && bodyHeight - height > 0) {
ny = bodyHeight - height;
}
if (nx > bodyWidth - width) {
nx = bodyWidth - width;
}
el.style.left = nx + 'px';
el.style.top = ny + 'px';
}
}),
(el.onmouseup = function(e) {
if (onDrag) {
onDrag = false;
}
});
},
};
window.kz_vm = new Vue({
el: '#appPlugin',
directives: {
drag: drag,
},
template: ` <div class="float-page" ref="float" v-drag> <el-card class="box-card" :body-style="{ padding: '15px' }"> <div slot="header" class="clearfix" style="cursor: move"> <span>懸浮窗</span> <el-button style="float: right; padding: 3px 0" type="text" @click="toggle">{{ show ? '收起' : '展開'}}</el-button> </div> <transition name="ul"> <div v-if="show" class="ul-box"> <span> {{user}} </span> </div> </transition> </el-card> </div> `,
data: function() {
return {
show: true,
list: [],
user: {
username: '',
follow: 0,
title: '',
view: 0,
},
};
},
mounted() {},
methods: {
toggle() {
this.show = !this.show;
},
},
});
}
複制代碼

因為只能在js中編寫vue組件,所以得用template模板,同時使用了directives,給組件添加了拖拽的功能(尤其是window.onmousemove,如果是元素綁定他自身的鼠標移動事件,那麼拖拽鼠標將會十分卡頓),還使用了transition來進行緩慢動畫效果其中注入的css代碼如下

.float-page {
width: 400px;
border-radius: 8px;
position: fixed;
left: 50%;
top: 25%;
z-index: 1000001;
}
.el-card__header {
padding: 10px 15px !important
}
.ul-box {
height: 200px;
overflow: hidden;
}
.ul-enter-active,
.ul-leave-active {
transition: all 0.5s;
}
.ul-enter,
.ul-leave-to {
height: 0;
}
複制代碼

相關邏輯可自行觀看,這裏不在贅述了,並不複雜。

也順帶是複習一下HTML中鼠標事件和vue自定義命令了

功能實現

主要功能

  • 檢測視頻頁面,輸出對應up主,關注數以及視頻標題播放(參數過多就不一一顯示了)

  • 監控關鍵詞根據內容判斷是否點贊,例如文本出現了下次一定,那麼就點贊。

輸出相關信息

這個其實只要接觸過一丟丟爬蟲的肯定都會知道如何實現,通過右鍵審查元素,像這樣

image-20210918104907148

然後使用dom操作,選擇對應的元素,輸出便可

> document.querySelector("#v_upinfo > div.up-info_right > div.name > a.username").innerText
< '老番茄'
複制代碼

當然使用JQuery效果也是一樣的。後續我都會使用JQuery來進行操作

在src/content-script/bilibili.js中寫下如下代碼

window.onload = function() {
console.log('加載完畢');
function getInfo() {
let username = $('#v_upinfo > div.up-info_right > div.name > a.username').text();
let follow = $(`#v_upinfo > div.up-info_right > div.btn-panel > div.default-btn.follow-btn.btn-transition.b-gz.following > span > span > span`).text();
let title = $(`#viewbox_report > h1 > span`).text();
let view = $('#viewbox_report > div > span.view').attr('title');
console.log(username, follow, title, view);
}
getInfo();
};
複制代碼

重新加載插件,然後輸出查看結果

加載完畢
bilibili.js:19 老番茄 1606.0萬 頂級畫質 總播放數2368406
複制代碼

這些數據肯定單純的輸出肯定是沒什麼作用的,要能顯示到內嵌懸浮窗口,或者是popup頁面上(甚至發送ajax請求到遠程服務器上保存)

對上面代碼微改一下

window.onload = function() {
console.log('加載完畢');
function getInfo() {
let username = $('#v_upinfo > div.up-info_right > div.name > a.username').text().trim()
let follow = $(`#v_upinfo > div.up-info_right > div.btn-panel > div.default-btn.follow-btn.btn-transition.b-gz.following > span > span > span`).text();
let title = $(`#viewbox_report > h1 > span`).text();
let view = $('#viewbox_report > div > span.view').attr('title');
//console.log(username, follow, title, view);
window.kz_vm.user = {
username,
follow,
title,
view,
};
}
getInfo();
};
複制代碼

其中window.kz_vm是通過window.kz_vm = new Vue() 初始化的,方便我們操作vm對象,就需要通過jquery選擇元素在添加屬性了。如果你想的話也可以直接在content-script.js上編寫代碼,這樣就無需使用window對象,但這樣導致一些業務邏輯都堆在一個文件裏,所以我習慣分成bilibili.js 然後注入方式為document_end,然後在操作dom元素嗎,實現效果如下

image-20210918110958104

如果像顯示到popup頁面只需要通過頁面通信就行了,不過前提得先popup打開才行,所以一般都是通過background來進行中轉,一般來說很少 content –> popup(因為操作popup的前提都是popup要打開),相對更多的是content –> background 或 popup –> content

content-script主動發消息給後臺 我是小茗同學 - 博客園 (cnblogs.com)

實現評論

這邊簡單編寫了一下頁面,通過popup給content,讓content輸入評論內容,與點擊發送,先看效果

bilibili_comment

同樣的,找到對應元素比特置

// 評論文本框
$('#comment > div > div.comment > div > div.comment-send > div.textarea-container > textarea').val("要回複的內容");
// 評論按鈕
$('#comment > div > div.comment > div > div.comment-send > div.textarea-container > button').click();
複制代碼

接著就是寫頁面通信的了,可以看到是popup向content發送請求

window.onload = function() {
console.log('content加載完畢');
function comment() {
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
let { cmd, message } = request;
if (cmd === 'addComment') {
$('#comment > div > div.comment > div > div.comment-send > div.textarea-container > textarea').val(message);
$('#comment > div > div.comment > div > div.comment-send > div.textarea-container > button').click();
}
sendResponse('我收到了你的消息!');
});
}
comment();
};
複制代碼
<template>
<div>
<el-container>
<el-header height="24">B站小工具</el-header>
<el-main>
<el-row :gutter="5">
<el-input type="textarea" :rows="2" placeholder="請輸入內容" v-model="message" class="mb-5" >
</el-input>
<div>
<el-button @click="addComment">評論</el-button>
</div>
</el-row>
</el-main>
</el-container>
</div>
</template>
<script> export default { name: 'App', data() { return { message: '', list: [], open: false, } }, created() { chrome.storage.sync.get('list', (obj) => { this.list = obj['list'] }) }, mounted() { chrome.runtime.onMessage.addListener(function ( request, sender, sendResponse ) { console.log('收到來自content-script的消息:') console.log(request, sender, sendResponse) sendResponse('我是後臺,我已收到你的消息:' + JSON.stringify(request)) }) }, methods: { sendMessageToContentScript(message, callback) { chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { chrome.tabs.sendMessage(tabs[0].id, message, function (response) { if (callback) callback(response) }) }) }, addComment() { this.sendMessageToContentScript( { cmd: 'addComment', message: this.message }, function () { console.log('來自content的回複:' + response) } ) }, }, } </script>
複制代碼

代碼就不解讀了,調用sendMessageToContentScript方法即可。相關源碼可自行下載查看

實現類似點贊功能也是同理的。

整體體驗

當時寫Chrome插件的效率不能說慢,反正不快就是了,像一些tips,都得自行封裝。用過Vue的都知道寫網頁很方便,寫Chrome插件未嘗不是編寫一個網頁,當時的我在接觸了Vue後就萌發了使用vue來編寫Chrome的想法,當然肯定不止我一個這麼想過,所以我在github上就能搜索到相應的源碼,於是就有了這篇文章。

如果有涉及到爬取數據相關的,我肯定是首選使用HTTP協議,如果在搞不定我會選擇使用puppeteerjs,不過Chrome插件主要還是增强頁面功能的,可以實現原本頁面不具備的功能。

本文僅僅只是初步體驗,簡單編寫了個小項目,後期有可能會實現一個百度網盤一鍵填寫提取碼,Js自吐Hooke相關的。(原本是打算做pdd商家自動回複的,客戶說要用客戶端而不是網頁端(客戶端可以多號登陸),無奈,這篇博客就拿B站來演示了)

版权声明:本文为[愧怍]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/09/20210918144647191N.html