如何編寫屬於自己的PostCSS 8插件?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"筆者近期在將前端架構webpack升級到5時,一些配套模塊也需要進行升級,其中包括了css處理模塊PostCSS。舊版本使用的是PostCSS 7,在升級至PostCSS 8的過程中,筆者發現部分插件前置依賴還是停留在7版本,且年久失修,在PostCSS 8中出現各種各樣的問題,無奈只能研究源碼,將目前部分舊版本插件升級至新版本。這裏,筆者將升級插件的過程進行簡化和提煉,讓讀者自己也可以編寫一個PostCSS 8插件。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"插件工作原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PostCSS是一個允許使用 JS 插件轉換樣式的工具。開發者可以根據自己的實際需求,在編譯過程將指定css樣式進行轉換和處理。目前PostCSS官方收錄插件有200多款,其中包括使用最廣泛的"},{"type":"codeinline","content":[{"type":"text","text":"Autoprefixer"}]},{"type":"text","text":"自動補全css前綴插件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PostCSS和插件的工作原理其實很簡單,就是先將css源碼轉換爲AST,插件基於轉換後AST的信息進行個性化處理,最後PostCSS再將處理後的AST信息轉換爲css源碼,完成css樣式轉換,其流程可以歸結爲下圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f3\/f3809e4c3d12332001583638fb60fc47.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們通過實際例子看看PostCSS會將css源碼轉換成的AST格式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const postcss = require('postcss')\npostcss().process(`\n.demo {\n font-size: 14px; \/*this is a comment*\/\n}\n`).then(result => {\n console.log(result)\n})\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼中直接引用PostCSS,在不經過任何插件的情況下將css源碼進行轉換,AST轉換結果如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"js"},"content":[{"type":"text","text":"{\n \"processor\": {\n \"version\": \"8.3.6\",\n \"plugins\": []\n },\n \"messages\": [],\n \"root\": {\n \"raws\": {\n \"semicolon\": false,\n \"after\": \"\\n\"\n },\n \"type\": \"root\",\n \/\/ ↓ nodes字段內容重點關注\n \"nodes\": [\n {\n \"raws\": {\n \"before\": \"\\n\",\n \"between\": \" \",\n \"semicolon\": true,\n \"after\": \"\\n\"\n },\n \"type\": \"rule\",\n \"nodes\": [\n {\n \"raws\": {\n \"before\": \"\\n \",\n \"between\": \": \"\n },\n \"type\": \"decl\",\n \"source\": {\n \"inputId\": 0,\n \"start\": {\n \"offset\": 11,\n \"line\": 3,\n \"column\": 3\n },\n \"end\": {\n \"offset\": 26,\n \"line\": 3,\n \"column\": 18\n }\n },\n \"prop\": \"font-size\", \/\/ css屬性和值\n \"value\": \"14px\"\n },\n {\n \"raws\": {\n \"before\": \" \",\n \"left\": \"\",\n \"right\": \"\"\n },\n \"type\": \"comment\", \/\/ 註釋類\n \"source\": {\n \"inputId\": 0,\n \"start\": {\n \"offset\": 28,\n \"line\": 3,\n \"column\": 20\n },\n \"end\": {\n \"offset\": 48,\n \"line\": 3,\n \"column\": 40\n }\n },\n \"text\": \"this is a comment\"\n }\n ],\n \"source\": {\n \"inputId\": 0,\n \"start\": {\n \"offset\": 1,\n \"line\": 2,\n \"column\": 1\n },\n \"end\": {\n \"offset\": 28,\n \"line\": 4,\n \"column\": 1\n }\n },\n \"selector\": \".demo\", \/\/ 類名\n \"lastEach\": 1,\n \"indexes\": {}\n }\n ],\n \"source\": {\n \"inputId\": 0,\n \"start\": {\n \"offset\": 0,\n \"line\": 1,\n \"column\": 1\n }\n },\n \"lastEach\": 1,\n \"indexes\": {},\n \"inputs\": [\n {\n \"hasBOM\": false,\n \"css\": \"\\n.demo {\\n font-size: 14px;\\n}\\n\",\n \"id\": \"\"\n }\n ]\n },\n \"opts\": {},\n \"css\": \"\\n.demo {\\n font-size: 14px;\\n}\\n\"\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"AST對象中nodes字段裏的內容尤爲重要,其中存儲了css源碼的關鍵字、註釋、源碼的起始、結束位置以及css的屬性和屬性值,類名使用"},{"type":"codeinline","content":[{"type":"text","text":"selector"}]},{"type":"text","text":"存儲,每個類下又存儲一個nodes數組,該數組下存放的就是該類的屬性("},{"type":"codeinline","content":[{"type":"text","text":"prop"}]},{"type":"text","text":")和屬性值("},{"type":"codeinline","content":[{"type":"text","text":"value"}]},{"type":"text","text":")。那麼插件就可以基於AST字段對css屬性進行修改,從而實現css的轉換。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"PostCSS插件格式規範及API"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PostCSS插件其實就是一個JS對象,其基本形式和解析如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"module.exports = (opts = { }) => {\n \/\/ 此處可對插件配置opts進行處理\n return {\n postcssPlugin: 'postcss-test', \/\/ 插件名字,以postcss-開頭\n \n Once (root, postcss) {\n \/\/ 此處root即爲轉換後的AST,此方法轉換一次css將調用一次\n },\n \n Declaration (decl, postcss) {\n \/\/ postcss遍歷css樣式時調用,在這裏可以快速獲得type爲decl的節點(請參考第二節的AST對象)\n },\n \n Declaration: {\n color(decl, postcss) {\n \/\/ 可以進一步獲得decl節點指定的屬性值,這裏是獲得屬性爲color的值\n }\n },\n \n Comment (comment, postcss) {\n \/\/ 可以快速訪問AST註釋節點(type爲comment)\n },\n \n AtRule(atRule, postcss) {\n \/\/ 可以快速訪問css如@media,@import等@定義的節點(type爲atRule)\n }\n \n }\n}\nmodule.exports.postcss = true\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"更多的PostCSS插件API可以詳細參考"},{"type":"link","attrs":{"href":"https:\/\/postcss.org\/api\/","title":"","type":null},"content":[{"type":"text","text":"官方postcss8文檔"}]},{"type":"text","text":",基本原理就是PostCSS會遍歷每一個css樣式屬性值、註釋等節點,之後開發者就可以針對個性需求對節點進行處理即可。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"實際開發一個PostCSS 8插件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"瞭解了PostCSS插件的格式和API,我們將根據實際需求來開發一個簡易的插件,有如下css:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"css"},"content":[{"type":"text","text":".demo {\n font-size: 14px; \/*this is a comment*\/\n color: #ffffff;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"需求如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"刪除css內註釋"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"將所有顏色爲十六進制的"},{"type":"codeinline","content":[{"type":"text","text":"#ffffff"}]},{"type":"text","text":"轉爲css內置的顏色變量"},{"type":"codeinline","content":[{"type":"text","text":"white"}]}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據第三節的插件格式,本次開發只需使用"},{"type":"codeinline","content":[{"type":"text","text":"Comment"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"Declaration"}]},{"type":"text","text":"接口即可:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ plugin.js\nmodule.exports = (opts = { }) => {\n\n return {\n postcssPlugin: 'postcss-test',\n \n Declaration (decl, postcss) {\n if (decl.value === '#ffffff') {\n decl.value = 'white'\n }\n },\n \n Comment(comment) {\n comment.text = ''\n }\n \n }\n}\nmodule.exports.postcss = true\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在PostCSS中使用該插件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ index.js\nconst plugin = require('.\/plugin.js')\n\npostcss([plugin]).process(`\n.demo {\n font-size: 14px; \/*this is a comment*\/\n color: #ffffff;\n}\n`).then(result => {\n console.log(result.css)\n})\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"運行結果如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"css"},"content":[{"type":"text","text":".demo {\n font-size: 14px; \/**\/\n color: white;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,字體顏色值已經成功做了轉換,註釋內容已經刪掉,但註釋標識符還依舊存在,這是因爲註釋節點是包含"},{"type":"codeinline","content":[{"type":"text","text":"\/**\/"}]},{"type":"text","text":"內容存在的,只要AST裏註釋節點還存在,最後PostCSS還原AST時還是會把這段內容還原,要做到徹底刪掉註釋,需要對AST的nodes字段進行遍歷,將type爲comment的節點進行刪除,插件源碼修改如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ plugin.js\nmodule.exports = (opts = { }) => {\n\n \/\/ Work with options here\n \/\/ https:\/\/postcss.org\/api\/#plugin\n\n return {\n postcssPlugin: 'postcss-test',\n \n Once (root, postcss) {\n \/\/ Transform CSS AST here\n root.nodes.forEach(node => {\n if (node.type === 'rule') {\n node.nodes.forEach((n, i) => {\n if (n.type === 'comment') {\n node.nodes.splice(i, 1)\n }\n })\n }\n })\n },\n \n\n \n Declaration (decl, postcss) {\n \/\/ The faster way to find Declaration node\n if (decl.value === '#ffffff') {\n decl.value = 'white'\n }\n }\n \n }\n}\nmodule.exports.postcss = true\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重新執行PostCSS,結果如下,符合預期。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"css"},"content":[{"type":"text","text":".demo {\n font-size: 14px;\n color: white;\n}\n"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"插件開發注意事項"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過實操開發可以看到,開發一個PostCSS插件其實很簡單,但在實際的插件開發中,開發者需要注意以下事項:"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.儘量使插件簡單,使用者可以到手即用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Build code that is short, simple, clear, and modular."}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘量使你的插件和使用者代碼解耦,開放有限的API,同時開發者在使用你的插件時從名字就可以知道插件的功能。這裏推薦一個簡單而優雅的PostCSS插件"},{"type":"link","attrs":{"href":"https:\/\/github.com\/postcss\/postcss-focus","title":"","type":null},"content":[{"type":"text","text":"postcss-focus"}]},{"type":"text","text":",讀者可以從這個插件的源碼中體會這個設計理念。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.開發插件前確認是否有現成的輪子"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你對自己的項目有個新點子,想自己開發一個插件去實現,在開始寫代碼前,可以先到PostCSS官方註冊的"},{"type":"link","attrs":{"href":"https:\/\/github.com\/postcss\/postcss\/blob\/main\/docs\/plugins.md","title":"","type":null},"content":[{"type":"text","text":"插件列表"}]},{"type":"text","text":"中查看是否有符合自己需求的插件,避免重複造輪子。不過截止目前(2021.8),大部分插件依舊停留在PostCSS 8以下,雖然PostCSS 8已經對舊版本插件做了處理,但在AST的解析處理上還是有差異,從實際使用過程中我就發現PostCss8使用低版本插件會導致AST內的"},{"type":"link","attrs":{"href":"https:\/\/github.com\/leodido\/postcss-clean\/issues\/17","title":"","type":null},"content":[{"type":"text","text":"source map丟失"}]},{"type":"text","text":",因此目前而言完全兼容PostCSS 8的插件還需各位開發者去升級。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"從低版本PostCSS遷移"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"升級你的PostCSS插件具體可以參考官方給出的"},{"type":"link","attrs":{"href":"https:\/\/evilmartians.com\/chronicles\/postcss-8-plugin-migration","title":"","type":null},"content":[{"type":"text","text":"升級指引"}]},{"type":"text","text":"。這裏只對部分關鍵部分做下解釋:"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.升級API"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將舊版"},{"type":"codeinline","content":[{"type":"text","text":"module.exports = postcss.plugin(name, creator)"}]},{"type":"text","text":"替換爲"},{"type":"codeinline","content":[{"type":"text","text":"module.exports = creator"}]},{"type":"text","text":";"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"新版插件將直接返回一個對象,對象內包含"},{"type":"codeinline","content":[{"type":"text","text":"Once"}]},{"type":"text","text":"方法回調;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將原插件邏輯代碼轉移至"},{"type":"codeinline","content":[{"type":"text","text":"Once"}]},{"type":"text","text":"方法內;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"插件源碼最後加上"},{"type":"codeinline","content":[{"type":"text","text":"module.exports.postcss = true"}]},{"type":"text","text":";"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體示例如下。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"舊版插件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"- module.exports = postcss.plugin('postcss-dark-theme-class', (opts = {}) => {\n- checkOpts(opts)\n- return (root, result) => {\n root.walkAtRules(atrule => { … })\n- }\n- })\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"升級後插件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"+ module.exports = (opts = {}) => {\n+ checkOpts(opts)\n+ return {\n+ postcssPlugin: 'postcss-dark-theme-class',\n+ Once (root, { result }) {\n root.walkAtRules(atrule => { … })\n+ }\n+ }\n+ }\n+ module.exports.postcss = true\n"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.提取邏輯代碼至新版API"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"把邏輯代碼都放在"},{"type":"codeinline","content":[{"type":"text","text":"Once"}]},{"type":"text","text":"回調內還不夠優雅,PostCSS 8已經實現了單個css的代碼掃描,提供了"},{"type":"codeinline","content":[{"type":"text","text":"Declaration()"}]},{"type":"text","text":", "},{"type":"codeinline","content":[{"type":"text","text":"Rule()"}]},{"type":"text","text":", "},{"type":"codeinline","content":[{"type":"text","text":"AtRule()"}]},{"type":"text","text":", "},{"type":"codeinline","content":[{"type":"text","text":"Comment() "}]},{"type":"text","text":"等方法,舊版插件類似"},{"type":"codeinline","content":[{"type":"text","text":"root.walkAtRules"}]},{"type":"text","text":"的方法就可以分別進行重構,插件效率也會得到提升:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":" module.exports = {\n postcssPlugin: 'postcss-dark-theme-class',\n- Once (root) {\n- root.walkAtRules(atRule => {\n- \/\/ Slow\n- })\n- }\n+ AtRule (atRule) {\n+ \/\/ Faster\n+ }\n }\n module.exports.postcss = true\n"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過本文的介紹,讀者可以瞭解PostCSS 8工作的基本原理,根據具體需求快速開發一個PostCSS 8插件,並在最後引用官方示例中介紹瞭如何快速升級舊版 PostCSS 插件。目前PostCSS 8還有大量還沒進行升級兼容的PostCSS 插件,希望讀者可以在閱讀本文後可以獲得啓發,對PostCSS 8的插件生態做出貢獻。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章