Akawa

ETY001的博客

年前重新开发了我的浏览器扩展“温故知新”。“温故知新”这个扩展的目的就是在你每次打开新网页或者标签页的时候,能够随机从你的收藏夹/书签栏里抽一个出来展示。这样利用了零碎的时间,就把你的书签栏整理了,对于书签数量巨大的用户来说,非常赞。

这次重新开发,除了解决老版本中的问题,提升操作体验,增加用户要求的新功能外,最重要的就是优化了数据统计功能。

之前由于对 Google Analytics 不熟悉,所以在事件埋点方面,设置的参照坐标很乱,导致在面板看到的数据意义不大。

这次优化后,只关注几个重要指标,比如获取书签操作、删除书签操作等。下面是2020年2月20日到2月26日的数据报表截图:

photo_2020-02-27_12-08-23.jpg

图中 getbookmark_from_mini 和 getbookmark_from_full 就是获取书签的操作,remove_bookmark 就是删除书签的操作。“事件总数”就是事件发生的次数,“唯一身份事件数”可以看做是用户数。

可以看到在过去的7天,“温故知新”帮助了 285 + 127 位用户,重新回顾了 8888 + 448 个书签,并且有 38 个用户,删除了 95 个对他们来说没用的书签。

我觉得这应该算是一个让我感到振奋的事情!

在这个版本之前,我只能看到有多少用户在用我开发的扩展,而并不能了解到我可以帮助到他们多少?现在有了这些新的指标后,我能够看到我对于新版本付出的努力没有白费。

抛开数字统计,我觉得还有一个很重要的点,那就是我在开发工具时的思路也发生了变化。

在过去,我开发的目的就是玩或者解决自己的问题,但是现在我觉得开发出来的东西帮助到更多人,才是有意思的事情。尽管很多开发者,一起步的时候,就做到了这一点,不过我现在能意识到这一点也不算晚。

总结一下,数字可以帮助开发者量化自己的劳动成果。开发者需要收集各种信息,来看看可以帮助用户再做些什么。

PS:点击【这里】,可以下载安装我的这个扩展,也可以去吐槽这个扩展。

最近为了把新版的**温故知新**扩展上传到各个浏览器,真的是操碎了心了。这篇文章就来说说在通过火狐审核的时候的遇到的最棘手的问题。

由于我的扩展使用了 webpack,代码 build 后,没有可读性,所以火狐要求需要上传源代码,然后会进行审查。审查的步骤就是根据我提供的源代码和编译方法,审核人员编译一次,然后把编译后的代码打包,最后与我上传的压缩包比对,看是否一致。负责审查我的那位审查员,用的是 Beyond Compare 这个软件来检查两个压缩包是否一样。

最开始并没有注意到这里,审查员说他的编译结果跟我的不一样,我以为是我的开发环境对打包环境有污染。所以再次送审的时候,我是直接从 github 上下载的源码,重新来了一遍。结果审查结果依然是不一致。

于是我在本地进行测试,发现即使我在自己本地,两次编译后,用zip打包后的文件 md5 sum 都不一样。

WTF!

经过各种查询,最后确定应该是文件的 metainfo 导致的,看了下 zip 命令的参数,发现 -X 可以移除 metainfo 来打包,于是使用命令 zip -r -X extension.zip dist/ 打包,对编译后的 dist 目录打包了两次,对比了下,发现终于特么的一致了。

既然一致了,就赶紧更新审核包和审核文案吧。就在更新过程中,我突然想到,我应该按照审核员的步骤再操作一遍,于是我又从下载源码开始来了一遍,最后惊奇的发现,这一遍生成的压缩包和上一遍的压缩包 md5 sum 又特么的不一样啊!!!!

经过各种试验后,排除了大部分的可能,最后猜测应该是文件日期导致的,毕竟前后两遍编译,经过各种排除后,只有时间这个变量了。

