本拓展程序灵感来自于政采云前端团队的 这篇文章
不同于其知识分享系统,本系统是用于解决不同团队对不同网站的依赖,而开发的一套书签协同管理 OA 系统,方便多个公用平台的管理。

本篇文章用于记录一下个人的开发思路以及踩的坑:

共享系统设计思路

React   TypeScript 写了一个用作书签共享的Chrome插件 - 知乎 - 图1

像笔者本身团队中就有很多网站需要存储(还有很多没有截图到…),如果有新同学入职的话,还需要将这些网站一个个分享。非常不方便,该系统可以将需要的网站进行分类存储,将自己团队的同学拉入一个组中,达到共享书签的目的。

明确了此思路,来整理一下需求:

用户关系:管理员、普通用户。

  • 管理员权限:添加文章、查看文章、删除文章、添加分类标签、删除分类标签、移除组成员、解散组
  • 普通用户权限:添加文章、查看文章、退出组

UI 形式:chrome 插件、后台管理系统

  • chrome 插件:注册登陆、创建 / 加入组、文章添加、退出登录
  • 后台管理系统:查看文章、退出 / 解散组、添加 / 删除标签、移除组成员、解散组

React   TypeScript 写了一个用作书签共享的Chrome插件 - 知乎 - 图2

项目构建

别问为什么用 React+Ts,问就是 React learn once, write anywhere,Ts 大法真香。这里直接跑命令构建。

  1. npx create-react-app my-app --template typescript

完成后我们创建一下自己需要的目录结构。

  1. ├── config # ts path配置
  2. ├── public # 存放 manifest.json 注意:该文件用于chrome拓展的信息识别
  3. ├── script # 需要的bash脚本(后面会说)
  4. ├── src
  5. ├── apis # 接口
  6. ├── components # 组件
  7. ├── configs # 开发/生产环境区分
  8. ├── contentScripts # 用于 chrome
  9. ├── pages # 页面
  10. ├── router # 路由
  11. ├── stores # mbox仓库
  12. ├── utils # 工具函数
  13. ├── App.tsx
  14. └── index.tsx
  15. ├── craco.config.js # 不想eject故通过craco修改配置项
  16. ├── tsconfig.json # 一些ts的配置
  17. └── package.json

chrome 拓展打开,就相当于一个网页打开,关闭就是一个网页的关闭。所以我们的拓展需要两个功能。

  1. 保存用户点击 tab 栏的位置(不能用户每次打开的时候都在第一页,要记录用户的点击信息)
  2. 保存当前浏览器的网页信息(方便用户添加书签)

React   TypeScript 写了一个用作书签共享的Chrome插件 - 知乎 - 图3

这里不再去重复普通页面的写法。所以着重说一下作为 chrome 拓展程序和一般 web 项目不同的地方。

chrome 拓展需要识别拓展信息:故需要 manifest.json 文件。

  1. {
  2. "manifest_version": 2,
  3. "name": "Tnshare",
  4. "description": "A plugin for sharing knowledge and common tools",
  5. "version": "1.0",
  6. "permissions": ["tabs", "storage"],
  7. "icons": {
  8. "322": "icon.png"
  9. },
  10. "browser_action": {
  11. "default_icon": "icon.png",
  12. "default_popup": "index.html"
  13. },
  14. "content_scripts": [
  15. {
  16. "matches": ["<all_urls>"],
  17. "js": ["./contentScripts/get_data.js"],
  18. "run_at": "document_start"
  19. }
  20. ],
  21. "content_security_policy": "script-src 'self' 'sha256-hrABjXgkmzJSAYJz7Tb8+vCZlVwt6UMWGfHKxDlE+2k='; object-src 'self'"
  22. }

这里主要看一下 permissionscontent_scriptscontent_security_policy

permissions:我们开发所需要用到的权限。tabs 为拓展与用户页面的信息交互、strong 类似于 localstorage 用于存储用户点击信息

content_scripts :这个是 chrome 的规定写法,要想获取用户信息,必须创建一个 js 脚本,Chrome 会自动将该脚本注入用户页面,以获取用户网页信息。(注意:content_scripts 中 js 只能获取用户页面的 html/css,无法获取其 js 内容)

content_security_policy:因为 webpack 打包后 html 会有内联 js,chrome 插件默认禁止使用内联 js,需要这里添加上 js 的解释形式即可(或者在 craco 中注入 webpack 全局配置取消内联注入亦可)

