多屏互动——H5中级进阶

作者 : 开心源码 本文共12717个字,预计阅读时间需要32分钟 发布时间: 2022-05-11 共71人阅读

前言

随着智可以硬件的普及,手机,平板,PC甚至路边的电子广告牌,现代浏览器已经无处不在。在浏览器里编织出我们自己的一片天地已经轻车熟路,但是这还不够,H5赋予了浏览器太多的新特性,等待我们去用。这篇文章详情利使用手机浏览器的罗盘API,在PC的浏览器实时地绘制一个3D盒模型。

这种炫酷的玩法叫做“多屏互动”,就像是把手机当做游戏手柄,PC显示器当做电视机,不过这些都是在浏览器里实现的。

先上效果图

多屏互动——H5中级进阶

(测试机是刷了小米系统的裂了屏幕的HTC霹雳2+Chrome浏览器)

源码请戳这里:https://coding.net/u/OverTree/p/webSocketDemo/git

本地测试过程:

  1. 在PC上,用命令 node index.js,自动打开项目主页。(请关闭ADsafe,如有虚拟机,请停使用虚拟网卡)

  2. 创立一个“房间”并自动进入“房间”。

  3. 使用手机扫描“房间”内任意位置的二维码。

  4. 确保手机和PC能相互PING通

ADsafe是个很好使用的去广告软件,但是会阻止本机IP访问,可可以造成项目首页打不开,所以请先暂时关闭

本程序会自动获取本机IP,假如有虚拟网卡,IP地址可可以获取不正确


用户端(浏览器)

1. 手机浏览器端

一个物体在空间内的旋转体位,都能使用一个方向向量(x,y,z)和旋转角度(angle)来表示。也就是CSS3transform的rotate3d(x,y,z,angle)这个函数的4个参数。

想要在浏览器里方便的绘制一个立体模型的的旋转,重点就是利使用手机浏览器的H5新特性去获取手机旋转状态的数据,而后转化成这4个参数。

1.1 重力感应API

devicemotion 顾名思义设施运动

其实不仅仅有重力感应的数据,还有移动加速度,摆动角度。

不过这个接口倾向于运动时瞬间的数据展现,静止时,除了重力加速度,其余数据(移动加速度,摆动角度)基本为0。

window.addEventListener('devicemotion', deviceMotionHandler, true);function deviceMotionHandler(evt){ if(evt.accelerationIncludingGravity){ document.body.innerHTML =

“x轴加速度: ” + evt.accelerationIncludingGravity.x + “

+ “y轴加速度: ” + evt.accelerationIncludingGravity.y + “

+ “z轴加速度: ” + evt.accelerationIncludingGravity.z + “

} if(evt.rotationRate ){ document.body.innerHTML +=

“x轴扭转: ” + evt.rotationRate.beta + “

+ “y轴扭转: ” + evt.rotationRate.gamma + “

+ “z轴扭转: ” + evt.rotationRate.alpha + “

}

}

(魅族老机型,安卓4.4.4的自带浏览器对此API支持不完全,请另外安装QQ浏览器)

在手机浏览器里运行以上代码,并略微晃动,会看到打印数据狂跳。

拿到了数据,接下来开始观察规律。

手机屏幕朝上,水平静止放置,Z轴重力加速度为9.8,Y,X为0。

手机屏幕朝下,水平静止放置,Z轴重力加速度为-9.8,Y,X为0。

手机话筒朝下,竖直静止放置,Y重力加速度为9.8, X,Z为0。

手机话筒朝上,竖直静止放置,Y重力加速度为-9.8, X,Z为0。

手机右侧朝上,竖直静止放置,X重力加速度为9.8, Y,Z为0。

手机左侧朝下,竖直静止放置,X重力加速度为-9.8, Y,Z为0。

那么手机的空间坐标如下图:

多屏互动——H5中级进阶箭头指向都是坐标正方向。

当手机开始倾斜,X,Y,Z轴的加速度分量都有值,且绝对值都小于9.8。根据分量的数值,是能算出手机在三维空间的倾斜状态,只不过这个计算过程复杂,而且在手机运动时,重力加速度的值并不精确表达当前倾斜。一般不使用这个数据去计算手机在三维空间的倾斜。

