從 Ajax 攔截到使用 Vue3 + Element Plus 構建 Chrome Extension

寫在前面,尤大前一陣子發了一條微博,內容是 Vite + Vue3 + TS + VScode + Volar 誰用誰知道。好嘛,我用了,我確實知道了,我又行了,我學的動.jpg ...javascript

導語

本文不會着墨過多的插件內部邏輯和技術棧的基本使用,旨在介紹如何使用 ViteVue3 從 0開發一個實際項目,以及一些代碼設計的取捨和打包構建遇到的問題及其解決方案。關於技術棧的選擇,沒什麼技巧,個人項目我說的算,我想用哪一個就哪一個😎css

從請求攔截提及🤔

前端在實際業務中和後端 Battle 的橋樑就是 Ajax[/ˈeɪdʒæks/] 請求,使用請求能夠順着網線聯繫到後端的各類服務。在一個 webApp 之中,咱們幾乎都會使用一些手段,來作請求發出的攔截,好比混入一些公共參數 tokenuid、加密一些數據、刪除一些數據、甚至取消(abort)一些請求;或者作請求響應的攔截,好比統一的錯誤碼處理,統一的數據格式化等等。社區之中,大名鼎鼎的 Axios[æk'sioʊ] 提供了上述的兩種 Interceptors[ˌɪntərˈseptə(r)],來統一處理數據上報前與響應後。 那麼假如不用 Axios,咱們能夠怎麼來攔截請求呢?有的同窗說了,「你能夠修改 xhr 的原型方法,你能夠替換原始的 fetch,來魔改啊~」 確實能夠,可是我不用。今天就來給你們介紹使用 Chrome Extension 的能力,來攔截請求。可惡,又讓我裝到了!html

說幹就幹🕶

初始化倉庫

先記住幾個關鍵點:前端

  • Chrome Extension 的產物是多個 HTML,因此咱們要建立一個多 Page 的 Vite 工程,強大的 Vite 已經支持
  • 打包出來的產物必須有 manifest.json,這至關於 Chrome Extension 的入口文件,每 release 一次 version 自動 +1

建立項目

yarn create @vitejs/app
複製代碼

按照提示咱們建立一個 vue-ts 的工程。vue

建立 manifest.json

{
  "name": "Bad Request",
  "version": "0.0.0",
  "description": "Bad Request",
  "permissions": [
    "activeTab",
    "declarativeContent",
    "storage", // 獲取存儲權限,來存咱們要攔截的 api 連接
    "webRequest", // 獲取請求讀取權限,來搞事情
    "webRequestBlocking", // 獲取請求 abort 權限,來直接 abort 請求
    "<all_urls>" // 獲取全部 url 的權限
  ],
  "background": {
    "page": "background/index.html",
    "persistent": true // 保證 background.js 一直在後臺運行,攔截一直生效
  },
  "options_page": "options/index.html",
  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
  "page_action": {
    "default_title": "request",
    "default_icon": "images/logo.png",
    "default_popup": "popup/index.html"
  },
  "devtools_page": "devtool/index.html",
  "icons": {
    "16": "images/logo.png",
    "32": "images/logo.png",
    "48": "images/logo.png",
    "128": "images/logo.png"
  },
  "manifest_version": 2,
  "content_scripts": [
    {
      "matches": [
        "<all_urls>"
      ],
      "js": [
        "content.js"
      ]
    }
  ]
}
複製代碼

改造爲多入口工程

而後根據Vite 官方文檔把它改形成一個多頁面的工程。多 Page 的 vite.config.ts 以下:java

export default defineConfig{
  // other setting...
  build: {
    rollupOptions: {
      input: {
        /** * 點擊插件圖標出現的彈窗 * */
        popup: resolve(__dirname, 'popup/index.html'), 
        /** * chrome devtool pane 頁面 * */
        devtoolPage: resolve(__dirname, 'devtoolPage/index.html'), 
        /** * 插件的核心 JS,一直活躍在後臺,來監聽全部請求 * */
        background: resolve(
          __dirname,
          'background/index.html'
        ),
        /** * 加載 chrome devtool pane 的入口 * */
        devtool: resolve(__dirname, 'devtool/index.html'),
        /** * 插件設置頁面 * */
        options: resolve(__dirname, 'options/index.html'),
        /** * 與頁面同級,並在某個時機執行,能夠拿到頁面的 document * */
        content: resolve(__dirname, 'src/content.ts'),
      },
      output: {
        entryFileNames: '[name].js',
      },
    },
  },
  // other setting...
}
複製代碼

引入 Element Plus

根據其官方文檔,配置 Vite 按需加載,配好的 vite.config.ts 以下:node

export default defineConfig{
  // other setting...
  plugins: [
    vue(),
    styleImport({
      libs: [
        {
          libraryName: 'element-plus',
          esModule: true,
          ensureStyleFile: true,
          resolveStyle: (name) => {
            return `element-plus/lib/theme-chalk/${name}.css`
          },
          resolveComponent: (name) => {
            return `element-plus/es/${name}`
          },
        },
      ],
    }),
  ],
  // other setting...
}
複製代碼

Element Plus 針不戳!大工搞成,讓咱們愉快的開發吧~ios

如何在 Chrome Extension 攔截請求呢?🖐

1行代碼足以

background.ts 寫上這麼一行代碼:git

chrome.webRequest.onBeforeRequest.addListener(
    handlerRequest,
    {
      urls: ['<all_urls>'],
    },
    // 定義獲取哪些權限
    ['blocking', 'requestBody', 'extraHeaders']
)
複製代碼

handlerRequest 裏咱們能夠拿到 details 參數,根據這個函數返回值的不一樣,Chrome 會執行以下操做:github

  • return { redirectUrl: newurl},轉發請求
  • return { cancel: true }abort 請求
// 其類型是 chrome.webRequest.WebRequestDetails
function handlerRequest( details: chrome.webRequest.WebRequestDetails ) {
  // 注意 proxy 和 block 須要你本身定義
  /** * 代理轉發 */
  if (proxy) {
    return {
      redirectUrl: details.url.replace(
        proxy.origin,
        proxy.target
      ),
    }
  }

  /** * 請求攔截 * */
   if (block) {
       return { cancel: true }
   }
}
複製代碼

高手過招,點到爲止;攔截一個請求就是如此的簡單。

加點細節📝

知道原理以後咱們就能夠完善一下咱們總體的插件的需求內容

  • 支持攔截對應的請求,好比發到 www.baidu.com 的請求或比較關鍵的接口
  • 給這個攔截作一個開關,咱們能夠啓用攔截或者關閉攔截,點擊插件圖標彈出攔截開關
  • 對特殊請求作請求監聽,好比埋點請求,而後在新建一個 devtool Pane 記錄咱們的埋點流

評審完這個三個需求咱們來作設計稿,先作插件彈窗的設計稿devtoolPane 的設計稿吧~

🕐 🕑 🕒 🕓

設計稿作好了,上圖

彈窗

用了 Element-plus 來作基本的佈局和表單控件

devtool Pane

參考 Vue Devtool 的面板作了設計

開關的代碼設計

  • 使用 Extension 自帶的 storage,與 localStorage 相似,存儲開關狀態
  • backgroud.js 能夠讀取存儲的值,來個判斷是否攔截
  • 每次激活 Chrome Extension Popup 時候讀取這個 storage,展現出來

代碼以下:

<!-- 我還用 setup 語法糖 -->
<script setup lang="ts"> import { ElIcon, ElForm, ElFormItem, ElInput, ElSwitch, } from 'element-plus' import { ref, watch } from 'vue' /** * 存儲狀態 */ function saveCurrentStatus( type: string, value: boolean | string | Array<any> ) { // eslint-disable-next-line no-undef chrome.storage?.sync?.set({ [type]: value }, () => { console.log('設置成功') }) } // 定義開關 const blocking = ref(false) // 利用 Vue 3 的 watch,在每次值變化時,存儲狀態 watch([blocking], () => { saveCurrentStatus('blocking', blocking.value) }) // 每次組件初始化時取得開關狀態,setup 至關於 created 生命週期 const initStatus = () => { const storage = chrome.storage?.sync storage?.get('blocking', (data) => { blocking.value = data.blocking || false }) } initStatus() <script> <template> <el-form> <el-form-item label="攔截" size="mini"> <el-switch v-model="blocking" active-color="#2F86F6" /> </el-form-item> </el-form> </template> 複製代碼

background.js 監聽 storage 變化

/** * 監聽 storage 的變化 */
chrome.storage.onChanged.addListener((changes)=> {
    console.log(changes)
})
複製代碼

devtoolPane 的代碼設計

這裏關鍵點在於 background.js 和 devtoolPane 之間的通訊問題,以爲也很簡單
PostMessage 便可

  • devtoolPane 建立鏈接,發送消息
const backgroundPageConnection = chrome.runtime?.connect({
    name: 'devtool-page',
})
backgroundPageConnection?.postMessage({
    name: 'init',
    tabId: chrome.devtools.inspectedWindow.tabId, // 當前 devtoolPane tabId
})
複製代碼
  • background.js 接口消息,拿到這個 Pane
let devtool = null
const connections: Record<string, any> = {}

chrome.runtime.onConnect.addListener((port) => {
    port.onMessage.addListener(message => {
        if (message.name === 'init') {
            connections[message.tabId] = port
            devtool = port
        }
    })
})
// 而後就能夠 使用 devtool 來往 devtoolPane 派發消息了
// 這裏咱們把埋點的請求體所有發過去進行解析
function devtoolandler(details: any) {
  devtool && devtool.postMessage(details)
}
複製代碼

構建與發佈📦

上文提到,每次構建時候須要把 manifest.json 版本號 +1,同時拷貝過去,來看看我是怎麼作的~

  • 拷貝,使用 rollup-plugin-copy ,每次構建結束以後拷貝文件到 dist,很快啊~

vite.config.ts 配置以下

export default defineConfig{
  // other setting...
  plugins: [
    copy({
      verbose: true,
      hook: 'writeBundle',
      targets: [
        {
          src: 'manifest.json',
          dest: 'dist',
        },
      ],
    }),
  ],
  // other setting...
}
複製代碼
  • 版本號自動加1

    • 使用 node-semver 升級版本號
    • 使用 sed 命令修改文件,node fs 也行
  • 構建完了以後要 壓縮整個 dist,必需要壓縮才能發佈到 Chrome Extension Store

綜上,得出咱們的發佈腳本

#!/usr/bin/env zx

const semverInc = require('semver/functions/inc')

let manifest = require('../manifest.json')
console.log(
  chalk.yellow.bold(`Current verion: ${manifest.version}`)
)

let types = ['patch', 'major', 'minor']
let type = await question(
  chalk.cyan(
    'Release type? Press Tab twice for suggestion \n'
  ),
  {
    choices: types,
  }
)
let version = ''
if (type !== '' || types.includes(type)) {
  version = semverInc(manifest.version, type)
  console.log(
    chalk.green.bold(`Release verion? ${version}`)
  )
  // 使用 sed 命令修改 version
  $`sed -i '' s/${manifest.version}/${version}/g manifest.json`
} else {
  await $`exit 1`
}

// 構建
await $`yarn build`

// git
await $`git add .`
await $`git commit -m 'Update version to ${version}'`
await $`git tag v${version}`
await $`git push origin refs/tags/v${version}`
await $`git push origin HEAD:refs/for/master`

// 壓縮
await $`cd dist && zip -r bundle.zip * && mv bundle.zip ../`
複製代碼

而後就能夠去 Chrome Extension Store 發佈了

注意:

  • 開發者須要交納 5美圓,才能夠發佈代碼到Chrome Extension Store
  • 審覈時間不定,疫情期間可能很漫長,由於 Google 每天放假

至此,筆者使用了 Vue3+ Element Plus + TS + Vite 開發出來一個 Chrome Extension,效率很高,代碼很帥,尤大果真沒有騙我。

yyx! yyds!

最後,有什麼想指教筆者的,直接留言吧,歡迎你跟我溝通呀~

👋

相關文章
相關標籤/搜索