webpack是目前一個很熱門的前端打包工具,官網說得很清楚,webpack的出現就是要把requirejs干掉。同時它還提供了十分便利的本地開發的環境。網上並不容易找到一個講解得比較詳細完整的教程,本文結合實踐經驗,總結一套可用的開發和上線的配置和流程。
首先,Require JS有什么問題
RequireJs存在的問題
博主先是使用了RequireJs,后來又轉了webpack,綜合比較,requirejs確實存在一些缺點:
1.寫法比較笨拙
需要把所有的依賴模塊寫在require函數里面,當模塊很多的時候,看起來逼格就不高了,感受如下:
而webpack既兼容requirejs的寫法,也兼容commonjs的寫法,也就是說,使用webpack你既可以繼續像上面那樣寫,也可以像node那樣寫,感受如下:
var modules = { signHandler: require("module/sign-log"), chatHandler: require("module/chat-win"), mapHandler: require("lib/map"), util: require("lib/util") };
可以在需要的時候再去require,而不是搞個大括號把全部的模塊一下子寫到一起。(模塊的導出用module.exports = ....)
當然這兩種寫法不僅是感光上的區別,邏輯上也有區別。用中括號加載的模塊通常webpack是動態去加載,而沒有中括號是和主文件打包在一起的。
2. 沒有通用模塊的概念
例如有一個彈框模塊,用在登陸注冊,並且所有頁面都有登陸注冊,所以這是一個所有頁面的通用模塊。如果頁面的其它模塊都沒調到通用模塊里面的東西的話,用RequireJs沒什么問題。但是實際情況上不是這樣的,例如util模塊既會被登陸注冊的模塊調用,也會被很多其它模塊調用。這個時候合並壓縮就有問題了:合並后的通用模塊如common-app.js會帶上util的代碼、另外一個頁面的例如detail.js也會帶上util的代碼,以后一改util.js里面的東西,就會一並改動其它所有用到util的頁面js,就得重新打所有js的版本號。這樣無論對布署上線,還是對於用戶的緩存來說都是不利的。
webpack可以把幾個文件的通用模塊抽出來單獨作為一個模塊common-chunk.js,引用的時候每個頁面先引一個common-chunk.js,再引一個該頁面自己的js文件如detail.js,原detail.js里面和其它js文件共用的模塊已經被提取到common-chunk.js里面。
3. 沒辦法直接動態合並壓縮一個需要異步加載的模塊
這個問題是這樣的,假設我的聊天模塊文件有500Kb這么大,並不希望一刷頁面就加載,而是用戶點了聊天再去加載。這個聊天模塊有一個入口文件和其它幾個模塊文件,我合並壓縮了入口文件,需要有一個輸出文件,而入口文件define的模塊名和壓縮優化后的輸出文件的路徑肯定是要不一樣的,但是壓縮之后他並不會自動去改變輸出文件的模塊名。這樣就導致你要手動去改一下壓縮文件的模塊名,不然會require不到。我之前找了一下,沒有找到解決方案,所以采取了一個壓縮兩次的比較笨拙的方法。
而webpack有一個文件束chunkFile的概念,它會自動去把需要異步加載的文件變成一個chunkFile,然后觸發加載的時候再去加載chunkFile。
4. 需要借助gulp等管理工具進行開發
webpack本身有一些插件和第三方的插件,可以在本地開一個webpack-dev-server,文件一保存的時候就會自動打包編譯js/css/less/sass等。
使用RequireJs雖然看起來缺點比較多,但是使用RequireJs也有webpack不具備的優點,那就是RequireJs開發的時候在瀏覽器里面,每個模塊都是單獨一個文件,跟本地文件保持一致,而webpack是把主文件和該文件都用到的模塊都打包成了一個文件,這樣在調試的時候就需要你去搜索找到要調試的位置,而使用requireJs直接根據第幾行就可以了。不過,考慮到使用webpack可以搭建一個很方便的本地開發環境,所以這個缺點也不是很明顯。
使用webpack
用一句概括就是:寫一個配置文件,然后執行下webpack,就可以把生成的文件輸出,可壓縮帶版本號,同時生成一個source-map文件,這個文件包含了每個模塊的js和css的實際(帶版本號)路徑,根據這個路徑就可以把html里面的js/css等換成真實的路徑。
webpack是一個打包的工具,它有一個重要的概念,就是把js/css/image/coffee都當成地位相等的資源,你可以在js里面require一個css,也可以require一個image。但是這種模式比較適用於React等框架,都是用js控制。
webpack的其它幾個重要概念:
1. loader加載器
上面說到,各種各樣的資源都可以在webpack里面加載,而這些資源都需要相應的加載器,webpack才能識別,然后解析成正常的瀏覽器認識的資源。
換句話說,你可以給webpack加載各種各樣的資源:css/less/sass/png/babel等,然后在代碼里面進行管理。
例如要加一個sass的loader,需要先安裝:
npm install sass-loader node-sass
然后在配置文件添加一個loader:
{ test: /\.sass$/, loaders: ["style", "css", "sass"]
},
這樣當你require(“hello.sass”)的時候,webpack就能處理這種.sass結尾的文件。這樣子有兩個好處,一個是webpack能夠自動編譯sass為css,另一個是require進來的style,webpack會把它解析成一個object,這個object的key就是類名,就可以在js使用樣式的類名,這種比較適合類似於react的開發模式。
2. 文件束chunk
上面提到的,會把動態加載的文件生成一個個的chunk,在配置文件的output里面加一行:
chunkFilename: "bundle-[id].js"
就會根據id區分不同動態加載的chunk文件,而這些chunk文件名對於我們來說是無關緊要,因為這個是webpack管理的,開發者無需關心叫什么又是怎么加載的。
3. webpack-dev-server
這是webpack的一個插件,可以在本地開一個靜態服務,用來作為本地開發的重要工具。具體步驟就是html里面引用的資源用一個假的域名,如develop.com:
<script src="//develop.com/site/app-init.js"></script>
然后再把develop.com綁到本地回路:
127.0.0.1 fedren.com
這樣請求就打到了本地的80端口。同時在本地開一個nginx監聽在80端口,nginx收到80端口的請求后,再把請求轉發到webpack的服務(默認是8080端口)。這樣就能夠實現本地開發,下文會具體介紹。
下面一步步介紹怎么配置和使用webpack
webpack的基本配置
首先,npm init創建一個node的配置文件package.json,然后安裝webpack:
npm install webpack sudo npm install webpack -g //安裝一個全局的命令
再創建一個webpack.config.js文件,加入最基本的配置:
module.exports = { // The standard entry point and output config //每個頁面的js文件 entry: { home: "js/home", detail: "js/detail" }, output: { path: "assets", //打包輸出目錄 publicPath: "/static/build/", //webpack-dev-server訪問的路徑 filename: "[name].js", //輸出文件名 chunkFilename: "bundle-[id].js" //輸出chunk文件名 } };
工程的js都放到js目錄下,一個叫home.js,另一個叫detail.js,輸出到assets目錄,publicPath是為webpack-dev-server所使用
然后在當前目錄執行webpack,發現webpack報錯了:
ERROR in Entry module not found: Error: Cannot resolve module 'js/home' in /Users/yincheng/code/blog-webpack
找不到js/home的模塊,只要在配置里面加一句resolve:
resolve: { modulesDirectories: ['.'] }
告訴webpack所有模塊的啟始目錄由當前目錄開始,再執行下webpack就可以正常輸出了:
到目前為此,當前工程的目錄結構就是這樣的了:
接下來,創建html:home.html,里面引入js文件,"static/build"即為上面定義的publicPath:
<body> <p>home.html</p> <script src="//develop.com/static/build/home.js"></script> </body>
注意我們用了一個develop.com的域名,把這個域名綁到本地回路:
127.0.0.1 develop.com
然后配置nginx,打開nginx.conf,加多一個server:
server {
listen 80;
server_name payment-admin.com;
charset utf-8;
#工程路徑
root /Users/yincheng/code/demo;
autoindex on;
autoindex_exact_size on;
location ~* /.+\.[a-z]+$ {
proxy_set_header x-request-filename $request_filename;
# webpack的服務
proxy_pass http://127.0.0.1:8080;
}
}
啟動nginx或者重啟下nginx
然后再裝一個webpack-dev-server:
npm install webpack-dev-server --save-dev
sudo npm install webpack-dev-server -g
然后啟動webpack-dev-server,執行:
webpack-dev-sever --port=8080 //不加port參數,默認就為8080端口
然后就可以訪問:http://develop.com/html/home.html
這個時候,只要一改變home.js的內容,webpack-dev-server就會自動打包新的文件 ,一刷新頁面,就是最新的修改了。這樣就實現了最基本的本地開發,不管你用的jsp/php,都不需要把js/css往服務器上傳。 注意webpack-dev-server是在內存生成的文件,你在本地是找不到static/build目錄的,只有執行了webpack打包才會輸出文件到assets目錄。一個為上面配置里的publicPath,另一個為path。
引入樣式文件——首先創建css/home.css:
body{ color: #f00; }
然后在js里面引入這個css文件:
require("css/home.css");
一保存之后,會發現webpack-dev-server報錯了:
ERROR in ./css/home.css Module parse failed: /Users/yincheng/code/blog-webpack/css/home.css Unexpected token (1:4) You may need an appropriate loader to handle this file type.
根據提示,我們需要加裝一個css loader,讓webpack能夠處理css文件,更改webpack.config.js,加入一個loader:
module.exports = { entry: ..., output: ..., resolve: ..., module: { loaders: [ { test: /\.css$/, loader: "style-loader!css-loader" }, ] } };
當然要先安裝一下:npm install style-loader css-loader --save-dev,然后再重啟下webpack-dev-server,就可以加載樣式了,我們發現webpack是把樣式動態插到了head標簽的style里面,但是一般並不希望直接寫到head里面,而是獨立的一個css文件,這個時候借助一個分離css的插件就可以了:
npm install extract-text-webpack-plugin --save-dev
同時把配置文件的loader改一下:
var ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { module: { loaders: [ // Extract css files { test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader") }, ] }, plugins: [ new ExtractTextPlugin("[name].css") ] };
就會生成和js相同路徑和名字的css文件,在home.html里面引入css文件:
<link rel="stylesheet" href="//develop.com/static/build/home.css"></link>
你也可以加載各種各樣的loader,如加載一個sass/less loader,require一個sass/less文件后就可以寫sass/less了,webpack會把它編譯成和上面一樣普通的css文件,讀者可以自己試試,還可以再裝一個png/jpg的loader,指定一個小於多少個k的圖片的參數,webpack就會把小於指定尺寸的圖片轉成base64的格式。各種loader的安裝查一查就有了。
到這里一個最基本的本地開發環境就已經搭起來了。接下來討論自動刷新
自動刷新
上面一保存js/css的時候,webpack server就會自動打包,刷新頁面的時候就是最新的修改。這個刷新只要使用webpack的hot模式就可以自動實現,即一保存就自動打包刷新。將上面運行webpack-dev-server的命令再加多兩個參數,按照官方文檔的方式:
webpack-dev-server --port=8383 --hot --inline
如果沒有意外,在你的電腦上將會報錯:
ERROR in multi home
Module not found: Error: Cannot resolve module 'webpack/hot/dev-server' in /Users/yincheng/code/blog-webpack
@ multi home
這個問題困惑了筆者好久,因為在node_modules里面是有這個"webpack/hot/dev-server"的,其實只要認真看下上面的提示,就會發現它並不是說在node_modules里面,而是在當前工程目錄里,所以把node_modules里的webpack文件夾拷一份到外面就可以正常運行了。(如果你又配了個context的參數的話,那就根據提示拷到context指定的目錄)
使用hot模式,只要一保存js/css就可以自動刷新了,這個功能確實很方便。如果不寫參數,也可以把它寫在配置文件里面:
var hotModuleReplacementPlugin = require("webpack/lib/HotModuleReplacementPlugin"); module.exports = { plugins: [ new ExtractTextPlugin("[name].css"), new hotModuleReplacementPlugin() ], devServer: { historyApiFallback: true, hot: true, inline: true, progress: true } };
然后運行server就不用帶上后面那兩個參數了。
Common chunk
如上文提到,webpack可以將幾個js的公共模塊提取成一個chunk,需要借助一個commonChunkPlugin,在上面的plugins再添加一個:
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); plugins: [ new CommonsChunkPlugin({ //minChunks: 3, name: "common-app.chunk", chunks: ["home", "detail", "list"] }) ]
這樣就可以把home、detail、list三個js和css用到的公共模塊提取到common-app.chunk.js和common-app.chunk.css這兩個文件了。注意頁面要先引入這兩個文件,然后再引入具體頁面的js,webpack在common chunk里面定義了它的require函數。如上面的home.html:
<script src="//develop.com/static/build/common-app.chunk.js"></script> <script src="//develop.com/static/build/home.js"></script>
可以指定一個minChunk的參數,指定模塊至少被require幾次才能提取出來,默認是3
還可以定義兩個commonChunk,例如在詳情頁、列表頁和首頁都有搜索的模塊,而其它頁面沒有搜索的模塊,也就是說除了所有頁面都有的公共模塊如登陸注冊外,還有一個搜索的公共模塊有三個頁面要用到。如果都用一個common chunk,會把搜索的也放進來,但其它很多頁面並不需要用到。這個時候需要加多一個common chunk:
plugins: [ new CommonsChunkPlugin({ name: "search-app.chunk", chunks: ["search-app-init", "home", "detail", "list"] }), new CommonsChunkPlugin({ name: "common-app.chunk", chunks: ["home", "detail", "search-map", "search-app.chunk", "sell", "about", "blog"] }) ]
注意要把search-app.chunk也寫到下面那個所有頁面的chunk里面,否則webpack會定義兩個一樣的require函數,頁面的模塊也會跟着混亂,一刷頁面就報錯。頁面引用js的順序就變成了:
<script src="//develop.com/static/build/common-app.chunk.js"></script> <script src="//develop.com/static/build/search-app.chunk.js"></script> <script src="//develop.com/static/build/home.js"></script>
壓縮和版本號
壓縮只需要要在plugins里面再添加一個用來壓縮的插件:
var webpack = require("webpack"); plugins: [ new webpack.optimize.UglifyJsPlugin() ]
這樣執行webpack輸出的js/css就是壓縮的
版本號就是在輸出帶上hash的替換符,如下:
module.exports = { output: { path: "assets", publicPath: "/static/build/", filename: "[name]-[chunkhash].js", chunkFilename: "bundle-[chunkhash].js" }, plugins: [ new ExtractTextPlugin("[name]-[contenthash].css") ], }
其中js用的是webpack的chunkhash,而css用的是contenthash,contenthash是根據內容生成的hash。如果不用contenthash,那么一改js,css的版本號也會跟着改變,這個就有問題了。webpack還有另外一個自帶的叫做"[hash]",這個hash是所有文件都用的同一個哈希,也就是說某個文件改了,所有文件的版本號都會跟着改,所以一般不用這個。
運行webpack,如果報了下面這個錯誤:
ERROR in chunk detail [entry] [name]-[chunkhash].js Cannot use [chunkhash] for chunk in '[name]-[chunkhash].js' (use [hash] instead)
那你就把plugins里面的熱替換插件注釋掉就好了,上線的config不需要熱替換:
plugins: [ //new hotModuleReplacementPlugin(), ],
成功執行后,就會在設定的output目錄下面輸出加上版本號的文件:
. ├── detail-d19e4614a1c4f3c1581b.js ├── home-11198f8526424e8c58ce10a2799793e3.css └── home-5ec13a52eea2a6faf96a.js
有了版本號之后,下一步是要把html里面的js/css換成帶版本號的路徑
替換Html里js/css路徑
之前在html里的路徑是test.com,現在要把它換成cdn且帶版本號的路徑,也就是說,目標是要把下面的引入:
<script src="//develop.com/static/build/home.js"></script>
替換成下面的引入,並把新生成的html輸出到built目錄
<script src="//cdn.mycdn.com/test/home-5ec13a52eea2a6faf96a.js"></script>
目測沒有現成符合格式的插件可以用,可以自已用node寫一個,不費事。
首先要知道所有文件的對應的版本號,可以用AssetsPlugin,生成source-map:
var AssetsPlugin = require('assets-webpack-plugin'); output: { publicPath: "//cdn.mycdn.com/static/build/" }, plugins: [ new AssetsPlugin({filename: './source-map.json', prettyPrint: true}), ]
執行webpack之后,就會生成source-map.json,打開這個文件:
{ "detail": { "js": "//cdn.mycdn.com/static/build/detail-c8a2c82ebe2e48e06564.js" }, "home": { "js": "//cdn.mycdn.com/static/build/home-380af86bfeb6fcb477a4.js", "css": "//cdn.mycdn.com/static/build/home-11198f8526424e8c58ce10a2799793e3.css" } }
根據develop.com開頭的以及最后面的home.js/home.css,就可以在上面找到對應的路徑名。筆者寫了個腳本,可以實現這個功能,詳見:version-control-replace-html
到這里,整個流程就基本完成了。還有一些優化的步驟
優化
1. 優化模塊id
webpack對於每個模塊都是用id標志,而不是用模塊的名字,只是為了節省空間。還可以再節省,就是用它自帶的occurrence-order插件將最常用的模塊靠前,這樣可以再節省一點點空間,因為id是從0開始排的,從一位數到n位數。
new webpack.optimize.OccurenceOrderPlugin()
2. 移出版本號
在上面用了common-chunk的插件,抽離公共模塊,在這個common-chunk.js里,webpack會定義每個模塊加載的src,以便於加載那些需要動態加載的chunk,如下:
script.src = __webpack_require__.p + "" + chunkId + "-" + {"0":"0cb48ff1ab1d1156015d","5":"e9e7f761f306c648ccef","6":"cbbdf8e3ad1aba34ced0"}[chunkId] + ".js";
從上面可以看出它會把版本號也寫在里面,這樣就導致一個問題,每改一個js文件,它的版本號就會變化,就會導致common chunk里面的內容發生變化,所以它的版本號也得跟着變,也就是說改了一個文件,影響了兩個文件。所以需要把它抽出來,有個插件已經做了這樣的事情,叫做ChunkManifestPlugin:
var ChunkManifestPlugin = require('chunk-manifest-webpack-plugin'); plugins: [ new ChunkManifestPlugin({ filename: "chunk-manifest.json", manifestVariable: "webpackManifest" }) ]
傳兩個參數,一個是輸出文件名,另一個是變量名,用於上面的script.src,執行webpack后,它會把上面script.src的那一坨東西放到chunk-manifest.json,然后在頁面寫一個內聯的script,定義一個全局變量window.webpackManifest,值為manifest.json里面的內容。筆者已在上面的替換版本號的腳本做處理,只需在頁面合適的地方寫上一行:
<!--%webpack manifest%-->
就會把這行替換成一個script標簽。
3. 多個common-chunk的優化
在上面寫了兩個common chunk,在生成的兩個chunk文件里面,你會發現大量的的重復代碼,已經失去了公共模塊的作用,這個問題可以用一個MoveToParentMergingPlugin解決,它會把search-app用到的common-app的模塊全部移到了common-app,search-app就不會重復common-app的內容了。
html保存自動刷新
上面提到,只要一保存css/js,webpack-dev-server就會自動保存和刷新,但是html/jsp沒辦法(如果你用react開發,可以用react-hot-loader),其實可以手動解決這個問題。打開node_modules/webpack-dev-server/client/index.js這個文件,可以發現webpack是用的sockjs實現自動刷新的。瀏覽器使用sockjs創建socket客戶端,連接到webpack的服務,保存更改的時候,服務就向瀏覽器的socket發送消息,接收到這個消息后客戶端就調window.location.reload刷新頁面。所以可以模仿這個過程,在本地另開一個服務,監聽html的修改,然后向瀏覽器端發送刷新頁面的消息。
具體來說,首先在上面的node_modules/webpack-dev-server/client/index.js這個文件最后面再添加一個socket連接:
/*自定義reload window*/ var reload = new SockJS("http://localhost:9999/reload"); reload.onopen = function(){ console.log("customer reload start......."); } reload.onclose = function(){ console.log("customer reload close......."); } reload.onmessage = function(_msg){ var msg = JSON.parse(_msg.data); if(msg.type === "reload"){ console.log("customer reload window now"); window.location.reload(); } }
這個9999端口的server就是下面要在本地監聽的一個socket服務。在開這個socket服務之前,需要先在本地開一個監聽文件修改的服務,然后再向這個socket服務發送消息。監聽的服務比較好寫,有現成的node包可以用:chokidar,使用也非常簡單。監聽到修改之后就可以執行上傳服務器的命令,然后(使用進程間的通信)再向socket服務發送一個需要刷新的消息,再傳遞給瀏覽器的scoket,如上面的代碼,一收到消息就刷新頁面。具體代碼查看github
除了優化,在使用中會遇到的一些問題:
解決問題
1. umd的require模式
有時候會引入外部的庫,這些庫可能會用umd的require模式,判斷是要用requirejs還是commonjs或是寫個全局的函數:
/* CommonJS */ if (typeof require === 'function' && typeof module === 'object' && module && typeof exports === 'object' && exports) module['exports'] = init(require("ByteBuffer")); /* AMD */ else if (typeof define === 'function' && define["amd"]) define("lib/chat/ProtoBuf", ["./ByteBuffer"], init); /* Global */ else(global["dcodeIO"] = global["dcodeIO"] || {})["ProtoBuf"] = init(global["dcodeIO"]["ByteBuffer"]);
這個的問題就在於,只要頁面上有require出現,webpack就會去打包,不管你是寫if里面還click事件里面。因為像上面說的,webpack會把異步加載的文件打包成一個boundle文件,同時也會把非異步的打包到一起。像上面那樣寫,它會重復打包,生成好多個bundle。只要加多一個umdREquirePlugin,webpack就能正常打包了。
2. 如何加載外部資源
webpack是一個打包的工具,它並不是像requireJs那樣可以支持直接require一個外部資源。
例如我要require谷歌地圖:https://maps.googleapis.com/maps/api/js,打包的時候webpack會給出一個warning,說加載不到這個外部資源,運行代碼的時候會報錯,提示沒有這個模塊。
另外一個問題是,我需要if else判斷,如果是中國的環境就加載中國域名的谷歌地圖:http://ditu.google.cn/maps/api/js 否則就加載上面的,使用webpack是沒辦法做到的, 使用requireJs就可以很簡單地直接require一下就行。
但其實這個問題很好解決只要自己寫一個動態加載script的函數就好了,一個兼容性很好的版本:
function loadScript(url, callback){ var script = document.createElement("script") script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; callback(); } }; } else { //Others script.onload = function(){ callback(); }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
詳見:The best way to load external JavaScript
webpack雖然是一個利器,但是坑也不少,目前遇到過的不太好解決的問題:
遇到的困難
1. chunkhash
使用chunkhash有兩個問題,一個是css改變之后,js的版本號也會跟着改變,即使js沒有修改,但是比較這兩個js文件的時候,你會發現這兩個版本號不一樣的文件內容是完全一模一樣的。因為chunkhash不是根據文件內容算的hash值。第二個問題是,相同的代碼在不同人的機器上打的包的版本號不一樣。如果使用一些根據文件內容打版本號的插件,如webpack-md5-hash,這個插件是用文件內容作一個md5的計算得出一個版本號,這樣可以解決上面的兩個問題,但是又引發了新的問題,這個md5的時不時就會出現打的版本號不唯一的情況,文件內容不同、版本號相同,而且這個概率還不小。所以最后還是放棄了使用這個插件,然后又嘗試了另外一個使用sha算法計算,但是這個改了一個文件會使幾個文件的版本號也發生變化。現在還是使用chunkhash
2. 模塊id發生變化
上文提到,webpack的模塊是用id標志的,每個模塊對就一個id,例如util對應2,但是這個id不是固定不變的,在n次修改和打包之后,util的id可能會變成了3,這個就比較坑了,給增量上線造成了阻力,即單獨上一個html有風險。因為在common-chunk里面,util的id是上次打包的時候定的,但是你這次打包util的id變了,而你只想上home.html,在home.html里面引的home.js里面使用到的util的id對不上common-chunk里面的,導致不能在home里面正常地加載util這個模塊。一個臨時的解決辦法是,home.js不要使用common-chunk,所有的模塊都打包到home.js里面就不會有這個問題。
綜上對於webpack的介紹基本說完了,后續會繼續研究webpack的打包方式和怎么樣寫一個webpack的插件。如果上面有什么不合理或可以優化的地方還請指出。