用户脚本

用户脚本是一段 JS 代码,能为网站添加新的功能。编写 JS 脚本比 crx 扩展简单得多,不必为了实现一个简单的功能加入其他的文件。

使用用户脚本需要安装一个脚本管理器,最常用的就是油猴(Tampermonkey)或暴力猴(Violentmonkey),Firefox 应用商店、Edge 应用商店或 chrome 应用商店搜索 Tampermonkey 或 Violentmonkey,然后下载安装即可。

Tampermonkey vs Violentmonkey

云端备份、导出备份、自动更新、筛选排序这些脚本管理的重要功能,油猴和暴力猴上都有。

Tampermonkey 多了一些定制,编辑工具功能也多一些。

微信截图_20220905232306.png


Violentmonkey 安装包体积较小,界面比较清爽一些,设置也比较简单。

微信截图_20220905232614.png


Violentmonkey 还多了一个比较实用的功能,为当前网站查找匹配的脚本。

微信截图_20220905233157.png

编写脚本

以 Violentmonkey 为例。

  1. 新建脚本。首先点击 Violentmonkey 扩展图标上面的 + 号,新建一个脚本。

微信截图_20220905233417.png

  1. 设置脚本。以 // ==UserScript== 开头,// ==/UserScript== 结尾。下面是一些常用的设置:
  • @namespace 和 @name 组合是发布到脚本网站时,用户脚本的唯一标识符。
  • @author:脚本的作者。
  • @version:脚本版本,用于更新发布脚本。
  • @description:脚本的描述,可以添加命名来国际化,比如 @description:en。
  • @include、@exclude:脚本应该和不应该运行的页面。允许指定多个。
  • @require:加载外部脚本的 URL。允许指定多个。
  • @resource:一些外部静态资源。可以通过 GM_getResourceURL 和 GM_getResourceText 方法访问。
    1
    2
    // @resource logo https://my.cdn.com/logo.png
    // @resource text https://my.cdn.com/some-text.txt
  • @connect:定义允许被 GM_xmlhttpRequest 方法访问的域名。允许指定多个。
  • @grant:给 GM_* 方法授权并可在脚本执行时使用。如果使用任何特殊 API,则必须明确授予。
    1
    2
    3
    4
    5
    // @grant GM_getValue
    // @grant GM_setValue
    // @grant GM_setClipboard
    // @grant GM_openInTab
    // @grant GM_xmlhttpRequest
    除了GM API之外,还可以授予以下权限:
    1
    2
    3
    4
    5
    6
    7
    8
    // @grant window.close
    // @grant window.focus
    // @grant window.onurlchange

    if (window.onurlchange === null) {
    // feature is supported
    window.addEventListener('urlchange', (info) => ...);
    }
  • @run-at:定义脚本何时执行。
    • document-end 默认值
      • 脚本在 DOMContentLoaded 被触发时执行。此时,页面的基本 HTML 已准备就绪,图像等其他资源可能仍在加载中。
    • document-start
      • 脚本会尽快执行。不能保证脚本在页面中的其他脚本之前执行。在 Greasemonkey v3 中,甚至可以在加载 HTML 之前确保脚本执行,但对于 Violentmonkey 作为 Web 扩展来说这是不可能的。
    • document-idle
      • 脚本在 DOMContentLoaded 触发后执行。
  1. 加入代码。

    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
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    // ==UserScript==
    // @name 无需梯子 谷歌划词翻译 translate.google.cn
    // @namespace https://violentmonkey.github.io
    // @version 1.13
    // @description 基于 translate.google.cn,中译英,英译中,拼音、音标显示
    // @license https://www.apache.org/licenses/LICENSE-2.0
    // @author zkrisj
    // @include *
    // @exclude https://juejin.cn/editor/drafts/*
    // @exclude https://translate.google.cn/*
    // @run-at document-end
    // @connect translate.google.cn
    // @grant GM_xmlhttpRequest
    // ==/UserScript==
    (function() {
    'use strict';
    // var googleUrl = 'https://translate.google.cn/translate_a/single?client=gtx&dt=t&dt=bd&dj=1&source=input&hl=auto&sl=auto';
    // 无需梯子
    var googleUrl = 'https://translate.google.cn/_/TranslateWebserverUi/data/batchexecute?&source-path=%2F&rpcids=MkEWBc&soc-app=1&soc-platform=1&soc-device=1&_reqid=632656&rt=c';
    var icon = document.createElement('div');
    var word = '';
    icon.innerHTML = '<svg style="position: absolute;margin: 4px;" width="24" height="24" viewBox="0 0 768 768">' +
    '<path d="M672 640.5v-417c0-18-13.5-31.5-31.5-31.5h-282l37.5 129h61.5v-33h34.5v33h115.5v33h-40.5c-10.5 40.5-33 79.5-61.5 112.5l87 85.5-22.5 24-87-85.5-28.5 28.5 25.5 88.5-64.5 64.5h225c18 0 31.5-13.5 31.5-31.5zM447 388.5c7.5 15 19.5 34.5 36 54 39-46.5 49.5-88.5 49.5-88.5h-127.5l10.5 34.5h31.5zM423 412.5l19.5 70.5 18-16.5c-15-16.5-27-34.5-37.5-54zM355.5 339c0-7.381-0.211-16.921-3-22.5h-126v49.5h70.5c-4.5 19.5-24 48-67.5 48-42 0-76.5-36-76.5-78s34.5-78 76.5-78c24 0 39 10.5 48 19.5l3 1.5 39-37.5-3-1.5c-24-22.5-54-34.5-87-34.5-72 0-130.5 58.5-130.5 130.5s58.5 130.5 130.5 130.5c73.5 0 126-52.5 126-127.5zM640.5 160.5c34.5 0 63 28.5 63 63v417c0 34.5-28.5 63-63 63h-256.5l-31.5-96h-225c-34.5 0-63-28.5-63-63v-417c0-34.5 28.5-63 63-63h192l28.5 96h292.5z" style="fill:#3e84f4;"/></svg>';
    icon.setAttribute('style', 'width:32px;' + 'height:32px;' + 'display:none;' + 'background:#fff;' + 'border-radius:16px;' +
    'box-shadow:4px 4px 8px #888;' + 'position:absolute;' + 'z-index:2147483647;');
    // 添加翻译图标到 DOM
    document.documentElement.appendChild(icon);
    document.addEventListener('mousedown', function(e) {
    if (e.target == icon || (e.target.parentNode && e.target.parentNode == icon) || (e.target.parentNode.parentNode && e.target.parentNode
    .parentNode == icon)) { // 点击翻译图标时阻止选中的文本消失
    e.preventDefault();
    }
    });
    // 选中变化事件
    document.addEventListener("selectionchange", function() {
    if (!window.getSelection().toString().trim()) {
    icon.style.display = 'none';
    // server.containerDestroy();
    }
    });
    // 显示、隐藏翻译图标
    document.addEventListener('mouseup', function(e) {
    if (e.target == icon || (e.target.parentNode && e.target.parentNode == icon) || (e.target.parentNode.parentNode && e.target.parentNode
    .parentNode == icon)) { // 点击了翻译图标
    e.preventDefault();
    return;
    }
    var text = window.getSelection().toString().trim();
    if (text && text.length < 800 && icon.style.display == 'none') {
    icon.style.top = e.pageY + 12 + 'px';
    icon.style.left = e.pageX + 'px';
    icon.style.display = 'block';
    } else if (!text) {
    icon.style.display = 'none';
    for (var i = 0; i < server.rendered.length; i++) { // 点击了翻译内容面板
    if (e.target == server.rendered[i]) return; // 不再创建翻译图标
    }
    server.containerDestroy(); // 销毁翻译内容面板
    }
    });
    // 翻译图标点击事件
    icon.addEventListener('click', function(e) {
    var text = window.getSelection().toString().trim();
    if (text) {
    icon.style.display = 'none';
    server.containerDestroy(); // 销毁翻译内容面板
    // 新建翻译内容面板
    var container = server.container();
    container.style.top = e.pageY + 'px';
    if (e.pageX + 350 <= document.body.clientWidth) // container 面板css最大宽度为250px
    container.style.left = e.pageX + 'px';
    else container.style.left = document.body.clientWidth - 350 + 'px';
    document.body.appendChild(container);
    server.rendered.push(container);
    if (isChina(text)) {
    // ajax(googleUrl + '&tl=en&q=' + encodeURIComponent(text), container);
    ajax(googleUrl, container, 'POST', "f.req=" + JSON.stringify([[["MkEWBc", "[[" + encodeURIComponent(text) + ",'zh-CN','en']]"]]]));
    } else {
    // ajax(googleUrl + '&tl=zh&dt=t&q=' + encodeURIComponent(text), container);
    text = text.replace(/[A-Z][^A-Z ]/g, function(v) { return ' ' + v.toLowerCase() }).replace(/\p{P}/gu, ' ').replace(/ /g, ' ').trim();
    word = text;
    ajax(googleUrl, container, 'POST', "f.req=" + JSON.stringify([[["MkEWBc", "[[" + encodeURIComponent(text) + ",'auto','zh-CN']]"]]]));
    }
    }
    });

    function isChina(str) {
    var reg = /^([\u4E00-\u9FA5]|[\uFF00-\uFF20]|[\u3000-\u301C])+$/;
    return reg.test(str);
    }

    function ajax(url, element, method, data, headers) {
    if (!method) method = 'GET';
    // 因为Tampermonkey跨域访问(a.com)时会自动携带对应域名(a.com)的对应cookie,不会携带当前域名的cookie
    // 所以,GM_xmlhttpRequest【不存在】cookie跨域访问安全性问题
    if (!headers) headers = { "content-type": "application/x-www-form-urlencoded;charset=UTF-8", };
    GM_xmlhttpRequest({
    method: method,
    url: url,
    headers: headers,
    data: data,
    onload: function(res) {
    console.log(url, data, res);
    // google(res.responseText, element);
    if (res.responseText.startsWith('<!DOCTYPE html>')) {
    displaycontainer("获取失败", element);
    } else {
    res = JSON.parse(JSON.parse(res.responseText.match(/^\)]}'\n\n\d+\n(\[\[.*(?!\n\d)\]\])/)[1])[0][2]);
    var phonetic = res[0][0] ? res[0][0] + "\r\n" : "";
    var translation = res[1][0][0][5][0][0];
    if (res[3] && word === res[3][0] && res[3][5] && res[3][5][0] && res[3][5][0][0] && res[3][5][0][0][1] && res[3][5][0][0][1][0])
    translation = res[3][5][0][0][1][0][0];
    displaycontainer(phonetic.toLowerCase() + translation, element);
    }
    },
    onerror: function(res) {
    displaycontainer("连接失败", element);
    }
    });
    }

    function google(rst, element) {
    var json = JSON.parse(rst), html = '';
    console.log(json);
    for (var i = 0; i < json.sentences.length; i++) {
    html += json.sentences[i].trans;
    }
    displaycontainer(html, element);
    // console.log(word, html, element);
    }

    function displaycontainer(text, element) {
    element.textContent = text;
    element.style.display = 'block'; // 显示结果
    }
    var server = {
    // 存放已经生成的翻译内容面板(销毁的时候用)
    rendered: [],
    // 销毁已经生成的翻译内容面板
    containerDestroy: function() {
    for (var i = this.rendered.length - 1; i >= 0; i--) {
    if (this.rendered[i] && this.rendered[i].parentNode) {
    this.rendered[i].parentNode.removeChild(this.rendered[i]);
    }
    }
    },
    // 生成翻译结果面板 DOM (此时还未添加到页面)
    container: function() {
    var pre = document.createElement('pre');
    pre.setAttribute('style', 'display:none;' + 'position:absolute;' + 'font-size:13px;' + 'overflow:auto;' + 'background:#fff;' +
    'font-family:sans-serif,Arial;' + 'font-weight:normal;' + 'text-align:left;' + 'color:#000;' + 'padding:0.5em 1em;' +
    'line-height:1.5em;' + 'border-radius:5px;' + 'border:1px solid #ccc;' + 'box-shadow:4px 4px 8px #888;' + 'max-width:350px;' +
    'max-height:216px;' + 'z-index:2147483647;');
    return pre;
    }
    };
    })();

    如果自己使用,不需要分享给其他人,下面步骤就省了。

  2. GreasyFork 上面发布。没有注册需要先注册。

  3. 脚本地址:谷歌划词翻译 translate.google.cn谷歌划词翻译 translate.googleapis.com