JS边角料: 通过JS实现局域网多端设备图片/文字互传(NodeJS+AutoJS+WebSocket+TamperMonkey+Workflow)

---阅读时间约 7 分钟,复现时间约 15 分钟---

(更新)不用读了,开源万岁,你一定在找这个:https://github.com/localsend/localsend

由于之前一直在用的扩展 QPush 停止服务了,苦于一人凑齐了 Window, Android, Mac, ios 四种系统的设备,Apple与其他厂商提供的互传又无法协同,有时只是需要在多设备使用同一串文字就在通讯App之间辗转登录非常影响当下如火如荼的状态,甚至当微信发送长文字时,微信还会偷偷的剪裁,而且从 QPush 以后市面竟然没有找到任何一款既不打广又这样轻量的文字协同App,一怒之下自己写了这样一套基于浏览器的简易工具。

本文从配置到代码,内容较多小白友好,老司机们直接右下角菜单按钮索引到代码部分吧。

* 最近发现一个可以实现这一需求的免费软件:AirDroid (超过流量限制需要订阅,但仅传文字和少量图片是足够的)


粮草先行

- 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应用,也可连接电脑开发。 [ 官方文档 ]

如果你使用的是 ios,JSBox / Scriptable 可以替代 AutoJS,如果你会编写JS, 那么workflow 从此可以弃用了,当然假如你更熟悉其他语言,那么大抵能找到更顺手的替代品。

Download: Android / 鸿蒙 应用商店 (JSBox、Scriptable - Apple Store)

- WebSocket

WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。  [ MDN ]

讲人话就是  即时通讯  ,使服务器与多个客户端能高并发地保持通讯状态,我们日常生活中的大部分操作都基于  HTTP  请求,比如点击外卖App的某家店铺发出了请求,而App公司的服务器将这家店铺的每个菜单的文字和图片返回到手机并展示出来;又比如我们刷短视频时每次下滑下一条视频,服务器将下条视频通过  HTTP  返回给我们。再直白点就是我们的每个操作都像是网购,只不过流量成为这次交易的货币,而卖家把商品交给我们也要承担包邮部分的运费。

可是当我们想将这项技术用于聊天和通知时  HTTP  就不那么适用了,因为  TCP  协议起点是发出请求,也就是客户端主动向服务端伸手以表达握手意向,从来都是我们通过点击行为主动去买,卖家不会在我们没付钱的情况下主动将商品送上门来。想象一下,如果我们在 Web 上正和朋友对某款新上架的游戏聊得热火朝天时,要像微博那样点击刷新按钮才能看到最新的内容,如果此时朋友的女朋友问他为什么最近对她疏于关心,那么我们将要面临的会是无尽的点击刷新,最后只能在心里痛骂对方重色轻友然后关闭 Web 下线,这将是何等的扫兴。这样的情况是由 Web 的功能贫瘠导致的,它仅仅是计算机中的一个窗口程序,它当然能调用系统中的  Socket ,使用  P2P   对等网络, 但它并没有开放给 Web 开发者,这也是以前网页端的功能性不及桌面端、大多是作为媒体内容展示和填写表单的原因,也是 Facebook 这种博客式或者说微博式Web交友与KFS模式能在零几年火起来的原因,真不是有头脑的人少,英雄也要倚靠时势。

于是在  WebSocket  正式普及以前大家只能通过定时器  轮询  来实现此方法,也就是我通过一次性的付费订阅了今年的杂志,定时器作为送报员每个月都会从邮局将最新的杂志送上门,此时送报员就扮演了这个点击的角色,这样没隔几秒就会刷新聊天或通知列表了。虽然定时器可以让我们不需要每几秒就点击一次,但对浏览器的性能是一个很大的困扰,再者网络、服务器波动、信号抖动等等原因造成的失败概率也会随着请求基数的增加而倍数增长,聊天和通知的对象不止一个,大量的  HTTP  请求会不断给服务器造成压力,好比2009.11.11的阿里巴巴,2019的暴雪娱乐,和每一年的新浪微博。 WebSocket 正是为了解决这样的问题,它使服务端也能够主动向客户端发送消息,它就像邮局一样,我们可以随时将信件寄出给朋友,我们的朋友也同样如此,并且它最终会将朋友的信件交到我们手中。