于是我用 find dist | xargs touch -mt 202002110000 先对编译结果强制修改文件的时间,然后再打压缩包,然后再从源码来一遍后,也修改成这个时间,再打包,最后对比两个压缩包的 md5 sum,终于一样了!!!!

最后提交审核,两天后,终于过审了!!!!!

对于书签巨多的人来说,我这个插件值得你去体验下~~~

Chrome版地址https://chrome.google.com/webstore/detail/review-bookmarks/oacajkekkegmjcnccaeijghfodogjnom

Firefox版地址https://addons.mozilla.org/zh-CN/firefox/addon/review-bookmarks/

Microsoft Edge版地址https://microsoftedge.microsoft.com/addons/detail/pibjmfgfgamgohlaehhhbdkjboaopjkj

反馈https://creatorsdaily.com/9999e88d-0b00-46dc-8ff1-e1d311695324?utm_source=vote

欢迎使用,欢迎点赞!!

最近在做浏览器扩展《温故知新》的新版本。其中,最让我头疼的就是用 Google Analytics 统计信息了。

Google 官方提供的 SDK 使用的话,需要外部引入 SDK,并且配置 CSP,而 Firefox 浏览器不允许配置 CSP

无奈,只能自己去写一个简单的封装了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
export class GA {
constructor(ua, cid, debug = false) {
this.ua = ua;
this.cid = cid; // client id
this.gaApi = debug ? 'https://www.google-analytics.com/debug/collect' : 'https://www.google-analytics.com/collect';
this.version = '1';
}
ga(t, ...items) {
let payload = `v=${this.version}&tid=${this.ua}&cid=${this.cid}`;
let params = [];
switch (t) {
case 'pageview': // Pageview hit type
// dh -- Document hostname
// dp -- Page
// dt -- Title
params = ['dh', 'dp', 'dt'];
break;
case 'event':
// ec -- Event Category. Required
// ea -- Event Action. Required
// el -- Event label.
// ev -- Event value.
params = ['ec', 'ea', 'el', 'ev'];
}
if (params === []) return;
payload = `${payload}&t=${t}`;
items.forEach((v, i) => {
payload = `${payload}&${params[i]}=${encodeURIComponent(v)}`;
});
const request = new XMLHttpRequest();
request.open('POST', this.gaApi, true);
request.send(payload);
}
}

const uid = 'xxxx-xxxx-xxx-xxx';
const debug = false;
const gaID = 'UA-xxxxxx-x';
const gaObj = new GA(gaID, uid, debug);
function sendEvent(eventCategory, eventAction, eventLabel = '', eventValue = 1) {
if (store.getters.config.ga === false) return;
gaObj.ga('event', eventCategory, eventAction, eventLabel, eventValue);
}
// dh -- Document hostname, dp -- Page, dt -- Title
function sendPageview(dp, dh = '', dt = '') {
if (store.getters.config.ga === false) return;
gaObj.ga('pageview', dh, dp, dt);
}

这就是我根据官方文档简单写的封装。

这里面需要注意几个问题。

一个是正式环境的地址是 /collect 而测试地址是 /debug/collect。这个在之前的时候,没有注意到还有测试地址,所以绕了弯路,也没有发现提交的参数错误。

另外一个就是在 event 类型中,Event value 必须是整型。

还有个小技巧,就是调试的时候,可以切换到“实时”选项卡,在那下面可以看到发送到 /collect 的实时数据。

我的Chrome扩展重构进度已经60%了,目前又遇到了新问题。

这个问题的缘由得慢慢说来。

我的扩展由于需要用自定义的页面替换新标签页。在我的早期版本的实现是这样的:

1
2
3
4
5
chrome.tabs.onCreated.addListener(function(tab){
if(Mini.get_status()=='off'&&(tab.url=="chrome://newtab/"||tab.url=="chrome://newtab")){
chrome.tabs.update(tab.id, {url:chrome.runtime.getURL('show.html')});
}
});

也就是新建标签页的时候,用我自己的页面 URL 替换掉 Chrome 默认页面。