当手机水平放置,拨动手机,使其慢慢旋转,重力加速度的数据并没有变化。

所以,重力感应的这个API,只可以获取设施当前的倾斜状态,而无法获取设施的旋转方向。而少量简单的功可以,比方摇一摇,晃一晃,即可以使用这个接口去实现。

利使用重力感应的API,能轻松利使用高中数学的反三角函数,实现XY二维平面的旋转,效果如下:

多屏互动——H5中级进阶

代码如下:

function deviceMotionHandler(evt){

var angle =

Math.atan2(

0 – evt.accelerationIncludingGravity.x ,

    evt.accelerationIncludingGravity.y

).toFixed(2) / Math.PI * 180 ;

}

这个 angle 即可以直接应使用在DOM的CSS属性transform:rotate(angle deg)上。

1.2 罗盘API

window.addEventListener('deviceorientation', deviceOrientationHandler, true);

function deviceMotionHandler(evt){

document.body.innerHTML =

“z轴旋转(罗盘方向) alpha:  ” + event.alpha + “

+ “y轴旋转 gamma:  ” + event.gamma + “

+ “x轴旋转 beta:  ” + event.beta

}

重点来了,deviceorientation可以够很好的体现物体在空间中的状态,旋转方向,倾斜角度,无论是静止还是运动或者者加速运动。

这里要和devicemotion 的 evt.rotationRate区分一下,尽管都有alpha,gamma,beta 但是 devicemotion 形容的是旋转变化了的角度值,物体角度变化才会有数据,静止了之后就变为0,而 deviceorientation 的是形容是静止时的角度值。

这三个数值的单位都是deg,如何转化为CSS3transform:rotate3d(x,y,z,angle) 的4个参数,对于没有任何3D知识的前台狗来说是个挺麻烦的问题。

现在要引入一个概念:四元数

四元数是个高阶复数 q = [w,x,y,z]。

四元数的基本数学方程为 :

q = cos (a/2) + i(x * sin(a/2)) + j(y * sin(a/2)) + k(z * sin(a/2)) 其中a表示旋转角度,(x,y,z)表示旋转轴。

四元数表示一个完整的旋转。

四元数能由各轴旋转角(alpha,beta,gamma)求得。

四元数能转换旋转轴(x,y,z)和旋转角度(angle)。

作为初试,本篇并不深入探讨四元数的具体定义,难点是获取四元数[w,x,y,z]。

好在官方提供了旋转角(alpha,beta,gamma)转换成四元数的方法

https://w3c.github.io/deviceorientation/…

在这个页面内搜索 getQuaternion

另外我根据数学公式反求,写了一个四元数转(x,y,z,angle) 的函数 getAcQuaternion

代码如下:

var degtorad = Math.PI / 180;

function getQuaternion( alpha, beta, gamma ) {//官方求四元数方法

var _x = beta ? beta * degtorad : 0; // beta value

var _y = gamma ? gamma * degtorad : 0; // gamma value

var _z = alpha ? alpha * degtorad : 0; // alpha value

var cX = Math.cos( _x/2 );

var cY = Math.cos( _y/2 );

var cZ = Math.cos( _z/2 );

var sX = Math.sin( _x/2 );

var sY = Math.sin( _y/2 );

var sZ = Math.sin( _z/2 );

var w = cX * cY * cZ – sX * sY * sZ;

var x = sX * cY * cZ – cX * sY * sZ;

var y = cX * sY * cZ + sX * cY * sZ;

var z = cX * cY * sZ + sX * sY * cZ;

return [ w, x, y, z ];

}

function getAcQuaternion( _w, _x, _y, _z ) {//我的四元数转旋转轴和旋转角度方法

var rotate = 2 * Math.acos(_w)/degtorad ;

var x = _x / Math.sin(degtorad * rotate/2) || 0;

var y = _y / Math.sin(degtorad * rotate/2) || 0;

var z = _z / Math.sin(degtorad * rotate/2) || 0;

return {x:x,y:y,z:z,rotate:rotate};

}

function deviceMotionHandler(evt){// deviceorientation 事件解决函数

var qu = getQuaternion(evt.alpha,evt.beta,evt.gamma);

var rotate3d = getAcQuaternion(qu[0],qu[1],qu[2],qu[3]);

// rotate3d的参数已经有了,随你解决咯。我是把他送给服务器,交给PC,在PC上显示旋转

}