Download: 项目依赖,不需要手动安装,下文会详细说明。

- TamperMonkey

俗称油猴,也是基于浏览器扩展程序的 JS 语言,功能是将我们的 JS 代码植入到每个浏览的页面中执行(如果没有设置 URL 正则的话),某某网站抢票、购物网站0点秒杀、自动挂网课、网站去广告基本都是用的它,这里就不过多介绍了。

Download: Chrome等各大浏览器商店,没有梯子的可以试试 [ 扩展迷 ]。

 

采用这几个工具的重点在于,它们的生态都很好且稳定,文档与API保持更新,短期内不会被淘汰。


代码部分

上文提到数据由  服务层  提供,我们可以通过 Node.js 启动服务实现中转; 通讯协议  作为媒介,可以选择即时通讯的 WebSocket,或者依赖用户行为的 HTTP,结合自身应用场景而定;由浏览器的 TamperMonkey  监听剪贴板  事件;  协同设备  通过 Auto.js 接收复制好的文本流。

整个思路已经理清了,服务层 作为本业务的控制中枢,所以由 Node.js 的开发先开始。

- Node.js

由上文提到官网入口下载,安装包会附带一个  npm 插件  ,它是一个包管理器,直接作用是通过在 cmd 输入 URI 的方式将网络上的资源下载到我们的电脑,从这点来讲可以理解为一个全世界在用的大号云网盘,从本质来讲也可以理解为一个庞大的工程仓库。

安装好后打开环境变量:

找到系统变量中的 path 编辑,将 npm 和 nodejs 的路径 copy 至末尾:

(Windows 8)

(Windows 10)

然后  window + R  ,键入 cmd,回车

在命令行窗口输入 npm -v 和 node -v 检查安装与环境变量是否配置成功:

如图返回版本号即为成功,接着输入下行代码安装 cnpm:

npm install -g cnpm --registry=https://registry.npm.taobao.org