srcipt 中的bash

细心的同学可以看到,我们在content_scripts中指定了需要注入的 js 文件位置,打包的时候我们也需要保证该文件位置的一致性。由于不想 eject,所以需要来写一个 bash srcipt,在打包完成后将该文件夹下的内容注入到 build 目录下。

  1. # 注意需要加下权限:chmod +x script/build.sh
  2. build() {
  3. mkdir -p build/contentScripts
  4. cp -r src/contentScripts/* build/contentScripts
  5. }
  6. build

utils/chrome_methods.ts下,规避开发环境没有 chrome 全局变量的问题

还有一个问题,由于 chrome 拓展中的功能使用到的是 Chrome 注入的全局变量。但是在本地开发的时候,没有这个变量,导致页面报错,如果我们直接注释代码又太不优雅。。。故需要将生产环境与开发环境的变量分离。

为了减少行数,代码内容把 tab 选项卡点击类型做了删减、减少一下重复度,一大坨代码要我我也看不下去

  1. import React from "react"
  2. import { stores } from "@/stores/index"
  3. export interface IChromeMethodsProps {
  4. ChromeTabsQuery: (cb: Function) => void // 获取外部链接
  5. ChromeSetTabIdx: (key: React.Key, cb: Function) => void // 设置tab选项卡点击位置
  6. ChromeGetTabIdx: (cb: (tab: string) => void) => void // 获取tab选项卡点击位置
  7. ChromeSetToken: (token: string) => Promise<string> // 设置token
  8. ChromeGetToken: () => Promise<string> // 获取token
  9. }
  10. const dev_methods: IChromeMethodsProps = {
  11. ChromeTabsQuery: cb => {
  12. cb({
  13. title: "i m title",
  14. link: "i m link",
  15. description: "im description",
  16. })
  17. },
  18. ChromeSetToken: token => {
  19. return new Promise(resolve => {
  20. localStorage.setItem("token", token)
  21. stores.stateStore.handleSetToken(token)
  22. resolve(token)
  23. })
  24. },
  25. ChromeGetToken: () => {
  26. return new Promise(resolve => {
  27. const token = localStorage.getItem("token") || ""
  28. stores.stateStore.handleSetToken(token)
  29. resolve(token)
  30. })
  31. },
  32. }
  33. const prod_methods: IChromeMethodsProps = {
  34. ChromeTabsQuery(cb) {
  35. chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
  36. chrome.tabs.sendMessage(tabs[0].id as number, { action: "GET_PAGE_DATA" }, function (response) {
  37. cb(response)
  38. })
  39. })
  40. },
  41. ChromeSetToken: token => {
  42. return new Promise(resolve => {
  43. chrome.storage.local.set({ token }, function () {
  44. stores.stateStore.handleSetToken(token)
  45. resolve(token)
  46. })
  47. })
  48. },
  49. ChromeGetToken: () => {
  50. return new Promise(resolve => {
  51. chrome.storage.local.get(["token"], function (res) {
  52. const token = res ? res.token : ""
  53. stores.stateStore.handleSetToken(token)
  54. resolve(token)
  55. })
  56. })
  57. },
  58. }
  59. const Methods = process.env.REACT_APP_ENV === "dev" ? dev_methods : prod_methods
  60. export default {
  61. ...Methods,
  62. }

这里有个地方要注意:chrome.storage是异步存储(为了提高本地 io 速度),localstorage是同步存储。所以我们这里需要用 promise 包一层才能保证调用方式的一致性。

其他

  • 在 ts 环境中,我们直接使用 chrome 全局变量是会报错的,下载一个@types/chrome即可获取其类型构造。

后台页面

React   TypeScript 写了一个用作书签共享的Chrome插件 - 知乎 - 图4

后台本身没有什么复杂的难点,即常规的页面开发。这里的 gif 就放上图片,作为一个完整项目的功能展示 。。。

最后

chrome 插件使用文档和下载地址可在我的 github 中查看

由于目前该服务在我自己的小霸王服务器上跑,故没有开源。大家可以体验一下~如果有开源需求的话,点赞这篇文章,如果多的话我会开源分享给大家,不过会停止本服务器上的线上服务。
https://zhuanlan.zhihu.com/p/226597333?utm_source=ZHShareTargetIDMore&utm_medium=social&utm_oi=53706733125632