1.3 校准

这里有个3D里的概念,摄像机位置。我们的PC显示器就是一个摄像机。只可以被动的从某一个角度展现拍摄的景象。正常情况下,手机所在平面应该和显示器所在平面平行,且垂直于地平面的角度。就好比是,摄像机正对着手机正面拍摄。

假如校准的时候手机并没有垂直于地平面,摄像机的位置就不肯定是正前方了。这时候展现的画面并不是水平同步的了。

如下图所示,校准时,手机屏幕朝上。这时候摄像机位置就在天花板上了,你看到的成像就是俯视图。

多屏互动——H5中级进阶

同理,校准时,手机屏幕朝下,这时候摄像机的位置就是在地上,往上拍摄,你看到的成像就是仰视图。

总结起来就是:校准时,手机屏幕朝着哪里,摄像机就在那里拍摄着屏幕,一动不动。

1.4 兼容性

demo的兼容性测试并不理想

在iOS平台上测试良好,且流畅。

在安卓平台上,除了chrome浏览器之外的浏览器,会出现各种问题,主要体现在罗盘数据不精确。

而chrome浏览器并没有扫一扫功可以,由于在国外并不流行这个玩意。所以在安卓平台上就很蛋疼,还要多装一个我查查,才可以完整体验。

(假如出现旋转不精确的问题,能尝试校准罗盘,大概就是拿着手机画8。百度一下方法有很多)

代码假如有兼容写法,或者者有其余兼容问题请赐教,能在coding上私信我(OverTree ),不胜感激。

2. PC浏览器端

PC浏览器的作使用就是可以够显示房间信息,创立房间。

显示房间,创立时间,参加人数,点击进入。

创立一个房间,成功后自动进入房间。

在房间内,接受服务器转发的移动端的消息,并作出相应动作,包括上线,校准,旋转,下线。

上线时,安排就坐(隐藏二维码,显示模型)

校准时,重新设置模型的显示角度。

旋转时,就旋转咯。

下线时,重新显示二维码(显示二维码,隐藏模型)

2.1初始化, 建立ws连接

重点是房间里的事情。所以这里就只详情进入房间发生的事吧。

首先房间参数要正确,至少有房间编号。

房间路由:

/room/[roomNumber]

roomNumber是一串16位随机字符串。

座位路由:

/room/[roomNumber]/[seatNumber]

var uri = win.location.pathname.split('/'),roomNumber;

function initUrlData(){

if(uri.length>=3 && uri[1] == “room”){

roomNumber = uri[2];

document.title = “虚拟房间 “+ roomNumber + “号”

return 1;

}else{

window.location.href = “/index”;

return 0;

}

}

function initWebSocket(){

var wsUri = “ws://”+ window.location.hostname +”:”+”/ws/room”; //这里使用了一个ejs的占位符,已便在服务器更改websocket端口时能及时用正确端口。

var websocket = new WebSocket(wsUri);

websocket.onopen = function(evt) {

websocket.send(JSON.stringify({room:roomNumber}));

}; //链接建立后,发送一个消息,表明在哪个房间

websocket.onclose = function(evt) {

};

websocket.onmessage = function(evt) {

parseMessage(evt.data) //解析数据

};

websocket.onerror = function(evt) {

};

//绑定了这些解决函数之后,websocket开始建立链接,而不是 New 的时候开始建立

}

$(“.room-place .qrcode”).each(function(index,item){

$(item).qrcode({

“size”: 200,

“color”: “#3a3”,

“text”: window.location.origin + “/room/” + roomNumber + “/” + (index+1)

});

//这里使用jQuery的插件,jquery-qrcode 按照座位路由初始化二维码

})

2.2 纯CSS3立体模型

做为一名普通的前台人员,想要画一个3D的模型,按照最熟习的方法就是使用CSS3了。

(假如是使用Three.js的大神请跳过本节)

不过要很快画出一个六面体出来,还是需要想一想的,毕竟这个技可以很少使用。

画一个长方体

前span>figure>

后span>figure>

右span>figure>

左span>figure>

顶span>figure>

底span>figure>

div>

section>

*{

margin: 0; /*不加会歪*/

}

