1 写在前面
最近使用 typescript 与 angular 编写 chrome 扩展, 对繁复的 contextMenus 创建步骤进行了提炼,并总结一个类
2 重构思路
2.1 一般方法
在编写 chrome 扩展中的 contextMenu 中,一般的思路是定义一个 JSON,并且遍历这个JSON数据并且以此创建 menu, 如:
1 const menu = { 2 "menus": [ 3 {"id": "main", "visible": true, "title": "main"}, 4 {"id": "sub1", "visible": true, "title": "sub1", "parentId": "main"}, 5 {"id": "sub11", "visible": true, "title": "sub11", "parentId": "sub1"}, 6 {"id": "sub12", "visible": true, "title": "sub12", "parentId": "sub1"}, 7 {"id": "sub2", "visible": true, "title": "sub2", "parentId": "main"} 8 ] 9 }; 10 11 const createMenu = () => { 12 menu.menus.forEach(value => { 13 chrome.contextMenus.create(value); 14 }) 15 }; 16 createMenu();
2.2 重构
在 menu 的个数很少的情况下,上述的传统方式可能不会构成问题,但在选项多的场景下(如编写工具类扩展),contextMenu 的可读性就变得极差
2.2.1 编写 Menu 类
因此我们将用 typescript 来构建一个层次结构清晰的类以组合编排 父-子 menu 的层次结构, 类的结构如下:
1 interface Menu { 2 createProperties: CreateProperties; 3 children?: Menu[]; 4 }
这个类的结构很简单,但是已经足以代表需要创建 ContextMenu 的数据
现在上边使用过的 json 数据现在可以重写为:
1 const defaultMenu: Menu = { 2 createProperties: { 3 id: 'main', 4 visible: true, 5 title: 'main' 6 }, 7 children: [ 8 { 9 createProperties: { 10 id: 'sub1', 11 visible: true, 12 title: 'sub1', 13 14 }, 15 children: [ 16 { 17 createProperties: { 18 id: 'sub11', 19 visible: true, 20 title: 'sub11', 21 parentId: 'sub1' 22 } 23 }, 24 { 25 createProperties: { 26 id: 'sub12', 27 visible: true, 28 title: 'sub12', 29 parentId: 'sub1' 30 } 31 } 32 ] 33 }, 34 { 35 createProperties: { 36 id: 'sub2', 37 visible: true, 38 title: 'sub2', 39 parentId: 'main' 40 } 41 } 42 ] 43 };
结构似乎很清晰,但是不够完善,我们不想手动管理 menu 之间的父子关系,现在,删掉所有的 parentId 属性,我们将在接下来的节点来讲述如何动态维护关系
2.2.2 编写 collectMenuCreateProperties 方法
在上一节点中,我们创建了一个足以涵盖创建 contextMenu 的类 Menu,但是 chrome.contextMenus.create(property); 要求我们每次传入一个 CreateProperties 类,因此我们需要一个工具类从 Menu 类中抽取所有的 CreateProperties 信息,并且不要忘记了该方法必须能够动态维护 menu 间的父子关系(设置子menu 的 parentId 为上一层的 menu 的 id)
1 function collectMenuCreateProperties(parent: Menu): CreateProperties[] { 2 if (parent.createProperties.id === undefined) { 3 throw new Error('parent contextMenu must has id'); 4 } 5 let result: CreateProperties[] = []; 6 result.push(parent.createProperties); 7 if (parent.children) { 8 parent.children.forEach(child => { 9 // 确保每一层的层级关系 10 child.createProperties.parentId = parent.createProperties.id; 11 result = result.concat(collectMenuCreateProperties(child)); 12 }); 13 } 14 return result; 15 }
2.2.3 编写 createMenu 方法
一切就绪,我们需要创建一个 contextMenu,这里有一些注意的点:
- 如果在文件manifest.json 中对 background.js 设置了属性 "persistent": false ,可能会出现多次创建同一个menu
- 我们的 menu 只需要创建一次,比如扩展初次安装的时候就很合适
- 当调用者忘记传入构建源 Menu 时,需要确保运行不出错,这一点我们将采用ts中的默认值
1 function createMenu(menu: Menu = defaultMenu): void { 2 chrome.runtime.onInstalled.addListener(details => { 3 const properties: CreateProperties[] = collectMenuCreateProperties(menu); 4 // alert(JSON.stringify(properties)); 5 properties.forEach(property => { 6 chrome.contextMenus.create(property); 7 }); 8 9 }); 10 }
最后,别忘了在顶层导出
1 export {Menu, defaultMenu}; 2 export {createMenu}; 3 export {collectMenuCreateProperties};
附:
完整的代码
1 import CreateProperties = chrome.contextMenus.CreateProperties; 2 3 export {Menu, defaultMenu}; 4 export {createMenu}; 5 export {collectMenuCreateProperties}; 6 7 8 /** 9 * 一系列创建 {@link contextMenus} 需要的数据 10 * @see defaultMenu 11 */ 12 interface Menu { 13 createProperties: CreateProperties; 14 children?: Menu[]; 15 } 16 17 /** 18 * 默认的的数据 : 19 * [{"id":"main","visible":true,"title":"main"}, 20 * {"id":"sub1","visible":true,"title":"sub1","parentId":"main"}, 21 * {"id":"sub11","visible":true,"title":"sub11","parentId":"sub1"}, 22 * {"id":"sub12","visible":true,"title":"sub12","parentId":"sub1"}, 23 * {"id":"sub2","visible":true,"title":"sub2","parentId":"main"}] 24 */ 25 const defaultMenu: Menu = { 26 createProperties: { 27 id: 'main', 28 visible: true, 29 title: 'main' 30 }, 31 children: [ 32 { 33 createProperties: { 34 id: 'sub1', 35 visible: true, 36 title: 'sub1', 37 38 }, 39 children: [ 40 { 41 createProperties: { 42 id: 'sub11', 43 visible: true, 44 title: 'sub11', 45 } 46 }, 47 { 48 createProperties: { 49 id: 'sub12', 50 visible: true, 51 title: 'sub12', 52 } 53 } 54 ] 55 }, 56 { 57 createProperties: { 58 id: 'sub2', 59 visible: true, 60 title: 'sub2', 61 } 62 } 63 ] 64 }; 65 66 67 /** 68 * 主要方法 69 * 创建 contextMenus 70 * 1 监听 {@link chrome.runtime}事件,事件的实体可能是:("install", "update", "chrome_update", or "shared_module_update") 71 * 2 移除之前创建的所有 {@link chrome.contextMenus} 72 * 3 执行创建 73 * 74 * @param menu 执行创建的上下文信息 75 * @see Menu 76 * @see defaultMenu 77 */ 78 function createMenu(menu: Menu = defaultMenu): void { 79 chrome.runtime.onInstalled.addListener(details => { 80 const properties: CreateProperties[] = collectMenuCreateProperties(menu); 81 // alert(JSON.stringify(properties)); 82 properties.forEach(property => { 83 chrome.contextMenus.create(property); 84 }); 85 86 }); 87 } 88 89 /** 90 * 递归方式返回 parent 中包含的{@link CreateProperties} 对象, 91 * 每一层的 {@link CreateProperties.id} 必须不为空 92 * 并以编码的方式保证: 第二层开始, {@link CreateProperties.parentId} 被正确设置 93 * 94 * @param parent 顶层 95 */ 96 function collectMenuCreateProperties(parent: Menu): CreateProperties[] { 97 if (parent.createProperties.id === undefined) { 98 throw new Error('parent contextMenu must has id'); 99 } 100 let result: CreateProperties[] = []; 101 result.push(parent.createProperties); 102 if (parent.children) { 103 parent.children.forEach(child => { 104 // 确保每一层的层级关系 105 child.createProperties.parentId = parent.createProperties.id; 106 result = result.concat(collectMenuCreateProperties(child)); 107 }); 108 } 109 return result; 110 }
用例:
1 import {Component, OnInit} from '@angular/core'; 2 import {createMenu} from './contextMenus'; 3 4 @Component({ 5 selector: 'app-event-page', 6 templateUrl: './event-page.component.html', 7 styleUrls: ['./event-page.component.css'] 8 }) 9 10 /** 11 * @author siweipancc 12 * @version 1.0.0 13 */ 14 export class EventPageComponent implements OnInit { 15 ngOnInit() { 16 createMenu(); 17 } 18 19 }