一步步教你用 WebVR 實現虛擬現實遊戲

翻譯:瘋狂的技術宅javascript

www.smashingmagazine.com/2018/11/vir…html

在本教程中,咱們將建立三維對象併爲它們添加簡單的交互。此外,你還能夠學到如何在客戶端和服務器之間創建簡單的消息傳遞系統。前端

虛擬現實(VR)是一種依賴計算機生成環境的體驗,其應用範圍普遍:美國利用虛擬現實進行冬季奧運會的運動訓練;外科醫生正在試驗用虛擬進行醫學培訓;把虛擬現實用於遊戲是最多見的一種應用。java

咱們將把目光放在最後一類程序上,並將專一於點擊式冒險遊戲。這是一種休閒類遊戲,遊戲的目標是經過選擇場景中的三維對象來完成拼圖。在本教程中,咱們將在虛擬現實中構建一個簡單的版本。這是一篇關於三維編程的介紹,是在 Web 上部署虛擬現實模型的獨立入門指南。你將使用 webVR 進行構建,這個框架具備雙重優點 —— 用戶能夠在VR中玩遊戲,而沒有VR眼鏡的用戶也能夠在手機或桌面上玩。node

在本教程的後半部分中,你將爲桌面構建一個「鏡像」。這意味着在移動設備上進行的全部移動都將會在桌面預覽中進行鏡像。這樣你能夠看到玩家所看到的內容,容許你提供指導、記錄遊戲,或只是讓客人娛*樂。android

至於爲何「娛」和「樂」中間有一個奇怪的符號,由於,這兩個字連在一塊兒在掘金是違規的,哈哈,不信你發文的時候能夠試試~web

前提條件

在開始以前你須要準備如下內容。對於本教程的後半部分,你將須要一臺Mac OSX。雖然代碼能夠應用於任何平臺,但下面依賴項的安裝說明適用於Mac。express

  • 互聯網接入,特別是glitch.com;
  • VR 眼鏡(可選,推薦)。我用的是Google Cardboard,每一個售價15美圓。

步驟1:設置虛擬現實(VR)模型

在此步驟中,咱們將設置一個包含單個靜態 HTML 頁面的網站。這樣能夠容許你從桌面進行編碼並自動部署到Web上,而後能夠將部署的網站加載到手機上並放入VR眼鏡內。或者部署的網站能夠由獨立的 VR 眼鏡加載。首先打開https://glitch.com/。而後npm

  1. 單擊右上角的 「New Project」 。
  2. 單擊下拉列表中的「hello-express」。

打開 glitch.com

接下來,單擊左側邊欄中的 views/index.html。咱們將此稱爲你的「編輯器」。編程

下一步是點擊左側邊欄中的views/index.html

要預覽網頁,請單擊左上角的「Preview」。咱們將此做爲你的預覽。請注意,編輯器中的任何更改都將會自動反映在預覽中,除非出現錯誤或不受支持的瀏覽器。

返回編輯器,將當前HTML替換爲下面 VR 模型的代碼框架。

<!DOCTYPE html>
<html>
  <head>
      <script src="https://aframe.io/releases/0.7.0/aframe.min.js"></script>
  </head>
  <body>
    <a-scene>

      <!-- blue sky -->
      <a-sky color="#a3d0ed"></a-sky>

      <!-- camera with wasd and panning controls -->
      <a-entity camera look-controls wasd-controls position="0 0.5 2" rotation="0 0 0"></a-entity>

      <!-- brown ground -->
      <a-box shadow id="ground" shadow="receive:true" color="#847452" width="10" height="0.1" depth="10"></a-box>

      <!-- start code here -->
      <!-- end code here -->
    </a-scene>
  </body>
</html>
複製代碼

以後能夠看到如下內容:

切換到預覽時,你會看到藍色和棕色的背景色。

要在VR眼鏡上預覽此功能,請使用 omnibar 中的URL。在上圖中,URL 爲 https://point-and-click-vr-game.glitch.me/。你的工做環境如今已創建,能夠隨時與家人和朋友分享這個URL。在下一步中,你將建立一個虛擬現實模型。

步驟2:建立一個樹的模型