.container {

width: 300px;

height: 200px;

position: relative;

perspective: 1200px; /*摄像机距离,设置小的的话,立方体显示会变形*/

}

#box figure {

display: block;

position: absolute;

border: 2px solid black;

line-height: 200px;

font-size: 40px;

text-align: center;

font-weight: bold;

color: white;

box-sizing: border-box; /*由于有2px宽的border,假如不设置为此值,那么每个面的宽高都要少设置4个像素,才可以对齐*/

}

#box {

width: 100%;

height: 100%;

position: absolute;

transform-style: preserve-3d;/*这个很重要,默认是平面变形flat*/

}

#box .front,

#box .back {

width: 300px;

height: 200px;

}

#box .right,

#box .left {

width: 100px;

height: 200px;

left:100px; /*调整*/

}

#box .top,

#box .bottom {

width: 300px;

height: 100px;

top:50px; /*调整*/

line-height:100px;

}

/*给每个面上半透明的颜色*/

#box .front { background: hsla( 000, 100%, 50%, 0.7 ); }

#box .back { background: hsla( 160, 100%, 50%, 0.7 ); }

#box .right { background: hsla( 120, 100%, 50%, 0.7 ); }

#box .left { background: hsla( 180, 100%, 50%, 0.7 ); }

#box .top { background: hsla( 240, 100%, 50%, 0.7 ); }

#box .bottom { background: hsla( 300, 100%, 50%, 0.7 ); }

#box .front { /*这个距离乘以2为前后面的距离*/

transform: translateZ( 50px );

}

#box .back { /*front面沿着x轴旋转180度,做后面*/

transform: rotateX( -180deg ) translateZ( 50px );

}

#box .right { /*这个距离乘以2为左右面的距离*/

transform: rotateY( 90deg ) translateZ( 150px );

}

#box .left { /*front面沿着y轴旋转90度,做侧面*/

transform: rotateY( -90deg ) translateZ( 150px );

}

#box .top { /*这个距离乘以2为长方体高*/

transform: rotateX( 90deg ) translateZ( 100px );

}

#box .bottom { /*front面沿着x轴旋转90度,做底面*/

transform: rotateX( -90deg ) translateZ( 100px );

}

style>

对这样的css有什么要吐槽的么?

这样的stylesheet简直是刀耕火种时期的

假如使用sass写法,那么只要要写一次#box和多层嵌套即可以了。

效果如下:

多屏互动——H5中级进阶

假如我们用webGL去绘制的话,导入少量现成的3D模型,无论物体还是人物,都能360度无死角的玩弄于手掌了。

(假如有苍老师的模型,想想还有点小激动呢,VR的感觉说来就来啊 – -)

接下来就是等待来自移动端的旋转信息,x,y,z,angle,使#box进行transform旋转就是了。

