# NodeJS(Max)第 6 節: Working with Dynamic Content & Adding Templating Engines > Udemy課程:[NodeJS - The Complete Guide (MVC, REST APIs, GraphQL, Deno) ](https://www.udemy.com/course/nodejs-the-complete-guide/) `20231209Sat.~20231211Mon.` ## 6-79. Sharing Data Across Requests & Users 在[3-38](https://hackmd.io/@noz915/rJI7Y7sET#3-38-Using-the-Node-Modules-System)曾經提過exports用法。 可以是以下兩種,後者為簡寫方式,是用於輸出模組(module)的。 ```! module.exports=任何資料; exports=任何資料; ``` 而exports本身為一個物件,故也能擁有自己的屬性,所以當我們一個檔案中有多於一個模組想輸出,那麼就可以利用屬性的方式去輸出模組,如下圖所示。 此處的"products"變數將用於暫時作為儲存使用者輸入資料的容器。 ![image](https://hackmd.io/_uploads/SkvwSdbIa.png) 輸出模組後,來到另一個檔案`app.js`,我們將在`app.js`中輸入(import)這養個模組進來使用。 ![image (1)](https://hackmd.io/_uploads/rJ1mF_-Lp.png) **▎admin.js** ![2023-12-09 13-21-50 的螢幕擷圖](https://hackmd.io/_uploads/B14KFub8a.png) 當我們送出表單的同時,就會發出POST request給server,因此在上方程式碼中,當收到POST request(即按下送出表單的按鈕),就會將`req.body.title`弄成一個物件,推到變數products陣列中儲存起來。 不過這樣的方法只會在本機端看到,當不同機器運行時,變數又會被清空。(現在只是試著取得POST request並把他暫時存取) **** ## 6-80. Templating Engines - An Overview 讓html動起來! ### **▊ Available Templating Engines** * EJS * Pug(Jade) * Handlebars ![2023-12-09 13-43-15 的螢幕擷圖](https://hackmd.io/_uploads/B1BqROZLa.png) 看起來老師三個都會教😰😰😰😰我想先跳過... **** ## 6-81. Installing & Implementing Pug 一次把三個下載下來(當然要分開下載也是可以啦): ```javascript! npm install --save ejs pug express-handlebars ``` 那教程會從pug開始,在express中使用pug,首先要先告訴express,我們要使用templating engine來渲染動態的內容。(這裡用來我們處理templating engine的是express而非node,因為用node來處理會困難非常多) ### **▊ app.set(name, value)** > 參考資料: > 1. [express API文件](https://expressjs.com/en/4x/api.html#app.set) > 2. [Using template engines with Express](https://expressjs.com/en/guide/using-template-engines.html) `app.set(name, value)`允許我們在該express application上設置任何值變為全域的。 其中兩個name是最主要我們會用到的:view、view engine。 ![image](https://hackmd.io/_uploads/SJqIwxmLp.png) 因此,我們會在`app.js`檔案中寫下以下兩行程式碼: ```javascript! app.set("view engine", "pug"); app.set("views", "views"); ``` 1. `app.set("view engine", "pug");` 使用前記得先`npm install --save pug`。這裡是用來告訴express:「我現在要使用的templating engine是pug。」 2. `app.set("views", "views");` 這裡是用來告訴express:「我把template放在一個叫做views的資料夾中,所以你想解析請到views找檔案解析。」 ![image](https://hackmd.io/_uploads/H1Gt2g7I6.png) ### **▊ 使用pug** 建立`shop.pug`,利用vscode幫我們快速生成html5 ![image](https://hackmd.io/_uploads/B17IJf78a.png) 生成的同時,vscode會直接幫我們轉成pug,不需要擔心 ![image](https://hackmd.io/_uploads/SyiIJMmUp.png) 這裡直接放上成品的程式碼:(我好懶) ```pug! doctype html html(lang="en") head meta(charset="UTF-8") meta(name="viewport", content="width=device-width, initial-scale=1.0") title My Shop link(rel="stylesheet", href="/css/main.css") link(rel="stylesheet", href="/css/product.css") body header.main-header nav.main-header__nav ul.main-header__item-list li.main-header__item a.active(href="/") Shop li.main-header__item a(href="/admin/add-product") Add Product ``` 但是完成後,執行`npm start`並不會渲染到我們的網頁上。因為目前我們在`app.set()`的部份,只告訴了express:「當我要渲染template時,請用pug這個template engine來渲染。」 但是,我們目前還沒去做「渲染」template這個動作! ### **▊ res.render() > 渲染template** > 參考資料:[res.render(view [, locals] [, callback])](https://expressjs.com/en/4x/api.html#res.render) 所以回到`app.js`,當router的path為`"/"`時,我們要渲染這個template,就需要新的express函式:`res.render()`。 實作方式很簡單,如下所示: **app.js** ```javascript! router.get('/', (req, res, next) => { res.render("shop"); }); ``` 其中,"shop"代表的是"shop.pug",不過預設就會是.pug檔案了,所以我們可以忽略,同樣地,也不需要寫出.pug檔案的path,因為預設會從views資料夾開始找起。 這些預設來自我們先前在`app.js`中,`app.set()`的設置: ```javascript! app.set("view engine", "pug"); app.set("views", "views"); ``` ![image](https://hackmd.io/_uploads/S1nVDf7La.png) **** ## 6-82. Outputting Dynamic Content 接著要添加一些動態的內容,同樣會用到上一章節提到的`res.render()`,把我們想加入的內容,用key value的方式裝在一個物件中作為參數傳入`res.render()`。 這裡我們先用靜態的內容,來看看他是如何運作、操作。 **▎shop.js** 這裡我們想在.pug檔案中加入新的title文字,所以把我們想加入的內容,map成key value的方式作為參數傳入`res.render()`。 ```javascript! router.get('/', (req, res, next) => { res.render("shop", {docTitle: "shop"}); }); ``` **▎shop.pug** 若想要使用`res.render()`中加入的物件,在pug中填入key值,並用`#{}`包住,如下所示: ```pug! title #{docTitle} ``` **▎結果** ![image](https://hackmd.io/_uploads/ryYYTfXLa.png) **▎程式碼圖片支援** ![image](https://hackmd.io/_uploads/H1JGRM7IT.png) ~~~~~再來加點動態的內容~~~~~ **▎shop.js** 一樣從`shop.js`中使用`res.render()`。 這裡會用到先前6-79中建立的products變數。 ```javascript! router.get('/', (req, res, next) => { const products = adminData.products; res.render("shop", {prods: products, docTitle: "shop"}); }); ``` **▎shop.pug** 這裡因為是動態改變,我們不會知道有多少東西,所以會用類似迴圈的東西來處理,而在pug語法中即為`each ... in ...`。 ```pug! main h1 My Products p List of all the products... .grid each product in prods article.card.product-item header.card__header h1.product__title #{product.title} .card__image img(src='https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png' alt='A Book') .card__content h2.product__price $19.99 p.product__description A very interesting book about so many even more interesting things! .card__actions button.btn Add to Cart ``` **▎結果** ![image](https://hackmd.io/_uploads/r10IrX7Ua.png) **▎程式碼圖片支援** ![image](https://hackmd.io/_uploads/SkxU_QQ8p.png) ![image](https://hackmd.io/_uploads/ByFI_mXUT.png) ~~~~~加點判斷式~~~~ **▎shop.pug** 加上if else來判斷現在是否有products,如果沒有的話,我們可以讓他顯示"No Product"。 ```pug! main if prods.length > 0 h1 My Products p List of all the products... .grid each product in prods article.card.product-item header.card__header h1.product__title #{product.title} .card__image img(src='https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png' alt='A Book') .card__content h2.product__price $19.99 p.product__description A very interesting book about so many even more interesting things! .card__actions button.btn Add to Cart else h1 No Product ``` **▎結果** 沒有product時會如下顯示: ![2023-12-10 20-03-33 的螢幕擷圖](https://hackmd.io/_uploads/Byg4htmQI6.png) :::success 注意! 若我們修改了.pug檔案、html檔案等等,即便儲存了,回到瀏覽器之後,仍然需要重整網頁,nodemon不會幫我們刷新頁面,因為這些提及的檔案並非server side的程式碼,nodemon自然也不會去處理。 ::: > Pug official docs: https://pugjs.org/api/getting-started.html **** ## 6-85. Adding a Layout 6-84將html檔案都修改成pug檔(這裡就不再多加說明),會發現我們三個pug檔案有許多內容都重複了,如下所示為三個檔案皆重複的內容: ```pug! doctype html html(lang="en") head meta(charset="UTF-8") meta(name="viewport", content="width=device-width, initial-scale=1.0") title #{pageTitle} link(rel="stylesheet", href="/css/main.css") body header.main-header nav.main-header__nav ul.main-header__item-list li.main-header__item a(href="/") Shop li.main-header__item a(href="/admin/add-product") Add Product ``` 因此我們現在要用layout的方式將重複的部份提取出來、獨立出來。同樣在views的資料夾中建立一個資料夾叫做"layouts",並新增一個pug檔案叫做"main-layout.pug"。 ![2023-12-10 20-20-52 的螢幕擷圖](https://hackmd.io/_uploads/H18VTmQI6.png) 把上方重複的程式碼全數貼到`main-layout.pug`裡面,不過這裡會用到一個關鍵字"block",因為重複到的link只有`main.css`,但有些pug檔需要額外的css,因此我們需用關鍵字"block"來處理。 ```pug! block stlyes block content ``` ![image](https://hackmd.io/_uploads/Hkib1VQL6.png) 接著依序來看看各個pug檔如何使用這個layout,這裡會使用到另一個關鍵字"extends"來指出要延伸哪個layout。 **▎404.pug** ```pug! extends layouts/main-layout.pug block content h1 Page Not Found! ``` **▎add-product.pug** ```pug! extends layouts/main-layout.pug block styles link(rel="stylesheet", href="/css/forms.css") link(rel="stylesheet", href="/css/product.css") block content main form.product-form(action="/admin/add-product", method="POST") .form-control label(for="title") Title input(type="text", name="title")#title button.btn(type="submit") Add Product ``` **▎shop.pug** ```pug! extends layouts/main-layout.pug block styles link(rel="stylesheet", href="/css/main.css") link(rel="stylesheet", href="/css/product.css") block content main if prods.length > 0 h1 My Products p List of all the products... .grid each product in prods article.card.product-item header.card__header h1.product__title #{product.title} .card__image img(src='https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png' alt='A Book') .card__content h2.product__price $19.99 p.product__description A very interesting book about so many even more interesting things! .card__actions button.btn Add to Cart else h1 No Product ``` **** ## 6-86. Finishing the Pug Template 先注意看看原先的navbar,無論我們身處在哪個頁面,他都不會顯示,因為目前還沒在css加上active樣式。 不過這裡試著用點邏輯直接在pug檔案裡面添加active樣式,想法是身處在什麼頁面,該頁面在navbar上會變黃字。 先來看看目前的樣式: ![image](https://hackmd.io/_uploads/rk4FIyNLT.png) **▎admin.js** 在`admin.js`的render處加上path key。 ```javascript! router.get('/add-product', (req, res, next) => { res.render("add-product", {pageTitle: "Add Product", path: "/admin/add-product"}) }); ``` ![image](https://hackmd.io/_uploads/SkIdqkVUp.png) **▎shop.js** 同樣地,在`shop.js`的render處加上path key。 ```javascript! router.get('/', (req, res, next) => { const products = adminData.products; res.render("shop", {prods: products, pageTitle: "shop", path: "/"}); }); ``` ![image](https://hackmd.io/_uploads/HJO_9J4I6.png) **▎main-layout.pug** 在`main-layout.pug`頁面中加上三元運算子,當頁面的網址與我們設定的path相符時,會加上一個新的class叫做active,即讓navbar的a tag變黃色,否則class為一個空。 ```javascript! //對應到shop頁面 a(href="/", class=(path === "/" ? "active" : "")) //對應到add-product頁面 a(href="/admin/add-product", class=(path === "/admin/add-product" ? "active" : "")) ``` ![image](https://hackmd.io/_uploads/Bykvq1VIa.png) **▎結果** ![2023-12-11 09-50-18 的螢幕擷圖](https://hackmd.io/_uploads/B1-xs1EUT.png) ![2023-12-11 09-50-22 的螢幕擷圖](https://hackmd.io/_uploads/rJbls1V8T.png) **** ## 6-87~90 87~90都是在講解handlebars,先跳過不做筆記。 **** ## 6-91. Working with EJS > 1. 官方網站:[EJS](https://ejs.co/) > 2. 之前上過EJS課程的筆記:[第 34 節](https://hackmd.io/@noz915/Skmisshyq) > ![2023-12-09 13-30-51 的螢幕擷圖](https://hackmd.io/_uploads/ryK3sd-Up.png) EJS載入方法同pug,記得先`npm install ejs --save`。 ```javascript! app.set("view engine", "ejs"); app.set("views", "views"); ``` 在pug中若要使用動態的內容,我們會使用`#{}`包起來,而在ejs則會使用`<%= %>`來包裹住。 至於if else判斷式在ejs中的寫法,只要前後使用`<% %>`tags來包住,裡面我們可以直接寫上原生的javascript寫法,不過一行就要包裹一次,如下所示: ```ejs! <% if(prods.length > 0){ %> ...some html here... <% }else{ %> ...some html here... <% } %> ``` 這裡不寫太多,之前在[第 34 節](https://hackmd.io/@noz915/Skmisshyq)就寫滿詳細夠用了! **** ## 6-92. Working on the Layout with Partials 我們希望可以把重複的東西從各個ejs拉出來行程一個layout,但ejs不同於pug,ejs沒有像pug一樣的語法extends、block來處理layout。 不過我們可以使用所謂叫做"partials"或是"includes"的東西來處理。 先在view資料夾中建立一個資料夾叫做"includes"(當然這個名字任我們隨意取),而底下建立三個檔案,分別是`end.ejs`、`head.ejs`、`navigation.ejs`。 因為ejs沒有block的方法,所以我們得把重複的部份給拆成不同檔案,之後才能才有辦法分開插入ejs檔中。 **▎head.ejs** ```htmlembedded! <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Page Not Found</title> <link rel="stylesheet" href="/css/main.css"> ``` **▎navagation.ejs** ```htmlembedded! <header class="main-header"> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"><a href="/">Shop</a></li> <li class="main-header__item"><a href="/admin/add-product">Add Product</a></li> </ul> </nav> </header> ``` **▎end.ejs** ```htmlembedded! </body> </html> ``` ### **▊ <%- include("layout的檔案路徑") %>** 而要如何插入到我們的ejs檔案呢?利用`<%- include("") %>`關鍵字。 **▎404.ejs** ```htmlembedded! <%- include("includes/head.ejs") %> </head> <body> <%- include("includes/navigation.ejs") %> <h1>Page Not Found!</h1> <%- include("includes/end.ejs") %> ``` **▎add-product.ejs** ```htmlembedded! <%- include("includes/head.ejs") %> <link rel="stylesheet" href="/css/forms.css"> <link rel="stylesheet" href="/css/product.css"> </head> <body> <header class="main-header"> <%- include("includes/navigation.ejs") %> </header> <main> <form class="product-form" action="/admin/add-product" method="POST"> <div class="form-control"> <label for="title">Title</label> <input type="text" name="title" id="title"> </div> <button class="btn" type="submit">Add Product</button> </form> </main> <%- include("includes/end.ejs") %> ``` **▎shop.ejs** ```htmlembedded! <%- include("includes/head.ejs") %> <link rel="stylesheet" href="/css/product.css"> </head> <body> <header class="main-header"> <%- include("includes/navigation.ejs") %> </header> <main> <% if(prods.length > 0){ %> <h1>My Products</h1> <p>List of all the products...</p> <div class="grid"> <% for(let product of prods){ %> <article class="card product-item"> <header class="card__header"> <h1 class="product__title"><%= product.title %></h1> </header> <div class="card__image"> <img src="https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png" alt="A Book"> </div> <div class="card__content"> <h2 class="product__price">$19.99</h2> <p class="product__description">A very interesting book about so many even more interesting things!</p> </div> <div class="card__actions"> <button class="btn">Add to Cart</button> </div> </article> <% } %> </div> <% }else{ %> <h1>No Product Found</h1> <% } %> </main> <%- include("includes/end.ejs") %> ``` 這裡想再加上active樣式如同[6-68](https://hackmd.io/@noz915/B1teoDbLa#6-86-Finishing-the-Pug-Template)一樣,不過我們改成用`<%= %>`來包裹住邏輯部份。 **navigation.ejs** ```htmlembedded! <header class="main-header"> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"><a href="/" class=" <%= path === '/' ? 'active' : '' %> ">Shop</a></li> <li class="main-header__item"><a href="/admin/add-product" class=" <%= path === '/admin/add-product' ? 'active' : '' %> ">Add Product</a></li> </ul> </nav> </header> ```