部落格軟體開發[前端小菜鳥] 前端工程師成長計畫 - 讀書篇(1) | Internet

[前端小菜鳥] 前端工程師成長計畫 - 讀書篇(1) | Internet

按照初章節的前端讀書計畫目錄,第一項目我想從網路Internet的知識下手,此項目的知識關乎網際網路是如何溝通,前端又是如何和後端聯繫,這部分的知識由於在我作為後端工程師的前幾年面試也常被問到,所以將它作為首要研讀的項目。

[前端小菜鳥] 前端工程師成長計畫 - 讀書篇(1) | Internet

第一篇讀書計畫的主題是網路 Internet。

Internet 的知識作為前端工程師的 Roadmap第一區塊的知識,描述了網路的基本概念、HTTP 協議、前後端如何溝通等相關知識,無論是前端還是後端,在最一開始都是面試最常被詢問的面試題,甚至是以此知識展開圍繞的其他概念,常常都是重要的核心概念。(想當初我就有被問爆幾次,還好現在要重新走一次這條路,知道這塊很重要 Q.Q)。

除了原本規劃的知識之外,也會補上一些前幾年在後端打滾遇到的一些經驗以及想到一些相關的後端知識也想一併補充在下面。

本來是有想將自己讀書的內容開一個 Github 的 Repo 存放,確實有開但我發現維護困難,加上我時常搞不太清楚該怎麼分層如何分類,後來就想說改成以文章方式撰寫。

本日讀書項目

可以點擊項目跳到對應的內容。

詳請可參考完整讀書計畫目錄

第一區 - 網路(Internet)基本知識

關於基本概念與術語 Basic Concepts and Terminology

  • Packet (封包): A small unit of data that is transmitted over the internet. (簡單來說資料是由一個一個小小的封包傳入網路)
  • Router (路由): A device that directs packets of data between different networks. (路由器能將資料在不同的網路中傳遞)
  • IP Address (IP 位置): A unique identifier assigned to each device on a network, used to route data to the correct destination. (每個電子裝置都賦予像是地址位置的 IP, 範例 191.232.1.11)
  • Domain Name (網域): A human-readable name that is used to identify a website, such as google.com. (就是賦予 IP 人類看得懂的名字,稱作網域)
  • DNS (域名系統): The Domain Name System is responsible for translating domain names into IP addresses. (域名系統是對應各個網域及 IP 位置)
  • HTTP: The Hypertext Transfer Protocol is used to transfer data between a client (such as a web browser) and a server (such as a website). (HTTP 是一種傳輸協議,在客戶端與伺服器端傳輸資料,關於更多傳輸協議可找wiki)
    • 同層次的傳輸協議還有: FTP(傳輸資料), SMTP (通常用作寄信), MQTT (目前 IoT 物聯網感測器相關使用的溝通協議)
  • HTTPS: An encrypted version of HTTP that is used to provide secure communication between a client and server. (加密過後的 HTTP 協議,稱作 HTTPS)
  • SSL/TLS: The Secure Sockets Layer and Transport Layer Security protocols are used to provide secure communication over the internet. (一種加密協議,用作加密網際網路傳輸,使其擁有安全的網路溝通)

參考網站: https://cs.fyi/guide/how-does-internet-work