后来,Chrome 在某个版本后,新建标签页后,地址栏是空的,也就是 tab.url 是没有值,这就导致我之前的代码就失效了。

查看文档,发现正确的方法是在 manifest.json 中增加 chrome_url_overrides 这个选项,具体可以查看这里

这就引入了新的问题。

我的扩展里原来是有一个开关的,这个开关可以控制新标签页是否显示我的自定义页面,也就是上面代码里的那个 Mini.get_status() == 'off'

现在用了 manifest.json 直接替换掉了默认页,而 manifest.json 是无法在扩展里修改的,同时 Chrome 没有提供相关的 API,那么如何实现这个开关就很尴尬了。

这个 BUG 我一直放着没有处理。

这次重构进行到了自定义页面重构后,这个问题就必须要解决了。

最终的解决方案是,监听页面更新,如果 URL 符合 chrome://newtab/,那么就替换成访问 chrome-search://local-ntp/local-ntp.html,代码如下:

1
2
3
4
5
6
7
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (tab.url === 'chrome://newtab/') {
if (store.getters.config.mini === false) {
chrome.tabs.update(tabId, { url: 'chrome-search://local-ntp/local-ntp.html' });
}
}
});

之所以用 chrome.tabs.onUpdated,有两个原因:一个是因为 onCreated 拿不到 URL,一个是在当前标签页,如果点击主页按钮的时候,也会进入 chrome://newtab/

这次重构我的Chrome扩展由于引入了很多新的东西,所以遇到的小问题还是挺多的。

比如在 content script 模式中,我的UI样式会被某些网站的 CSS 给影响到,以至于我的插件的 UI 显示不是预期。

在开发文档中,对于 manifest.json 中的 content_scriptsCSS 描述是: Optional. The list of CSS files to be injected into matching pages. These are injected in the order they appear in this array, before any DOM is constructed or displayed for the page.

这就是为啥 Tab 中网页可能会影响我的插件 UI 的原因。

因为我的插件的 CSS 早于 Tab 中网页的载入,这样 Tab 中网页里的 CSS 里面如果有重名的 Class 或者直接对 HTML 标签加样式,就会影响到我。

之前的版本,因为我是自己手写样式,且都用的 div 标签,所以很大程度上避免了冲突。

而这次重构引入了饿了么的 UI,所以增大了冲突的概率。我在 content script 中用的是 notification 这个组件,里面涉及到了 <h2 /><i />这两个标签。有些网站是直接对这两个标签先进行样式全局设置的,这就导致在访问这些网站的时候,我的 UI 会受到影响。

目前我采用的解决方案是,把饿了么的 UI 库克隆到我的账号下,然后自己修改里面的代码,把 <h2 /><i /> 替换成别的标签,重新打包,引入我自己定义的库后,问题得到解决。

《Chrome扩展中使用Vuejs》 文章中,我们提到过 @font-face 我是用的 CDN 的方式来搞定的。但是用 CDN 的缺点就是网络不畅的时候,扩展中使用 @font-face 的地方显示就是个方块了。

为了解决这个问题,我搜索了下,找到了解决方案。

这里面有几个要点,一个是字体文件可以在任意页面访问到,一个是如何获取到扩展ID。

正常情况下,扩展的资源文件是与所有网页隔离开的,扩展相当于是在一个沙盒里面运行,如果想要在 Content Scripts 模式下,让 JS 或者 CSS 文件访问到扩展中的资源文件,那么就需要在 manifest.json 中增加配置项 web_accessible_resources

1
"web_accessible_resources": ["fonts/*"]

就像上面这样增加配置后,我们就可以在 Content Scripts 模式下访问到 fonts/ 目录下的所有资源文件了。

再来说下第二个问题。

我们知道扩展的地址结构是 chrome://[扩展ID],那么我们的 CSS 文件中的 @font-face 的路径中的扩展ID该怎么获取呢?

