# 前言
electron默認沿用系統UI,并沒有提供很多接口供使用者定制樣式,如果想要完全自定義的樣式,目前我能想到的方案只能是通過前端自定義樣式,然后通過進程通信來實現系統基礎功能:最大/小化、關閉、拖動窗口等。
效果圖:
在這里插入圖片描述
在這里插入圖片描述
通過前面系列文章我們可以了解到,窗口是通過實例化BrowserWindow對象創建的,實例化時會傳入一些窗口的參數。
要實現窗口自定義,就必須把窗口默認樣式都屏蔽。幾個關鍵的參數如下:
transparent: true,
backgroundColor:'rgba(0,0,0,0)',
frame:false,
參數含義很好理解,窗體透明無邊框,參數詳解請查詢官網。
把系統自帶的窗體樣式去掉后,我們會得到一個只有主體的窗口,這個主體就是前端(vue)渲染的窗口,我們可以通過控制它,來實現任何樣式的窗口。
但是這會帶來一個問題,那就是窗口對象提供的很多便捷功能無法使用了,所以如果需要做最大化、最小化、拖動窗口等功能,只能通過進程通信,前端發送指令,主進程接收指令后,完成相應功能。具體原理請參考本系列前面關于進程通信的文章。
這里簡單列一個示例代碼,以最大化為例:
//vue代碼部分,在某個dom元素上監聽事件
<img class="title-icon" src="@/assets/img/maxsize.png" alt="最大化" @click="handleMaxSize()">
function handleMaxSize(){
renderApi.handleMaxSize()
}
//preload.js中定義通信的api,下面是我項目中所有渲染進程到主進程的通信
const handleSendPageName=(pageName)=> ipcRenderer.send('send-page-name', pageName) //渲染進程主動到主進程
const handleMinSize=()=> ipcRenderer.send('send-min-size') //渲染進程主動到主進程
const handleMaxSize=()=> ipcRenderer.send('send-max-size') //渲染進程主動到主進程
const handleRestore=()=> ipcRenderer.send('send-restore') //渲染進程主動到主進程
const handleRelaunch=()=> ipcRenderer.send('user-click-Dock-Icon') //渲染進程主動到主進程
const handleCloseWin=()=>{
ipcRenderer.send('send-auto-close')
}
contextBridge.exposeInMainWorld('renderApi', {
//監聽渲染進程事件
handleGetStoreFiles,
handleSendPageName,
handleMinSize,
handleMaxSize,
handleCloseWin,
handleRestore,
handleRelaunch
})
//主進程main.js中接收對應的通信
ipcMain.on('send-max-size', ()=> {
if(win.isMaximized()){
win.unmaximize()
}else{
win.maximize()
}
})
至此,模擬窗口最大化功能的全部過程就打通了。
這里值得注意的是,拖拽窗口不止是要配置參數,還要給對應dom元素增加類。
比如說我想實現拖動類名為“c-drag”的元素時,拖動窗口移動,大致代碼如下:
<div class="c-drag">
</div>
.c-drag{
-webkit-app-region: drag;
}
-webkit-app-region: drag是electron提供的css樣式,具體可查詢官網。
這是一個比較小眾的知識點,網上資料目前較少,這里記錄一下。
這一部分邏輯略微復雜。
窗口大小的設置一定是在主進程中設置,如果僅僅依靠vue部分控制顯示區域大小,不顯示區域設置為透明,雖然視覺上可以實現不同的窗體大小,但是這是一種偽實現,因為透明部分只是人眼看不到而已,鼠標點擊、拖拽等功能仍然存在,就會對軟件用戶造成困擾。
在主進程中設置窗口大小,最重要的就是進入不同頁面時,要主動向主進程發送指令,并告訴主進程,我現在進入登錄頁了,我現在進入正常頁了,我現在進入xx頁……
主進程接收指令后,根據參數,控制窗口的大小即可。
在我的項目中,各頁面有一個統一的路由跳轉方法,所以我在跳轉路由后,同時將活躍頁面的name通handleSendPageName發送給主進程。代碼如下:
function turnToPage_menu(name) {
console.log(name)
turnToPage(router, name)
renderApi.handleSendPageName(activeName.value) //發送pageName到主進程,以此判斷窗口大小
}
主進程接收到指令后,根據參數,決定窗口設置為多大,代碼:
ipcMain.on('send-page-name', (event, pageName)=> {
console.log('setWindowSize',pageName)
const loginSize={
width:500,
height:580
}
const pageSize={
width:1000,
height: 950
}
if (pageName && pageName=='normalLogin') {
win.setSize(loginSize.width, loginSize.height)
win.center()
win.setMenuBarVisibility(false)
} else {
if(this.judgePageSet(win,pageSize,loginSize)){
win.setSize(pageSize.width, pageSize.height)
win.center()
win.setMenuBarVisibility(true)
}
}
})
至此不同頁面實現不同大小的窗口功能,就實現了。
其實在我們項目中,還有另一種需求場景,那就是當通過注冊表把軟件注冊到系統右鍵后,上傳文件時,右下角有一個簡易窗口,窗口高度根據上傳文件數量來計算。這就需要判斷命令行參數、獲取文件下載地址等操作,更加復雜,但是應用場景應該不多,有興趣的同學可以私聊,此處不再贅述。
我并沒有在實際項目中真正實現過右鍵菜單的自定義,但是道理和窗口是相通的,如果electron提供的右鍵菜單樣式無法滿足要求,那就舍棄框架提供的便捷菜單,通過進程間通信,手動實現。
由于業務需求, 筆者要為公司開發幾款實用的瀏覽器插件,所以大致花了一天的時間,看完了谷歌瀏覽器插件開發文檔,在這里特地總結一下經驗, 并通過一個實際案例來復盤插件開發的流程和注意事項.
在開始正文之前,我們先來看看筆者總結的概覽:
如果對瀏覽器插件開發比較熟悉的朋友可以直接看最后一節插件開發實戰。
首先我們看看的瀏覽器插件的定義:
瀏覽器插件是基于Web技術(例如HTML,JavaScript和CSS)構建的可以定制瀏覽體驗的小型軟件程序。它們使用戶可以根據個人需要或偏好來定制Chrome功能和行為。
要想開發一款瀏覽器插件,我們只需要有一個manifest.json文件即可, 為了快速上手瀏覽器插件開發,我們需要把瀏覽器開發者工具打開, 具體步驟如下:
通過以上三個步驟我們就可以開啟瀏覽器插件開發之旅了.瀏覽器插件一般放在瀏覽器地址欄右側,我們可以在manifest.json文件配置插件的icon,并配置一定的規則,就能看到我們的瀏覽器插件圖標了,如下圖:
下面我們來具體講解一下瀏覽器插件開發的核心概念.
瀏覽器插件一般涉及以下幾個核心文件:
筆者畫了一張簡圖來大致表示一下它們之間的關系:
接下來我們來具體了解一下以上幾個核心知識點.
谷歌官網給我們提供了一份簡單的配置,如下:
{
"name": "My Extension",
"version": "2.1",
"description": "Gets information from Google.",
"icons": {
"128": "icon_16.png",
"128": "icon_32.png",
"128": "icon_48.png",
"128": "icon_128.png"
},
"background": {
"persistent": false,
"scripts": ["background_script.js"]
},
"permissions": ["https://*.google.com/", "activeTab"],
"browser_action": {
"default_icon": "icon_16.png",
"default_popup": "popup.html"
}
}
復制代碼
各字段含義介紹如下:
文末會給出完整的配置文件地址,方便大家學習參考.
background頁面主要用來提供一些全局配置, 事件監聽, 業務轉發等.舉幾個常用案例:
// background.js
const systems={
a: '趣談前端',
b: '掘金',
c: '微信'
}
chrome.runtime.onInstalled.addListener(function() {
// 上下文菜單
for (let key of Object.keys(systems)) {
chrome.contextMenus.create({
id: key,
title: systems[key],
type: 'normal',
contexts: ['selection'],
});
}
});
// manifest.json
{
"permissions": ["contextMenus"]
}
復制代碼
效果如下:
chrome.runtime.onInstalled.addListener(function() {
// 類似于什么時候激活瀏覽器插件圖標這種感覺
chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
chrome.declarativeContent.onPageChanged.addRules([{
conditions: [new chrome.declarativeContent.PageStateMatcher({
pageUrl: {hostSuffix: '.com'},
})
],
actions: [new chrome.declarativeContent.ShowPageAction()]
}]);
});
});
復制代碼
如下圖所示,當頁面地址的后綴不等于.com時,插件icon將不被激活:
3. 和content_script或者popup頁面進行消息通信
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting=="hello")
sendResponse({farewell: "goodbye"});
});
復制代碼
內容腳本一般植入會被植入到頁面中, 并且可以控制頁面中的dom. 我們可以利用它實現屏蔽網頁廣告, 定制頁面皮膚等操作. 在manifest.json中的基本配置如下:
{
"content_scripts": [{
"matches": [
"http://*/*",
"https://*/*"
],
"js": [
"lib/jquery3.4.min.js",
"content_script.js"
],
"css": ["base.css"]
}],
}
復制代碼
以上代碼中我們定義了content_scripts允許注入的頁面范圍, 插入頁面的js以及css, 這樣我們就能輕松改變某一個頁面的樣式.比如我們可以在頁面中注入一個按鈕:
在后面的瀏覽器插件案例中筆者會詳細介紹content_scripts的用法.
popup是用戶點擊插件圖標時打開的一個小窗口,當失去焦點后窗口就立即關閉,我們一般用它來處理一些簡單的用戶交互和插件說明。
由于popup窗口也是一個網頁,所以我們一般會建立一個popup.html和popup.js用來控制popup的頁面展示和交互.我們在manifest.json中配置如下:
{
"page_action": {
"default_title": "小夕圖片提取插件",
"default_popup": "popup.html"
},
}
復制代碼
這里要注意一點的是,我們在popup.html中不能直接使用script腳本,需要用引入腳本文件的方式.如下:
<!DOCTYPE html>
<html>
<head>
<title>在線圖片提取工具</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div class="pop-wrap">
</div>
<script src="lib/jquery3.4.min.js"></script>
<script src="popup.js"></script>
</body>
</html>
復制代碼
以下是筆者寫的一個插件的popup頁面:
對于一個相對復雜的瀏覽器插件來說,我們不僅僅只操作dom或者提供基本的功能就行了,我們還需要向第三方或者自己的服務器抓取有用的頁面數據,這個時候就需要用到插件的通信機制了.
因為content_script腳本存在于當前頁面,受同源策略影響,導致我們無法將捕獲到的數據傳給第三方平臺或者自己的服務器, 所以我們需要基于瀏覽器的通信API.以下是谷歌瀏覽器插件的通信流程:
由官方文檔可知popup可以直接訪問background頁的方法,所以popup可以直接與其通信:
// background.js
var getData=(data)=> { console.log('拿到數據:' + data) }
// popup.js
let bgObj=chrome.extension.getBackgroundPage();
bgObj.getData(); // 訪問bg的函數
復制代碼
這里我們使用chrome的tabs API,如下:
// popup.js
// 發送消息給content_script
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, "activeBtn", function(response) {
console.log(response);
});
});
// 接收消息
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting=="hello")
sendResponse({farewell: "goodbye"});
});
復制代碼
content_script接收和發送消息:
// 接收消息
chrome.runtime.onMessage.addListener(
function(message, sender, sendResponse) {
if (message=="activeBtn"){
// ...
sendResponse({farewell: "激活成功"});
}
});
// 主動發送消息
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
console.log(response, document.body);
// document.body.style.backgroundColor="orange"
});
復制代碼
有關消息的長連接,在谷歌官網也寫的很清楚:
我們可以采用如下方式進行長連接:
// content_script.js
var port=chrome.runtime.connect({name: "徐小夕"});
port.postMessage({Ling: "你好"});
port.onMessage.addListener(function(msg) {
if (msg.question=="你是做什么滴?")
port.postMessage({answer: "搬磚"});
else if (msg.question=="搬磚有錢嗎?")
port.postMessage({answer: "木有"});
});
// popup.js
chrome.runtime.onConnect.addListener(function(port) {
port.onMessage.addListener(function(msg) {
if (msg.Ling=="你好")
port.postMessage({question: "你是做什么滴?"});
else if (msg.answer=="搬磚")
port.postMessage({question: "搬磚有錢嗎?"});
else if (msg.answer=="木有")
port.postMessage({question: "太難了."});
});
});
復制代碼
chrome.storage用來針對插件全局進行數據存儲,我們在任何一個頁面(popup或content_script或background)下存儲了數據,我們在以上三個頁面都可以獲取到, 具體用法如下:
獲取數據
chrome.storage.sync.get('imgArr', function(data) {
console.log(data)
});
// 保存數據
chrome.storage.sync.set({'imgArr': imgArr}, function() {
console.log('保存成功');
});
// 另一種方式
chrome.storage.local.set({key: value}, function() {
console.log('Value is set to ' + value);
});
復制代碼
谷歌瀏覽器的插件應用場景很多,正如文章開頭的思維導圖中寫的.以下是筆者總結的一些應用場景,大家感興趣可以嘗試去實現:
還有很多實用工具可以開發,大家可以好好把玩。接下來就來通過實現一個網頁圖片提取插件,來總結以下瀏覽器插件開發流程。
首先還是按照筆者的風格,在開發任何一種工具之前都要明確需求,所以我們來看看該插件的功能點:
基本上就這幾個功能,接下來我會展示核心代碼,在介紹代碼之前我們先預覽一下插件的實現效果:
插件目錄結構如下:
因為插件的開發比較簡單,所以我直接用jquery開發。這里我們主要關注popup.js和content_script.js, popup.js中主要用來獲取從content_script頁傳過來的圖片數據,并展示在popup.html中,另外又一個需要注意的是當頁面沒有注入生成按鈕時,popupu需要發送信息給content頁面,主動讓其生成按鈕,代碼如下:
chrome.storage.sync.get('imgArr', function(data) {
data.imgArr && data.imgArr.forEach(item=> {
var imgWrap=$("<div class='img-box'></div>")
var img=$("<img src='" + item + "' alt='" + item + "' />")
imgWrap.append(img);
$('#content').append(imgWrap);
$('.empty').hide();
})
});
$('#activeBtn').click(function(element) {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, "activeBtn", function(response) {
console.log(response);
});
});
});
復制代碼
對于content頁面,我們需要實現的是動態生成按鈕,并且在頁面中植入彈窗來展示獲取到的圖片,另一方面需要將圖片數據傳遞給storage,以便popup頁面可以獲取圖片數據。
由于頁面比較簡單,筆者就不用過多的第三方庫了,筆者先簡單手寫一個modal組件,代碼如下:
// 彈窗
~function Modal() {
var modal;
if(this instanceof Modal) {
this.init=function(opt) {
modal=$("<div class='modal'></div>");
var title=$("<div class='modal-title'>" + opt.title + "</div>");
var close_btn=$("<span class='modal-close-btn'>X</span>");
var content=$("<div class='modal-content'></div>");
var mask=$("<div class='modal-mask'></div>");
close_btn.click(function(){
modal.hide()
})
title.append(close_btn);
content.append(title);
content.append(opt.content);
modal.append(content);
modal.append(mask);
$('body').append(modal);
}
this.show=function(opt) {
if(modal) {
modal.show();
}else {
var options={
title: opt.title || '標題',
content: opt.content || ''
}
this.init(options)
modal.show();
}
}
this.hide=function() {
modal.hide();
}
}else {
window.Modal=new Modal()
}
}()
復制代碼
第一步,我們先批量獲取頁面圖片數據:
var imgArr=[]
$('img').each(function(i) {
var src=$(this).attr('src');
var realSrc=/^(http|https)/.test(src) ? src : location.protocol+ '//' + location.host + src;
imgArr.push(realSrc)
})
復制代碼
因為圖片的src路徑可能是相對地址,所以筆者在這里用正則簡單處理以下,當然我們可以進行更細粒度的控制。
第二步,將圖片數據存儲到storage中:
chrome.storage.sync.set({'imgArr': imgArr}, function() {
console.log('保存成功');
});
復制代碼
第三步,生成預覽圖片的彈窗,這里用筆者上面實現的modal組件:
Modal.show({
title: '提取結果',
content: imgBox
})
復制代碼
第四步,當popup發送激活按鈕的通知時,我們要在網頁中動態插入生成按鈕:
chrome.runtime.onMessage.addListener(
function(message, sender, sendResponse) {
if (message=="activeBtn"){
if(!$('.crawl-btn')) {
$('body').append("<div class='crawl-btn'>提取</div>")
}else {
$('.crawl-btn').css("background-color","orange");
setTimeout(()=> {
$('.crawl-btn').css("background-color","#06c");
}, 3000);
}
sendResponse({farewell: "激活成功"});
}
});
復制代碼
setTimeout那段純屬是為了吸引用戶視線,當然我們可以用更優雅的方式來處理。 插件核心代碼主要是這些,當然還有很多細節要考慮,我把配置文件和一些細節放到github了,如果感興趣的朋友可以安裝感受一下。
github地址:一款提取網頁圖片數據的瀏覽器插件
如果想學習更多H5游戲, webpack,node,gulp,css3,javascript,nodeJS,canvas數據可視化等前端知識和實戰,歡迎在《趣談前端》學習討論,共同探索前端的邊界。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。