補充知識: 網際網路七層模型 - OSI Model

  • 應用層(application layer: HTTP, HTTPS, MQTT
  • 表現層(presentation layer)
  • 會議層(session layer)
  • 傳輸層(transport layer)
  • 網路層(network layer)
  • 資料連結層(data link layer)
  • 實體層(physical layer)

第二區 - Inernet Connection

幾種協議 (Protocols) 跟溝通方式 HTTP Requests, Server-Sent Event, 雙向溝通技術 WebSockets。補充: MQTT, SOAP, GraphQL

這是我在 LinkedIn 上看到的圖,這些是比較常用的協定,有興趣可以深入探索,我也會在下面補充

有鑒於面試篇提及很多關於網路的知識,早期的我作為後端工程師對這些網路知識並沒有太多設略,但後來發現了解網路知識不但對面試或者是實作,無論是前端還是後端都很實用,是熱門的面試考題。

既然面試篇有提到 Protocol,那邊這篇先從各項協議 Protocols 開始介紹。

  • HTTP/HTTPS 協議
    • HTTP Request (請求 - 回應) (單向溝通技術 - 由客戶端發出)
    • Server-Sent Events (SSE) (單向溝通技術 - 由伺服器端發出)
    • RESTful API
  • TCP 協議
    • 三次握手 (three-way handshake)
    • 四次揮手 (four-way handshake)
    • WebSockets (雙向溝通技術)
  • HTTP Requests vs Websockets
  • UDP
  • TCP vs UDP
  • MQTT 通訊協議 (IoT 物聯網感測器通信)
  • 補充: SOAP
  • 補充: GraphQL

HTTP/HTTPS 協議

HTTP (HyperText Transfer Protocol),是一種居於 OSI 模型第七層應用層的協議,此協議提供傳輸資料像是 HTML 頁面、圖片、影音,為 請求 - 回應 (request - response)模型 傳輸模式,透過 HTTP/HTTPS 協定請求的資源取得內容。

每一個 HTTP 需求都是獨立,並沒有任何儲存的狀態。

HTTP Request (單向溝通技術 - 由客戶端發出)

是一種基於 HTTP/HTTPS 協議的單向溝通技術,客戶端通過向伺服器端發送請求,來取得資源,伺服器端則回應該請求。

目前較常用於前後端分離的狀態,後端實作不同的 API 來提供前端請求資料,而前端向後端的 API 發出請求 (Call APIs)。

而其 HTTP 方法有: GET、POST、PUT、DELETE 來進行溝通。

HTTP request 由三個部分組成

  • Starting line (Request line)
  • Headers
  • Message body
  1. Starting line (Request line): defines the type of the message.
GET /api/index.html HTTP/1.1
  • the method is GET
  • the path is /api/index.html
  • the HTTP version is HTTP/1.1
  1. Headers: characterize the message body, transmission parameters, and other information.
Accept-Language: fr, de
If-Modified-Since: Fri, 10 Dec 2004 11:22:13 GMT
  1. Message body: contains the actual data of the message. It must be separated from the headers by an empty line.

Server-Sent Events (SSE) (單向溝通技術 - 由伺服器端發出)

(還沒碰過任何 SSE 的經驗) SSE 是基於 HTTP/HTTPS 協議的單向溝通技術,被納入 HTML5 標準的 API,其概念是由伺服器端發出需求,由客戶端接收更新訊息,一但連線建立後,就只能接收 Server 端來的訊息。最常見的場景是需要即時的訊息更新、或者不間斷的單向資料串流。

網路上有看到前輩們分享的 SSE 經驗,有興趣可以拜讀:

第二篇文章有提及,基本上由於 WebSockets 的雙向溝通技術太強大了,所以 SSE 比較少人提及,SSE 能做到的事,WebSockets 可以做得更好。

不過,像某些應用上不需要客戶端向伺服器端送出請求,只需要一直接收伺服器端的更新的話,這時就只需要運用 SSE 的單向溝通技術來達成就好。自己也歷經許多專案的磨練,加上自己的命理服務的實作經驗,有的時候如果能用更簡單更單一的技術解決,就不用硬是應用一些功能多及更複雜的技術。針對服務的需求提出最適當的方案才是最好的~。

RESTFul API

RESTful(Representational State Transfer)其實是一種架構風格,在後端實踐 APIs 時,希望開發者基於 RESTful 的架構撰寫程式。

使用 HTTP 方法(GET、POST、PUT、DELETE)來執行 CRUD 操作,而 API 的終端點以 URL 進行識別,通常返回 JSON 或 XML 格式的資料。

TCP 協議、三次握手、四次握手

TCP 協議位於 OSI 模型的第六層表現層,以理論上來說,TCP 協議是確保在兩個端點之間建立和維護可靠的數據傳輸,所以其特性為連線一但通過建立後,其連線就會一直保持,直到斷開。

三次握手和四次握手(揮手)是 TCP 建立與斷開連線的過程。

  • 三次握手: 為 TCP 協議建立連線的過程。
  • 四次握手(揮手): 為 TCP 協議斷開連線的過程。

WebSockets

WebSockets 是基於 TCP 協議上可雙向溝通的一種技術,通常比較常使用在需要大量即時應用的場景,比方說聊天應用、遊戲、即時通訊。

與 HTTP 不同,WebSockets 連接一旦建立,可以持續存在,並且可以在任何時候雙向傳送資訊。(由於我也沒什麼經驗碰過 WebSockets,所以這個時候就可以斗膽猜測魔獸世界只要經過最一開始的登入頁面,登入等同連線建立,那之後的角色列表跟進入遊戲冒險應該都是不間斷的資料傳輸囉!?)

詳細應用可參考網站:JavaScript | WebSocket 讓前後端沒有距離

HTTP Requests vs Websockets

HTTP 主要基於 Client-Server Model,也就是由客戶端傳需求至伺服器端,再由伺服器端收到需求後處理完回傳回應(Response),每次 HTTP 需求都是獨立且斷開,而且只能由客戶端發送單向請求。

Websockets 則是適合資料即時串流以及雙向資料傳輸,建立在 TCP 協議的穩定連線上,使資料能夠雙向交流,且確保資料能夠穩定傳輸。

協議 HTTP WebSocket
連線方式 HTTP 是一種短連線協定,每次請求都需要建立新的連線。 WebSocket 是一種長連線協定,在建立連線後,客戶端和服務器可以一直保持連線狀態。
通訊方式 HTTP 是一種單向通訊協定,只能由客戶端向服務器發送請求。 WebSocket 是一種雙向通訊協定,客戶端和服務器都可以互相發送資料。
資料格式 HTTP 只能傳輸文字資料。 WebSocket 可以傳輸文字資料、二進位制資料其他類型資料
應用場景 HTTP 通常用於 Web 瀏覽檔案下載等應用。 WebSocket 通常用於即時通訊流媒體等應用。
優點 簡單易用、通用性強 效率高、延遲低
缺點 效率低、延遲高 複雜度高、通用性弱

HTTP 和 WebSocket 都是用於在客戶端和服務器之間傳輸資料的協定。HTTP 是一種簡單易用、通用性強的協定,但效率低、延遲高。WebSocket 是一種效率高、延遲低的協定,但複雜度高、通用性弱。在實際應用中,可以根據應用需求選擇合適的協定。

UDP

UDP(User Datagram Protocol)是使用者資料報協定,是一種簡單的面向資料包的通信協定,位於 OSI 模型的傳輸層(第 6 層)。

UDP 與 TCP(Transmission Control Protocol)是兩種最常用的傳輸層協定。TCP 是一種可靠的協定,它會在發送端和接收端之間建立連線,並在傳輸資料時進行錯誤檢查和糾正。UDP 則是一種不可靠的協定,它不會在發送端和接收端之間建立連線,也不進行錯誤檢查和糾正。

UDP 的優點是效率高、延遲低。由於 UDP 不需要進行錯誤檢查和糾正,因此其傳輸效率比 TCP 高。此外,UDP 也不需要建立連線,因此其延遲比 TCP 低。

UDP 的缺點是不可靠。由於 UDP 不進行錯誤檢查和糾正,因此其傳輸的資料可能會丟失或損壞。

UDP 通常用於以下應用:

  • 即時通訊: 即時通訊應用需要低延遲,因此通常使用 UDP 協定。

  • 流媒體: 流媒體應用需要高效率,因此通常使用 UDP 協定。

  • 遊戲: 遊戲應用需要低延遲,因此通常使用 UDP 協定。

TCP vs UDP

TCP 和 UDP 是兩種是最常用的「傳輸層」(第六層)協定。

協議 TCP UDP
解說 TCP 是一種可靠的協定,它會在發送端和接收端之間建立連線,並在傳輸資料時進行錯誤檢查和糾正。 UDP 則是一種不可靠的協定,它不會在發送端和接收端之間建立連線,也不進行錯誤檢查和糾正。
優缺點 TCP 的優點是可靠,能夠保證資料的完整性和正確性。TCP 的缺點是效率低、延遲高。 UDP 的優點是效率高、延遲低。UDP 的缺點是不可靠,資料可能會丟失或損壞。
運用場景 Web 瀏覽: Web 瀏覽需要傳輸大量資料,並且需要保證資料的完整性和正確性,因此通常使用 TCP 協定。 即時通訊: 即時通訊應用需要低延遲,因此通常使用 UDP 協定。
電子郵件: 電子郵件需要傳輸重要資訊,因此需要保證資料的完整性和正確性,因此通常使用 TCP 協定。 流媒體: 流媒體應用需要高效率,因此通常使用 UDP 協定。
檔案傳輸: 檔案傳輸需要保證資料的完整性和正確性,因此通常使用 TCP 協定。 遊戲: 遊戲應用需要低延遲,因此通常使用 UDP 協定。
實際運用案例 Web 瀏覽器(HTTP)、電子郵件(SMTP)、檔案傳輸(FTP)、遠端登入(Telnet)、資料庫存取(SQL) 即時通訊(VoIP)、域名解析(DNS)、簡單網路管理協定(SNMP)、流媒體(RTP、RTCP)、遊戲

在實際應用中,可以根據應用需求選擇合適的協定。如果需要保證資料的完整性和正確性,則應使用 TCP 協定。如果需要高效率或低延遲,則應使用 UDP 協定。

MQTT 通訊協議

有幸在作為軟體工程師的職涯中遇到 IoT 物聯網領域的專案,所以特別補充這項溝通協議。

台灣其實是電子產業很蓬勃發展的國家,在未來會有更多物聯網應用相關的服務需要被建立。MQTT 協議是在物聯網領域中被廣泛使用作為感測器資料溝通的方式,他是一種輕量且基於發布(Publish)/訂閱(Subscribe)的消息協定,由於大部分的電子感測器裝置都會被安置在環境、狀況不良的地方,而 MQTT 的協議是專為低帶寬、高延遲或不穩定網路環境而設計。

關於 MQTT 的技術,有興趣可以參考我撰寫的路燈裝置物聯網應用的文章,打個比方的話像是裝置在路燈內的感測器,他可能外殼面臨風吹雨打,可能為在網路不穩的區域,所以這個時候只要感測器有資料想送出、網路允許時,就趕緊資料都送往 MQTT 的伺服器端 (可以稱作 Broker)的某個頻道(Topic),Broker 會將接收到的資料存取 (與其說存取,不如說資料會存在一個 Queue 裡,對有訂閱 Topic 的客戶端發送訊息)。這個時候客戶端(就是資料接受端/網頁或軟體端)可以訂閱 MQTT Broker 特定的主題 (Topic)來接收這些感測器的資料。

MQTT 的 Broker (可以想像成伺服器端),可以擁有不同的主題(Topic)用來接收不同的感測器資料。

參考的實作案例,可以參考這篇:

既然扯到跟網路相關的內容,基於以下這張圖,我想特別來補充兩個額外的技術

補充: SOAP

SOAP 是一種基於 XML 的協定,使用 XML 格式來定義消息的格式與結構,可以運行於 HTTP, SMTP, TCP 協定上,通常會運用於企業級的網站、或者內部網站對接。

常見於大企業的內部網站資料交流使用,雖然後來 RESTful 的架構比較常運用,但 SOAP 的協議還是會運用在一些大公司或傳統的公司內部系統使用。

<?xml version="1.0"?>

<soap:Envelope
xmlns:soap="http://www.w3.org/2003/05/soap-envelope/"
soap:encodingStyle="http://www.w3.org/2003/05/soap-encoding">

<soap:Header>
...
</soap:Header>

<soap:Body>
...
  <soap:Fault>
  ...
  </soap:Fault>
</soap:Body>

</soap:Envelope>

補充: GraphQL

初次見面時是在玩基於 Vue.js 框架延伸出來的靜態生產網站(SSG): Gridsome,在 Gridsome 中讀取.md 檔資料就是使用 GraphQL。

根據維基百科的描述,GraphQL 是 Facebook 為不同的 API 而創造出來的資料查詢操作語言,大概在 2015 年左右公開發佈,算是蠻新興的技術,估計要整個紅起來或發展起來還需要些許時間,但可能成為一種未來的流行。

一些主要特點是

  • 可以獲取特定量的資料數據,減少過度傳輸,提高效率。
  • 在一個請求中可以獲得多個資料數據向,提高性能。
  • 他是強型別模式,所以能明確的理解資料結構跟類型。
  • ... 更多

不過之前在玩 Gridsome 的時候發現沒辦法傳輸中文(Unicode)的資料,不知道後續的版本會不會支援。

有時間的話不妨可以花些時間了解 GraphQL,也許會在未來的專案中遇到。

第三區 - Internet and API Calls: SWR, react-query, Axios, Ajax

上面提及了基本的網路知識以及幾項網路溝通的協議及方式,這個區塊想探索跟學習的是偏向前端如何向後端溝通,偏實作層面,是我想要著重於前端探索的部分。

這一區塊主要是在React Roadmap下方的區塊API Calls出現,和網路溝通有相關,所以我將它移至這裡學習。

在這個區塊中,我加上了 Ajax 這個名詞,主要是我本人並不了解這個詞彙,以及我還記得我四年前剛開始找第一份工作的面試時,當時的 Challedge Assessment 就有被要求用 Ajax 請求資料。這四年都是擔任後端工程師,並沒有機會接觸前端的作業,所以特地將 Ajax 擺在此。

API Calls: SWR, react-query, Axios, Ajax

來釐清這幾個單字,用 List 來寫關聯的話,會是這樣的:

  • JavaScript
    • Ajax
      • JQuery Ajax
    • fetch API (Ajax 替代方案)
    • axios
  • React
    • react-query
    • SWR

他們的共通點就是,都是發 HTTP Requests 跟 Server 端溝通。

Ajax

Ajax的全名叫做「Asynchronous JavaScript and XML」(非同步的 JavaScript 與 XML 技術)。

如果不知道什麼是非同步(Asynchronous)跟同步(Synchronous)的觀念,建議去理解一下,在前端跟後端的領域都有機會遇到。

簡單來說,同步的觀念就是前端今天向後端發送一個 Request 請求資料,必須得等到資料回來才渲染頁面,有強烈的先後順序。非同步的概念就是發送 Request 請求資料後,我可以先渲染頁面,等到資料回傳後再將資料呈現出來。

回到 Ajax 技術,查詢資料時,被說是一個上古時期的原生物件。

  • 他是很早期前端技術剛發展時,能夠支援前後端溝通的技術
  • JavaScript 原生的技術,不需要安裝任何其他函式庫。
  • 注意的一點是雖然叫做「and XML」,但實際上可以使用 XML, Json 跟 HTML 格式溝通。

程式碼大致上長這樣

// From w3schools
function loadDoc(url, cFunction) {
  var xhttp;
  xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function () {
    if (this.readyState == 4 && this.status == 200) {
      cFunction(this);
    }
  };
  xhttp.open("GET", url, true);
  xhttp.send();
}
function myFunction(xhttp) {
  document.getElementById("demo").innerHTML = xhttp.responseText;
}

JQuery Ajax

JQuery 大家應該都很熟悉,但是呢我跟他不熟,我在認真學習前端時,就幾乎沒遇到 Jquery。JQuery 是基於 JavaScript撰寫的一個函式庫,他運用 JavaScript 寫了很多函式,用途是讓使用者能更輕鬆方便的製作網站功能 (你想要手刻旋轉木馬(Carousel)呢?還是有人幫您想好呢?反正我是手刻,因為我還看不懂 JQuery 嗚嗚),而且是免費的。

JQuery Ajax 就是 JQuery 改良後的 Ajax。

  • 基於 Ajax 原生的 XHR 物件開發,改良 XHR 本身架構不清晰的問題,取代原生 XHR 物件成為主流的 Ajax 語法結構。
  • 目前還有許多開發者為了使用 JQuery Ajax,而導入 JQuery 函式庫
  • [缺點] 要使用 JQuery 就要把整個函式庫導入,整包項目 Size 太大,3.5.1 版本就有 281KB,連 min 版都有 88KB
  • [缺點] 要實作或者解決跨網站的 HTTP 要求(cross-site HTTP request)與 CORS(Cross-Origin Resource Sharing)不容易做。

fetch API

後續開發出來的 Call API 技術,基於 JavaScript 語言運用 Promise 物件的概念實作。被稱為現代的 Ajax 的替代方案。

  • 使用 ES6 的 Promise 物件做回傳的處理
  • 可以搭配 async/await
  • 不須引入函式庫
  • 可以直接呼叫定義的 Request 物件
  • 可以用 Headers 物件設定表頭
  • [缺點] 需要使用 catch 做錯誤(error)的捕捉與處理
  • [缺點] 只要伺服器端有回應,都會回傳成功的 Promise 物件,要自行判斷回應的 HTTP Code 跟內容。
  • [缺點] IE 不支援 (很重要蛤)

Reference: request 的方式? ajax & fetch & axios

實作 Code 長相

fetch(file)
  .then((x) => x.text())
  .then((y) => myDisplay(y));

// async
async function getText(file) {
  let myObject = await fetch(file);
  let myText = await myObject.text();
  myDisplay(myText);
}

有趣的是,我剛剛在我的 React.js 專案裡看到我當初誤打誤撞運用的 fetch 技術來 Call 後端 API

const response = await fetch(ROUTE_API + "/api/v2/", {
  method: "POST",
  body: JSON.stringify(body),
});

if (!response.ok) {
  // Error
  throw new Error("Network response was not ok");
}

const data = await response.json();
return data;

axios

axios 看起來...,算是這一列基於 JavaScript 語言中,擁有以上的優點跟優化以上的缺點的技術。

  • 使用方法類似於 jQuery
  • 支援搭配 async/await
  • 支持 Promise 觀念
  • 可以在 node.js 中使用
  • 支援防 CSRF
  • 輕量化的技術,約 13KB
  • 廣泛的瀏覽器支援
  • 可以取消要求
  • 可以自動轉換 JSON 資料
  • [缺點] 還是需要使用 catch 做錯誤的捕捉跟處理

實作的 Code

  • install npm install axios
  • 或者 ` ``js

```js

// Post method
axios.post('/login', {
  firstName: 'Finn',
  lastName: 'Williams'
}).then(response => console.log(response.data))

以下兩種方式都是基於 React.js 框架上運用的技術。

react-query

react-query 是一個 React 函式庫,它提供了一個更高級的 API 來管理數據狀態。

但敝女還沒使用過,所以不方便多做些講解,當初還以為我自己做的 react.js 專案是使用這個技術,結果剛剛查了程式碼,原來不是,而是使用 fetch 囧。

實作範例

import { useQuery } from "react-query";

const { data, error } = useQuery("users", () => fetch("/api/users"));

if (error) {
  return <div>Error loading users</div>;
}

if (!data) {
  return <div>Loading...</div>;
}

return (
  <ul>
    {data.map((user) => (
      <li key={user.id}>{user.name}</li>
    ))}
  </ul>
);

SWR

useSWR 是一個 React Hook,它使用 Axios 函式庫在後台發送 HTTP 請求。

這項技術根據文檔是說會先從 cache 查詢過期的資料,並同時發送 Request 請求(驗證資料是否過期或相同),最後得到最新資料。

以下是根據文檔中列出的特性

  • 輕量型可重複使用的 HTTP 請求技術
  • 減少 cache 跟 request 的重複使用
  • 支援 SSR / ISR / SSG support
  • 支援 TypeScript
  • 支援 React Native

目前看起來是蠻新的技術,很不幸的是敝女也還未使用,大家有空可以翻一下文件,這是繁體中文,可以拉到最底的 footer 改成英文來看。

實作範例

import useSWR from "swr";

function Profile() {
  const { data, error, isLoading } = useSWR("/api/user", fetcher);

  if (error) return <div>failed to load</div>;
  if (isLoading) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

以上介紹了這麼多前端 Call APIs 的技術,實際上還是請各位工程師、開發者根據自己的專案跟環境情況來做選擇,畢竟就算我自己寫 React.js 專案,邊學邊做時,也是無意間使用了 fetch 技術來 Call APIs,而非使用後來介紹的兩個 React.js 資料請求技術。

在這篇文章中,只會很粗略的帶過,用我喜歡的方式,就是先了解全部的可能性,然後一一細節去研讀跟了解,這樣就能知道未來若遇到不同的專案跟環境,需要運用哪個技術比較適合,並非最新就最好,只有適合。

好啦,那這篇跟網路相關的知識就讀到這,若有任何解釋上的問題,再麻煩糾正我,非常感謝您。

前端工程師讀書計畫InternetHTTP, TCP, UDPAjax react-query SWR fetchAPI
Mina Yu

Mina Yu

嗨,我是Mina. 一個ENTP奇女子,時常可以隨手拈來創意的Idea,正向、 活躍且浪漫。是軟體開發工程師、 部落客,還是多語學習者,喜歡去探索不同的文化跟發明些東東。

@MinaYu Signed

相關文章
N/A

贊助