通过看文档,找到了预设值 __MSG_@@extension_id__,这是出处:https://developer.chrome.com/extensions/i18n#overview-predefined

那么我们去修改下 element-variables.scss 中的 font-path 像这样:

1
$--font-path: 'chrome-extension://__MSG_@@extension_id__/fonts';

这样,最关键的两个问题就解决了,最后只要在 webpack.config.js 中需要复制的操作里,增加复制字体文件的操作即可,就像这样:

1
2
3
4
5
new CopyPlugin([
......
{ from: '../node_modules/element-ui/lib/theme-chalk/fonts', to: 'fonts' },
......
]),

通过这样设置后,我们通过扩展插入到网页中的 CSS 文件就能直接访问到扩展中的资源文件了。

开发的扩展四年多了,这两天终于突破了500用户数。很早就想要再进行开发了,但是无奈由于之前代码写的比较乱,并且用的 jQuery 去操作,很多东西开发起来还是很费劲的。

现在用户数已经 500 个用户了,之前用户呼声很高的功能,得花点心思搞一下了,要不就对不起这些铁杆用户了!

考虑再三,还是要重构。

jQuery 在做一些交互少的功能的时候,还是很不错的选择。不过考虑到接下来要开发的功能的交互的复杂度,我觉得还是要引入 Vuejs 或者 React。鉴于熟练程度,我最终选择了 Vuejs

由于我的 Chrome 扩展中将会使用 Content ScriptPopup PageTab Page这三种形式,这就意味着,如果我要使用 Vuejs 的话,那么我需要用 Webpack 配置三个入口和三个文件。

不过让我修改 Webpack 配置还好,自己写,真的是太蛋疼了。于是,我找到了这个,https://github.com/Kocal/vue-web-extension

这个 Vue 模板真的是太好用了,基本上把大部分的工作都做好了。

我们只需要按照 Kocal/vue-web-extension 库的文档操作,就可以完成基本的部署。

不过没有 Content Script 的支持,我们只需要自己在 src 目录下创建个新的目录,比如叫做 content-script

然后创建一个入口文件 content-script.js,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue';
import App from './App';
import store from '../store';

global.browser = require('webextension-polyfill');

Vue.prototype.$browser = global.browser;
Vue.use(ElementUI);

/* eslint-disable no-new */
new Vue({
el: '#review-bookmark',
store,
render: h => h(App),
});

再创建一个 App.vue 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
<button>测试一下</button>
</div>
</template>

<script>
export default {
data() {
return {};
},
};
</script>

<style lang="scss" scoped></style>

最后创建一个用于初始化的文件 cs-init.js,内容如下:

1
2
3
const reviewBookmarkDiv = document.createElement('div');
reviewBookmarkDiv.id = 'review-bookmark';
document.body.appendChild(reviewBookmarkDiv);

创建好这三个文件后,再打开 webpack.config.js 文件,修改 entry 项如下:

1
2
3
4
5
6
entry: {
background: './background.js',
'popup/popup': './popup/popup.js',
'tab/tab': './tab/tab.js',
'content-script/content-script': './content-script/content-script.js',
}

这样就设置好了多个编译入口,最后编译的时候就会在 dist 文件夹下生成四个编译好的 js 文件。

再修改 webpack.config.jsplugins 项中的 CopyPlugin 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
new CopyPlugin([
{ from: 'icons', to: 'icons' },
{ from: '_locales', to: '_locales' },
{ from: 'content-script/cs-init.js', to: 'content-script/cs-init.js' },
{ from: 'popup/popup.html', to: 'popup/popup.html', transform: transformHtml },
{ from: 'tab/tab.html', to: 'tab/tab.html', transform: transformHtml },
{
from: 'manifest.json',
to: 'manifest.json',
transform: content => {
const jsonContent = JSON.parse(content);
jsonContent.version = version;

if (config.mode === 'development') {
jsonContent['content_security_policy'] = "script-src 'self' 'unsafe-eval'; object-src 'self'";
}

return JSON.stringify(jsonContent, null, 2);
},
},
])

