前言

Etherscan 是以太坊上应用最广泛的区块链浏览器,日常工作中经常需要使用到它。在实际使用中,经常需要在不同的地址交易信息之间来回切换,有时候会忘记了哪个地址是什么的地址。于是乎某个周五的日常工(mo)作(yu)中和同事聊到了这个,在网上搜索也没有看见有类似的插件(有也当没看见,哈哈哈哈~~~),于是突发奇想——要是做个插件,让浏览器在加载页面的时候就将自己自定义的标签渲染出来,岂不美哉!


下面就是实现的效果:

image-20200714095712858

PS:由于本人CSS、Html等知识严重缺乏,没有对那些按钮啥的进行美化,全部都是用的默认样式,难看是难看了一点,能用就行~~~

概述

要完成一个自定义标签插件的实现,最开始面临的问题主要是两个方面:一是插件的编写;二是自定义标签数据存储在什么地方。

过程

首先是第一个问题,插件的编写。百度、谷歌一搜,一大堆的编写教程,这个倒不是什么大问题。

主要参考了一下文章:

从零开始编写一个chrome插件

一篇文章教你顺利入门和开发chrome扩展程序(插件)

chrome浏览器网页通过插件形式,自动调用js脚本

【干货】Chrome插件(扩展)开发全攻略

chrome官方插件开发文档

最基础的一个插件是由两个部分组成的:一个是manifest文件,它用于描述 Chrome 插件的源数据,配置信息等;二是js文件,js不用多解释吧,你要实现的功能基本都在里面写。

manifest.json文件基础内容如下所示:

 {
    "name": "Hello Extensions",
    "description" : "Hello world Extension",
    "version": "1.0",
    "manifest_version": 2,
    "icons":{
        "16": "img/icon.png",
        "48": "img/icon.png",
        "128": "img/icon.png"
      },
     "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "scripts/contentscript.js"
      ],
      "all_frames": false
    }
  ]
 }

name:必填项,插件的名字。

description:插件的描述,132个字符的限制。

version:插件的版本号,打包完成后用于判断插件是否需要更新。

manifest_version :必填项,指定插件使用的清单文件规范的版本,chrome官方文档使用的是2。

Content Scripts:运行在Web页面的上下文的JavaScript文件。通过标准的DOM,Content Scripts 可以操作(读取并修改)浏览器当前访问的Web页面的内容。

icons:插件的图标,可以用在 Chrome 商店展示(128 * 128) | 插件管理界面 (48 * 48) | 扩展页图标 (16 * 16) 最好是 png 格式。

mathches:选择插件默认在什么网站上生效。

js:引入自己写js文件。

all_frames:控制JS文件是否在匹配的Web页面中的所有框架中运行。默认false表示只在顶层框架中运行。


然后是第二个问题,自定义标签的数据存储在什么地方。

最开始的想的是能不能直接读取本地文件然后进行数据的更新,然鹅chrome的安全策略给了我当头一棒。

在js中尝试读取本地文件时,控制台中报了如下错误:

Not allowed to load local resource: file// XXXX 

然后百度、谷歌一顿搜索:

解决方法有这样的:
    修改快捷方式的属性中的目标为下面这样:
    "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --args --disable-web-security --allow-file-access-from-files
    
有这样的:
    安装LocalLinks插件

上面两种方式我都有试过,但是!不知道是不是我自己的原因,问题并没有得到解决,chrome还是一样的报错!

于是乎继续搜索:

解決chrome報Not allowed to load local resource錯誤的方法文中提到了Tomcat下可以使用目录映射的方式,可惜的是我没有用Tomcat呀!

解决Chrome浏览器Not allowed to load local resource这篇文章提到了使用搭建本地服务器的形式来解决这个问题,我最初的实现方式也是这样。

实现本地服务器的方式有很多,我以前用的主要是使用phpstudy以及nodejs。


phpstudy的使用方式,百度一堆,这里就不在赘述。

安装完成后将数据json文件放在网站根目录后,再次尝试在网站上访问,然后chrome报了这种类型的错:

