owCode 是高效、高性能的拖拽式低代碼開(kāi)發(fā)平臺(tái). 也是筆者最近一直在研究的方向, 對(duì)于可視化搭建平臺(tái)的實(shí)現(xiàn)方案筆者之前寫過(guò)很多文章, 這里帶大家探索一個(gè)新方向——基于自然流布局的可視化搭建平臺(tái).
在我們之前實(shí)現(xiàn)的 h5-dooring 搭建平臺(tái)中, 我們采用了網(wǎng)格布局的方式來(lái)實(shí)現(xiàn)拖拽生成H5頁(yè)面或者Web app, 其好處就是靈活簡(jiǎn)單, 用戶基本沒(méi)有任何使用成本, 在前端層也能做一定的橫向擴(kuò)展, 但是存在幾個(gè)缺陷:
雖然通過(guò)改造可以實(shí)現(xiàn)層和嵌套的問(wèn)題, 最近也在努力往這個(gè)方向?qū)崿F(xiàn)(雖然和設(shè)計(jì)初衷相悖, dooring的初衷是抹去層和嵌套的概念, 讓搭建扁平化和智能化, 所以沒(méi)有采用自由布局的方案)
但是如果一定要實(shí)現(xiàn)嵌套和層的功能, 有沒(méi)有另一種更簡(jiǎn)單的方案呢? 筆者目前想到了兩種解決方案:
因?yàn)榈谝环N方案筆者在dooring的早期已經(jīng)實(shí)現(xiàn)過(guò)一版, 最后棄用采用了網(wǎng)格布局, 所以說(shuō)我們來(lái)探討一下第二種方案的實(shí)現(xiàn).
自然流布局的好處就是我們不用通過(guò)定位的方式來(lái)限定元素的位置等信息, 而是以html文檔流的方式來(lái)布局元素, 并且用戶可以靈活的設(shè)置元素的層級(jí)(layer)和偏移(transform), 接下來(lái)我們來(lái)看看簡(jiǎn)單的實(shí)現(xiàn)效果.
H5建站, 頁(yè)面制作
H5制作, H5編輯器
由上圖的demo我們可以發(fā)現(xiàn)組件在畫布中的布局完全是默認(rèn)的文檔流的方式, 所以我們有更靈活的布局實(shí)現(xiàn).
具體實(shí)現(xiàn)思路主要分以下幾個(gè)部分:
第一點(diǎn)和第三點(diǎn)我們?cè)?H5-dooring中已經(jīng)實(shí)現(xiàn)了, 感興趣的可以看我之前的文章, 我們這里重點(diǎn)來(lái)實(shí)現(xiàn)畫布區(qū)拖拽, 也是比較核心的環(huán)節(jié).
拖放(Drag 和 drop)是 HTML5 標(biāo)準(zhǔn)的組成部分, 早已被大多數(shù)瀏覽器支持. 我們目前使用的拖放插件基本上基于 H5 拖放 API 來(lái)實(shí)現(xiàn)的, 其實(shí)實(shí)現(xiàn)第一點(diǎn)組件區(qū)拖拽至畫布我們完全可以用原生來(lái)實(shí)現(xiàn), 這里筆者簡(jiǎn)單來(lái)介紹以下.
首先我們來(lái)看看一個(gè)完整的拖放過(guò)程:
有了以上3個(gè)步驟, 我們就能實(shí)現(xiàn)第一點(diǎn)的需求, 筆者寫個(gè)簡(jiǎn)單demo來(lái)給大家參考一下:
<script type="text/javascript">
function allowDrop(ev) {
ev.preventDefault();
}
function drag(ev){
ev.dataTransfer.setData("Text",ev.target.id);
}
function drop(ev){
ev.preventDefault();
let data=ev.dataTransfer.getData("Text");
ev.target.appendChild(document.getElementById(data));
}
</script>
<div id="box" ondrop="drop(event)" ondragover="allowDrop(event)"></div>
<img id="drag" src="dooring.png" draggable="true" ondragstart="drag(event)" width="336" height="69" />
也就是對(duì)應(yīng)的我們的組件拖放區(qū)域, 如下圖所示:
因?yàn)橹暗陌姹疚覀儾捎昧司W(wǎng)格布局來(lái)實(shí)現(xiàn)智能拖拽, 由于內(nèi)部定位機(jī)制采用的是絕對(duì)定位(absolute), 所以是實(shí)現(xiàn)層級(jí)和固定組件比較困難, 如果組件的呈現(xiàn)完全脫離了定位的束縛, 我們就可以實(shí)現(xiàn)以上的困境了. 所以這里我們調(diào)研了一種方案——拖拽排序機(jī)制.
自然流布局的規(guī)律就是默認(rèn)情況下html頁(yè)面是基于dom出現(xiàn)的順序來(lái)排列的, 也就是我們說(shuō)的堆疊.
H5制作
我們可以遵循這樣的設(shè)計(jì), 通過(guò)排序的方式改變組件的位置從而實(shí)現(xiàn)自然流布局的頁(yè)面搭建.
那么我們?cè)倩氐缴厦嬲f(shuō)的布局問(wèn)題, 比如說(shuō)要想實(shí)現(xiàn)柵格化布局, 我們只需要定義一個(gè)flex容器, 將組件拖拽到容器里就好了, 這樣也就解決了嵌套的問(wèn)題. 同時(shí)我們還可以設(shè)計(jì)嵌套容器的柵格數(shù), 這樣就可以實(shí)現(xiàn)類似如下的效果:
H5編輯器
拖拽排序的庫(kù)我們可以使用: sortable Vue.Draggable * react-dnd
還有很多優(yōu)秀的庫(kù), 這里就不一一舉例了.
其實(shí)在上面的實(shí)現(xiàn)思路中我們已經(jīng)解決了嵌套的問(wèn)題了, 即提供拖放的容器組件, 利用筆者在上文中介紹的拖放api即可實(shí)現(xiàn). 對(duì)于組件層級(jí)來(lái)說(shuō), 因?yàn)槲覀儾捎玫氖亲匀涣鞑季? 所以我們可以輕松的設(shè)置元素的定位屬性, 比如我們提供一個(gè)定位的設(shè)置:
拖拽搭建HTML5
關(guān)于如何設(shè)計(jì)一個(gè)動(dòng)態(tài)的屬性編輯器, 筆者之前文章中也就詳細(xì)地介紹, 大家可以參考:
以上就是自然流布局的基本實(shí)現(xiàn)方式, 后續(xù)筆者也會(huì)在github上同步我們最新的成果.
H5-Dooring編輯器wiki: https://github.com/MrXujiang/h5-Dooring/wiki
覺(jué)得有用 ?喜歡就收藏,順便點(diǎn)個(gè)贊吧,你的支持是我最大的鼓勵(lì)!搜 “趣談前端”,發(fā)現(xiàn)更多有趣的H5游戲, webpack,node,gulp,css3,javascript,nodeJS,canvas數(shù)據(jù)可視化等前端知識(shí)和實(shí)戰(zhàn).
話不說(shuō),先上效果吧。
Axure軟件其實(shí)是一款原型設(shè)計(jì)工具,可以設(shè)置網(wǎng)頁(yè)、app等原型,由于它基于html構(gòu)架,其中又包含了一些基本的函數(shù),所以我們可以通過(guò)制作一些游戲更加熟練地使用、了解這些函數(shù)。本文之后將根據(jù)我的另一篇文章中《以「用戶為中心」的產(chǎn)品設(shè)計(jì)詳述》提到的「五大用戶體驗(yàn)要素」一一講解構(gòu)建過(guò)程。
通過(guò)使用Axure制作一些小游戲,將多種函數(shù)及邏輯關(guān)系設(shè)定其中,達(dá)到增強(qiáng)自己邏輯思維能力及熟練使用Axure的目的。
本例準(zhǔn)備制作一款「人機(jī)乒乓球」游戲,基本游戲規(guī)則是:玩家通過(guò)移動(dòng)桌面底部球拍,是乒乓球在下落時(shí)正好落到球拍上,然后反彈乒乓球。
本例要實(shí)現(xiàn)的基本功能包括:
通過(guò)「范圍層」的梳理,我們可以簡(jiǎn)單設(shè)置出整個(gè)游戲的基本流程圖:
在游戲中,乒乓球移動(dòng)是最重要的,所以我們第一個(gè)考慮的是「循環(huán)」,通過(guò)獲得一個(gè)恒定的循環(huán)時(shí)間,控制乒乓球恒定速度移動(dòng),但是因?yàn)槲覀冇挚梢赃x擇乒乓球移動(dòng)速度,所以我們需要得到一個(gè)基準(zhǔn)速度v,然后在基準(zhǔn)速度上直接按倍數(shù)增加移動(dòng)速度。
首先在頁(yè)面載入時(shí)設(shè)置動(dòng)態(tài)面板「bit_time」和「bit_ball」向后循環(huán),循環(huán)間隔1毫秒,動(dòng)態(tài)面板「database」中的「time_begin」獲得系統(tǒng)載入時(shí)的時(shí)間戳。
在動(dòng)態(tài)面板「bit_time」中,我們?cè)O(shè)置「database」動(dòng)態(tài)面板中的「time_now」獲得系統(tǒng)現(xiàn)在時(shí)刻的時(shí)間戳,「time_bit」=time_now-time_begin。
此時(shí)「time_bit」就是一個(gè)以毫秒為單位不斷增長(zhǎng)的數(shù)值,它代表著系統(tǒng)現(xiàn)在時(shí)刻與之前time_begin的時(shí)間差值。
之后我們?cè)凇竧ime_ball」動(dòng)態(tài)面板設(shè)置觸發(fā)條件:當(dāng)「time_bit」>=level(level為小球速度等級(jí),默認(rèn)50)時(shí),「time_begin」重新賦值為當(dāng)前系統(tǒng)時(shí)間。這時(shí)「time_ball」就形成了一個(gè)每50毫秒自動(dòng)循環(huán)運(yùn)行的程序,如果level為100時(shí),「time_ball」就會(huì)每100毫秒自動(dòng)出發(fā)一次。
以上我們就獲得了一個(gè)可以控制的定時(shí)循環(huán)機(jī)制,其它功能都是在這個(gè)機(jī)制上實(shí)現(xiàn)的。
1. 加載初始化實(shí)現(xiàn)
第一步永遠(yuǎn)是最難的,我在這里設(shè)置了6個(gè)全局變量,方便對(duì)整個(gè)游戲的配置。
看了上述這些全局變量你可能還不明白,但請(qǐng)你一定要記住,因?yàn)槊恳粋€(gè)都非常重要之后我會(huì)詳細(xì)介紹。
除了初始化全局變量外,還需要初始化以下數(shù)據(jù):
2. 設(shè)置游戲難度
游戲難度體現(xiàn)在乒乓球移動(dòng)速度上,當(dāng)難度越高時(shí)速度越快,本例中共有5個(gè)等級(jí),默認(rèn)為中間等級(jí)。當(dāng)玩家選擇對(duì)應(yīng)等級(jí)時(shí),系統(tǒng)將等級(jí)賦值給全局變量Level,通過(guò)上文的「time_ball」控制乒乓球移動(dòng)速度,難度越高,Level值越低,則「time_bit」越短,乒乓球越快。
3. 游戲?qū)崟r(shí)顯示分?jǐn)?shù)
游戲分?jǐn)?shù)需要實(shí)時(shí)更新,筆者開(kāi)始通過(guò)判斷乒乓球接觸球拍且能成功返回時(shí),分?jǐn)?shù)增加,但是實(shí)現(xiàn)起來(lái)有些bug,所以舍棄。
本例中通過(guò)計(jì)算乒乓球移動(dòng)桌面的次數(shù)實(shí)現(xiàn)分?jǐn)?shù)增加,當(dāng)乒乓球從最上面到最下面時(shí)或從最下面移到最上面時(shí)(恒定移動(dòng)6次,后文詳述),score1自增,分?jǐn)?shù)=score*score1/6.
4. 游戲暫停及恢復(fù)
通過(guò)全局變量game_status的值控制。
當(dāng)game_status=begin時(shí),所有與游戲進(jìn)行的相關(guān)功能才可以使用,例如乒乓球移動(dòng)、分?jǐn)?shù)增加、移動(dòng)球拍等,所以當(dāng)點(diǎn)擊「暫停」時(shí),game_status=pause,乒乓球即自動(dòng)停止運(yùn)動(dòng)。
5. 乒乓球移動(dòng)速度
整個(gè)桌面大小為600*600.為了制作方便,乒乓球固定每次在y軸移動(dòng)angle==100距離,通過(guò)每次移動(dòng)的時(shí)間間隔控制乒乓球移動(dòng)速度。如初始等級(jí)正常,level==50時(shí),乒乓球每次移動(dòng)時(shí)間間隔為50毫秒。
6. 乒乓球隨機(jī)移動(dòng)方向
為了使游戲逼真,每次從上桌面彈出的乒乓球角度均為隨機(jī)值,本例為了制作方便,設(shè)置乒乓球初始從(300,0)坐標(biāo)開(kāi)始向下移動(dòng),如果碰到球拍,乒乓球按照原方向返回(300,0)坐標(biāo)。
本例目前控制乒乓球初始移動(dòng)角度不會(huì)彈到左、右桌壁(即初始角度較小)。
本例通過(guò)函數(shù)location_x=[[Math.tan((Math.random()*40-20)/57)]]得到初始移動(dòng)X軸方向移動(dòng)位移,乒乓球每次X軸的移動(dòng)距離為location_x*angle.
7. 球拍觸碰乒乓球反彈
球拍的寬度恒定為120,所以判斷當(dāng)乒乓球移動(dòng)到桌面底部時(shí),如果「-120<球拍.x-乒乓球.x<120」時(shí),則判斷此次成功碰到乒乓球,改變乒乓球y軸移動(dòng)全局變量angle=-angle,乒乓球反彈。
根據(jù)之前邏輯梳理,我們需要制作以下幾個(gè)動(dòng)態(tài)面板:
游戲桌面面板:包括首頁(yè)、游戲中、游戲結(jié)束狀態(tài)
設(shè)置面板:
信息欄面板:
bit_time:獲得基準(zhǔn)時(shí)間值
bit_ball:控制小球移動(dòng)
bit_down:判斷小球接觸球拍時(shí)成功或失敗
bit_status_up:小球觸碰上端桌面時(shí)隨便方向向下移動(dòng)
bit_score:獲得實(shí)時(shí)分?jǐn)?shù)
以下為最初設(shè)計(jì)的三個(gè)頁(yè)面效果圖
雖然看似是一個(gè)很簡(jiǎn)單的小游戲,但在制作過(guò)程中卻遇到了很多難題,有的可以直接解決,有的卻要花費(fèi)很大精力繞道實(shí)現(xiàn)。不過(guò)當(dāng)完成這款游戲后,相信你對(duì)各種通過(guò)循環(huán)功能實(shí)現(xiàn)的頁(yè)面輪播圖等功能實(shí)現(xiàn)會(huì)輕松自如了,而通過(guò)全局變量去控制各種邏輯狀態(tài)、通過(guò)鍵盤控制面板移動(dòng)等也是信手拈來(lái)了。
本游戲目前還十分簡(jiǎn)單,如果有時(shí)間,我近期會(huì)做如下改進(jìn):
自動(dòng)識(shí)別屏幕,使桌面寬度為屏幕寬度,且乒乓球及球拍大小也會(huì)對(duì)應(yīng)變換
乒乓球可以撞擊桌面左、右兩邊
通過(guò)中繼器儲(chǔ)存每次稱號(hào)及分?jǐn)?shù),并在結(jié)束后實(shí)時(shí)顯示
優(yōu)化速度控制方式,使手機(jī)端更加流暢
增加更多不確定因素
歡迎大家隨時(shí)交流,謝謝!
本文由 @escher 原創(chuàng)發(fā)布于人人都是產(chǎn)品經(jīng)理。未經(jīng)許可,禁止轉(zhuǎn)載。
題圖來(lái)自 Pixabay,基于 CC0 協(xié)議
去年曾寫過(guò)一個(gè)類似的拼拼樂(lè)小游戲,技術(shù)棧采用自己的Xuery框架和原生javascript實(shí)現(xiàn)的,腳手架采用gulp來(lái)實(shí)現(xiàn),為了滿足對(duì)vue的需求,這次我使用vue生態(tài)將其重構(gòu),腳手架采用比較火的vue-cli。
為了加深大家對(duì)vue的了解和vue項(xiàng)目實(shí)戰(zhàn),筆者采用vue生態(tài)來(lái)重構(gòu)此項(xiàng)目,方便大家學(xué)習(xí)和探索。技術(shù)棧如下:
因?yàn)樵搼?yīng)用屬于H5游戲,為了輕量化項(xiàng)目, 我沒(méi)有采用第三方ui庫(kù), 如果大家想采用基于vue的第三方移動(dòng)端ui庫(kù),推薦如下:
以上筆者推薦的都是社區(qū)比較完善,bug比較少的組件庫(kù),大家可以感受一下。
回到我們的小游戲開(kāi)發(fā),我們更多的是javascript和css3的掌握程度,在學(xué)習(xí)完這篇文章之后相信大家對(duì)javascript和css3的編程能力都會(huì)有極大的提升,后面還會(huì)介紹如何使用canvas實(shí)現(xiàn)生成戰(zhàn)績(jī)海報(bào)圖的功能。
我們先來(lái)看看游戲的預(yù)覽界面:
本文的算法實(shí)現(xiàn)方式在之前的拼拼樂(lè)文章中已經(jīng)說(shuō)明,這里主要介紹核心算法, 至于vue-cli的使用方法,筆者之前也寫過(guò)對(duì)應(yīng)的文章,大家可以研究學(xué)習(xí)一下。vue-cli搭建項(xiàng)目方式如下:
// 安裝
yarn global add @vue/cli
// 創(chuàng)建項(xiàng)目
vue create pinpinle
// 進(jìn)入項(xiàng)目并啟動(dòng)
cd pinpinle && yarn start
關(guān)于vue-cli3配置實(shí)戰(zhàn),可以移步我之前的文章: 一張圖教你快速玩轉(zhuǎn)vue-cli3
目前我主要整理了如下核心功能,接下來(lái)會(huì)一一帶大家實(shí)現(xiàn):
文件上傳預(yù)覽主要采用FileReader API實(shí)現(xiàn),原理就是將file對(duì)象傳給FileReader的readAsDataURL然后轉(zhuǎn)化為data:URL格式的字符串(base64編碼)以表示所讀取文件的內(nèi)容。 具體代碼如下:
// 2.文件上傳解析
let file=$('#file');
file.on('change', function(e){
var file=this.files[0];
var fileReader=new FileReader();
// 讀取完成觸發(fā)的事件
fileReader.onload=function(e) {
$('.file-wrap')[0].style.backgroundImage='url(' + fileReader.result + ')';
imgSrc=fileReader.result;
}
file && fileReader.readAsDataURL(file);
})
一般我們處理這種拼圖游戲都會(huì)有如下方案: 用canvas分割圖片 采用n張不同的切好的切片圖片(方法簡(jiǎn)單,但是會(huì)造成多次請(qǐng)求) * 動(dòng)態(tài)背景分割
經(jīng)過(guò)權(quán)衡,筆者想出了第三種方法,也是自認(rèn)為比較優(yōu)雅的方法,即動(dòng)態(tài)背景分割,我們只需要使用1張圖片,然后利于css的方式切割圖片,有點(diǎn)經(jīng)典的雪碧圖的感覺(jué),如下:
本質(zhì)就是我們?cè)O(shè)置九個(gè)div,每個(gè)div都使用同一張圖片,并且圖片大小等于游戲畫布大小,但是我們通過(guò)backgroundPosition(背景定位)的方式來(lái)實(shí)現(xiàn)切割圖片。這樣做的另一個(gè)好處是方便我們實(shí)現(xiàn)洗牌邏輯。
洗牌邏輯依托于隨機(jī)算法,這里我們結(jié)合坐標(biāo)系,實(shí)現(xiàn)一個(gè)隨機(jī)生成二維坐標(biāo)系的邏輯,然后通過(guò)改變每個(gè)切片的translate位置,配合過(guò)渡動(dòng)畫,即可實(shí)現(xiàn)洗牌功能和洗牌動(dòng)畫。
數(shù)組亂序比較簡(jiǎn)單,代碼如下:
// 數(shù)組亂序
function upsetArr(arr) {
arr.sort(function(a,b){
return Math.random() > 0.5 ? -1 : 1
})
}
洗牌邏輯基于數(shù)組亂序,具體邏輯如下:
// 洗牌方法
function shuffle(els, arr) {
upsetArr(arr);
for(var i=0, len=els.length; i< len; i++) {
var el=els[i];
el.setAttribute('index', i); // 將打亂后的數(shù)組索引緩存到元素中
el.style.transform='translate(' + arr[i].x + 'vw,' + arr[i].y + 'vh'+ ')';
}
}
n維矩陣主要用來(lái)做洗牌和計(jì)算成功率的,具體實(shí)現(xiàn)如下:
// 生成n維矩陣坐標(biāo)
function generateMatrix(n, dx, dy) {
var arr=[], index=0;
for(var i=0; i< n; i++) {
for(var j=0; j< n; j++) {
arr.push({x: j*dx, y: i*dy, index: index});
index++;
}
}
return arr
}
置換算法主要用來(lái)切換拼圖的,比如用戶想移動(dòng)拼圖,可以用置換來(lái)實(shí)現(xiàn):
// 數(shù)組置換
function swap(arr, indexA, indexB) {
let cache=arr[indexA];
arr[indexA]=arr[indexB];
arr[indexB]=cache;
}
生成戰(zhàn)績(jī)海報(bào)筆者采用canvas來(lái)實(shí)現(xiàn),對(duì)于canvas的api不熟悉的可以查看MDN,講的比較詳細(xì)。這里筆者簡(jiǎn)單實(shí)現(xiàn)一個(gè)供大家參考:
function generateImg() {
var canvas=document.createElement("canvas");
if(canvas.getContext) {
var winW=window.innerWidth,
winH=window.innerHeight,
ctx=canvas.getContext('2d');
canvas.width=winW;
canvas.height=winH;
// 繪制背景
// ctx.fillStyle='#06c';
var linear=ctx.createLinearGradient(0, 0, 0, winH);
linear.addColorStop(0, '#a1c4fd');
linear.addColorStop(1, '#c2e9fb');
ctx.fillStyle=linear;
ctx.fillRect(0, 0, winW, winH);
ctx.fill();
// 繪制頂部圖像
var imgH=0;
img=new Image();
img.src=imgSrc;
img.onload=function(){
// 繪制的圖片寬為.7winW, 根據(jù)等比換算繪制的圖片高度為 .7winW*imgH/imgW
imgH=.6*winW*this.height/this.width;
ctx.drawImage(img, .2*winW, .1*winH, .6*winW, imgH);
drawText();
drawTip();
drawCode();
}
// 繪制文字
function drawText() {
ctx.save();
ctx.fillStyle='#fff';
ctx.font=20 + 'px Helvetica';
ctx.textBaseline='hanging';
ctx.textAlign='center';
ctx.fillText('我只用了' + (180 -dealtime) + 's,' + '快來(lái)挑戰(zhàn)!', winW/2, .15*winH + imgH);
ctx.restore();
}
// 繪制提示文字
function drawTip() {
ctx.save();
ctx.fillStyle='#000';
ctx.font=14 + 'px Helvetica';
ctx.textBaseline='hanging';
ctx.textAlign='center';
ctx.fillText('關(guān)注下方二維碼開(kāi)始游戲', winW/2, .25*winH + imgH);
ctx.restore();
}
// 繪制二維碼
function drawCode() {
var imgCode=new Image();
imgCode.src='/piecePlay/images/logo.png';
imgCode.onload=function(){
ctx.drawImage(imgCode, .35*winW, .3*winH + imgH, .3*winW, .3*winW);
// 生成預(yù)覽圖
var img=new Image();
img.src=convertCanvasToImage(canvas, 1).src;
img.className='previewImg';
img.onload=function(){
$('.preview-page')[0].appendChild(this);
startDx=startDx - 100;
transformX(wrap, startDx + 'vw');
}
}
}
} else {
alert('瀏覽器不支持canvas!')
}
}
H5拼圖小游戲筆者已在github開(kāi)源, 感興趣的可以學(xué)習(xí)參考。以上的邏輯部分的代碼可以直接整合到vue項(xiàng)目中即可,由于實(shí)現(xiàn)比較簡(jiǎn)單, 這里筆者就不詳細(xì)介紹了。
目前我也在持續(xù)更新H5編輯器H5-Dooring, 它是一款輕松拖拽式制作H5頁(yè)面的工具, 功能非常強(qiáng)大, 感興趣的朋友也可以搜索體驗(yàn)一下.
如果想學(xué)習(xí)更多H5游戲, webpack,node,gulp,css3,javascript,nodeJS,canvas數(shù)據(jù)可視化等前端知識(shí)和實(shí)戰(zhàn),歡迎在《趣談前端》學(xué)習(xí)討論,共同探索前端的邊界。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。