摘要:定時器在和瀏覽器中的表現形式是相同的。關于定時器的一個重要的事情是,我們提供的延遲不代表在這個時間之后回調就會被執行。它的真正含義是,一旦主線程完成所有操作包括微任務并且沒有其它具有更高優先級的定時器,將在此時間之后執行回調。
眾成翻譯
原文鏈接
關于作者
2018年6月21日出版
?
本指南面向了解Javascript但尚未十分熟悉Node.js的前端開發人員。我這里不專注于語言本身 -- Node.js 使用 V8 引擎,所以和Google Chrome的解釋器是一樣的,這點您或許已經了解(但是,它也可以在不同的VM上運行,請參閱 node-chakracore)目錄
Node 版本
不需要Babel
回調風格
事件循環
事件發射器
流
模塊系統
環境變量
綜合運用
總結
?我們經常跟Node.js打交道,即使你是一名前端開發人員 -- npm腳本,webpack配置,gulp任務,程序打包 或 運行測試等。即使你真的不需要深入理解這些任務,但有時候你會感到困惑,會因為缺少Node.js的一些核心概念而以非常奇怪的方式來編碼。熟悉Node.js之后,您還可以讓某些原本需要手動操作的東西自動執行,讓您可以更自信地查看服務器端代碼,并??編寫更復雜的腳本。
?
Node.js與客戶端代碼最大的區別在于您可以根據運行環境來決定,并且可以完全清楚它支持哪些特性 -- 您可以根據具體的需求和可用的服務器來選擇使用哪個版本。
Node.js有一個公開發布時間表,告訴我們奇數版本沒有被長期支持。當前的LTS(long-term support)版本將被積極開發到2019年4月,然后2019年12月31日之前,通過更新關鍵代碼進行維護。Node.js新版本正在積極開發,它們帶來了許多新功能,以及安全性和性能方面的提升。這也許是使用當前活躍版本的一個好理由。然而,沒有人真正強迫你,如果你不想這樣做,使用舊版本也可以,等到您覺得時機合適再更新就行。
Node.js被廣泛應用于現代前端工具鏈 - 我們很難想象一個現代項目沒有使用Node工具進行任何處理。因此,您可能已經熟悉nvm(node版本管理器),它允許你同時安裝幾個Node版本,為每個項目選擇正確的版本。使用這種工具的原因在于,不同項目經常使用不同的Node版本,并且你不想永遠保持它們同步,您只想保留編寫和測試它們的環境。其它語言也有很多這樣的工具,例如用于Python的virtualenv,用于Ruby的rbenv等等。
不需要Babel由于您可以自由選擇任何Node.js版本,所以您很有可能使用LTS版本。該版本在本文撰寫時為8.11.3,幾乎支持所有ECMAScript 2015的規范,除了尾遞歸。
這意味著我們不需要Babel,除非您遇到一個非常舊的Node.js版本,需要轉換JSX,或者需要其它前沿的轉換器。在實踐中,Babel并不是那么重要,所以您運行的代碼可以和編寫的代碼相同,不需要任何編譯器 -- 這個我們已經遺忘的客戶端天才。
我們也不需要webpack或browserify,那么我們就沒有工具來重新加載我們的代碼 -- 如果您在開發類似Web服務器的東西,您可以使用nodemon,在文件更改后來重新加載您的應用程序。
而且因為我們不在任何地方傳送代碼,所以不需要縮小它 -- 省了一步:您只需原封不動地使用代碼,真的很神奇!
回調風格以前,Node.js中的異步函數接受帶有簽名(err,data)的回調,其中第一個參數代表錯誤信息 - 如果它為null,則全部正確,否則您必須處理錯誤。這些處理程序會在操作完成,我們得到響應后調用。例如,讓我們讀取一個文件:
const fs = require("fs"); fs.readFile("myFile.js", (err, file) => { if (err) { console.error("There was an error reading file :("); // process is a global object in Node // https://nodejs.org/api/process.html#process_process_exit_code process.exit(1); } // do something with file content });
我們很快就發現,這種風格很難編寫可讀和可維護的代碼,甚至造成回調地獄。后來,一種新的原生的異步處理方式 Promise被引入了。它在ECMAScript 2015上標準化(是瀏覽器和Node.js運行時的全局對象)。近來,async / await 在ECMAScript 2017中標準化了,Node.js 7.6+ 都支持這個規范,所以您可以在LTS版本中使用它。
有了 Promise,我們避免了“回調地獄”。但是,現在我們遇到的問題是舊代碼和許多內置模塊仍然使用回調的方式。將它們轉換為 Promise 并不是很難 -- 為了闡釋清楚,我們將fs.readFile轉成Promise:
const fs = require("fs"); function readFile(...arguments) { return new Promise((resolve, reject) => { fs.readFile(...arguments, (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); }
這種模式可以很容易地擴展到任何函數,并且內置的utils模塊中有一個特殊的函數 - utils.promisify。官方文檔中的示例:
const util = require("util"); const fs = require("fs"); const stat = util.promisify(fs.stat); stat(".").then((stats) => { // Do something with stats }).catch((error) => { // Handle the error. });
Node.js核心團隊明白我們需要從舊風格中遷移出來,他們嘗試引入一個內置模塊的promisified版本 - 已經有promisified文件系統模塊了,雖然寫這篇文章時它還在處于試驗階段。
你仍然會遇到很多舊式的、帶回調的Node.js代碼,為了保持一致性,建議使用 utils.promisify 把它們包裝一下。
事件循環事件循環幾乎與在瀏覽器環境下一樣,只是有一些擴展。然而,由于這個主題比較高深,我將全面講解下,不僅僅是差異(我會重點強調這部分,讓您知道哪些是Node.js特有的)。
Node.js中的事件循環JavaScript在構建時考慮了異步行為,因此我們通常不會馬上執行所有操作。以下列舉的方法,事件不會直接按順序執行:
microtasks
例如,立即處理Promises,如Promise.resolve。它意味著這段代碼會在同一個的事件循環中被執行,但得等到所有同步代碼執行完后。
process.nextTick
這是Node.js特有的方法,它不存在于任何瀏覽器(以及進程對象)中。它的行為類似于微任務(microtask),但具有優先級。這意味著它將在所有同步代碼之后立即執行,即使之前引入了其他微任務 - 這是很危險的,可能導致無限循環。從命名上講是不對的,因為它是在同一個事件循環中執行的,而不是在它的next tick中執行。但是由于兼容性原因,它可能保持不變。
setImmediate
雖然它確實存在于某些瀏覽器中,但并未在所有瀏覽器中達到一致的行為,因此在瀏覽器中使用時,您需要非常小心。它類似于 setTimeout(0)代碼,但有時會優先于它。這里的命名也不是最好的 - 我們在談論下一個事件循環迭代,它并不是真正的immidiate。
setTimeout/setInterval
定時器在Node和瀏覽器中的表現形式是相同的。關于定時器的一個重要的事情是,我們提供的延遲不代表在這個時間之后回調就會被執行。它的真正含義是,一旦主線程完成所有操作(包括微任務)并且沒有其它具有更高優先級的定時器,Node.js將在此時間之后執行回調。
讓我們看看這個例子:
往下看我會給出腳本執行后正確的輸出,但是如果你愿意,請嘗試自己完成它(當一回“JavaScript解釋器”):
const fs = require("fs"); console.log("beginning of the program"); const promise = new Promise(resolve => { // function, passed to the Promise constructor // is executed synchronously! console.log("I am in the promise function!"); resolve("resolved message"); }); promise.then(() => { console.log("I am in the first resolved promise"); }).then(() => { console.log("I am in the second resolved promise"); }); process.nextTick(() => { console.log("I am in the process next tick now"); }); fs.readFile("index.html", () => { console.log("=================="); setTimeout(() => { console.log("I am in the callback from setTimeout with 0ms delay"); }, 0); setImmediate(() => { console.log("I am from setImmediate callback"); }); }); setTimeout(() => { console.log("I am in the callback from setTimeout with 0ms delay"); }, 0); setImmediate(() => { console.log("I am from setImmediate callback"); });
正確的執行順序如下:
node event-loop.js beginning of the program I am in the promise function! I am in the process next tick now I am in the first resolved promise I am in the second resolved promise I am in the callback from setTimeout with 0ms delay I am from setImmediate callback ================== I am from setImmediate callback I am in the callback from setTimeout with 0ms delay
您可以在Node.js官方文檔中獲取更多有關事件循環和process.nextTick的信息。
事件發射器Node.js中的許多核心模塊派發或接收不同的事件。它有一個EventEmitter的實現,是一個發布 - 訂閱模式。這與瀏覽器DOM事件非常相似,語法略有不同,理解它最好的方式就是親自來實現一下:
class EventEmitter { constructor() { this.events = {}; } checkExistence(event) { if (!this.events[event]) { this.events[event] = []; } } once(event, cb) { this.checkExistence(event); const cbWithRemove = (...args) => { cb(...args); this.off(event, cbWithRemove); }; this.events[event].push(cbWithRemove); } on(event, cb) { this.checkExistence(event); this.events[event].push(cb); } off(event, cb) { this.checkExistence(event); this.events[event] = this.events[event].filter( registeredCallback => registeredCallback !== cb ); } emit(event, ...args) { this.checkExistence(event); this.events[event].forEach(cb => cb(...args)); } }
以上代碼只顯示模式本身,并沒有針對確切的功能 - 請不要在您的代碼中使用它!
這是我們需要的所有基礎代碼!它允許您訂閱事件,稍后取消訂閱,并派發不同的事件。例如,響應體,請求體,流 - 它們實際上都擴展或實現了EventEmitter!
正因為它是一個如此簡單的概念,所以被用于許多的NPM包。所以,如果你想在瀏覽器中使用相同的事件發射器,可以隨時使用它們。
流“Streams是Node.js最好用、最容易被誤解的概念。”
多米尼克塔爾(Dominic Tarr)
Streams允許您以塊的形式來處理數據,而不僅僅是完整操作(如讀取文件)。為了理解它們的作用,讓我們來看個簡單的例子:假設我們想要向用戶返回任意大小的請求文件。我們的代碼可能如下所示:
function (req, res) { const filename = req.url.slice(1); fs.readFile(filename, (err, data) => { if (err) { res.statusCode = 500; res.end("Something went wrong"); } else { res.end(data); } }); }
這段代碼可以使用,特別是在本地開發的機器上,但它可也能會失敗 - 您看出問題了嗎?如果文件太大,我們讀取文件時就會遇到問題,我們將所有內容放入內存中,如果沒有足夠的內存空間,這將無法正常工作。如果我們有很多并發請求,這段代碼也不會生效 - 我們必須將數據對象保留在內存中,直到我們發送了所有內容。
然而,我們根本不需要這個文件 - 我們只需要從文件系統返回它,我們自己不會查看內容,所以我們可以讀取它的一部分,立即返回給客戶端來釋放我們的內存,重復這樣一個過程,直到我們完成了整個文件的發送。這是對 Streams 的簡短介紹 - 我們有一種以塊的形式來接收數據的機制,并且 我們 決定如何處理這些數據。例如,我們同樣可以這樣處理:
function (req, res) { const filename = req.url.slice(1); const filestream = fs.createReadStream(filename, { encoding: "utf-8" }); let result = ""; filestream.on("data", chunk => { result += chunk; }); filestream.on("end", () => { res.end(result); }); // if file does not exist, error callback will be called filestream.on("error", () => { res.statusCode = 500; res.end("Something went wrong"); }); }
這里我們創建一個 流 來讀取文件 - 這個流執行EventEmitter這個類,在data事件上我們接收下一個塊,在end事件中,我們得到一個信號,表示流已結束,然后讀取完整文件。這樣的實現跟前面的一樣 - 我們等待整個文件被讀取,然后在響應中返回它。此外,它也有同樣的問題:我們將整個文件保留在內存中,然后再發送回來。如果我們知道響應對象本身實現了可寫流,我們可以解決這個問題,我們可以將信息寫入該流而不將其保留在內存中:
function (req, res) { const filename = req.u?rl.slice(1); const filestream = fs.createReadStream(filename, { encoding: "utf-8" }); filestream.on("data", chunk => { res.write(chunk); }); filestream.on("end", () => { res.end(); }); // if file does not exist, error callback will be called filestream.on("error", () => { res.statusCode = 500; res.end("Something went wrong"); }); }
響應體實現可寫流,fs.createReadStream 創建可讀流,還有雙向和轉換流。它們之間的區別以及工作原理,不在本教程的范圍內,但是了解它們的存在還是大有裨益的。
這樣我們不再需要結果變量了,只需要把已讀的 塊 立即寫入響應體,不將它保留在內存中!這意味著我們甚至可以讀取大文件,而不必擔心高并發請求 - 因為文件沒有被保存在內存中,所以不會超出內存所能承載的數量。但是,存在一個問題。在我們的解決方案中,我們從一個流(文件系統讀取文件)中讀取文件,并將其寫入另一個(網絡請求),這兩個事物具有不同的延遲。這里強調是真的不同,經過一段時間后,我們的響應流將不堪重負,因為它要慢得多。這個問題是對背壓的描述,Node有一個解決方案:每個可讀流都有一個管道方法,它將所有數據重定向到與其負載相關的給定流中:如果它正忙,它將暫停原始流并恢復它。使用此方法,我們可以將代碼簡化為:
function (req, res) { const filename = req.url.slice(1); const filestream = fs.createReadStream(filename, { encoding: "utf-8" }); filestream.pipe(res); // if file does not exist, error callback will be called filestream.on("error", () => { res.statusCode = 500; res.end("Something went wrong"); }); }
在Node的歷史進程中,Streams改變了幾次,所以在閱讀舊手冊時要格外小心,并經常查看官方文檔!模塊系統
Node.js使用commonjs模塊。您或許使用過 - 每次使用require來獲取webpack配置中的某個模塊時,您實際上就使用了commonjs模塊; 每次聲明 module.exports 時也在使用它。然而,您可能還會看到像 exports.some = {} 這樣的寫法,沒有 module,在這一節中我們將看下它究竟是如何工作的。
首先,我們來討論commonjs模塊,它們通常都有 .js 的擴展,而不是 .esm / .mjs 文件(ECMAScript模塊),它們允許您使用 import/export 的語法。另外,重要的是要明白,webpack和browserify(以及其它打包工具)使用自己的require函數,所以請不要混淆 - 這里不講解它們,只要明白它們是不同的東西就行(即使它們表現得非常相似)。
那么,我們實際上是在哪里獲得這些“全局”對象,如 module,requier 和 exports ?實際上,是Node.js在運行時添加的 - 它不是僅執行給定的javascript文件,實際上是將它包含在具有所有這些變量的函數中:
function (exports, require, module, __filename, __dirname) { // your module }
您可以在命令行中執行以下代碼段來查看這個包:
1node -e "console.log(require("module").wrapper)"
這些是注入到模塊中的變量,可以作為“全局”變量使用,即使它們不是真正的全局變量。我強烈建議你研究它們,尤其是模塊變量。你可以在javascript文件中調用 console.log(module),對比從 main 文件打印和從 required 的文件打印出來的結果。
接下來,讓我們看一下 exports 對象 - 這里有一個小例子,顯示一些與之相關的警告:
exports.name = "our name"; // this works exports = { name: "our name" }; // this doesn"t work module.exports = { name: "our name" }; // this works!
上面的例子可能會讓你感到困惑 為什么會這樣?答案是exports對象的本質 - 它只是一個傳遞給函數的參數,所以在我們給它指定一個新對象的情況時,我們只是重寫這個變量,舊的引用就不存在了。盡管它沒有完全消失 - module.exports是同一個對象 - 所以它們實際上是對單個對象的相同引用:
module.exports === exports; // true
最后一部分是 require - 它是一個獲取模塊名稱并返回該模塊的 exports對象 的函數。它究竟是如何解析模塊的?有一個非常簡單的規則:
根據名稱檢索核心模塊
如果路徑以 ./ 或 ../開頭,則嘗試解析文件
如果找不到文件,嘗試在其中找到包含index.js文件的目錄
如果path 不以 ./ 或 ../ 開頭,請轉到node_modules /并檢查文件夾/文件:
在我們運行腳本的文件夾中
上面一級,直到我們到達/ node_modules
還有其它一些位置,主要是為了兼容性,您還可以通過指定變量 NODE_PATH 來提供查找路徑,這也許很有用。如果您要查看解析node_modules的確切順序,只需在腳本中打印模塊對象并查找paths屬性。我操作后,打印了如下內容:
? tmp node test.js Module { id: ".", exports: {}, parent: null, filename: "/Users/seva.zaikov/tmp/test.js", loaded: false, children: [], paths: [ "/Users/seva.zaikov/tmp/node_modules", "/Users/seva.zaikov/node_modules", "/Users/node_modules", "/node_modules" ] }
關于 require 的另一個有趣的事情是,在第一個require調用模塊被緩存后,將不會再次執行,我們將只返回緩存的export對象 - 這意味著你可以做一些邏輯并確保它會在第一次require調用之后只執行一次(這不完全正確 - 如果再次需要,你可以從require.cache中刪除模塊id ,然后重新加載模塊)
環境變量正如在十二因素應用程序所述,將配置存儲在環境變量中是一種很好的做法。您可以為shell會話設置變量:
export MY_VARIABLE="some variable value"
Node是一個跨平臺引擎,理想情況下,您的應用程序應該可以在任何平臺上運行(例如,開發環境。您選擇生產環境來運行您的代碼,通常它是一些Linux分發版)。我的示例僅涵蓋MacOS / Linux,不適用于Windows。Windows中環境變量的語法跟這里的不同,你可以使用像cross-env這樣的東西,但在其它情況下,你也應該記住這點。
您可以把下面這行代碼添加到 bash / zsh 配置文件中,以便在任何新的終端會話中進行設置。然而,您通常只在運行應用程序時,為這些實例提供特有的變量:
APP_DB_URI="....." SECRET_KEY="secret key value" node server.js
您可以使用 process.env 對象來訪問 Node.js 應用程序中的這些變量:
const CONFIG = { db: process.env.APP_DB_URI, secret: process.env.SECRET_KEY }綜合運用
在下面的例子中,我們將創建一個簡單的http服務,它將返回一個文件,以url/后面的字符串來命名。如果文件不存在,我們將返回 404 Not Found 的錯誤信息,如果用戶試圖投機取巧,使用相對路徑或嵌套路徑,我們則返回403錯誤。我們之前使用過其中的一些函數,但沒有真正記錄它們 - 這次它將包含大量的信息:
// we require only built-in modules, so Node.js // does not traverse our node_modules folders // https://nodejs.org/api/http.html#http_http_createserver_options_requestlistener const { createServer } = require("http"); const fs = require("fs"); const url = require("url"); const path = require("path"); // we pass the folder name with files as an environment variable // so we can use a different folder locally const FOLDER_NAME = process.env.FOLDER_NAME; const PORT = process.env.PORT || 8080; const server = createServer((req, res) => { // req.url contains full url, with querystring // we ignored it before, but here we want to ensure // that we only get pathname, without querystring // https://nodejs.org/api/http.html#http_message_url const parsedURL = url.parse(req.url); // we don"t need the first / symbol const pathname = parsedURL.pathname.slice(1); // in order to return a response, we have to call res.end() // https://nodejs.org/api/http.html#http_response_end_data_encoding_callback // // > The method, response.end(), MUST be called on each response. // if we don"t call it, the connection won"t close and a requester // will wait for it until the timeout // // by default, we return a response with [code 200](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) // in case something went wrong, we are supposed to return // a correct status code, using the res.statusCode = ... property: // https://nodejs.org/api/http.html#http_response_statuscode if (pathname.startsWith(".")) { res.statusCode = 403; res.end("Relative paths are not allowed"); } else if (pathname.includes("/")) { res.statusCode = 403; res.end("Nested paths are not allowed"); } else { // https://nodejs.org/en/docs/guides/working-with-different-filesystems/ // in order to stay cross-platform, we can"t just create a path on our own // we have to use the platform-specific separator as a delimiter // path.join() does exactly that for us: // https://nodejs.org/api/path.html#path_path_join_paths const filePath = path.join(__dirname, FOLDER_NAME, pathname); const fileStream = fs.createReadStream(filePath); fileStream.pipe(res); fileStream.on("error", e => { // we handle only non-existant files, but there are plenty // of possible error codes. you can get all common codes from the docs: // https://nodejs.org/api/errors.html#errors_common_system_errors if (e.code === "ENOENT") { res.statusCode = 404; res.end("This file does not exist."); } else { res.statusCode = 500; res.end("Internal server error"); } });} }); server.listen(PORT, () => { console.log(application is listening at the port ${PORT}); });總結
在本指南中,我們介紹了許多基本的Node.js原則。我們沒有深入研究特定的API,我們確實錯過了一些東西。但是,本指南應該是一個很好的起點,讓您在閱讀API,編輯現有的代碼,或者創建新腳本時有信心。您現在能夠理解錯誤,清楚內置模塊使用的接口,以及從典型的Node.js對象和接口中能獲取到哪些東西。
下一次,我們將深入介紹使用Node.js的Web服務,Node.js REPL,如何編寫CLI應用程序,以及如何使用Node.js編寫小腳本。您可以訂閱以獲取有關這些新文章的通知。
相關文章2017年7月9日? Node.js REPL深度2018年6月5日? 不要使用縮略詞
2018 年 6月3日? 單元測試
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108427.html
摘要:全文為這些年,我曾閱讀深入理解過或正在閱讀學習即將閱讀的一些優秀經典前端后端書籍。當然,如果您喜歡這篇文章,可以動手點點贊或者收藏。 全文為這些年,我曾閱讀、深入理解過(或正在閱讀學習、即將閱讀)的一些優秀經典前端/Java后端書籍。全文為純原創,且將持續更新,未經許可,不得進行轉載。當然,如果您喜歡這篇文章,可以動手點點贊或者收藏。 基礎 基礎書籍 進階 進階階段,深入學習的書...
摘要:全文為這些年,我曾閱讀深入理解過或正在閱讀學習即將閱讀的一些優秀經典前端后端書籍。當然,如果您喜歡這篇文章,可以動手點點贊或者收藏。 全文為這些年,我曾閱讀、深入理解過(或正在閱讀學習、即將閱讀)的一些優秀經典前端/Java后端書籍。全文為純原創,且將持續更新,未經許可,不得進行轉載。當然,如果您喜歡這篇文章,可以動手點點贊或者收藏。 基礎 基礎書籍 進階 進階階段,深入學習的書...
摘要:全文為這些年,我曾閱讀深入理解過或正在閱讀學習即將閱讀的一些優秀經典前端后端書籍。當然,如果您喜歡這篇文章,可以動手點點贊或者收藏。 全文為這些年,我曾閱讀、深入理解過(或正在閱讀學習、即將閱讀)的一些優秀經典前端/Java后端書籍。全文為純原創,且將持續更新,未經許可,不得進行轉載。當然,如果您喜歡這篇文章,可以動手點點贊或者收藏。 基礎 基礎書籍 進階 進階階段,深入學習的書...
摘要:全文為這些年,我曾閱讀深入理解過或正在閱讀學習即將閱讀的一些優秀經典前端后端書籍。當然,如果您喜歡這篇文章,可以動手點點贊或者收藏。 全文為這些年,我曾閱讀、深入理解過(或正在閱讀學習、即將閱讀)的一些優秀經典前端/Java后端書籍。全文為純原創,且將持續更新,未經許可,不得進行轉載。當然,如果您喜歡這篇文章,可以動手點點贊或者收藏。 基礎 基礎書籍 進階 進階階段,深入學習的書...
摘要:正在暑假中的課多周刊第期我們的微信公眾號,更多精彩內容皆在微信公眾號,歡迎關注。若有幫助,請把課多周刊推薦給你的朋友,你的支持是我們最大的動力。原理微信熱更新方案漲知識了,熱更新是以后的標配。 正在暑假中的《課多周刊》(第1期) 我們的微信公眾號:fed-talk,更多精彩內容皆在微信公眾號,歡迎關注。 若有幫助,請把 課多周刊 推薦給你的朋友,你的支持是我們最大的動力。 遠上寒山石徑...
摘要:正在暑假中的課多周刊第期我們的微信公眾號,更多精彩內容皆在微信公眾號,歡迎關注。若有幫助,請把課多周刊推薦給你的朋友,你的支持是我們最大的動力。原理微信熱更新方案漲知識了,熱更新是以后的標配。 正在暑假中的《課多周刊》(第1期) 我們的微信公眾號:fed-talk,更多精彩內容皆在微信公眾號,歡迎關注。 若有幫助,請把 課多周刊 推薦給你的朋友,你的支持是我們最大的動力。 遠上寒山石徑...
閱讀 853·2021-11-24 09:38
閱讀 1085·2021-10-08 10:05
閱讀 2577·2021-09-10 11:21
閱讀 2800·2019-08-30 15:53
閱讀 1826·2019-08-30 15:52
閱讀 1964·2019-08-29 12:17
閱讀 3417·2019-08-29 11:21
閱讀 1609·2019-08-26 12:17