这样我们就把扩展需要的必要文件都复制进了 dist 目录。尤其是 cs-init.js。这个文件在下面会讲。

现在打开 manifest.json 文件,调整如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"name": "__MSG_appname__",
"description": "__MSG_appdesc__",
"version": null,
"manifest_version": 2,
"default_locale": "en",
"author": "ETY001",
"homepage_url": "https://bm.to0l.cn/",
"chrome_url_overrides": {
"newtab": "tab/tab.html"
},
"icons": {
"16": "icons/icon-16.png",
"19": "icons/icon-19.png",
"38": "icons/icon-38.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"browser_action": {
"default_title": "__MSG_appname__",
"default_popup": "popup/popup.html"
},
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_scripts": [
{
"matches": ["*://*/*"],
"css": ["content-script/content-script.css"],
"js": ["content-script/cs-init.js", "content-script/content-script.js"],
"run_at": "document_end"
}
],
"web_accessible_resources": ["tab/tab.html"],
"permissions": ["activeTab", "notifications", "bookmarks", "tabs", "background", "https://www.google-analytics.com/", "storage"]
}

好了所有的文件都准备好了,接一下原理。

通过看 manifest.jsoncontent_scripts,可以看到我们引入了 cs-init.jscontent-script.js。其中 cs-init.js 文件的作用就是为了能在页面里面插入一个 <div /> 作为模板的插入点,然后 content-script.js 就可以把相关的页面和逻辑就能插入进我们准备好的 <div /> 中去了。

除了 Content Scripts 这个坑需要自己填以外,还有一个字体库的坑也需要自己填一下。

由于我还使用了 Element UI 库,但是在测试的时候,发现 font icon 的库一直引入不了。最后,我用的 cdn 上的字体资源。

具体方法在 《在项目中改变 SCSS 变量》这个文档中可以看到,只需要修改 font-path 这个参数即可,如下:

1
2
3
4
5
6
7
/* 改变主题色变量 */
$--color-primary: teal;

/* 改变 icon 字体路径变量,必需 */
$--font-path: 'https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.13.0/theme-chalk/fonts';

@import '~element-ui/packages/theme-chalk/src/index';

OK,花了两天的时间,把基础框架搭建好了!接下来就可以开始重构工作了,剩下的相对就简单了。

前言

《快速搭建私有单节点 Bitshares Testnet(一)》 中,讲解了如何搭建一个私有测试网络,这一篇将会讲解如何部署 Web 界面用于注册新用户,以及向新用户转账测试币。

部署

拉取代码

1
git clone https://github.com/ety001/bitshares-testnet-for-dapp-developers.git

编译镜像

1
2
cd bitshares-testnet-for-dapp-developers
docker build -t btfdd:latest .

你也可以直接使用我编译好的镜像,docker pull ety001/btfdd:latest

运行

1
2
3
4
5
6
7
8
9
docker run -itd \
--restart always \
--name btfdd \
-p 3000:3000 \
-e PRIV_KEY=5Jxxxxxx \
-e API_URL=ws://192.168.0.10/ws \
-e CHAIN_ID=2d20869f3d925cdeb57da14dec65bbc18261f38db0ac2197327fc3414585b0c5 \
-e CORE_ASSET=TEST \
ety001/btfdd:latest

环境变量说明

  • PRIV_KEY 是你用来作为水龙头的账号的 active 私钥
  • API_URL 是你私有测试网络的地址。这里需要注意下地址是否可访问。
  • CHAIN_ID 是你私有测试网络的 CHAIN_ID
  • CORE_ASSET 是你私有网络的测试币名

访问

如果一切正常,这个时候浏览器访问 http://localhost:3000 即可看到工具页面了。

目前工具只提供创建账号和转账的功能。

公共服务

目前我也搭建了公共服务免费供大家使用,地址: https://testnet.61bts.com

疑问