npm 是我们刚刚配置变量索引到的程序,install 是 安装 关键字,-g 是 安装 到全局(global),cnpm 是淘宝镜像,由于 npm 起于墙外,无论是服务器支持还是其内数据远在天边,虽然Tim Berners-Lee博士的CDN技术解决了这一痛点,但假设C端网络波动大或缓存服务器波动/高并发都可能会导致下载速度缓慢且 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

  1 //导入express框架
  2 var express = require("express");
  3 var app = express();
  4 //解决跨域问题
  5 const cors = require('cors');
  6 // 中间件 获取参数的
  7 const bodyParser = require('body-parser');
  8 //读写文件流
  9 var fs = require("fs")
 10 //引入websocket
 11 const ws = require('nodejs-websocket');
 12 
 13 app.use(bodyParser.json());
 14 app.use(bodyParser.urlencoded({extended: true}));
 15 app.use(cors());
 16 
 17 app.all("*", function(req, res, next) {
 18     res.header("Access-Control-Allow-Origin", "*");
 19     res.header("Access-Control-Allow-Headers", "X-Requested-With");
 20     res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OP0TIONS");
 21     res.header("X-Powered-By", "3.2.1");
 22     res.header("Content-Type", "application/json;charset=utf-8");
 23     next();
 24 });
 25 
 26 
 27 app.get('/getString', function(req, res) {
 28     // console.log(5555,req.query,666,req.params,888,req.body)
 29     console.log(req.query)
 30     res.status(200)
 31     //json格式
 32     // res.json(data)
 33     //获取json
 34     fs.readFile('./data.json','utf-8',function(err,data) {
 35         console.log(data)
 36         let params = {}
 37         if(err) {
 38             console.error()
 39             params = {
 40                 code:500,
 41                 message:"读取失败"
 42             }
 43         } else {
 44             params = {
 45                 code:200,
 46                 message:"成功",
 47                 data:data
 48             }
 49         }
 50         //传入页面
 51         res.send(params)
 52     })
 53     
 54 });
 55 
 56 app.get('/setString', function(req, res) {
 57     // console.log(5555,req.query,666,req.params,888,req.body)
 58     console.log(req.query)
 59     res.status(200)
 60     //json格式
 61     // res.json(data)
 62     //传入页面
 63     fs.readFile("./data.json",function(err,data){
 64         if(err) {
 65             return console.error(err)
 66         }
 67         let obj = {
 68             clips: req.query
 69         }
 70         let str = JSON.stringify(obj)
 71         fs.writeFile("./data.json",str,function(err){
 72             if(err) {
 73                 console.error(err)
 74             }
 75             console.log('-------修改成功-------')
 76         })
 77     })
 78     let params = {
 79         code:200,
 80         message:"成功"
 81     }
 82     res.send(params)
 83 });
 84 
 85 let padKey = '';
 86 const webServer = ws.createServer(conn => {
 87     // console.log('有一名用户连接进来了...')
 88     conn.on("text", function (res) {
 89         let resa = JSON.parse(res);
 90         if(resa.msg && resa.msg === 'Request connection.') {
 91             console.log(`${resa.role} 请求连接...`)
 92             console.log('key: ', conn.key)
 93             conn.sendText(JSON.stringify({
 94                 "sid": conn.key,
 95                 "msg": "服务器连接成功!"
 96             }));//返回给客户端的数据
 97             setTimeout(() => {
 98                 conn.sendText(JSON.stringify({
 99                 "sid": conn.key,
100                 "msg": `Hi, ${resa.role}.`
101             }))
102             }, 800)
103             if(resa.role === 'Pad') {
104                 padKey = conn.key
105             }
106         }
107         if(resa.clips && resa.role === 'Borwser') {
108             console.log(`剪贴板更新: ${resa.clips}`)
109             webServer.connections.forEach(function (conn) {
110                 if(conn.key == padKey) {
111                     conn.sendText(JSON.stringify(resa))//返回给所有客户端的数据(相当于公告、通知)
112                 }
113             })
114         }
115     })
116     //监听关闭
117     conn.on("close", function (code, reason) {
118         console.log("连接断开...")
119     })
120     //监听异常
121     conn.on("error",() => {
122         console.log('服务异常关闭...')
123     })
124 }).listen(8088)
125 
126 var server = app.listen(3000, function() {
127     var host = server.address().address;
128     var port = server.address().port;
129 
130     console.log("服务启动: ", port);
131 })

如果你对代码感兴趣,打开抽屉并查看释义:

授人以渔<・)))><<
----------------- 必要部分 ----------------

行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( ) 的接口服务,对应请求路径应为 'http://IPv4 Address:端口号/getString',IPv4可以通过cmd中键入ipconfig查询,下文不再赘述。

行27 - 函数括号内两个形参 req 接收请求体,res 接收响应体。

行29 - http.get 请求通过query传参,例如请求路径'http://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,则我的油猴脚本请求路径应为'http://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服务,也就是上述中请求路径的 'http://192.168.31.109:3000/getString' 。

- TamperMonkey

手动版思路:监听浏览器剪贴板事件 -> 将剪贴板内容通过http发送给服务层,node.js接收到query将其保存至data.json文件中,移动设备执行auto.js的代码向服务层发起请求,node.js拿到data.json中的剪贴板内容放进响应体返回给移动设备,移动设备通过auto.js API - setClip()将内容设置到设备剪贴板。

 1 // ==UserScript==
 2 // @name         setClipString
 3 // @namespace    http://tampermonkey.net/
 4 // @license     GPL version 3
 5 // @encoding    utf-8
 6 // @description  try to take over the world!
 7 // @author       97z4moon
 8 // @include      *
 9 // @icon         https://www.google.com/s2/favicons?domain=tampermonkey.net
