关于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小时内与您取得联系。