$seat.find(“#box”).

css(“transform”,”rotate3d(“

+ (-parseFloat(content.x))+”,” //取反

+ (+parseFloat(content.y))+”,”

+ (-parseFloat(content.z))+”,” //取反

+ content.rotate +”deg)”);

不取反的话,旋转是错误的。我曾屡次尝试给不同的坐标取反,最终得出这个取反方法,是唯一显示正常的组合。

无法了解这两个取反,猜测是由于css的x,y,z的坐标和物理设施x,y,z的坐标方向有差异吧。毕竟显示器是平面的,他的x,y,z的定义不可以和手机传感器一致。

2.3 校准

PC端的校准就简单多了,在#box外套一层div.adjust。

当接受来自移动端的校准信息 x,y,z,angle,设置外套的 div.adjust 的旋转为 x,y,z,-angle 就好了。

$seat.find(“.adjust”).

css(“transform”,”rotate3d(“

+ (-parseFloat(content.x))+”,”

+ (+parseFloat(content.y))+”,”

+ (-parseFloat(content.z))+”,”

+ (-parseFloat(content.rotate)) +”deg)”); //取反

当然,这个adjust的样式至少包含以下样式

.adjust{

position: absolute;

transform-style:preserve-3d;

}


2.4 兼容性

PC端的兼容性就好多了,只需是现代H5浏览器基本上没有兼容性问题。

服务端

1.数据结构

这个服务只做临时数据的保存和消息转发。

临时数据:比方,各端的webSocket连接句柄,房间信息等,我把它们放在global全局对象下,就好比是共享内存,访问方便,速度快。

global.ShareMem = {

rooms:{

“12345678”:{ //房间号做为key,方便查找

player:[{socket:connection,place:place}], //移动端数组:连接句柄,座位号

projector:[], //PC端数组

id:”12345678″,

startTime:Date.now(),

maxplayer:2, //最多座位数

type:”ddd” //房间类型

}

}

};


2.webServer

假如您是nodejs的大神,或者者在使用koajs、express等nodejs框架,请跳过本大节。由于我使用原生的nodejs写了一遍webServer,尽管重复造轮子不好,但是复习复习webServer的基本知识,还是不错的,本节适合新手入门。

包含知识点:header解析,静态文件查找,gzip,文件hash计算,状态码。

2.1 目录结构

/API

/funMap.js/*http功可以函数集合*/

/xxx.js

/socketAPI

/funMap.js /*webSocket功可以函数集合*/

/xxx.js

/Util/*工具目录,获取本地IP,打开默认浏览器*/

/webRoot

/common /*公共资源目录*/

/js

/lib

/css

/m /*手机端html,js,css等*/

/p /*PC端html,js,css等*/

/index.js /*入口文件*/

/config.js/*配置文件,端口号,ws最大数据包大小等*/

/socketServer.js/*webSocket解决函数*/

/webServer.js

2.2 webServer

基本规则是这样的,搭建静态服务器,静态资源正常读取返回,html文件使用ejs渲染后返回。

因为ejs的起因,html文件并没有被修改,但是渲染后的内容被修改,比方,更改了ws的端口,但是html文件没有修改。所以不可以用Last-Modified来判断是文件能否最新,而是要根据返回内容有没有被改变来判断,所以要使用Etag。

Etag需要根据内容算出hash值,一般使用md5计算。

返回内容之前,需要进行gzip压缩,使用来节省带宽。90KB的jquery.min.js能被gzip到30KB,压缩才是王道。

由于移动端和PC端执行的是完全不同的代码,所以要判断从用户端传过来的user-agent能否包含Mobile字符串,以来区分用户端是PC还是手机,以便返回正确的资源。

通过简单的商定,来区分静态文件和REST请求

if (libPath.extname(pathName) == “”) {

//假如路径没有扩展名

if(params.length<=2){

pathName += “/”; //访问根目录

}else if(params[1]==”api”){ //访问以api开头

parseAPI(params,req,res); //功可以函数

return ;

}else{

pathName = params[1]+”.html”;

}

}

我在这里做了一个简单的框架,在API目录或者者socketAPI目录下新添加js文件,一个js文件对应一个解决函数,而后在funMap.js中聚合为一个Map,方便查找函数,也容易隔离和修改函数名。

var funMap = {

“room”:require(“./room”),

“changeName”:require(“./xxx”),

“changeName2”:require(“./xxxyyy”)

};

module.exports = funMap;

用户端访问时即可以通过 /api/[functionName] 来访问想要的服务了。

3 webSocketServer

nodejs本身并没有提供webSockerServer的板块,所以需要另外安装一个。

在npm install的时候会安装一个ws板块,require(“ws”) 即可以使用了。使用法与http板块类似,都使用 createServer({options},MainHandlerFunction) 创立服务,只是ws多了几个参数。

主要是port,注意不要和webserver端口重复。

还有一个 maxPayload 就是单个ws数据包最大大小,单位是bytes,自己预计项目传输数据时候数据包大小。默认值是65535 即 64KB。一般webSocket使用于小包传输,不使用太大,我设置了1024 , 1KB。

主解决函数MainHandlerFunction,在有用户端连接进来时会传入一个参数connection,这个对象内容非常丰富,不看手册,能打印出来也慢慢研究。

成功建立连接的方法就是要connection绑定message方法。

因为wsSocket访问是能带着url的,所以我们能使用url隔离不同的功可以函数,而不是去解析message主体。

var connectHandler = function(connection){

// :4002/api/Function1

var URIarray = connection.upgradeReq.url.split(“/”)

if(funMap[URIarray[2]]){

funMap[URIarray[2]](connection);

}else{

connection.send(“{err:Function Not Found!!}”);

}

}


3.1 消息,广播,保活

每当有ws连接进来,都有相似文件形容符的id来区分每个不同的连接。

connection._ultron.id 使用它能区分自己与别人的连接,很有使用。

//消息格式

function msgPack(){

return JSON.stringify({

“who”:arguments[0], // Mobile , PC

“place”:arguments[1], // 座位

“dowhat”:arguments[2], // “connect”,”ready”,”message”,”lost”

“content”:arguments[3]||”” // 内容

})

}

//以room为单位广播,广播房间内所有角色

function boradCast(room,msg,ignore){

room.projector.forEach(function(item,index){

if(ignore&amp;&amp;ignore._ultron.id===item.socket._ultron.id){

// console.log(“ignore!!!”)

// 忽略自己不发送给自己

}

else{

try{

item.socket.send(msg);

}catch(e){

console.log(e);

}

}

});

room.player.forEach(function(item,index){

if(ignore&amp;&amp;ignore._ultron.id===item.socket._ultron.id){

// console.log(“ignore!!!”)

// 忽略自己不发送给自己

}

else{

try{

item.socket.send(msg);

}catch(e){

console.log(e);

}

}

});

}

为了检查用户端能否掉线,在建立连接时手动加入保活机制,方法很简单:

给用户端发送空消息时lastkeeplife为1,只需用户端返回任意消息,那么升级lastkeeplife为0,假如5秒之内,没有任何回复判定为掉线。

假如用户端掉线,那么关闭连接,从连接池中移除。并广播掉线消息给房间内其余角色。

var keeplifeHandler = setInterval(function(){

if(lastkeeplife == 0){

connection.close();

connection.emit(“close”);

clearInterval(keeplifeHandler);

}

try{

lastkeeplife = 0;

connection.send(“{}”);

}catch(e){

console.log(“keep live error! “+ e +”\n\n”);

connection.close();

connection.emit(“close”);

clearInterval(keeplifeHandler);

}

},5000)

connection.on('close',function(msg){

if(keeplifeHandler){ //关闭保活循环

clearInterval(keeplifeHandler);

}

console.log(“close!”,roomid,place);

var room = global.ShareMem.rooms[roomid];

if(!room)

return;

//从连接池移除连接句柄

if(platform === PC){

room.projector.forEach(function(item,index){

if(item.socket === connection){

room.projector.splice(index,1);

return false;

}

})

}else{

room.player.forEach(function(item,index){

if(item.socket === connection){

room.player.splice(index,1);

return false;

}

})

}

//发送掉线消息

boradCast( room, msgPack(platform,place,”lost”) , connection );

});

iOS设施假如锁屏,会发送断开信息给服务器,而安卓不会。想要断开链接,必需等到默认120秒超时后关闭。

ws初始化时并没有提供初始化timeout的配置。通过修改

ws._server.timeout = 1000;//1秒超时

并不会生效。问题来了,怎样修改才可以设置超时时间呢?

目前只可以使用上述比较捉急的方法来及时断开掉线设施。


最后

多屏互动已经不是新鲜的东西了,我做这个Demo还是受chrome试验室一个叫做【光剑出鞘】的项目的启发。由于体验时需要移动端和PC同时翻-墙,导致体验差,而后自己才想做一个。做出来的时候感觉好酷炫,好神奇,好兴奋。

后续还是有很多能拓展和改进的,希望最终能变为一个成熟的产品,而不是仅仅止步于Demo。

―――――――END―――――――

相关阅读

无需Flash实现图片裁剪——HTML5中级进阶

5个提高Node.js应使用性可以的技巧

浏览器存储及用

――――――――――――――

作者信息

作者来自力谱宿云 LeapCloud 团队_UX成员:王诗诗 【原创】

首发地址:https://blog.maxleap.cn/archives/985

王诗诗(John王),前台新人,专职前台工作两年。曾供职于AMI做底层软件开发。喜欢分析H5代码,追崇使用简单的CSS,构建精美动效,做前台之前,这些是业余爱好。现任职于MaxLeap UX 组,负责MaxWon 的开发和维护。现热衷于Real-time WebApp。

若有转载需要,请转发时注意自带作者信息一栏并本自媒体公号:力谱宿云 LeapCloud,尊重原创作者的劳动成果~ 谢谢配合~

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 多屏互动——H5中级进阶

发表回复