写一个获取元素 xpath 的 chrome 插件

·  阅读 327
写一个获取元素 xpath 的 chrome 插件

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

需求来源

上次开每周例会的时候,大家一起嗨皮的聊天,聊到了测试是怎么做测试的。

然后测试就给我们演示了一下,每次编写测试用例,都要通过 chrome 获取元素的 xpath,很麻烦。

我就多嘴问了一句,为什么不自己弄一个自动获取 xpath 然后读成想要格式的浏览器插件呢?

测试:我不会,没研究过。
老板:那你来帮他实现一个吧。***,你把需求理顺了放进 tapd 。
我 :。。。
复制代码

好在之前对 chrome 插件也略有所闻,嘿嘿。

梳理

通过沟通和需求的梳理,简单总结了一下要实现的逻辑:

  • 鼠标双击一个元素的时候,自动读取该元素的 xpath
  • 然后将读取的结果放到另一个页面中展示,以方便用户直接选取复制
  • 成功以后提示用户“auto saved”

为什么要实现 chrome 插件呢?

因为 chrome 插件支持几乎所有的 webkit 内核的浏览器,例如 360、搜狗、QQ等,使用起来非常方便

另外,对于 chrome 插件开发者而言,chrome 做了最大的努力使得插件的开发简单而高效。

ok,开搞。

目录结构

chrome 插件的项目结构十分简单,一个插件目录结构可以包含以下几个文件夹,使目录看起来非常像一个网站目录

├── html
├── js
├── img
├── manifest.json
└── style
复制代码

因此开发者可以自由的将精力和时间放置在真正的代码和逻辑层面,而不必关注额外的事情

但是插件目录没有严格的标准,即便你只有一个manifest.json也是可以的,因为只有这个文件使必须的,其他都是可选择的,比如我要开发的插件十分简单,因此项目结构也做的非常简单。

├── background.html
├── background.js
├── icon.png
├── manifest.json
└── xpath.js
复制代码

大概说一下:

  • manifest.json 是必须的文件,必须放在根目录下,用来记录这个插件相关的信息和配置
  • background.html 常驻页面,生命周期最长,随浏览器打开而打开,随浏览器关闭而关闭,所以把需要一直运行的、启动就运行的、全局的代码放在 background 里面
  • background.js 常驻页面的 js 逻辑
  • icon 插件安装到 chrome 后在浏览器右上角显示的插件图标
  • xpath.js 插件要实现的需求具体的逻辑代码
manifest.json 简介

请注意,manifest.json 是插件的入口文件,因此它是必须的文件,这个文件内配置了许多参数

其中有些必须参数如下:

"manifest_version": 2, // manifest文件的版本,推荐值是2 或者 3

"name": "myplus", // 插件的名称 

"version": "1.0.0", // 插件的版本
复制代码

推荐的参数:

"default_locale": "zh_CN",// 默认语言 

"description": "我的第一个扩展插件", // 插件的描述 

"icons": { "16": "img/icon.png", "48": "img/icon.png", "128": "img/icon.png" }, // 图标

"permissions": [ // 声明插件所需要的权限
    "contextMenus", // 右键菜单 
    "tabs", // 标签 
    "notifications", // 通知 
    "webRequest", // web请求 
    "webRequestBlocking", 
    "storage", // 插件本地存储 
    "http://*/*", // 可以通过executeScript或者insertCSS访问的网站 
    "https://*/*" // 可以通过executeScript或者insertCSS访问的网站
    "cookies", // cookie信息
]

复制代码

其他的参数,我挑选了一些放在这里


"background": { // 常驻的后台JS或后台页面
    // 第一种方式
    "page": "background.html" //通过指定html文件
    // 第二种方式
    "scripts": [ // 通过指定的js文件,PS: 如果指定了这种方式,会自动生成一个 html
      "scripts/background.js",
    ]
},

//browser_action和page_action只能添加一个
"browser_action": { //浏览器级别行为,所有页面均生效
    "default_icon": "images/16x16.png",//图标的图片
    "default_title": "myplus", //鼠标移到图标显示的文字 
    "default_popup": "html/popup.html" //单击图标后弹窗页面
}, 
"page_action":{ //页面级别的行为,只在特定页面下生效 
    "default_icon":{
        "24":"images/custom/24x24.png",
        "38":"images/custom/38x38.png"
    },
    "default_popup": "html/popup.html",
    "default_title":"myplus"
},

"content_scripts": [ // 定义对页面内容进行操作的脚本
    {
      "js":["js/insert.js"], // 多个文件会按顺序加载
      "css": ["css/insert.css"], // 同上
      "matches":["<all_urls>"], // 匹配所有地址
      "run_at": "document_start" // 可选的值还有为"document_end", 默认为 "document_idle"
    }
],

"devtools_page": "devtools.html" // devtools页面入口,只能是HTML文件

复制代码

manifest 参数内容很多很长,而且版本2 和 3 还有些不同,所以这里我就不细说了,详细了解请点击

background.html

这个页面用于展示 xpath 的结果,所有的 xpath 结果都会以换行的方式展示在#app这个div中

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #app{
      margin: 0;padding:0;
      font-size: 12px;
      background-color: #fff;
      color: silver;
      padding: 10px;
    }
  </style>
</head>
<body>
  <div id=app></div>
  <script src="background.js"></script>
</body>
</html>
复制代码

下面开始进入正题了

代码