10 // @grant        GM_xmlhttpRequest
11 // @grant        GM_download
12 // @run-at      document-end
13 // @version     1.0.0
14 // ==/UserScript==
15 
16 (function() {
17     // Your code here...
18     let urls = document.location.href
19     document.addEventListener("copy",function(e){
20         fetch("http://localhost:3000/setString?str="+window.getSelection(0).toString()+"&url="+urls,{
21             "headers":{
22                 "accept": "application/json, text/plain, */*",
23                 "accept-language": "zh-CN,zh;q=0.9",
24                 "authorization":"Basic " + btoa(JSON.stringify({
25                     "li":"administrator","pd":"superadmin"
26                 })),
27                 "referrer": urls,
28                 "referrerPolicy": "no-referrer-when-downgrade",
29                 "body": null,
30                 "method": "GET",
31                 "mode": "cors",
32                 "credentials": "include"
33             }}).then(response=>response.json()).then(data=>{
34                 console.log(data)
35             }).catch(e=>{
36                 console.log(e)
37             })
38     })
39 })();

行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将其设为剪贴板。

 

 1 // ==UserScript==
 2 // @name         setClipString2
 3 // @namespace    http://tampermonkey.net/
 4 // @license     GPL version 3
 5 // @encoding    utf-8
 6 // @description  try to take over the world!
 7 // @author       97z4moon
 8 // @include      *
 9 // @icon         https://www.google.com/s2/favicons?domain=tampermonkey.net
10 // @grant        GM_xmlhttpRequest
11 // @grant        GM_download
12 // @run-at      document-end
13 // @version     2.0.0
14 // ==/UserScript==
15 
16 (function() {
17     // Your code here...
18     let ws = new WebSocket('ws://localhost:8088');//实例化websocket
19     let obj = {
20         role: 'Borwser',
21         msg: 'Request connection.'
22     }
23     ws.onopen = function () {
24         console.log("socket has been opend")
25         ws.send(JSON.stringify(obj))
26     }
27     document.addEventListener("copy",function(e){
28         console.log("data: ", window.getSelection(0).toString())
29         obj.clips = window.getSelection(0).toString()
30         obj.msg = 'ClipBoard has been updated.'
31         ws.send(JSON.stringify(obj))
32     })
33 })();

 