如今,咱們將用 aframe.io 中的 primitives 建立一個樹。這是 Aframe 爲便於使用而預編程的一些標準對象。具體來講,Aframe 將對象稱爲實體(entities)。與實體相關的概念有三個:

  1. 幾何和材質,
  2. 轉換軸,
  3. 相對轉換。

首先,幾何材質是代碼中全部三維對象的兩個構建塊。幾何定義了一系列的「形狀」 —— 立方體,球體,金字塔等。材質定義了形狀的靜態屬性,例如顏色、反射率、粗糙度。

Aframe 經過定義基元來簡化這個概念,例如 <a-box><a-sphere><a-cylinder> 以及許多其餘基本原理來簡化幾何體及其材料。首先定義一個綠色球體。在代碼的第19行,也就是 <!-- start code here -->以後添加如下內容。

<!-- start code here -->
<a-sphere color="green" radius="0.5"></a-sphere>  <!-- new line -->
<!-- end code here -->
複製代碼

其次,有三個軸能夠轉換對象。 x 軸是水平運動的,當咱們向右移動時,x 值會增長。 y 軸垂直運行,y 值隨着咱們向上移動而增長。 z 軸用垂直你的屏幕,當對象向你移動時,z 值會增長。咱們能夠沿這三個軸平移,旋轉或縮放實體。

例如,要將對象向「右」移動,咱們須要增長其x值。要向上旋轉對象,咱們須要沿 y 軸旋轉它。下面咱們修改第19行來「向上」移動球體 —— 這意味着你須要增長球體的 y 值。請注意,全部轉換都指定爲 <x> <y> <z>,意味着要增長其y值,須要增長第二個值。默認狀況下,全部對象都位於 0,0,0 位置。在下面添加 position

<!-- start code here -->
<a-sphere color="green" radius="0.5" position="0 1 0"></a-sphere> <!-- edited line -->
<!-- end code here -->
複製代碼

第三,全部變換都相對於其父對象。要在樹中添加樹幹,就在上方球體內添加圓柱體,這樣可肯定樹幹相對於球體的位置,還能夠將你的樹木整合爲一個單元。在<a-sphere ...></ a-sphere>標籤之間添加<a-cylinder>實體。

<a-sphere color="green" radius="0.5" position="0 1 0">
	<a-cylinder color="#84651e" position="0 -0.9 0" radius="0.05"></a-cylinder> <!-- new line -->
</a-sphere>
複製代碼

接着添加兩個的綠色球體做爲更多的葉子。

<a-sphere color="green" radius="0.5" position="0 0.75 0">
	<a-cylinder color="#84651e" position="0 -0.9 0" radius="0.05"></a-cylinder>
	<a-sphere color="green" radius="0.35" position="0 0.5 0"></a-sphere> <!-- new line -->
	<a-sphere color="green" radius="0.2" position="0 0.8 0"></a-sphere> <!-- new line -->
</a-sphere>
複製代碼

切換回預覽,你將看到下面這顆樹:

回到預覽時,你如今能夠看到放置在背景中的樹了

從新加載VR眼鏡上的網站預覽並查看。在下一節中,咱們將使這棵樹具備交互性。

步驟3:將Click Interaction添加到Model

要使實體具備交互性,你須要:

  • 添加動畫,
  • 點擊時觸發動畫。

因爲最終用戶使用VR眼鏡,點擊動做至關於凝視:換句話說,盯着一個對象就是「點擊」它。要實現這些更改,咱們將從光標開始。用如下內容替換第13行來從新定義相機。

<a-entity camera look-controls wasd-controls position="0 0.5 2" rotation="0 0 0">
  <a-entity cursor="fuse: true; fuseTimeout: 250" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03" material="color: black; shader: flat" scale="0.5 0.5 0.5" raycaster="far: 20; interval: 1000; objects: .clickable">
    <!-- add animation here -->
  </a-entity>
</a-entity>
複製代碼

上面的代碼添加了一個能夠觸發單擊操做的遊標。注意 objects: .clickable 屬性。這意味着具備「可點擊」類的全部對象將觸發動畫,並在適當的時候接收「單擊」命令。咱們還將向單擊光標添加動畫,以便使用戶知道光標什麼時候觸發單擊。當指向可點擊的對象時,光標將緩慢收縮,在一秒鐘後捕捉以表示已單擊的對象。用如下代碼替換註釋 <!-- add animation here -->

