---
# System prepended metadata

title: NodeJS(Max)第 6 節： Working with Dynamic Content & Adding Templating Engines
tags: [Node.js, Udemy]

---

# 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>
```