如果有问题可以给我发邮件 work#akawa.ink 或者提交 issue

整理了一下用 js-sdk 创建新用户的最简单的 Demo 实现代码。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import {PrivateKey, key, FetchChain, TransactionBuilder} from 'bitsharesjs';
import {Apis, ChainConfig} from 'bitsharesjs-ws';

generateKeyFromPassword(accountName, role, password) {
const seed = accountName + role + password;
const privKey = PrivateKey.fromSeed(seed);
const pubKey = privKey.toPublicKey().toString();
return {privKey, pubKey};
}

registerUser(username, password, registrar, referrer) {
const privKey = '这里是用来签名数据的用户active私钥';
const pKey = PrivateKey.fromWif(this.privKey);
const referrerPercent = 0;
const {pubKey: ownerPubkey} = generateKeyFromPassword(
username,
'owner',
password
);
const {pubKey: activePubkey} = generateKeyFromPassword(
username,
'active',
password
);
const {pubKey: memoPubkey} = generateKeyFromPassword(
username,
'memo',
password
);

try {
return Promise.all([
FetchChain("getAccount", registrar),
FetchChain("getAccount", referrer)
]).then((res) => {
const [chainRegistrar, chainReferrer] = res;
const tr = new TransactionBuilder();
tr.add_type_operation("account_create", {
fee: {
amount: 0,
asset_id: 0
},
registrar: chainRegistrar.get("id"),
referrer: chainReferrer.get("id"),
referrer_percent: referrerPercent,
name: username,
owner: {
weight_threshold: 1,
account_auths: [],
key_auths: [[ownerPubkey, 1]],
address_auths: []
},
active: {
weight_threshold: 1,
account_auths: [],
key_auths: [[activePubkey, 1]],
address_auths: []
},
options: {
memo_key: memoPubkey,
voting_account: "1.2.1",
num_witness: 0,
num_committee: 0,
votes: []
}
});
return tr.set_required_fees().then(() => {
tr.add_signer(pKey);
console.log("serialized transaction:", tr.serialize());
tr.broadcast();
return true;
});
}).catch((err) => {
console.log('err:', err);
});
} catch(e) {
console.log('unexpected_error:', e);
}
}

registerUser('新用户名', '新用户的密码', '用来签名的用户的用户名', '推荐用户的用户名');

说明

  • registerUser 函数参数中的 registrar 是用来签发数据的用户的用户名,函数里面的 privKeyregistraractive 私钥
  • referrerPercent 是分成比例,这里注意是基于手续费的 50% 再分成。也就是 referrerPercent 设置为 10000 ,则代表 registrar 分成 0%,referrer 分成 50%。
  • 提交的数据中的 voting_account 是设置投票代理人是谁。

由于国内家庭网络的 80 和 443 端口是被运营商封锁的,因此为了提供 web 服务,我就要用一台公网服务器做一下转发。

之前选择的是 frp 这个方案,不过最近在开发 Bitshares Testnet 的工具集的时候,发现 frp 非常的不稳定,连接经常莫名其妙的断掉,如下图

这蛋疼的断线,让我一直以为是 bitshares-js 库我使用的不对,看了大半周的源码。

在网上搜索了下,都说断线是 mtu 的问题,我折腾了一周,也没有弄好,最后放弃了,换了另外一个方案 nps

这是 nps 的官方库: https://github.com/cnlh/nps

配置也非常的简单,服务端根据文档配置下 nps.conf,启动后,在 web 界面配置下客户端。

客户端指定服务器的 IP 和端口,以及密码,就可以连接到服务端了。

目前我觉得 npsfrp 好的两点,一个是配置可以在服务端通过 web 轻松解决,一个是服务端和客户端的连接可以是基于 KCP 协议的,这意味着在网络不是很好的情况下,依然可以提供相对稳定的服务。

至于稳定性,容我再观测一段时间。现在终于可以正常使用我的 wss://api.61bts.com 节点了。

0%