Mixed Content: The page at 'https://googlesamples.github.io/web-fundamentals/fundamentals/security/prevent-mixed-content/simple-example.html' was loaded over HTTPS, but requested an insecure script 'http://googlesamples.github.io/web-fundamentals/fundamentals/security/prevent-mixed-content/simple-example.js'. This request has been blocked; the content must be served over HTTPS.

大概意思就是不能在https网站中使用http请求来访问资源。

又双叒叕是一通百度,最后发现在phpstudy中可以切换为https:

image-20200714111524304

只不过需要一个SSL证书,这个倒不是什么大问题,我的网站之前就有证书,直接拿下来用,改下host就可以了。

image-20200714111702158

改好之后便可以通过https://localhost来访问本地服务器中的文件了!

image-20200714135033177

在js中使用Jquery获取数据:

$.getJSON("https://xxxxx.cn:18081",function(result){
    for(var key in result){
        tag[key.toLowerCase()] = result[key]
    }
});

然后是nodejs搭建本地服务器的方式,使用了express框架。

参考文章:

使用Express搭建https服务器

全部代码如下所示:

var app = require('express')();
var fs = require('fs');
var http = require('http');
var https = require('https');
var privateKey  = fs.readFileSync("./https/https.key", 'utf8');
var certificate = fs.readFileSync("./https/https.crt", 'utf8');
var credentials = {key: privateKey, cert: certificate};

var httpServer = http.createServer(app);
var httpsServer = https.createServer(credentials, app);
var PORT = 18080;
var SSLPORT = 18081;


//设置允许跨域
app.all('*',function(req,res,next){
  res.header("Access-Control-Allow-Origin","*");
  res.header("Access-Control-Allow-Methods","PUT,GET,POST,DELETE,OPTIONS");
  res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
  next();
});

httpServer.listen(PORT, function() {
    console.log('HTTP Server is running on: http://localhost:%s', PORT);
});
httpsServer.listen(SSLPORT, function() {
    console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT);
});

// Welcome
app.get('/', function(req, res) {
    if(req.protocol === 'https') {
        var file="./addr_tag.json";
        var result=JSON.parse(fs.readFileSync(file));
        console.log(result)
        res.send(result)
    }
    else {
        res.status(200).send('Welcome!');
    }
});

在采用nodejs方式搭建时,由于有了phpstudy的失败经历,nodejs直接选择了https形式的搭建,过程中没有在遇到其他的问题,成功实现了数据的获取。

image-20200714115438099


做完之后,不禁想到,搭建服务器的方式有点太复杂了,https证书也麻烦,于是又想了一下。想到了直接在插件的js文件中保存数据即可,直接将数据存入addr_tag.js文件中,在manifest文件中导入后,直接在contentscript.js文件中使用即可,以后每次修改数据直接在add_tag文件中修改即可,不需要经历繁琐的步骤去搭建服务器了。

image-20200714113039494


后来第二天上班的时候,把我做好的这个两个版本给同事看了之后,他问我为啥不用localstorage来存储数据???Excuse me???为啥我把这个给忘了!

于是我又改了改代码,直接使用localstorage.getItem来获取里面的数据,然后在加载到页面上。

var tokenTag = document.getElementsByClassName('hash-tag text-truncate');
for(var x =0;x<tokenTag.length;x++){
    if(localStorage.getItem((ethTag[x].innerText.toLowerCase()))!=undefined){
    tokenTag[x].innerText =	'Local:'+localStorage.getItem(ethTag[x].innerText.toLowerCase());
    }
}

image-20200714120032496

虽然能够避免繁琐的前置步骤了,但在使用中,同


事又提出了新的”需求“——每次更新数据都要打开F12,太麻烦了。

参考文章:

js动态往div里添加按钮的两种方式

JS打开选择本地文件的对话框

javascript实现生成并下载txt文件

