# NodeJS(Max)第 5 節: Working with Express.js > Udemy課程:[NodeJS - The Complete Guide (MVC, REST APIs, GraphQL, Deno) ](https://www.udemy.com/course/nodejs-the-complete-guide/) `20231204Mon.~20231209Sat.` ## Summary ![image](https://hackmd.io/_uploads/r1F39PbLp.png) ## 5-58. What is Express.js? ![image](https://hackmd.io/_uploads/B1T6o7sra.png) Express為一個框架! **** ## 5-59. Installing Express.js **● 初始化npm** ```javascript! npm init ``` **● 下載nodemon(在devdependcies即可)** ```javascript! npm install --save-dev nodemon ``` **● 新增script(之後可以直接用`npm start`執行程式)** ```javascript! "start": "nodemon app.js" ``` **● 下載express.js(在dependcies)** ```javascript! npm install --save express //也可以不寫--save,畢竟預設即--save npm install express ``` ![image](https://hackmd.io/_uploads/HyqnyNoST.png) **● 在app.js中引入express** ```javascript! const express = require("express"); ``` **● 將express作為函式,存入一個變數app** ```javascript! const express = require("express"); const app = express(); ``` 至於為什麼?可以從以下方法得知,首先,滑鼠放在express上方,同時按下ctrl鍵,express的字樣會出現底線,出現底線後點擊。 ![2023-12-04 19-02-37 的螢幕擷圖](https://hackmd.io/_uploads/r1ZJMViS6.png) 點擊後會跑出express的檔案,從中可知他export的是一個function,因此express便需要以執行function的方式操作。 ![image](https://hackmd.io/_uploads/BJ32f4oBa.png) **** ## 5-60. Adding Middleware Express 是一個本身功能極簡的路由與中介軟體 Web 架構:本質上,Express 應用程式是一系列的中介軟體函數呼叫。 會有多個blocks構成整個application,而非一段function去處理所有的邏輯。 ![image](https://hackmd.io/_uploads/rkqKS4iBp.png) 老師這裡先介紹了express中的use()函式。use()讓我們得以增加一個新的middleware funciton。 :::danger [註]**middleware function** Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle. To load the middleware function, call app.use(), specifying the middleware function. ::: use()非常的彈性,除了可以接受request handler作為參數,還有許多的cases。 那這裡會先使用use()中最簡單的用法,以另一個function作為app.use()的參數,而該傳入的function,會在每一次收到request時去執行,且該function可以接收3個參數:req, res, next(這是一般多數人的寫法,三者想rename都是沒問題的)。 ```javascript! app.use((req, res, next) => { ... }) ``` req, res即request、respones object,有許多的method, 屬性等等可以使用,這在[第3節](https://hackmd.io/@noz915/rJI7Y7sET)已提過,至於next,他是一個function,這個next function允許我們可以travel(感覺這裡的travel意思滿像資結中一棵樹的「走訪」之意)到下一個middleware。 ~~~~~直接來看看程式碼~~~~~ ### **▊ 只有一個app.use()** 這裡先只放上單一的app.use(),接著利用`npm start`執行該程式,再到瀏覽器中輸入`localhost:3000`。 ```javascript! const http = require("http"); const express = require("express"); const app = express(); app.use((req, res, next) => { console.log("In the middleware!"); }) const server = http.createServer(app); server.listen(3000); ``` 接著回到VS code中的terminal查看,可以看見app.use()裡頭的console.log(),確實有被執行印出"In the middleware!"。 ![image](https://hackmd.io/_uploads/B1NtxBira.png) ### **▊ 放上兩個app.use()** 這次再增加一個app.ues()在下方,利用`npm start`執行該程式,再到瀏覽器中進入到`localhost:3000`。(或者前一個例子結束後不要關掉,直接重整即可) ```javascript! const http = require("http"); const express = require("express"); const app = express(); app.use((req, res, next) => { console.log("In the middleware!"); }) app.use((req, res, next) => { console.log("In another middleware!!!"); }) const server = http.createServer(app); server.listen(3000); ``` ![image](https://hackmd.io/_uploads/BJ9d-BjHp.png) ### **▊ next()該出場了!** 這裡我們只要再加上一個next(),他才能從目前的middleware travel至下一個middleware。 因此若沒有next的幫助,他們之間的橋樑就會掛掉 ;( ,如同前一步驟,第二個middleware根本到不了。 / 這裡同樣地,寫完下方程式碼後,在瀏覽器的`localhost:3000`頁面重整,再看看結果如何。 ```javascript! const http = require("http"); const express = require("express"); const app = express(); app.use((req, res, next) => { console.log("In the middleware!"); next(); }) app.use((req, res, next) => { console.log("In another middleware!!!"); //... send the response(下一章節的內容) }) const server = http.createServer(app); server.listen(3000); ``` ![image](https://hackmd.io/_uploads/rkwvXBsrp.png) :::danger [註] middleware function 應只能圈裡面的`(req, res, next)=>{...}`,外面的`app.use()`是用來載入middleware function的方法。 ::: **** ## 5-61. How Middleware Works 接著如果已經來到最後一個middleware,那麼在這裡要記得在該middleware 中 send response,而send response 的方法即是使用response object。 在這裡,我們依然可以像[第3節](https://hackmd.io/@noz915/rJI7Y7sET)一樣,使用`res.setHeader()`, `res.write()` 以及 `res.end()`,但有了express,我們有新的function可以去完成這些事情:`res.send()`。 ![image](https://hackmd.io/_uploads/H1EaPSiHT.png) ### **▊ res.send(簡單的HTML)** 這裡直接看程式碼: ```javascript! const http = require("http"); const express = require("express"); const app = express(); app.use((req, res, next) => { console.log("In the middleware!"); next(); }) app.use((req, res, next) => { console.log("In another middleware!!!"); res.send(`<h1>Hello from Express!!!!</h1>`); }) const server = http.createServer(app); server.listen(3000); ``` ![image](https://hackmd.io/_uploads/S183wHjHT.png) 打開`localhost:3000`。 ![2023-12-04 20-36-41 的螢幕擷圖](https://hackmd.io/_uploads/r12J_HoH6.png) 再打開dev tool的network來看,可以看見已幫我們寫上content-type: text/html的部份了(先前content-type需要使用res.setHeader("content-type", "text/html");才會設置) PS. 記得勾選"Disable cache"才看得見,不然會cache到舊版本的內容。 ![image](https://hackmd.io/_uploads/SyAgqBsS6.png) **** ## 5-62. Express.js - Looking Behind the Scenes > 參考資料:[express github](https://github.com/expressjs/express) 我們可以從[express github]((https://github.com/expressjs/express))的code repository看到express的運作方式,也可以從中了解express如何使用。(以下會看一些例子) ### **▊ res.send()** 在上一章節5-61中提到了send(),若我們放入html作為參數,express會自動幫我們將content-type令作text/html,而當我們希望設置成別種type呢? 這時候直接來看看[express github]((https://github.com/expressjs/express))。 lib > response.js 打開後搜尋"send",可以從下圖得知。 ![image](https://hackmd.io/_uploads/Skd4oqpST.png) ### **▊ app.listen(port號碼)** 在先前,我們若要建立Server,並監聽至該server的port時,總是會用到兩行程式: ```javascript! ... const server = http.createServer(app); server.listen(3000); ``` 不過現在有了express,我們便可以將之利用listen()濃縮成一行式子: ```javascript! const http = require("http"); const express = require("express"); const app = express(); ... app.listen(3000); ``` 這也有被寫在[express github](https://github.com/expressjs/express)之中。 lib > application.js 搜尋"listen",可以從下圖發現,其實listen()函式,其實就是把原有的兩行式子給包在一起了。 ![2023-12-06 15-04-27 的螢幕擷圖](https://hackmd.io/_uploads/rkBMaqaBT.png) **** ## 5-63. Handling Different Routes 我們可以從express官方文件"[Writing middleware for use in Express apps](https://expressjs.com/en/guide/writing-middleware.html#writing-middleware-for-use-in-express-apps)"或者是到[官方API文件](https://expressjs.com/en/4x/api.html#app.use)查看use()用法,會發現use()有很多種使用方式。 ### **▊ [官方API文件](https://expressjs.com/en/4x/api.html#app.use)查看方法:** ![image](https://hackmd.io/_uploads/SyobZjTBT.png) ### **▊ app.use()的方法:** ![image](https://hackmd.io/_uploads/SkoCMsaBa.png) PS 因為所有的route,最前面都會有個斜線("/", slash),所以以下方的code來看,會發現無論到哪個router,網頁上印出的仍就是"Hello from Express!!!!"。 ```javascript! const http = require("http"); const express = require("express"); const app = express(); app.use("/", (req, res, next) => { console.log("In another middleware!!!"); res.send(`<h1>Hello from Express!!!!</h1>`); }) app.listen(3000); ``` ![image](https://hackmd.io/_uploads/rymeSs6ra.png) ![image](https://hackmd.io/_uploads/r1Nkro6ra.png) ### **▊ 用app.use()增加不同route:** 先來看看程式碼: ```javascript! const http = require("http"); const express = require("express"); const app = express(); app.use("/add-product", (req, res, next) => { res.send(`<h1>The "Add Product" Page!</h1>`); }) app.use("/", (req, res, next) => { res.send(`<h1>Hello from Express!!!!</h1>`); }) app.listen(3000); ``` 這樣的結果就是當我們在`localhost:3000/add-product`這個route、這個頁面時,會回傳有"The "Add Product" Page!"字樣的頁面,而其他route將會回傳有"Hello from Express!!!!"字樣的頁面。 ![image](https://hackmd.io/_uploads/S1tmOiarT.png) 這是因為程式碼中兩個middleware function中都沒有next(),因此他們會直接在該route回傳response,不會再往下一個middleware走去。 ![image](https://hackmd.io/_uploads/ByxBuoTH6.png) **** ## 作業 2: Time to Practice - Express.js ![2023-12-06 15-57-48 的螢幕擷圖](https://hackmd.io/_uploads/S1gcKoprp.png) :::danger req, res, next三者順序不要亂掉,否則會報錯! ```javascript! app.use((req, res, next) => { ... }) ``` ::: **** ## 5-64. Parsing Incoming Requests ### **▊ req.body** request提供了一個很便利的屬性叫做body給我們,但是request預設是不幫忙parse(解析)incoming(傳入) request body。 而為了解析request body,我們需要register一個parser。而這個parser要放置在所有處理route的middleware之前,因為body的parsing必須在request結束之前完成。 首先,我們要先透過npm下載第三方套件body-parser。 ```javascript! npm install --save body-paser ``` 接著,將套件引入我們的程式檔案中。 ```javascript! const bodyParser = require("body-parser"); ``` 並且開始使用這個套件,而他其實也同樣是個middleware,其中也傳入了一個callback function作為參數,只是跟先前不同的是,先前傳入的都是處理(req, res, next)的函式。且該函式`bodyParser.urlencoded()`會在他完成所有request parsing之後,執行next()函式,因此可以持續往我們下一個middleware前進。 ```javascript! app.use(bodyParser.urlencoded()) ``` ### **▊ req.body實作** 這裡來實作整段程式碼: ```javascript! const http = require("http"); const bodyParser = require("body-parser"); const express = require("express"); const app = express(); app.use(bodyParser.urlencoded()); app.use("/", (req,res,next) => { next(); }) app.use("/add-product", (req, res, next) => { res.send(` <form action="/product" method="POST"> <input type="text" name="title"> <button type="submit">Add</button> </form> `); }) app.use("/product", (req, res, next) => { console.log(req.body); res.redirect("/"); }) app.use("/", (req, res, next) => { res.send(`<h1>Hello from Express!!!!</h1>`); }) app.listen(3000); ``` 執行會發現他發出一條警告: ![2023-12-06 17-12-02 的螢幕擷圖](https://hackmd.io/_uploads/Hy6Zo26rp.png) 這裡只需要在`bodyParser.urlencoded()`丟入參數`{extended: false}`即可。 ```javascript! app.use(bodyParser.urlencoded({extended: false})); ``` ### **▊ req.body結果** 我先在/add-product這個route的表單中填入"aaa"並送出,頁面會redirect到根目錄。 ![2023-12-06 17-07-27 的螢幕擷圖](https://hackmd.io/_uploads/S1UZ5npHp.png) 回到VS code中的terminal,可以看到上面印出的`req.body`如下所示,一個`{title: aaa}`: ![2023-12-06 17-07-10 的螢幕擷圖](https://hackmd.io/_uploads/rk-852pB6.png) req即request,也就是我們發出的請求,從表單中發出的。 這跟我們在[第 3 節](https://hackmd.io/@noz915/rJI7Y7sET)用node.js寫出以下程式碼是一樣的結果,只是express更加簡潔: ```javascript! if(url === "/message" && method === "POST"){ const body = []; req.on("data", (chunk) => { console.log(chunk); body.push(chunk); }) return req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); const message = parsedBody.split("=")[1]; fs.writeFile("message.txt", message,(err) => { res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); }); }) } ``` **** ## 5-65. Limiting Middleware Execution to POST Requests ```javascript! ...略... app.use("/product", (req, res, next) => { console.log(req.body); res.redirect("/"); }) ...略... ``` 從上一章節的程式碼,擷取上方這段程式碼來看,目前無論表單method為get或是post,他都會執行。但我們希望的是,他只單一處理get或post,舉例來說上一章節的程式碼中的表單method為post,那我就希望"/product" route只會處理post的request就好。 因此這裡提到另外兩種寫法app.get()及app.post(),基本上功能與app.use()相同,只差在前者只處理get request,後者只處理post request。 所以將上方程式碼簡單修改即可: ```javascript! ...略... app.post("/product", (req, res, next) => { console.log(req.body); res.redirect("/"); }) ...略... ``` 這實際操作上有什麼差異呢? 先把整段程式碼看過: ```javascript! const http = require("http"); const bodyParser = require("body-parser"); const express = require("express"); const app = express(); app.use(bodyParser.urlencoded({extended: false})); app.use("/", (req,res,next) => { next(); }) app.use("/add-product", (req, res, next) => { res.send(` <form action="/product" method="POST"> <input type="text" name="title"> <button type="submit">Add</button> </form> `); }) app.post("/product", (req, res, next) => { console.log(req.body); res.redirect("/"); }) app.use("/", (req, res, next) => { res.send(`<h1>Hello from Express!!!!</h1>`); }) app.listen(3000); ``` 這裡我們分別用get request以及post request方法來看看:(照理說只有post request才會成功印出"req.body") ### **▊ GET request** 直接在瀏覽器中輸入`localhost:3000/product`,進入該頁面,並沒有印出"req.body"。 ![2023-12-06 17-29-05 的螢幕擷圖](https://hackmd.io/_uploads/BJQBk6arp.png) ![2023-12-06 17-30-40 的螢幕擷圖](https://hackmd.io/_uploads/SkmLyp6Sp.png) ### **▊ POST request** 必須從`localhost:3000/add-product`,並在表單中輸入內容,按下按鈕才會發送post request,而我們按下按鈕後,可以從terminal上看見印出的"req.body"。 ![2023-12-06 17-32-18 的螢幕擷圖](https://hackmd.io/_uploads/Byy61TaHT.png) ![2023-12-06 17-32-24 的螢幕擷圖](https://hackmd.io/_uploads/HJJpyapB6.png) **** ## 5-66. Using Express Router 雖然目前我們的程式還非常小,但當逐漸壯大時,就希望能把他們分門別類,所以這個章節就是要講如何利用express的router來將頁面分到不同程式檔,最後在app.js做統整使用。 首先,這是我們希望區分的邏輯方式: ![image](https://hackmd.io/_uploads/Hy7vOcCH6.png) 首先,先建立router並且將它exports,待等會可以直接import該router。 ```javascript! const express = require("express"); const router = express.Router(); module.exports = router; ``` 接著,開始使用這個router,其實就把router想像成一個小型的express,前面我們是怎麼使用express的? ```javascript! const express = require("express"); const app = express(); app.use("/", (req, res, next) => { ...略... }) ``` 而我們只有把router當作是一個小型的express,事實上在座的事情跟express一樣,所以擁有相同的use function可以使用: ```javascript! const express = require("express"); const router = express.Router(); router.use("/", (req, res, next) => { ...略... }) ``` 同理,router也能用先前說的get(), post(): ```javascript! //get() router.get("/", (req, res, next) => { ...略... }) //post() router.post("/", (req, res, next) => { ...略... }) ``` 所以就可以把admin.js以及shop.js給實作出來啦。 ### **▊ admin.js** 這個route主要處理商品的新增。 ```javascript const express = require("express"); const router = express.Router(); router.get("/add-product", (req, res, next) => { res.send(` <form action="/product" method="POST"> <input type="text" name="title"> <button type="submit">Add</button> </form> `); }) router.post("/product", (req, res, next) => { console.log(req.body); res.redirect("/"); }) module.exports = router; ``` ### **▊ shop.js** 這個route主要是shop的頁面。 ```javascript! const express = require("express"); const router = express.Router(); router.get("/", (req, res, next) => { res.send(`<h1>Hello from Express!!!!</h1>`); }) module.exports = router; ``` :::success 注意! 先前我們使用`.use("/", (req, res, next) => {...})`時,第一個path參數用`"/"`,我們在網頁上只要是`"/"`slash開頭的網址都會被導到該處。 但若使用`.get("/", (req, res, next) => {...})`,則只有在符合path參數`"/"`時才會被導到該處,其餘的route若沒被定義,則無法顯示。 因為要符合path參數`"/"`,他才有辦法get request,也就才能執行.get()。 ::: 兩個頁面實作完成後,就要想辦法import到我們的主程式app.js中了。 首先,利用require來import檔案後,就可直接使用.use(),並把剛剛做好的兩個router作為物件丟進.use()中。 ```javascript! const adminRoutes = require("./routes/admin"); const shopRoutes = require("./routes/shop"); app.use(adminRoutes); app.use(shopRoutes); ``` ### **▊ app.js** 整段完成的樣子! ```javascript! const http = require("http"); const bodyParser = require("body-parser"); const express = require("express"); const app = express(); const adminRoutes = require("./routes/admin"); const shopRoutes = require("./routes/shop"); app.use(bodyParser.urlencoded({extended: false})); app.use(adminRoutes); app.use(shopRoutes); app.listen(3000); ``` **** ## 5-67. Adding a 404 Error Page 在上一章節(5-66)中,有提到「若使用`.get("/", (req, res, next) => {...})`,則只有在符合path參數"/"時才會被導到該處」,那不符合又沒被定義的呢? 我們除了會得到如下的頁面,還可以從dev tool的network中看見得到了一個404頁面。 ![2023-12-07 09-45-57 的螢幕擷圖](https://hackmd.io/_uploads/rk7z4iCr6.png) ![2023-12-07 09-45-44 的螢幕擷圖](https://hackmd.io/_uploads/rJ7GEjAST.png) 因此我們可以在app.js程式碼中,設置一個path來catch這些error,讓這些error導到"Page not Found"的頁面: ![2023-12-07 09-48-58 的螢幕擷圖](https://hackmd.io/_uploads/rya3NoABT.png) ```javascript! app.use("/", (req, res, next) => { res.send(`<h1>Page not Found!</h1>`); }) ``` 那path(use()的第一個參數)預設即為`"/"`,因此我們可以直接忽略不寫也沒問題的: ```javascript! app.use((req, res, next) => { res.send(`<h1>Page not Found!</h1>`); }) ``` 不過因為我們有一個route來接這些根本不應該存在的route, ,因此讓這些本應該出現status為404的頁面,都變成了正常的200。 ![image](https://hackmd.io/_uploads/Bylars0Hp.png) 因此,我們可以在res物件上用status method來改變這些route的狀態: ```javascript! app.use("/", (req, res, next) => { res.status(404).send(`<h1>Page not Found!</h1>`); }) ``` ![image](https://hackmd.io/_uploads/r1pdLjCHp.png) **** ## 5-68. Filtering Paths 接著我們希望這些routes可以在path上也分類,讓我們更清楚目前的route在什麼頁面之下,舉例來說,原先有`/add-product`跟`/product`兩個routes,他們都隸屬於admin範疇,那麼我們就希望他們的path分別更名為`/admin/add-product`以及`/admin/product`,甚至直接讓兩個routes都叫做`/admin/add-product`,畢竟一個是GET request,一個是POST request,互相不影響。 所以一開始的想法可能如下: ```javascript! const express = require("express"); const router = express.Router(); router.get("/admin/add-product", (req, res, next) => { res.send(` <form action="/admin/add-product" method="POST"> <input type="text" name="title"> <button type="submit">Add</button> </form> `); }) router.post("/admin/add-product", (req, res, next) => { console.log(req.body); res.redirect("/"); }) module.exports = router; ``` 上方只有兩個router要處理,那假如有多個,每個都得加上/admin/就麻煩了,於是我們可以直接在app.js上加上/admin/: ```javascript! //app.js ...略... app.use("/admin", adminRoutes); ...略... //./routes/admin.js ...略... router.get("/add-product", (req, res, next) => { res.send(` <form action="/admin/add-product" method="POST"> <input type="text" name="title"> <button type="submit">Add</button> </form> `); }) router.post("/add-product", (req, res, next) => { console.log(req.body); res.redirect("/"); }) ...略... ``` `app.use("/admin", adminRoutes);`的意思就是當我們的網址為`/admin`開頭才會進去adminRoutes這個檔案之中。 也就是我們現在想要到add product的頁面,就必須輸入`/admin/add-product`才能連到該網頁。 ![image](https://hackmd.io/_uploads/ByIke30rT.png) **** ## 5-70. Serving HTML Pages > node module: path 相關資料: > 1. [Node.js v21.4.0 documentation](https://nodejs.org/api/path.html) > 2. [[node] Path Module](https://pjchender.dev/nodejs/node-path/) 先前我們發送request後,得到的response都是一個text而已,但我們希望可以創建html檔案,並且讓response回傳整個html file給我們。 ![image](https://hackmd.io/_uploads/rJ_ysnAST.png) 這裡就需要新method:"sendFile"來取代過去的method:"send",不過要餵什麼參數給sendFile呢?當然就是檔案的位置了,不過若直接給`"/views/shop.html"`或是`"./views/shop.html"`都會報錯,這是因為此處的slash"/",代表的根目錄,所參考的是我們整個OS的根目錄,而非參考這個專案的目錄。 所以這裡有個方法!利用node.js提供的path模組。 ### **▊ path module** 如同其他模組,利用require將模組給import進來使用。 ```javascript! const path = require("path"); ``` ### **▊ path.join()** * path.join():可以把指定的路徑連結在一起。 * `__dirname`:獲得當前被執行檔案所在的資料夾路徑。 連在一起的路徑不須填上slash,因為在linux中檔案路徑是正斜線("/"),而在window裡則是反斜線("\")。 eg linux裡:/user/bin/... eg windows裡:\user\bin\... 因此若給出slash斜線的話,在不同系統下會有無法執行的情況發生,因此留給path 模組幫助我們去偵測系統判別該使用何者即可。 `__dirname`代表獲得當前被執行檔案所在的資料夾路徑,在下方例子中則代表是"routes"這個資料夾。 ```javascript! const path = require("path"); const express = require("express"); const router = express.Router(); router.get("/", (req, res, next) => { res.sendFile(path.join(__dirname, "../", "views", "shop.html")); }) module.exports = router; ``` ![2023-12-07 11-40-57 的螢幕擷圖](https://hackmd.io/_uploads/S1P11pASp.png) 看到以下畫面出現,就代表成功send response了。 ![2023-12-07 11-41-45 的螢幕擷圖](https://hackmd.io/_uploads/rJF-kaCHp.png) **** ## 5-73. Using a Helper Function for Navigation 前面若想send某個html檔案,需要用sendFile(),其中最麻煩的地方在於路徑。原先我們利用`--dirname`,來取得當前資料夾位置後,再去尋找我們要的目標檔案在哪裡,並一層層寫下,但是很容易看出、出錯。 我們更希望可以每次都取得這個專案的根目錄,如此一來,大家都是同樣的起跑點(都是從根目錄開始找起),除了更易讀外,在寫檔案路徑時也叫不容易跳錯地方。 ### **▊ path.dirname()** > 參考資料: > 1. [Node.js path.dirname() Method](https://www.w3schools.com/nodejs/met_path_dirname.asp) > 2. [Node.js process.mainModule Property](https://www.geeksforgeeks.org/node-js-process-mainmodule-property/) > 3. [node.js官方文件](https://nodejs.org/api/process.html#processmainmodule) 首先,建立一個額外的資料夾`util`,並在其底下建立一個檔案`path.js`: ![2023-12-09 08-37-01 的螢幕擷圖](https://hackmd.io/_uploads/Syx6LV-Ip.png) 開始使用path.dirname(),可以將目標檔案作為參數,他會return該檔案的路徑。而我們這裡要用一個node.js的內建屬性,幫助我們找到目前專案的根目錄。 * 網路上比較能找到以下寫法,不過被棄用ㄌ ```javascript! const path = require("path"); module.exports = path.dirname(process.mainModule.filename); ``` ![2023-12-09 08-38-07 的螢幕擷圖](https://hackmd.io/_uploads/rkEZPEbLp.png) ![image](https://hackmd.io/_uploads/r1IFH4bIT.png) * 現在要改用以下的寫法,在[node.js官方文件](https://nodejs.org/api/process.html#processmainmodule)有提到: ```javascript! const path = require("path"); module.exports = path.dirname(require.main.filename); ``` ![2023-12-09 08-35-54 的螢幕擷圖](https://hackmd.io/_uploads/rJHYL4bUT.png) export好之後,就要到使用的資料夾中import了。這裡先用admin.js做示範: ```javascript! const rootDir = require("../util/path"); ...略... router.get("/add-product", (req, res, next) => { res.sendFile(path.join(rootDir, "views", "add-product.html")); }) ...略... ``` 看看前後對比: ![image](https://hackmd.io/_uploads/r1IE24-Lp.png) ### **▊ require.main.filename** 來看`require.main.filename`到底是什麼,首先把require.main.filename印出來,發現他是指我們這個專案的進入點app.js的檔案位置: ![image](https://hackmd.io/_uploads/SyXf1S-L6.png) 而`path.dirname()`會顯示該檔案的的所在位置,所以說`path.dirname(require.main.filename)`就會是顯示app.js檔的所在資料夾位置,亦即該專案的根目錄! ![image](https://hackmd.io/_uploads/BJnFbBbIa.png) **** ## 5-75. Serving Files Statically 這章節主要談如何在express中處理css檔案。 一般來說,大家習慣將css檔案放在一個資料夾中,而資料夾的名稱約定俗成叫做"public"。 ![2023-12-09 10-38-08 的螢幕擷圖](https://hackmd.io/_uploads/rksmXIWLa.png) 首先,我們從前面的課程可以知道,express進到某個頁面,都是透過"routes"來運作的(如下app.js所示),在express中,用檔案路徑到達想去的頁面是行不通的。 **app.js** ```javascript! const path = require("path") const http = require("http"); const bodyParser = require("body-parser"); const express = require("express"); const app = express(); const adminRoutes = require("./routes/admin"); const shopRoutes = require("./routes/shop"); app.use(bodyParser.urlencoded({extended: false})); app.use("/admin", adminRoutes); app.use(shopRoutes); app.use("/", (req, res, next) => { res.status(404).sendFile(path.join(__dirname, "views", "404.html")); }) app.listen(3000); ``` 但現在我們希望有些特例,讓某些檔案可以透過檔案路徑去讀取,就如同一般前端一樣,可以寫成下面的link讀取css: ```htmlembedded! <link rel="stylesheet" href="../public/css/main.css"> ``` 但很快地就會發現這果然在express行不通: ![image](https://hackmd.io/_uploads/HJw4N8bUT.png) 在express中,他提供了我們一種方法:serving files statically。 :::info **Static** 老師提到statically的意思: "Statically simply means not handled by the express router or other middleware but instead directly forward to the file system." > 參考資料:[What exactly does 'serving static files' mean?](https://stackoverflow.com/questions/28918845/what-exactly-does-serving-static-files-mean) Static files are typically files such as scripts, CSS files, images, etc... that aren't server-generated, but must be sent to the browser when requested. If node.js is your web server, it does not serve any static files by default, you must configure it to serve the static content you want it to serve. You use node.js as your web server when you want to use it and its technology for generating dynamic pages, serving APIs, etc... 看起來就是express不會去運行這些靜態檔案(例如說CSS, image, html等等),如果要提供影像、CSS 檔案和 html 檔案等之類的靜態檔案,就必須使用 Express 中的 express.static 內建中介軟體函數。 ::: ### **▊ express.static()** > 參考資料:[express官方文件](https://expressjs.com/en/starter/static-files.html) 這裡會用到"express.static()"這個函式,他是一個middleware funciton(就如同先前接收req, res, next的函式一樣都是屬於middleware function),我們可以將路徑作為參數給"express.static()"函式,透過"express.static()"函式靜態地運行我們餵給他的路徑檔案。 之後再利用"app.use()"來載入這個middleware function。 **至於怎麼使用?** 我們先在`app.js`中打上以下內容,讓`app.use()`幫我們載入`express.static()`這個middleware function。 **▎app.js > 載入express.static()** ```javascript! app.use(express.static(path.join(__dirname, "public"))); ``` ![image](https://hackmd.io/_uploads/HyMrGwW8T.png) 這代表express會直接把`path.join(__dirname, "public")`作為root(根),而root底下的檔案都可以靜態方式運行。 也因如此,當我們來到`shop.html`,再次使用link tag,並放入css檔案路徑時,要以public資料夾為root,所以我們只需要打上剩下的路徑(如下所示) **▎shop.js > 使用靜態檔案** ```javascript! <link rel="stylesheet" href="/css/main.css"> ``` ![image](https://hackmd.io/_uploads/S1ttMw-Lp.png)