<a-animation begin="fusing" easing="ease-in" attribute="scale" fill="backwards" from="1 1 1" to="0.2 0.2 0.2" dur="250"></a-animation>
複製代碼

將樹向右移動 2 個單位,並修改第29行爲如下內容將類 「clickable」 添加到樹中。

<a-sphere color="green" radius="0.5" position="2 0.75 0" class="clickable">
複製代碼

接下來,咱們將:

  • 指定動畫,
  • 點擊便可觸發動畫。

感謝 Aframe 易於使用的動畫實體,這兩個步驟均可以快速連續完成。

在第33行添加一個 <a-animation> 標記,緊跟在 <a-cylinder> 標記以後但在 </a-sphere> 結尾以前。

<a-animation begin="click" attribute="position" from="2 0.75 0" to="2.2 0.75 0" fill="both" direction="alternate" repeat="1"></a-animation>
複製代碼

上述屬性指定了動畫的許多配置。動畫:

  • 由「click」事件觸發
  • 修改樹的position
  • 從原始位置 2 0.75 0開始
  • 結束於2.2 0.75 0(向右移動0.2個單位)
  • 往返目的地時的動畫
  • 在往返目的地之間的交替動畫
  • 重複此動畫一次。這意味着對象動畫總共播放兩次: 一次到目的地,一次回到原始位置。

最後,切換到預覽,而後從光標拖動到樹。一旦黑色圓圈放在樹上,樹就會向右和向後移動。

一旦黑色圓圈放在樹上,樹就會向右和向後移動

