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

像笔者本身团队中就有很多网站需要存储(还有很多没有截图到…),如果有新同学入职的话,还需要将这些网站一个个分享。非常不方便,该系统可以将需要的网站进行分类存储,将自己团队的同学拉入一个组中,达到共享书签的目的。
明确了此思路,来整理一下需求:
用户关系:管理员、普通用户。
- 管理员权限:添加文章、查看文章、删除文章、添加分类标签、删除分类标签、移除组成员、解散组
- 普通用户权限:添加文章、查看文章、退出组
UI 形式:chrome 插件、后台管理系统
- chrome 插件:注册登陆、创建 / 加入组、文章添加、退出登录
- 后台管理系统:查看文章、退出 / 解散组、添加 / 删除标签、移除组成员、解散组

项目构建
别问为什么用 React+Ts,问就是 React learn once, write anywhere,Ts 大法真香。这里直接跑命令构建。
npx create-react-app my-app --template typescript
完成后我们创建一下自己需要的目录结构。
├── config # ts path配置├── public # 存放 manifest.json 注意:该文件用于chrome拓展的信息识别├── script # 需要的bash脚本(后面会说)├── src│ ├── apis # 接口│ ├── components # 组件│ ├── configs # 开发/生产环境区分│ ├── contentScripts # 用于 chrome│ ├── pages # 页面│ ├── router # 路由│ ├── stores # mbox仓库│ ├── utils # 工具函数│ ├── App.tsx│ └── index.tsx├── craco.config.js # 不想eject故通过craco修改配置项├── tsconfig.json # 一些ts的配置└── package.json
chrome 拓展打开,就相当于一个网页打开,关闭就是一个网页的关闭。所以我们的拓展需要两个功能。
- 保存用户点击 tab 栏的位置(不能用户每次打开的时候都在第一页,要记录用户的点击信息)
- 保存当前浏览器的网页信息(方便用户添加书签)

这里不再去重复普通页面的写法。所以着重说一下作为 chrome 拓展程序和一般 web 项目不同的地方。
chrome 拓展需要识别拓展信息:故需要 manifest.json 文件。
{"manifest_version": 2,"name": "Tnshare","description": "A plugin for sharing knowledge and common tools","version": "1.0","permissions": ["tabs", "storage"],"icons": {"322": "icon.png"},"browser_action": {"default_icon": "icon.png","default_popup": "index.html"},"content_scripts": [{"matches": ["<all_urls>"],"js": ["./contentScripts/get_data.js"],"run_at": "document_start"}],"content_security_policy": "script-src 'self' 'sha256-hrABjXgkmzJSAYJz7Tb8+vCZlVwt6UMWGfHKxDlE+2k='; object-src 'self'"}
这里主要看一下 permissions 、 content_scripts 和 content_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 目录下。
# 注意需要加下权限:chmod +x script/build.shbuild() {mkdir -p build/contentScriptscp -r src/contentScripts/* build/contentScripts}build
utils/chrome_methods.ts下,规避开发环境没有 chrome 全局变量的问题
还有一个问题,由于 chrome 拓展中的功能使用到的是 Chrome 注入的全局变量。但是在本地开发的时候,没有这个变量,导致页面报错,如果我们直接注释代码又太不优雅。。。故需要将生产环境与开发环境的变量分离。
为了减少行数,代码内容把 tab 选项卡点击类型做了删减、减少一下重复度,一大坨代码要我我也看不下去
import React from "react"import { stores } from "@/stores/index"export interface IChromeMethodsProps {ChromeTabsQuery: (cb: Function) => void // 获取外部链接ChromeSetTabIdx: (key: React.Key, cb: Function) => void // 设置tab选项卡点击位置ChromeGetTabIdx: (cb: (tab: string) => void) => void // 获取tab选项卡点击位置ChromeSetToken: (token: string) => Promise<string> // 设置tokenChromeGetToken: () => Promise<string> // 获取token}const dev_methods: IChromeMethodsProps = {ChromeTabsQuery: cb => {cb({title: "i m title",link: "i m link",description: "im description",})},ChromeSetToken: token => {return new Promise(resolve => {localStorage.setItem("token", token)stores.stateStore.handleSetToken(token)resolve(token)})},ChromeGetToken: () => {return new Promise(resolve => {const token = localStorage.getItem("token") || ""stores.stateStore.handleSetToken(token)resolve(token)})},}const prod_methods: IChromeMethodsProps = {ChromeTabsQuery(cb) {chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {chrome.tabs.sendMessage(tabs[0].id as number, { action: "GET_PAGE_DATA" }, function (response) {cb(response)})})},ChromeSetToken: token => {return new Promise(resolve => {chrome.storage.local.set({ token }, function () {stores.stateStore.handleSetToken(token)resolve(token)})})},ChromeGetToken: () => {return new Promise(resolve => {chrome.storage.local.get(["token"], function (res) {const token = res ? res.token : ""stores.stateStore.handleSetToken(token)resolve(token)})})},}const Methods = process.env.REACT_APP_ENV === "dev" ? dev_methods : prod_methodsexport default {...Methods,}
这里有个地方要注意:chrome.storage是异步存储(为了提高本地 io 速度),localstorage是同步存储。所以我们这里需要用 promise 包一层才能保证调用方式的一致性。
其他
- 在 ts 环境中,我们直接使用 chrome 全局变量是会报错的,下载一个
@types/chrome即可获取其类型构造。
后台页面

后台本身没有什么复杂的难点,即常规的页面开发。这里的 gif 就放上图片,作为一个完整项目的功能展示 。。。
最后
chrome 插件使用文档和下载地址可在我的 github 中查看。
由于目前该服务在我自己的小霸王服务器上跑,故没有开源。大家可以体验一下~如果有开源需求的话,点赞这篇文章,如果多的话我会开源分享给大家,不过会停止本服务器上的线上服务。
https://zhuanlan.zhihu.com/p/226597333?utm_source=ZHShareTargetIDMore&utm_medium=social&utm_oi=53706733125632