获取元素的 xpath

首先,我们要完成获取元素的 xpath 的逻辑,这部分是我们插件最主要的逻辑,放在 xpath.js 中。

那么什么是元素的 xpath 呢?我们打开浏览器的开发者模式,选中一个元素,鼠标右击,会弹出一个对话框如下:

WeChataa3920f02db3fdfc8f265aa83138da75.png

这里我们能找到一个正常的 xpath 是这样的:

//*[@id="juejin-web-editor"]/div[2]/div/header/div[2]/button

或者一个全路径的 xpath 是这样的:

/html/body/div/div[2]/div/header/div[1]

所以 xpath 本质上就是遍历某个元素的父节点和兄弟节点,然后拼成节点层级的路径,这个元素在兄弟节点中的位置,用[n]来表示

所以我们先来写一个查找元素在兄弟元素位置中的函数,这个函数返回元素在兄弟中的位置 n,注意的 n 只的是相同的标签,比如

<header class="header editor-header">
    <div class="left-box"></div>
    <input class="title-input">
    <div class="right-box"></div>
</header>
复制代码

在上面这段代码中

  • .left-box 的 xpath 是/header/div[1]
  • .title-input 的 xpath 是/header/input
  • .right-box 的 xpath 是/header/div[2]

所以我们得出获取 n 的方法:

function elementsShareFamily(pre, sib) {
    // 判断是否是相同的 tagname
    return (pre.tagName === sib.tagName && (!pre.id || pre.id === sib.id));
}

function getElementIndex(el){
    var sib, index = 1; // 默认为1
    for (sib = el.previousSibling; sib; sib = sib.previousSibling) {
      // 前面有兄弟元素且 tagname 相同
      if (sib.nodeType === Node.ELEMENT_NODE && elementsShareFamily(el, sib)) {
        index++;
      }
    }
    if (index > 1) {
      return index;
    }
    for (sib = el.nextSibling; sib; sib = sib.nextSibling) {
      // 后面有兄弟元素且 tagname 相同
      if (sib.nodeType === Node.ELEMENT_NODE && elementsShareFamily(el, sib)) {
        return 1
      }
    }
    return 0;
}
复制代码

我们已经成功拿到了 n,下面要获取元素的祖先元素以及祖先元素的 n:

function makeQueryForElement(el) {
    var query = '';
    for (; el && el.nodeType === Node.ELEMENT_NODE; el = el.parentNode) {
      var component = el.tagName.toLowerCase();
      var index = getElementIndex(el);
      if (index >= 1) {
        component += '[' + index + ']';
      }
      query = '/' + component + query;
    }
    return query;
};
复制代码
将 xpath 传递给 background

上面的 query 就是/header/div[1]这种形式的 xpath,到这里我们已经实现了获取元素的 xpath,接下来要做的就是把结果传递到background.html中,并在当前页面给用户一个提示auto saved

这里跨页面通信,我们使用的是谷歌拓展的一个 API,叫做 extension,这个 API 有用于发送和接收信息的方法:

  • chrome.extension.sendRequest(obj, callBack)
  • chrome.extension.onRequest.addListener(function)

我们获取了完整的 xpath 后,用sendRequest方法将它发送给background.js

// xpath.js
(function (doc) {
  function elementsShareFamily...
  function getElementIndex...
  function makeQueryForElement...
  
  doc.addEventListener('dblclick', (e) => {
    var target = e.target,
        data = makeQueryForElement(target) + '\r\n';
    
    chrome.extension.sendRequest({ data }, function (response) {
      // 这里是通信返回的数据
      alert(response.farewell);
    });
  })
})(document)
复制代码
接受 xpath 数据并显示在页面

发送以后,我们需要在background.js中接受这个数据

chrome.extension.onRequest.addListener(
  function (request, sender, sendResponse) {
    // 将数据写入 #app 中
    document.getElementById('app').innerText += request.data
    sendResponse({ farewell: "auto saved" });
  });

// 在调用插件的时候在浏览器中打开 background.html
chrome.browserAction.onClicked.addListener(
  function () {
    if (chrome.runtime.openOptionsPage) {
      chrome.runtime.openOptionsPage();
    } else {
      window.open(chrome.runtime.getURL('background.html'));
    }
  }
);
复制代码

到这里我们的主要逻辑就写完了,然后来补充下manifest.json

manifest.json

manifest.json写好必须的数据就可以了

{
  "name": "xpath2.0",
  "description": "Get the current element xpath",
  "version": "1.0",
  "permissions": [
    "activeTab","tabs",
    "http://*/*",
    "https://*/*"
  ],
  "browser_action": {
      "default_title": "Get the current element xpath.",
      "default_icon": "icon.png"
  },
  "background": {
    "scripts": [
      "background.js"
    ]
  },
  "options_page": "background.html",
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "xpath.js"
      ]
    }
  ],
  "manifest_version": 2
}
复制代码

打包和发布

写好的插件可以直接按照文件夹的形式拖动到chrome://extensions/

image.png

安装以后就会在扩展管理中列出来

image.png

如果想发布的话,需要到https://chrome.google.com/webstore/developer/dashboard/上,访问这里需要登录谷歌账号

image.png

然后缴纳 5 美元的注册费

image.png

成功注册为开发者之后,就可以上传打包后的.zip文件包了

image.png

上传之后可以在“开发者信息中心”查看,我就不多赘述了。

分类:
前端