這就結束了在虛擬現實中構建點擊式冒險遊戲所需的全部基礎知識。要查看和播放此遊戲的更完整版本,請參閱如下短片(alvinwan.com/shift/scene…

任務是經過點擊場景中的各類物體打開大門並隱藏大門後面的樹

接下來,咱們設置一個簡單的nodeJS服務器來提供靜態演示。

步驟4:設置NodeJS服務器

在此步驟中,咱們將設置一個基本的、功能性的nodeJS服務器,爲你現有的VR模型提供服務。在編輯器的左側邊欄中,選擇package.json

首先刪除第2 - 4行。

"//1": "describes your app and its dependencies",
"//2": "https://docs.npmjs.com/files/package.json",
"//3": "updating this file will download and update your packages", 
複製代碼

將名稱改成mirrorvr

{
  "name": "mirrorvr", // change me
  "version": "0.0.1",
  ...
複製代碼

dependencies下,添加socket.io

"dependencies": {
  "express": "^4.16.3",
  "socketio": "^1.0.0",
},
複製代碼

更新存儲庫URL以匹配當前的glitch。示例glitch項目名爲 point-and-click-vr-game。用你的glitch項目名稱替換它。

"repository": {
  "url": "https://glitch.com/edit/#!/point-and-click-vr-game"
},
複製代碼

最後,將 "glitch" 標籤改成 "vr"

"keywords": [
  "node",
  "vr",  // change me
  "express"
]
複製代碼

仔細檢查你的package.json是否和如下內容一致。

{
  "name": "mirrorvr",
  "version": "0.0.1",
  "description": "Mirror virtual reality models",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.16.3",
    "socketio": "^1.0.0"
  },
  "engines": {
    "node": "8.x"
  },
  "repository": {
    "url": "https://glitch.com/edit/#!/point-and-click-vr-game"
  },
  "license": "MIT",
  "keywords": [
    "node",
    "vr",
    "express"
  ]
}
複製代碼

views/index.html中仔細檢查上一部分的代碼是否與如下內容一致。

<!DOCTYPE html>
<html>
  <head>
      <script src="https://aframe.io/releases/0.7.0/aframe.min.js"></script>
  </head>
  <body>
    <a-scene>

      <!-- blue sky -->
      <a-sky color="#a3d0ed"></a-sky>

      <!-- camera with wasd and panning controls -->
      <a-entity camera look-controls wasd-controls position="0 0.5 2" rotation="0 0 0">
        <a-entity cursor="fuse: true; fuseTimeout: 250" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03" material="color: black; shader: flat" scale="0.5 0.5 0.5" raycaster="far: 20; interval: 1000; objects: .clickable">
            <a-animation begin="fusing" easing="ease-in" attribute="scale" fill="backwards" from="1 1 1" to="0.2 0.2 0.2" dur="250"></a-animation>
        </a-entity>
      </a-entity>

      <!-- brown ground -->
      <a-box shadow id="ground" shadow="receive:true" color="#847452" width="10" height="0.1" depth="10"></a-box>

      <!-- start code here -->
      <a-sphere color="green" radius="0.5" position="2 0.75 0" class="clickable">
        <a-cylinder color="#84651e" position="0 -0.9 0" radius="0.05"></a-cylinder>
        <a-sphere color="green" radius="0.35" position="0 0.5 0"></a-sphere>
        <a-sphere color="green" radius="0.2" position="0 0.8 0"></a-sphere>
        <a-animation begin="click" attribute="position" from="2 0.75 0" to="2.2 0.75 0" fill="both" direction="alternate" repeat="1"></a-animation>
      </a-sphere>
      <!-- end code here -->
    </a-scene>
  </body>
</html>
複製代碼

接着修改server.js

首先導入幾個 NodeJS 包。

  • Express 這是服務器的Web框架。

  • http

    這容許咱們啓動一個守護進程,監聽各類端口上的活動。

  • socket.io 用套接字實現容許咱們能夠實時地在客戶端和服務器端之間進行通訊。

在導入這些包時,咱們還會初始化 ExpressJS 程序。請注意,前兩行已經爲你編寫好了。

var express = require('express');
var app = express();

/* start new code */
var http = require('http').Server(app);
var io = require('socket.io')(http);
/* end new code */

// we've started you off with Express, 
複製代碼

加載包後,服務器會返回 index.html 做爲主頁。請注意,下面沒有新的代碼;這只是對現有源代碼的解釋。

// http://expressjs.com/en/starter/basic-routing.html
app.get('/', function(request, response) {
  response.sendFile(__dirname + '/views/index.html');
});
複製代碼

最後,現有的源代碼指示程序綁定並偵聽默認狀況下爲3000的端口,除非另有說明。

// listen for requests :)
var listener = app.listen(process.env.PORT, function() {
  console.log('Your app is listening on port ' + listener.address().port);
});
複製代碼

完成編輯後,Glitch會自動從新加載服務器。單擊左上角的「Show」預覽你的應用程序。

你的Web程序現已啓動並運行。接下來,咱們將從客戶端向服務器發送消息。

步驟5:從客戶端向服務器發送信息

在此步驟中,咱們將用客戶端初始化與服務器的鏈接。客戶端還將通知服務器它是手機仍是桌面。首先,在 views/index.html 中導入即將添加的Javascript文件。

在第4行以後,包含一個新腳本。

<script src="/client.js" type="text/javascript"></script>
複製代碼

在第14行,將 camera-listener 添加到相機實體的屬性列表中。

<a-entity camera-listener camera look-controls...>
    ...
</a-entity>
複製代碼

而後,切換到左側邊欄中的 public/client.js。刪除此文件中全部的Javascript代碼。而後,定義一個工具函數,用於檢查客戶端是不是移動設備。

/** * Check if client is on mobile */
function mobilecheck() {
  var check = false;
  (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
  return check;
};
複製代碼

接下來,咱們將定義一系列與服務器端交換的消息。定義一個新的 socket.io 對象來表示客戶端與服務器的鏈接。套接字鏈接後,將消息記錄到控制檯。

var socket = io();

socket.on('connect', function() {
  console.log(' * Connection established');
});
複製代碼

檢查是否爲移動設備,並用 emit 函數將相應的信息發送到服務器。

if (mobilecheck()) {
  socket.emit('newHost');
} else {
  socket.emit('newMirror');
}
複製代碼

這樣就結束了客戶端的消息發送。如今修改服務器代碼,使其可以接收此消息並作出適當的反應。打開服務器端 server.js 文件。

處理新鏈接,並當即偵聽客戶端類型。在文件末尾添加如下內容。

/** * Handle socket interactions */

io.on('connection', function(socket) {

  socket.on('newMirror', function() {
    console.log(" * Participant registered as 'mirror'")
  });

  socket.on('newHost', function() {
    console.log(" * Participant registered as 'host'");
  });
});
複製代碼

再次經過單擊左上角的「Show」來預覽程序。在移動設備上加載相同的網址。在你的終端中,你將看到如下內容。

listening on *: 3000
 * Participant registered as 'host'
 * Participant registered as 'mirror'
複製代碼

這是第一個簡單的消息傳遞,咱們的客戶端將信息發送回服務器。退出正在運行的 NodeJS 進程。對於此步驟的最後一部分,咱們將讓客戶端將相機信息發送回服務器。打開public/client.js

在文件的最後,添加如下內容。

var camera;
if (mobilecheck()) {
  AFRAME.registerComponent('camera-listener', {
    tick: function () {
      camera = this.el.sceneEl.camera.el;
      var position = camera.getAttribute('position');
      var rotation = camera.getAttribute('rotation');
      socket.emit('onMove', {
        "position": position,
        "rotation": rotation
      });
    }
  });
}
複製代碼

保存並關閉。打開你的服務器代碼文件 server.js 來監聽這個 onMove 事件。

在套接字代碼的newHost塊中添加如下內容:

socket.on('newHost', function() {
    console.log(" * Participant registered as 'host'");
    /* start new code */
    socket.on('onMove', function(data) {
      console.log(data);
    });
    /* end new code */
  });
複製代碼

再次在桌面和移動設備上加載預覽。鏈接移動客戶端後,服務器將當即開始記錄從客戶端發送到服務器的攝像機位置和旋轉信息。接下來實現相反的操做,從服務器將信息發送回客戶端。

步驟6:從服務器向客戶端發送信息

在此步驟中,你將向全部鏡像發送主機的攝像機信息。打開主服務器源碼文件 server.js

onMove 事件處理更改成如下內容:

socket.on('onMove', function(data) {
  console.log(data);  // delete me
  socket.broadcast.emit('move', data)
});
複製代碼

broadcast修飾符可以確保服務器將此信息發送給鏈接到套接字的全部客戶端。將此信息發送到客戶端後,你須要相應地設置鏡像的相機。打開客戶端腳本 public/client.js

在這裏檢查客戶端是否爲桌面。若是是,則接收移動數據並相應地記錄。

if (!mobilecheck()) {
  socket.on('move', function(data) {
    console.log(data);
  });
}
複製代碼

在桌面和移動設備上加載預覽。在桌面瀏覽器中,打開開發控制檯。而後,在手機上加載應用程序。一旦手機成功加載程序,桌面上的開發控制檯就會顯示相機位置和旋轉等信息。

再次打開客戶端腳本 public/client.js。咱們最後將根據發送的信息調整客戶端攝像頭。

修改上面的事件處理程序以獲取 move 事件。

socket.on('move', function(data) {
  /* start new code */
  camera.setAttribute('rotation', data["rotation"]);
  camera.setAttribute('position', data["position"]);
  /* end new code */
});
複製代碼

在桌面和手機上加載程序。你手機上的每一個動做都會反映在桌面上相應的鏡像中!這樣就結束了程序的鏡像部分。做爲桌面用戶,你如今能夠預覽手機用戶看到的內容。本節介紹的概念對於進一步開發此遊戲相當重要,由於咱們還會將單人遊戲轉變爲多人遊戲。

結論

在本教程中,咱們建立了三維對象併爲這些對象添加了簡單的交互。還在客戶端和服務器之間構建了一個簡單的消息傳遞系統,以實現能對用戶看到的內容的在桌面進行預覽。

這些概念甚至超越了webVR,由於幾何和材料的概念擴展到了 iOS 上的 SceneKit(與ARKit相關),Three.js(Aframe的主幹)以及其餘三維庫。這些簡單的構建塊組合在一塊兒,使咱們可以靈活的建立一個徹底成熟的點擊式冒險遊戲。更重要的是,它們容許咱們使用基於點擊的界面建立任何遊戲。

如下是供你進一步探索的幾個資源和示例:

  • MirrorVR 上面實時預覽功能的徹底實現。只需一個Javascript連接,便可將移動設備上的任何虛擬現實模型的實時預覽添加到桌面。
  • Bit by Bit 兒童畫畫廊的虛擬現實模型。
  • Aframe 虛擬現實開發的例子、開發人員文檔和其它資源。
  • Google Cardboard Experiences 爲教師提供定製工具。

下一次咱們將構建一個完整的遊戲,使用網絡套接字來實現虛擬現實遊戲中玩家之間的實時通訊。

歡迎關注前端公衆號:前端先鋒,獲取更多前端乾貨。