行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

 1 let ws = $web.newWebSocket("ws://192.168.31.109:8088", {
 2     eventThread: 'this'
 3 });
 4 console.show();
 5 
 6 let padSid = '';
 7 ws.on("open",(res,ws)=>{
 8     log("WebSocket has been ready...")
 9 }).on("failure",(err,res,ws)=>{
10     log("Connect fail...")
11     ws.close(1000,null)
12     console.hide()
13 }).on("closing",(code,reason,ws)=>{
14     log("WebSocket is closing...")
15 }).on("text",(text, ws)=>{
16     let res = JSON.parse(text)
17     if(res.sid) {
18         padSid = res.sid
19     }
20     console.info("Receive msg: ", res.msg)
21     if(res.clips) {
22         setClip(res.clips)
23     }
24 }).on("binary",(bytes,ws)=>{
25     console.info("Receive binary:")
26     console.info("hex: ",bytes.hex())
27     console.info("base64: ",bytes.base64())
28     console.info("md5: ",bytes.md5())
29     console.info("size: ",bytes.size())
30     console.info("bytes: ",bytes.toByteArray())
31 }).on("closed",(code,reason,ws)=>{
32     log("WebSocket closed: code = %d, reason = %s")
33 })
34 
35 let params = {
36     role: 'Pad',
37     msg: 'Request connection.'
38 }
39 ws.send(JSON.stringify(params));
40 setTimeout(()=>{
41     log("connect not WebSocket...")
42     ws.close(1000,null)
43     console.hide()
44 },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通讯较高的延迟。


WebSocket版演示

IOS 通过 workflow 接收文字

利用上文中node.js提供的http服务,data.json的存储以及workflow的局域网请求。

使用时手动执行快捷指令接收,或者通过siri执行。

例如,当我在pc的浏览器中复制了想要传给iphone的长文字,只需要:

"hey, siri"

"pc剪贴板协同"(快捷指令的名字)

如果siri回答: "完成",此时iphone的剪贴板中,就是我们刚刚从PC复制好的文字了。

- 操作演示

PC与移动设备的图片互传

pc -> phone

首先如果phone要请求PC中图片的话,需要将图片放在node.js的项目目录中,通过node的fs模块获取,并使用public存储该静态资源目录(图片)。

在前几节所示node.js代码中,在express引入语句下新增:

app.use('/public', express.static('public'));

在Auto.js / workflow 中通过HTTP获取该文件,请求路径应为:'http://IPv4 address:port/public/images/xxx.png',

相应的,也应该在node项目的根目录中新建文件夹并命名为 'public',便于维护性分类在public文件夹中新建文件夹 'images',将图片放入其中,请求路径中的 'xxx.png' 应与图片文件名一致。

phone -> pc

与上述node.js节代码的两个app.get()相同,以http请求接收:

app.post('/imagesReceiver', function(req, res) {
	console.log('接收到图片传输请求...')
	console.log(req.host)
	// console.info('接收到数据: ', req)

	var form = new formidable.IncomingForm();   //创建上传表单
	form.encoding = 'utf-8';        //设置编辑
    form.uploadDir = 'public' + '/images/';     //设置上传目录
    form.keepExtensions = true;     //保留后缀
    form.maxFieldsSize = 2 * 1024 * 1024;   //文件大小

    let data = {
		msg: '发送成功!'
	};

    form.parse(req, function(err, fields, files) {
        if (err) {
			res.status(500);
			data.msg = '服务器解析错误...';
			res.json(JSON.stringify(data));
          return;        
        }
		console.log(fields)
       
        var extName = '';  //后缀名
        switch (files.image.type) {
            case 'image/pjpeg':
                extName = 'jpg';
                break;
            case 'image/jpeg':
                extName = 'jpg';
                break;         
            case 'image/png':
                extName = 'png';
                break;
            case 'image/x-png':
                extName = 'png';
                break;         
        }

        if(extName.length == 0){
			res.status(400);
			data.msg = '只支持png和jpg格式图片...';
			res.json(JSON.stringify(data));
            return;                   
        }

        let nd = new Date();
        let dateNow = `${nd.getFullYear()}${nd.getMonth()+1}${nd.getDate()}${nd.getHours()}${nd.getMinutes()}${nd.getSeconds()}${nd.getMilliseconds()}`;

        var imageName = fields.role + dateNow + '.' + extName;
        var newPath = form.uploadDir + imageName;

		res.status(200);
		data.msg = '发送成功...';
		res.json(JSON.stringify(data));
        console.log(newPath);
        fs.renameSync(files.image.path, newPath);  //重命名
    });

})

通过workflow传入(头部可忽略):

Android通过Auto.js传入不再赘述,值得一提的是Auto.js提供了workflow没有的websocket通讯,所以我们仍然可以通过油猴监听浏览器下载图片事件,将下载后的图片直接传入Android设备的相册app中,或者干脆像QQ微信那样直接将一台Android手机的照片通过服务中枢发送到另一台Android手机中。具体实现思路与传文本一致,图片处理代码直接引用本节pc->phone的http中逻辑即可。

执行结果:

- END -

posted @ 2021-09-13 16:54  97z4moon  阅读(3933)  评论(1编辑  收藏  举报
Title