于是有了下面的代码:

    var MyDiv =document.getElementById("logoAndNav");

    var addr_data =document.createElement('input');
    addr_data.setAttribute('type', 'text');//输入框的类型
    addr_data.setAttribute("placeholder", "地址");
    addr_data.setAttribute('id','addr_data')
    addr_data.style.width = "16%";
    MyDiv.appendChild(addr_data);

    var tag_data =document.createElement('input');
    tag_data.setAttribute('type', 'text');//输入框的类型
    tag_data.setAttribute("placeholder", "标签名");
    tag_data.setAttribute('id','tag_data')
    tag_data.style.width = "16%";
    MyDiv.appendChild(tag_data);

       var button = document.createElement("input");
    button.setAttribute("type", "button");
    button.setAttribute("value", "添加/修改标签");
    button.style.width = "17%";
    button.setAttribute("onclick", 
        "javascript:\
            var addr_data = document.getElementById('addr_data'); \
            var tag_data = document.getElementById('tag_data'); \
            if(addr_data.value!=''&&tag_data.value!=''){\
                localStorage.setItem(addr_data.value.toLowerCase(),tag_data.value);\
                document.location.reload();\
            }\
            else{\
                alert(\"请输入数据哦!\")\
            }\
        "
        )
    MyDiv.appendChild(button);

    var button2 = document.createElement("input");
    button2.setAttribute("type", "button");
    button2.setAttribute("value", "删除标签");
    button2.style.width = "17%";
    button2.setAttribute("onclick", 
        "javascript:\
            var addr_data = document.getElementById('addr_data'); \
            if(addr_data.value!=''){\
                localStorage.removeItem(addr_data.value.toLowerCase());\
                document.location.reload();\
            }\
            else{\
                alert(\"请输入要删除的地址哦!\")\
            }\
        "
        )
    MyDiv.appendChild(button2);

    var button3 = document.createElement("input");
    button3.setAttribute("type", "button");
    button3.setAttribute("value", "导出标签");
    button3.style.width = "17%";
    button3.setAttribute("onclick", 
        "javascript:\
            var output_data={};\
            for(var i=0;i<localStorage.length;i++){\
                output_data[localStorage.key(i)] = localStorage.getItem(localStorage.key(i));\
            }\
            function download(filename, text) {\
                var pom = document.createElement('a');\
                pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));\
                pom.setAttribute('download', filename);\
                if (document.createEvent) {\
                    var event = document.createEvent('MouseEvents');\
                    event.initEvent('click', true, true);\
                    pom.dispatchEvent(event)\
                } else {\
                    pom.click();\
                }\
            }\
            download('tag_data.txt',JSON.stringify(output_data));\
        "
        )
    MyDiv.appendChild(button3);


    var inputObj=document.createElement('input')
    inputObj.setAttribute('id','upload_data');
    inputObj.setAttribute('type','file');
    inputObj.setAttribute("style",'visibility:hidden');
    document.body.appendChild(inputObj);

    var button4 = document.createElement("input");
    button4.setAttribute("type", "button");
    button4.setAttribute("value", "批量导入标签");
    button4.style.width = "17%";
    button4.setAttribute("onclick", 
        "javascript:\
            var input =document.getElementById('upload_data');\
            input.click();\
            input.addEventListener('change',()=>{\
                var reader = new FileReader();\
                reader.readAsText(input.files[0],'utf8');\
                reader.onload = ()=>{\
                  	var input_data = JSON.parse(reader.result);\
                    for(var key in input_data){\
                        localStorage.setItem(key,input_data[key]);\
                    }\
                    document.location.reload();\
                }\
            }, false);	\
        ")
    MyDiv.appendChild(button4);

上面代码实现了在etherscan的页面上添加了两个输入框和四个按钮,分别是地址、标签的输入以及添加/修改标签按钮、删除标签按钮、导出标签按钮和批量导入标签按钮。

最终效果:

最终效果


2020.07.17更新


上个版本的插件已经能够满足日常的基本使用了,但是由于localStorage的5M大小的限制,使得标签的数量被限制在了10W以下(利用假数据简单测试结果是7W多条),为了能够储存更多的数据,不得不放弃localstorage从而寻找新的思路。

查阅资料可以使用chrome.storage,通过在赋予unlimitedStorage权限,在本地存储区储存的数据量大小不受限制。但在实际使用过程中遇到了chrome.storage未定义的问题,有可能是我的使用方式存在问题,查询大量资料之后无果,遂放弃。

image-20200717105731138

在Storage中,一共有五个储存数据的地方,如上图所示,详细介绍如下表所示:

