關于Chrome插件,大家可能或多或少的都會使用幾個,比如 廣告屏蔽插件AdBlock,大名鼎鼎的腳本管理插件 Tampermonkey(油猴)等,這些插件給予了我們日常使用瀏覽器很大的方便。那如何開發一個自己的插件呢,最近我研究了Google插件開發文檔和網絡上的一些文檔,整理了開發筆記。由于Google在推廣V3版本的插件,這里就使用最新的V3版本做研究。
在開發插件之前,先了解一下Chrome插件的結構和基本概念是很有必要的。下面就介紹一下幾個基本概念。
alt Architecture
Manifest(manifest.json) 是 Chrome 插件的配置文件,類似于前段工程中的 webpack.config.js或者后端maven項目中的pom.xml。 它是一個json文件,必須位于插件項目的根目錄中,而且名稱必須是 manifest.json。Manifest 記錄著重要的元數據,定義資源,聲明權限,并標識哪些文件在后臺和頁面上運行。下面列出一些重要的配置項。全部配置項可以查看官方文檔: https://developer.chrome.com/docs/extensions/mv3/manifest/
{
// 必須項
"manifest_version": 3, // 版本, V3是Chrome最新插件版本,類似前段node的版本或后端jdk版本
"name": "My Extension", // 插件名稱
"version": "1.0.1", // 插件版本
// 推薦項目
"action": {...}, // 用于點擊圖標彈出框,對于彈出框接受的是html文件
"description": "A plain text description", // 插件描述
"icons": {...}, // 插件圖標
// 可選項(重要, 可選不表示不重要)
"author": "developer@example.com", // 插件作者
"commands": {...}, // 使用命令 API 添加觸發擴展中操作的鍵盤快捷鍵
"background": {...}, // Service Worker 配置
"content_scripts": [{...}], // 內容腳本配置
"options_page": "options.html", // options_page 配置
"permissions": ["..."], // 插件需要使用的權限
"version_name": "1.0 beta",
"web_accessible_resources": [...] // Web 可訪問資源配置
}
Service Worker 是一個事件處理器,它可以監聽瀏覽器事件,例如導航到新頁面、刪除書簽或關閉選項卡等,一旦這些事件觸發,則 Service Worker 中注冊的事件處理器就會被調用。需要明確的是,這里的事件是瀏覽器的事件,并不是頁面文檔中js的事件(例如, 鼠標點擊事件,滑動事件)。Service Worker 有獨立的運行環境,和頁面js的運行環境是隔離的,所以它不能訪問頁面的DOM,但是它可是使用Chrome的API。
Content Script 又叫 ”內容腳本“, 它運行在網頁上下文中,能夠對DOM進行訪問和修改,或者將頁面信息傳遞給插件的其他部分,如 Service Worker,Popup Page。雖然 Content Script 運行在網頁上下文中,但是它和頁面引入的js運行環境是隔離的,也就是說,它不能訪問頁面定義的變量和方法。
Popup Page 又叫”彈出頁“, 是點擊插件圖標是彈出的頁面,如下圖:
它和普通web頁面很相似,可以有自己的js和css,但是不允許內聯js代碼。 彈出頁和普通web不同的地方是,彈出頁的js可是使用Chrome的API,但是它和內容腳本一樣,不能訪問普通頁面的DOM和普通頁面引入js。
Options Page 又叫”選項頁“, 顧名思義,它是插件的配置頁面,用戶在該頁面可以對插件進行設置。
我們先對這些基礎知識有個基本概念,后面會詳細說明它們的使用方式,給出相應的 Demo。
為了讓我們能夠快速上手開發一個自己的插件,這節我們就開發一個小插件。這個插件功能是和 選詞翻譯 插件功能類似,鼠標選中頁面中的文本,彈出一個提示框,顯示翻譯翻譯后的內容。我們這個插件功能是頁面選中文本,點擊鼠標右鍵彈出選中文本,不做翻譯的功能。 效果演示
└── src
├── icons
│ └── logo.png
├── manifest.json
└── scripts
├── content.js
└── lib
└── jQuery.js
我們插件項目結構和 Chrome 官方項目結構保持一致:
{
"manifest_version": 3,
"name": "演示插件",
"version": "1.0.0",
"description": "這是一個演示插件",
"icons": {
"16": "/icons/logo.png",
"32": "/icons/logo.png",
"48": "/icons/logo.png",
"128": "/icons/logo.png"
},
"content_scripts": [
{
"matches": [
"https://*/*",
"http://*/*",
"file:///*"
],
"js": [
"scripts/lib/jQuery.js",
"scripts/content.js"
],
"run_at": "document_idle"
}
]
}
manifest.json 主要關心一下配置項:
/**
* 顯示提示框
*
* @param {*} e 鼠標點擊事件
* @param {*} tip 顯示的文本內容
*/
function showTip(e, tip) {
$('#tipId').html(tip);
$('#tipDiv').css('left', e.pageX + 'px');
$('#tipDiv').css('top', e.pageY + 10 + 'px');
$('#tipDiv').css('display', 'block');
}
/**
* 隱藏提示框
*/
function hiddenTip() {
$('#tipDiv').css('display', 'none');
}
/**
* 初始化事件監聽器
*/
function initEventListeners() {
// 監聽鼠標按下事件,關閉提示框
document.addEventListener("mousedown", ()=> {
hiddenTip();
});
// 在提示框內點擊鼠標,阻止提示框關閉
$('#tipDiv').bind("mousedown", (e)=> {
e.stopPropagation();
})
// 點擊鼠標右鍵,彈出選擇的內容
document.oncontextmenu=(e)=> {
// 獲取選中的內容
const selected=window.getSelection().toString();
if (!selected) {
return true;
}
// 顯示選中的內容
showTip(e, selected);
return false;
}
}
(()=> {
// 頁面添加一個提示的div(tipDiv), 用于彈出提示框
$('body').append("<div id='tipDiv' style='position:absolute; float: left; z-index:1000; left: 0px; top: 0px; display: none; width: 600px; height: 100px;'></div>")
// style 太長了,分別設置css屬性
$('#tipDiv').css('background-color', 'lightsteelblue');
$('#tipDiv').css('font-size', '200%');
// 在 tipDiv 中添加一個 內容div, 用于放提示內容
$('#tipDiv').append("<div id='tipId'>測試彈出頁面</div>");
// 初始化事件監聽器
initEventListeners();
})();
content.js的代碼注釋寫的很清楚了,這里就不重復解釋代碼了。
本文先介紹 Chrome 插件開發過程中必備的基本概念,然后通過寫一個小Demo,來說明如何開發一個 Chrome 插件,幫助大家快速入門。后面會再出進階文章,對 Content Script, Service Worker,Popup Page 等做進一步研究。
eact Hooks已經出了有段時間了,不知道大伙有沒有嘗試著去用過,下面小編帶大伙對比看下三大基礎 Hooks 和傳統 class 組件的區別和用法。
我們所指的三個基礎 Hooks 是:
useState 允許我們在函數式組件中維護 state,傳統的做法需要使用類組件。舉個例子:我們需要一個輸入框,隨著輸入框內容的改變,組件內部的 label 標簽顯示的內容也同時改變。下面是兩種不同的寫法:
不使用 useState:
import React from "react"; // 1 export class ClassTest extends React.Component { // 2 state={ username: this.props.initialState } // 3 changeUserName(val) { this.setState({ username: val }) } // 4 render() { return ( <div> <label style={{ display: 'block' }} htmlFor="username">username: {this.state.username}</label> <input type="text" name="username" onChange={e=> this.changeUserName(e.target.value)} /> </div> ) } }
使用 useState:
// 1 import React, { useState } from "react"; export function UseStateTest({ initialState }) { // 2 let [username, changeUserName]=useState(initialState) // 3 return ( <div> <label style={{ display: 'block' }} htmlFor="username">username: {username}</label> <input type="text" name="username" onChange={e=> changeUserName(e.target.value)} /> </div> ) }
在父組件中使用:
import React from "react"; // 引入組件 import { UseStateTest } from './components/UseStateTest' // 4 const App=()=> ( <div> <UseStateTest initialState={'initial value'} /> </div> ) export default App;
用 useState 方法替換掉原有的 class 不僅性能會有所提升,而且可以看到代碼量減少很多,并且不再需要使用 this,所以能夠維護 state 的函數式組件真的很好用
useEffect 是專門用來處理副作用的,獲取數據、創建訂閱、手動更改 DOM 等這都是副作用。你可以想象它是 componentDidMount 和 componentDidUpdate 及 componentWillUnmount 的結合。
舉個例子,比方說我們創建一個 div 標簽,每當點擊就會發送 http 請求并將頁面 title 改為對應的數值:
import React from 'react' // 1 import { useState, useEffect } from 'react' export function UseEffectTest() { let [msg, changeMsg]=useState('loading...') // 2 async function getData(url) { // 獲取 json 數據 return await fetch(url).then(d=> d.json()) } // 3 async function handleClick() { // 點擊事件改變 state let data=await getData('https://httpbin.org/uuid').then(d=> d.uuid) changeMsg(data) } // 4 useEffect(()=> { // 副作用 document.title=msg }) return ( <div onClick={()=> handleClick()}>{msg}</div> ) }
如果使用傳統的類組件的寫法:
import React from 'react' // 1 export class ClassTest extends React.Component { // 2 state={ msg: 'loading...' } // 3 async getData(url) { // 獲取 json 數據 return await fetch(url).then(d=> d.json()) } handleClick=async ()=> { // 點擊事件改變 state let data=await this.getData('https://httpbin.org/uuid').then(d=> d.uuid) this.setState({ msg: data }) } // 4 componentDidMount() { document.title=this.state.msg } componentDidUpdate() { document.title=this.state.msg } // 5 render() { return ( <div onClick={this.handleClick}>{this.state.msg}</div> ) } }
使用 useEffect 不僅去掉了部分不必要的東西,而且合并了 componentDidMount 和 componentDidUpdate 方法,其中的代碼只需要寫一遍。
第一次渲染和每次更新之后都會觸發這個鉤子,如果需要手動修改自定義觸發規則
見文檔:https://zh-hans.reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
另外,官網還給了一個訂閱清除訂閱的例子:
使用 useEffect 直接 return 一個函數即可:
返回的函數是選填的,可以使用也可以不使用:
文檔:https://zh-hans.reactjs.org/docs/hooks-effect.html#recap
比方說我們使用 useEffect 來解綁事件處理函數:
useEffect(()=> { window.addEventListener('keydown', handleKeydown); return ()=> { window.removeEventListener('keydown', handleKeydown); } })
useContext 的最大的改變是可以在使用 Consumer 的時候不必在包裹 Children 了,比方說我們先創建一個上下文,這個上下文里頭有一個名為 username 的 state,以及一個修改 username 的方法 handleChangeUsername
創建上下文
不使用 useState:
不使用 state hooks 的代碼如下:
import React, { createContext } from 'react' // 1. 使用 createContext 創建上下文 export const UserContext=new createContext() // 2. 創建 Provider export class UserProvider extends React.Component { handleChangeUsername=(val)=> { this.setState({ username: val }) } state={ username: '', handleChangeUsername: this.handleChangeUsername } render() { return ( <UserContext.Provider value={this.state}> {this.props.children} </UserContext.Provider> ) } } // 3. 創建 Consumer export const UserConsumer=UserContext.Consumer
看看我們做了什么:
代碼比較冗長,可以使用上文提到的 useState 對其進行精簡:
使用 useState:
使用 state hooks:
import React, { createContext, useState } from 'react' // 1. 使用 createContext 創建上下文 export const UserContext=new createContext() // 2. 創建 Provider export const UserProvider=props=> { let [username, handleChangeUsername]=useState('') return ( <UserContext.Provider value={{username, handleChangeUsername}}> {props.children} </UserContext.Provider> ) } // 3. 創建 Consumer export const UserConsumer=UserContext.Consumer
使用 useState 創建上下文更加簡練。
使用上下文
上下文定義完畢后,我們再來看使用 useContext 和不使用 useContext 的區別是啥:
不使用 useContext:
import React from "react"; import { UserConsumer, UserProvider } from './UserContext' const Pannel=()=> ( <UserConsumer> {/* 不使用 useContext 需要調用 Consumer 包裹 children */} {({ username, handleChangeUsername })=> ( <div> <div>user: {username}</div> <input onChange={e=> handleChangeUsername(e.target.value)} /> </div> )} </UserConsumer> ) const Form=()=> <Pannel></Pannel> const App=()=> ( <div> <UserProvider> <Form></Form> </UserProvider> </div> ) export default App;
使用 useContext:
只需要引入 UserContext,使用 useContext 方法即可:
import React, { useContext } from "react"; // 1 import { UserProvider, UserContext } from './UserContext' // 2 const Pannel=()=> { const { username, handleChangeUsername }=useContext(UserContext) // 3 return ( <div> <div>user: {username}</div> <input onChange={e=> handleChangeUsername(e.target.value)} /> </div> ) } const Form=()=> <Pannel></Pannel> // 4 const App=()=> ( <div> <UserProvider> <Form></Form> </UserProvider> </div> ) export default App;
看看做了啥:
這樣通過 useContext 和 useState 就重構完畢了,看起來代碼又少了不少
果圖:
使用場景: 使用React渲染后臺返回的數據, 遍歷以列表的形式展示, 可能內容簡要字段需要鼠標放上去才顯示的
可以借助DOM的自定義屬性和CSS偽類的attr來實現
所有代碼:
*請認真填寫需求信息,我們會在24小時內與您取得聯系。