JS邊角料: NodeJS+AutoJS+WebSocket+TamperMonkey實現區域網多端文字互傳
—閱讀時間約 7 分鐘,復現時間約 15 分鐘—
由於之前一直在用的擴展 QPush 停止服務了,苦於一人湊齊了 Window, Android, Mac, ios 四種系統的設備,Apple與其他廠商提供的互傳又無法協同,有時只是需要在多設備使用同一串文字就在通訊App之間輾轉登錄非常影響當下如火如荼的狀態,甚至當複製長文字時微信還會偷偷的剪裁,而且從 QPush 以後市面竟然沒有找到任何一款既不打廣又這樣輕量的文字協同App,一怒之下自己寫了這樣一套基於瀏覽器的簡易工具。
本文從配置到程式碼,小白可入內容較多,老司機們直接菜單索引到程式碼吧。
糧草先行
– Node.js
Node.js 是一個跨平台 JavaScript 運行環境,使開發者可以搭建伺服器端的JavaScript應用程式。 [ MDN ]
如果你的項目夠健全,WebServer 是由許多模組構成的,但對於非職業選手來講,只要理解為 視圖層 和 服務層 就成,服務層提供數據,視圖層負責渲染,js 一直曾作為一個僅實現視圖層的腳本語言,必須基於瀏覽器且 Web API 貧瘠、瀏覽器廠商特立獨行的割據時代已經過了,現在的 js 可以寫 瀏覽器視圖層 / 服務層、App、小程式、遊戲、PC客戶端、3D動畫 等等等,Node.js 可以稱得上是改變前端命運的神話之一了。如MDN所述它可以使用js語言,為 視圖層 提供服務、數據。
Download: [ 官網 ]
– Auto.js
一個在Android、鴻蒙平台編寫、運行JavaScript程式碼的集成開發環境,包括程式碼補全的編輯器、單步調試、圖形化設計,可構建為獨立apk應用,也可連接電腦開發。 [ 官方文檔 ]
如果你會JS,ios 的 workflow 都得往後稍稍,如果你會其他語言,那麼你大抵能找到更順手的替代品。能跟 Auto.js 生態和穩定比較的,中國應該沒有幾家。
Download: Android / 鴻蒙 應用商店(Apple Store未提供)
– WebSocket
WebSocket
對象提供了用於創建和管理 WebSocket 連接,以及可以通過該連接發送和接收數據的 API。 [ MDN ]
講人話就是 即時通訊 ,使伺服器與多個客戶端能高並發地保持通訊狀態,我們日常生活中的大部分操作都基於 HTTP 請求,比如點擊外賣App的某家店鋪發出了請求,而App公司的伺服器將這家店鋪的每個菜單的文字和圖片返回到手機並展示出來;又比如我們刷短影片時每次下滑下一條影片,伺服器將下條影片通過 HTTP 返回給我們。再直白點就是我們的每個操作都像是網購,只不過流量成為這次交易的貨幣,而賣家把商品交給你也要承擔包郵的運費。
可是當我們觀看直播、網路通話等操作時 HTTP 就不那麼適用了,在 WebSocket 正式普及以前大家只能通過 輪詢 來實現此方法,也就是在一秒內不停地買 60 次,就可以觀看一秒 60 幀的影片直播了,雖然 Web API 不需要我們擁有1秒60下的手速,但對瀏覽器的性能是一個很大的困擾,再者網路或伺服器波動造成的失敗概率,也會隨著請求基數的增加而倍數增長,不是 1*60*0.01%,看直播的人不僅僅有一個,這些人會不斷地給伺服器造成壓力,好比2009.11.11的阿里巴巴,2019的暴雪娛樂,和每一年的新浪微博。這也是以前網電和直播不普及的原因,真不是有頭腦的人少,英雄也要倚靠時勢。
Download: 項目依賴包,不需要手動安裝,下文會詳細說明。
– TamperMonkey
俗稱油猴,也是基於瀏覽器擴展程式的 JS 語言,淘系0點秒殺、自動掛網課、腳本去廣告基本用的都是它,網上已經有非常多資源這裡就不介紹了。
Download: Chrome等各大瀏覽器商店,沒有梯子的可以試試 [ 擴展迷 ]。
採用這幾個工具的重點在於,它們的生態都很好且穩定,團隊保持更新,技術領先至少五年內不會被淘汰。
程式碼部分
上文提到數據由 服務層 提供,我們可以通過 Node.js 實現中轉; 通訊協議 作為媒介,可以使用 WebSocket;由瀏覽器的 TamperMonkey 監聽剪貼板 ; 協同設備 通過 Auto.js 接收複製好的文本流。
整個思路已經理清了,服務層 作為本業務的中樞,所以由 Node.js 的開發先開始。
– Node.js
由上文提到官網入口下載,安裝包會附帶一個 npm 插件 ,它是一個包管理器,直接作用是通過在 cmd 輸入 URI 的方式將網路上的資源下載到你的電腦,從這點來講可以理解為一個全世界在用的大號雲網盤。
安裝好後打開環境變數:
找到系統變數中的 path 編輯,將 npm 和 nodejs 的路徑 copy 至末尾:
然後 window + R ,鍵入 cmd,回車
在命令行窗口輸入 npm -v 和 node -v 檢查安裝與環境變數是否配置成功:
如圖返回版本號即為成功,接著輸入下行程式碼安裝 cnpm:
npm install -g cnpm --registry=//registry.npm.taobao.org
npm 是我們剛剛配置變數索引到的程式,install 是 安裝 關鍵字,-g 是 安裝 到全局(global),cnpm 是淘寶鏡像,由於 npm 起於牆外,無論是伺服器支援還是其內數據遠在天邊,都導致下載速度緩慢且大概率會 fail,後面一長串是下載路徑。
還是 cnpm -v 檢查,出現版本號就是安裝成功,由於是基於npm的鏡像,不需要配置環境變數。
隨便找個盤新建文件夾,名字不能隨意否則可能會造成不可預知錯誤,起碼中文是絕對不行的,也不建議駝峰式,我開發此項目過程中因此報過錯,建議小寫”a-z”與”_”組合:
直接在文件夾管理器的地址欄中鍵入 cmd 回車(下圖中文字選中高亮處),省的一直cd找URI了:
在命令行窗口中輸入 npm init,回車,緊跟著一連串配置(圖中黃字備註):
初始化後在根目錄生成一個package.json文件,該文件除了聲明項目描述,還註明了引入的依賴包和對應版本,不可刪除。
命令行保持這個文件夾路徑,依次鍵入安裝依賴包,項目相當於一台手機,依賴包是裡面的App,提供各種功能:
- cnpm install express –save 這個包作用是nodeJS基於此框架創建服務層業務
- cnpm install cors –save 作用是解決跨域問題(想了解跨域可以閱讀我的另一篇文章:瀏覽器:深度理解瀏覽器的同源策略)
- cnpm install body-parser –save 以此包獲取前台傳參的參數
- cnpm install mysql –save 幫助連接MySQL資料庫
- cnpm install multer –save 中間件上傳文件處理formdata類型的表單數據
- cnpm install cookie-parser –save 該包提供cookie的使用
安裝後根目錄會多出一個 node_modules 文件夾存放這些依賴包
package.json 也自動寫入了相應的註明:
裡面的文件不要改不要刪,也不用去看,否則會掉很多頭髮。
在根目錄新建一個文件 app.js,用程式碼編輯器打開,VSCode 提供了wifi 區域網連接手機 Auto.js 軟體調試的插件,小白的話找個秒開級的輕量編輯器就完全沒問題了:Sublime Text 3 官網
直接 Ctrl+C 和 Ctrl+V :
//導入express框架
var express = require("express");
var app = express();
//解決跨域問題
const cors = require('cors');
// 中間件 獲取參數的
const bodyParser = require('body-parser');
//讀寫文件流
var fs = require("fs")
//引入websocket
const ws = require('nodejs-websocket');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cors());
app.all("*", function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OP0TIONS");
res.header("X-Powered-By", "3.2.1");
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
app.get('/getString', function(req, res) {
// console.log(5555,req.query,666,req.params,888,req.body)
console.log(req.query)
res.status(200)
//json格式
// res.json(data)
//獲取json
fs.readFile('./data.json','utf-8',function(err,data) {
console.log(data)
let params = {}
if(err) {
console.error()
params = {
code:500,
message:"讀取失敗"
}
} else {
params = {
code:200,
message:"成功",
data:data
}
}
//傳入頁面
res.send(params)
})
});
app.get('/setString', function(req, res) {
// console.log(5555,req.query,666,req.params,888,req.body)
console.log(req.query)
res.status(200)
//json格式
// res.json(data)
//傳入頁面
fs.readFile("./data.json",function(err,data){
if(err) {
return console.error(err)
}
let obj = {
clips: req.query
}
let str = JSON.stringify(obj)
fs.writeFile("./data.json",str,function(err){
if(err) {
console.error(err)
}
console.log('-------修改成功-------')
})
})
let params = {
code:200,
message:"成功"
}
res.send(params)
});
let padKey = '';
const webServer = ws.createServer(conn => {
// console.log('有一名用戶連接進來了...')
conn.on("text", function (res) {
let resa = JSON.parse(res);
if(resa.msg && resa.msg === 'Request connection.') {
console.log(`${resa.role} 請求連接...`)
console.log('key: ', conn.key)
conn.sendText(JSON.stringify({
"sid": conn.key,
"msg": "伺服器連接成功!"
}));//返回給客戶端的數據
setTimeout(() => {
conn.sendText(JSON.stringify({
"sid": conn.key,
"msg": `Hi, ${resa.role}.`
}))
}, 800)
if(resa.role === 'Pad') {
padKey = conn.key
}
}
if(resa.clips && resa.role === 'Borwser') {
console.log(`剪貼板更新: ${resa.clips}`)
webServer.connections.forEach(function (conn) {
if(conn.key == padKey) {
conn.sendText(JSON.stringify(resa))//返回給所有客戶端的數據(相當於公告、通知)
}
})
}
})
//監聽關閉
conn.on("close", function (code, reason) {
console.log("連接斷開...")
})
//監聽異常
conn.on("error",() => {
console.log('服務異常關閉...')
})
}).listen(8088)
var server = app.listen(3000, function() {
var host = server.address().address;
var port = server.address().port;
console.log("服務啟動: ", port);
})
—————– 必要部分 —————-
行2、3 – 引入express框架,定義變數app接收將API實例化。
行5 – 引入cors,使得瀏覽器與其他設備可以跨域請求該服務。
———- 手動http部分(可選) ———-
行7 – 引入body-parser,以獲取 HTTP 請求的參數(僅使用 WebSocket 時可略)。
行9 – 引入node.js的fs模組,以讀寫文件流內容(僅使用 WebSocket 時可略)。
——- 自動websocket部分(可選) ——
行11 – 引入websocket,作為網路交互協議。
———- 手動http部分(可選) ———-
行13、14、15 – 對該服務激活跨域插件與中間件,變數app為行2引入express框架的實例化實現,下文不再贅述。
行17~14 – 設置所有 httpResponse 的響應頭。
行27~54 – 響應 http get( ) 的介面服務,對應請求路徑應為 ‘//IPv4 Address:埠號/getString’,IPv4可以通過cmd中鍵入ipconfig查詢,下文不再贅述。
行27 – 函數括弧內兩個形參 req 接收請求體,res 接收響應體。
行29 – http.get 請求通過query傳參,例如請求路徑’//192.168.0.1/getString?id=1&name=97z4moon’,服務就可通過上述引入的中間件依賴包獲取到兩個參數 { id: ‘1’, name: ’97z4moon’}。想傳不同參數時,只需要改變路徑’?’後面跟的值即可,多個參數以’&’連接。
行34 – 通過fs模組讀操作,’./data.json’,’./’為同目錄下,’../’為上一級,比如我的app.js文件路徑為 ‘C:\clipboard_project\app.js’,’./data.json’ 即為 ‘C:\clipboard_project\data.json’,’../data.json’ 為 ‘C:\data.json’,它們都是相對路徑,字面意思就是比較程式碼所處文件app.js位置的對應路徑。’utf-8’ 是以該編碼接收,參數err接收錯誤時實參,data接收讀取文件流的內容。
行51 – 將響應體發送至客戶端,也就是接收的人,該角色在本業務中對應的是持有Auto.js軟體的移動設備,實參params將所期待的剪貼板數據返回給請求者,假設一直不執行send()方法,請求者會將該進程掛起,直到網路請求超時。
行56~83 – 與getString同理,思路是油猴監聽瀏覽器剪貼板事件,在鍵盤鍵入複製操作時將剪貼板的內容寫入data.json文件中,以便移動端獲取。假設我的區域網ip為192.168.31.109,則我的油猴腳本請求路徑應為’//192.168.31.109:3000/setString?str=剪貼板文本’。
行71 – 通過fs模組寫操作,在行67~69定義一個對象obj,在obj的堆中增加一個鍵值對,如上所說,形參req接收的是請求體,req.query即為上述請求路徑中最後’?’緊跟的’str=剪貼板文本’。
——- 自動websocket部分(可選) ——
行85~124 – websocket通訊自動同步到移動設備部分。
行85 – 定義變數padKey存儲本次通訊接收者的唯一key,該key由node的websocket插件自動分配,如果需要多設備,則將行104改為:padKey+=conn.key,行110改為:if(padKey.indexOf(conn.key)>-1){ 。
行86 – ws已由行11部分實例化了websocket包,通過該包提供的API – createServer創建一個socket通訊,定義常量webServer接收,因為該服務保持通訊,僅隨著項目關閉或伺服器維護而關閉,所以定義為常量為最優,通過形參conn接收每一次建立起的socket通訊。
行88 – 通過某次連接的原型函數on(),監聽 ‘text’ 事件,並定義一個function在監聽到事件時執行,以形參res接收。
行93~96 – 將一個JSON字元串處理後的對象傳入給本次通訊連接的發起者。
行97~102 – 同上,通過定時器setTimeout延遲 800ms 執行。
行109 – webServer是本次socket服務實例,其[key]connections對應的是當前socket服務下所有的連接用戶,通過forEach遍歷找到需要接收的用戶,通過API – sendText() 向其發送剪貼板內容。
行117~119 – 監聽本次socket服務中所有成功連接的用戶的退出連接事件。
行121~123 – 監聽本次socket服務中所有成功連接的用戶的異常錯誤事件。
行124 – 以第一個實參8088為埠啟動socket服務 。
行126 – 以第一個實參3000為埠啟動http服務,也就是上述中請求路徑的 ‘//192.168.31.109:3000/getString’ 。
– TamperMonkey
手動版思路:監聽瀏覽器剪貼板事件 -> 將剪貼板內容通過http發送給服務層,node.js接收到query將其保存至data.json文件中,移動設備執行auto.js的程式碼向服務層發起請求,node.js拿到data.json中的剪貼板內容放進響應體返回給移動設備,移動設備通過auto.js API – setClip()將內容設置到設備剪貼板。
// ==UserScript==
// @name setClipString
// @namespace //tampermonkey.net/
// @license GPL version 3
// @encoding utf-8
// @description try to take over the world!
// @author 97z4moon
// @include *
// @icon //www.google.com/s2/favicons?domain=tampermonkey.net
// @grant GM_xmlhttpRequest
// @grant GM_download
// @run-at document-end
// @version 1.0.0
// ==/UserScript==
(function() {
// Your code here...
let urls = document.location.href
document.addEventListener("copy",function(e){
fetch("//localhost:3000/setString?str="+window.getSelection(0).toString()+"&url="+urls,{
"headers":{
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"authorization":"Basic " + btoa(JSON.stringify({
"li":"administrator","pd":"superadmin"
})),
"referrer": urls,
"referrerPolicy": "no-referrer-when-downgrade",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
}}).then(response=>response.json()).then(data=>{
console.log(data)
}).catch(e=>{
console.log(e)
})
})
})();
行1~14 – 腳本聲明與配置。
行16~39 – IIFE函數。
行18 – 通過DOM的location對象獲取到複製操作的網站鏈接,保存在定義的字元串變數urls中。
行19 – 對整個DOM設置監聽器,第一個參數定義監聽器監聽’copy’事件,第二個參數監聽到時執行函數。
行20 – 通過Fetch API對服務層發起請求,該方法提供了一種簡單,合理的方式來跨網路非同步獲取資源。fetch() 可以接受跨域cookie,也可以建立起跨域對話,fetch() 不會發送 cookie。如果需要在 IE11 及以下版本中使用 fetch,通過 Fetch Polyfill 來實現。[MDN]
行21~32 – 設置請求頭。
——-
自動版思路:在瀏覽器打開的頁面中建立websocket通訊,連接到node.js啟動在8088埠的socket服務,TamperMonkey監聽到瀏覽器複製操作時,將剪貼板內容發送至服務層node.js處理,node.js再將該內容下發到key值對應的移動設備中。只需將移動設備socket通訊時發送的參數role改變為預設值即可,如我在node.js程式碼中設置的條件是:if(resa.role === ‘Pad’) 。當移動設備接收到剪貼板內容時,使用auto.js將其設為剪貼板。
// ==UserScript==
// @name setClipString2
// @namespace //tampermonkey.net/
// @license GPL version 3
// @encoding utf-8
// @description try to take over the world!
// @author 97z4moon
// @include *
// @icon //www.google.com/s2/favicons?domain=tampermonkey.net
// @grant GM_xmlhttpRequest
// @grant GM_download
// @run-at document-end
// @version 2.0.0
// ==/UserScript==
(function() {
// Your code here...
let ws = new WebSocket('ws://localhost:8088');//實例化websocket
let obj = {
role: 'Borwser',
msg: 'Request connection.'
}
ws.onopen = function () {
console.log("socket has been opend")
ws.send(JSON.stringify(obj))
}
document.addEventListener("copy",function(e){
console.log("data: ", window.getSelection(0).toString())
obj.clips = window.getSelection(0).toString()
obj.msg = 'ClipBoard has been updated.'
ws.send(JSON.stringify(obj))
})
})();
行18 – 實例化websocket,路徑’ws://……’為關鍵字,’localhost’不可替換為IPv4,否則會報錯,8088為node.js設置的socket服務埠。(自行擴展可在node.js可以啟動多個socket服務,分別對應不同功能)
行23~26 – 在socket連接成功後在瀏覽器控制台輸出提示,並向node.js發送實參obj表明身份與來意。send()事件不可在連接成功前執行,否則會導致該頁面生命周期下的所有socket連接失敗。
行29 – window對象API – window.getSelection(0) 獲取剪貼板資訊,使用toString將其格式化為剪貼板內容。
– Auto.js
let ws = $web.newWebSocket("ws://192.168.31.109:8088", {
eventThread: 'this'
});
console.show();
let padSid = '';
ws.on("open",(res,ws)=>{
log("WebSocket has been ready...")
}).on("failure",(err,res,ws)=>{
log("Connect fail...")
ws.close(1000,null)
console.hide()
}).on("closing",(code,reason,ws)=>{
log("WebSocket is closing...")
}).on("text",(text, ws)=>{
let res = JSON.parse(text)
if(res.sid) {
padSid = res.sid
}
console.info("Receive msg: ", res.msg)
if(res.clips) {
setClip(res.clips)
}
}).on("binary",(bytes,ws)=>{
console.info("Receive binary:")
console.info("hex: ",bytes.hex())
console.info("base64: ",bytes.base64())
console.info("md5: ",bytes.md5())
console.info("size: ",bytes.size())
console.info("bytes: ",bytes.toByteArray())
}).on("closed",(code,reason,ws)=>{
log("WebSocket closed: code = %d, reason = %s")
})
let params = {
role: 'Pad',
msg: 'Request connection.'
}
ws.send(JSON.stringify(params));
setTimeout(()=>{
log("connect not WebSocket...")
ws.close(1000,null)
console.hide()
},600000)
行1 – 定義變數ws實例化一個socket服務,請求地址為 ‘ws://192.168.31.109:8088’ 。
行2 – eventThread定義為this事件將在創建WebSocket的執行緒觸發,如果該執行緒被阻塞,則事件也無法被及時派發。
行4 – 打開控制台懸浮窗。
行6 – 定義字元串變數padSid接收node.js中socket服務分配的本次通訊設備唯一key。
行7 – 監聽socket包服務的啟動事件。
行9 – 監聽與socket服務層斷線的事件。
行11 – 關閉本次socket通訊。
行12 – 隱藏控制台懸浮窗。
行13 – 監聽socket通訊關閉中事件。
行15 – 監聽socket通訊接收到文本事件。
行22 – Auto.js API – setClip() 設置剪貼板內容。
行24 – 監聽socket通訊接收到二進位資訊事件。
行31 – 監聽socket通訊關閉完成的生命周期。
行39 – 向服務層發送socket訊息表明身份和來意。
行40 – 定時器 10 分鐘後關閉本次socket通訊服務,如果不設則通訊會在執行完js後立即結束,如果想永久掛起,可以將行40~44改為:setInterval(()=>{}),需要注意的是這樣做會佔用許多不必要的性能資源,時間長了以後可能造成記憶體溢出,宏隊列擁擠造成socket通訊較高的延遲。