sessionStorage sessionStorage是个全局对象,它维护着在页面会话(page session)期间有效的存储空间。只要浏览器开着,页面会话周期就会一直持续。当页面重新载入(reload)或者被恢复(restores)时,页面会话也是一直存在的。每在新标签或者新窗口中打开一个新页面,都会初始化一个新的会话。
localStorage localStorage与sessionStorage相同,但应用了相同的规则,但它是持久性的。LocalStorage 在 2.5MB 到 10MB 之间(各家浏览器不同),而且不提供搜索功能,不能建立自定义的索引。
IndexedDB 通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
WebSQL WebSQL是一个在浏览器客户端的结构关系数据库,这是浏览器内的本地RDBMS(关系型数据库系统)
cookies 用于保存登陆信息

localStorage和sessionStorage都有着大小的限制,且存储的数据类型只能是键值对形式。

IndexedDB_API中推荐8种使用IndexedDB的更方便的方式:

localForage 一个简单名称的Polyfill:客户端数据存储的值语法,它在后台使用IndexedDB,但在不支持IndexedDB的浏览器中回退到WebSQL或localStorage。
Dexie.js IndexedDB的包装器,通过简单的语法,可以更快地进行代码开发。
ZangoDB 类似MongoDB的IndexedDB接口,支持MongoDB的大多数熟悉的过滤,投影,排序,更新和聚合功能。
JsStore 一个带有SQL语法的IndexedDB包装器。
MiniMongo 由localstorage支持的客户端内存中的mongodb,通过http进行服务器同步。MeteorJS使用MiniMongo。
PouchDB 使用IndexedDB在浏览器中实现CouchDB的客户端。
idb 一个微小的(〜1.15k)库,主要反映了IndexedDB的API,但小的改进,使一个很大的区别的可用性。
idb-keyval 使用IndexedDB实现的超简单小(~600B)基于Promise的键值存储。

本次我采用的是localForage。

引入localforage.js文件文件后便可以利用localforage中定义的方法来进行indexedDB的使用了,而不需要去写复杂的语句。

插件中使用localforage很顺利,但在网页上使用出现了问题:提示localforage未定义。

image-20200717112751028

查阅资料后,在【干货】Chrome插件(扩展)开发全攻略这篇文章中看到,文中提到content-script只能操作DOM,但DOM却不能调用它,也就是说DOM没办法直接使用插件内部的js文件,只有通过向页面注入代码后才能调用,注入代码如下所示:

// 向页面注入JS
function injectCustomJs(jsPath)
{
    jsPath = jsPath || 'js/inject.js';
    var temp = document.createElement('script');
    temp.setAttribute('type', 'text/javascript');
    // 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
    temp.src = chrome.extension.getURL(jsPath);
    temp.onload = function()
    {
        // 放在页面不好看,执行完后移除掉
        this.parentNode.removeChild(this);
    };
    document.head.appendChild(temp);
}

此外,还需要在配置文件中添加如下配置,不然会报 Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.

// 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
"web_accessible_resources": ["js/inject.js"],

向页面注入localforage.js后发现,能够使用localforage了:

image-20200717113439764

localforage的使用可以参考这个网站:http://localforage.docschina.org/

localforage简单测试:

window.onload=function(){
    localforage.setItem('key', 'value').then(
        localforage.getItem('key', function(err, value) {
    // 当离线仓库中的值被载入时,此处代码运行
            console.log(value);
        })
    )
}

导入插件后,刷新网页可以发现,成功插入了键值对key:value。

image-20200717114624467

image-20200717114611209


此外,如果想要打包扩展程序后生成crx文件后在其他浏览器中导入,如果没有在chrome商店中上传并通过审核,会遇到如下问题:

image-20200717113817539

解决方案有两个:

第一个当然是花费5刀注册为chrome插件开发者,然后上传自己的插件,不过这种方式耗时很长,短期可以选择另一种方式;

第二个是修改本地组策略,在已解决!该扩展程序未列在 Chrome 网上应用店中,并可能是在您不知情的情况下添加的这篇文章中介绍很详细。


最后

完结撒花★,°:.☆( ̄▽ ̄)/$:.°★ 。,虽然本次尝试制作的插件功能比较简单,但却对我来说有实际的价值,大佬们不喜勿喷。

PS:经过了近一年的审核,插件上架了啊哈哈哈哈~

chrome插件商店地址:Custom Address Tag

image-20220522232205825