# Node.js(七) View Engines
###### tags: `Node.js`
<br />
## View Engines
從前面學習到現在,在伺服器響應瀏覽器所用到 HTML 頁面都是靜態的 HTML,內容都是事先就定義好的,現在想要動態的注入數據,例如部落格文章或是個人資料頁面上的用戶資料,而如何很好地在網頁模板上輸出這些數據,我們可以使用稱為 View Engines 視圖引擎或是模板引擎,使用 View Engines 編寫的模板與 HTML 語法相似,且能夠做到將數據動態的注入到網頁中。而 View Engines 也有很多種,例如 Express Handlebars、pug、EJS,而接下來我們將要使用 EJS 繼續後面的學習。
<br />
## EJS
首先安裝 EJS,只要開啟專案在終端輸入 `npm install ejs`,這裡的專案使用上一筆記的 app.js

安裝 ejs

開啟 package.json 確認已安裝 ejs

<br />
接下來要使用 express 做模板引擎的開發,如果你開啟 EJS 的官網可以看到它是直接引入 ejs 模組使用,不一定要結合 express 使用。
開啟 app.js 註冊 view engine,使用 `app.set()` 設定一些程序設置。
其中一個設置是 `'view engine'`,告訴 express 使用什麼樣的模板引擎,這裡使用 `'ejs'`
```javascript=
const express = require('express');
const app = express();
app.set('view engine', 'ejs'); // register view engine
// app.set('views', 'views'); // specify the views directory
```
可以看到上面的 code 我們註解了最後一行,這是因為還有一個設置是 `'views'` 用來定義存放視圖、模板的目錄,但這項設置的預設值就是 `'views'` 所以可以不用設置,除非你要將視圖存放在別的目錄。
<br />
做完以上設置,接著開始製作模板,將 views 裡的三個 html 檔刪除,因為這裡不再需要純 HTML,取而代之的視圖副檔名為 ejs,所以再創建三個主檔名與之前一樣,但副檔名為 ejs 的檔案

<br />
然後開啟 index.ejs 開始編碼,目前編寫的內容依舊為純 HTML,包含了導覽功能以及顯示部落格內容的地方,其中一個連結為 `/blogs/create` 用於之後創建一個新的部落格內容功能的地方
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Joe</title>
</head>
<body>
<nav>
<div class="site-title">
<a href="/"><h1>Blog Joe</h1></a>
<p>A Joe Site</p>
</div>
<ul>
<li><a href="/">Blogs</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blogs/create">New Blog</a></li>
</ul>
</nav>
<div class="blogs content">
<h2>All Blogs</h2>
</div>
</body>
</html>
```
<br />
試著在瀏覽器上顯示該模板,開啟 app.js,這裡要用到 `res.render()` 方法渲染視圖取代之前的 `res.sendFile()`。
我們只需如下編碼,express 便會自動在設置視圖目錄中找到檔案,並使用 ejs 模板引擎渲染並發送回瀏覽器
```javascript=
app.get('/', (req, res) => {
res.render('index');
});
```
<br />
全部的 code
```javascript=
const express = require('express');
const app = express();
app.set('view engine', 'ejs'); // register view engine
app.listen(3000);
app.get('/', (req, res) => {
res.render('index');
});
app.get('/about', (req, res) => {
res.sendFile('./views/about.html', {root: __dirname});
});
app.get('/about-us', (req, res) => {
res.redirect('/about');
});
app.use((req, res) => {
res.status(404).sendFile('./views/404.html', {root: __dirname});
});
```
<br />
一樣使用 nodemon 執行,在瀏覽器輸入 `localhost:3000`,會看到以下畫面

<br />
點擊 About 或 New Blog 連結會出現如下畫面,因為刪除了 html 檔所以找不到

<br />
所以開始編輯 about.ejs,與 index.ejs 差不多,唯一不一樣的地方在顯示部落格內容的地方加了些假字。
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Joe</title>
</head>
<body>
<nav>
<div class="site-title">
<a href="/"><h1>Blog Joe</h1></a>
<p>A Joe Site</p>
</div>
<ul>
<li><a href="/">Blogs</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blogs/create">New Blog</a></li>
</ul>
</nav>
<div class="blogs content">
<h2>All Blogs</h2>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo, harum!</p>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo, harum!</p>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo, harum!</p>
</div>
</body>
</html>
```
<br />
404.ejs 也只在部落格內容多了找不到頁面的提示,並將 div 的 classname 去除 blogs 改為 not-found
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Joe</title>
</head>
<body>
<nav>
<div class="site-title">
<a href="/"><h1>Blog Joe</h1></a>
<p>A Joe Site</p>
</div>
<ul>
<li><a href="/">Blogs</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blogs/create">New Blog</a></li>
</ul>
</nav>
<div class="not-found content">
Page not found!!
</div>
</body>
</html>
```
<br />
完成以上後將它們都渲染出來,這裡不需要 `'/about-us'` 所以去除了
```javascript=
const express = require('express');
const app = express();
app.set('view engine', 'ejs'); // register view engine
app.listen(3000);
app.get('/', (req, res) => {
res.render('index');
});
app.get('/about', (req, res) => {
res.render('about');
});
app.use((req, res) => {
res.status(404).render('404');
});
```
<br />
執行,點擊 About 連結

<br />
點擊 New Blog 連結或是輸入其它的 URL,顯示 404 頁面

<br />
在 views 資料夾新增 create.ejs 視圖,與 index.ejs 不同於在部落格內容新增了表單,並將 classname 改為 create-blog,目前該表單並沒有作用,之後再添加功能
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Joe</title>
</head>
<body>
<nav>
<div class="site-title">
<a href="/"><h1>Blog Joe</h1></a>
<p>A Joe Site</p>
</div>
<ul>
<li><a href="/">Blogs</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blogs/create">New Blog</a></li>
</ul>
</nav>
<div class="create-blog content">
<form>
<label for="title">Blog title:</label>
<input type="text" id="title" required>
<label for="snippet">Blog snippet:</label>
<input type="text" id="snippet" required>
<label for="body">Blog body:</label>
<textarea id="body" required></textarea>
<button>Submit</button>
</form>
</div>
</body>
</html>
```
<br />
app.js 添加 `'/blogs/create'` 的選項
```javascript=
const express = require('express');
const app = express();
app.set('view engine', 'ejs'); // register view engine
// app.set('views', 'myviews'); // specify the views directory
app.listen(3000);
app.get('/', (req, res) => {
res.render('index');
});
app.get('/about', (req, res) => {
res.render('about');
});
app.get('/blogs/create', (req, res) => {
res.render('create');
});
app.use((req, res) => {
res.status(404).render('404');
});
```
<br />
執行,點擊 New Blog 連結

<br />
做到這裡其實與前面做的差不多,但 EJS 及其它模板引擎真正強大的作用在於之後動態的注入數據及邏輯。
<br />
## 將數據傳遞到 views
現在要開始將數據傳遞到 ejs 模板,使其不僅只有靜態內容,有些像是 PHP 的標籤,但不了解 PHP 也沒關係。
首先開啟 index.ejs,在 body 中編寫以下 code
```htmlmixed
<% const name = 'Joe' %> <!-- 定義常數 -->
```
`<%` 開始,`%>` 結束,而在裡面則可以編寫一些 code,例如我們可以宣告任何的 JavaScript 類型,就像這裡定義了字串常數 `name`,但這不會在前端瀏覽器運行,而是在後端伺服器運行,後面會再詳細說明這個過程,現在先理解 code。
<br />
定義了常數,那麼要如何讓它輸出顯示出來呢? 如下便會在 p 元素中顯示 `name` 常數的值,當然不一定要包覆在元素中,但要顯示在網頁中總要放在元素裡讓 css 樣式渲染
```htmlmixed
<p><%= name %></p> <!-- 顯示常數 -->
```
<br />
index.ejs 全部的 code
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Joe</title>
</head>
<body>
<% const name = 'Joe' %> <!-- 定義常數 -->
<nav>
<div class="site-title">
<p><%= name %></p> <!-- 顯示常數 -->
<a href="/"><h1>Blog Joe</h1></a>
<p>A Joe Site</p>
</div>
<ul>
<li><a href="/">Blogs</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blogs/create">New Blog</a></li>
</ul>
</nav>
<div class="blogs content">
<h2>All Blogs</h2>
</div>
</body>
</html>
```
<br />
現在執行看看,連接 `localhost:3000`

可以看到最上面多了一個 p 元素,內容為我們在伺服器端的設定
<br />
以上為一個簡單的示範,實際上並不會這麼做,因為我們更想的是傳遞應用程式中的數據,可能會與 database 進行溝通獲取數據,然後在將數據傳遞給視圖模板輸出,最後顯示在瀏覽器網頁上。
但現在先跳過數據庫的部分,現在僅先從我們負責處理程序的應用程式 app.js 傳遞數據給模板,這部分要用到 `res.render()` 的第二個參數,該參數是一個傳遞給視圖變數的物件,如下
```javascript=
app.get('/', (req, res) => {
res.render('index', { title: 'Home'});
});
```
<br />
然後在 index.ejs 就可以接收上面傳遞的 title 屬性,我們將它顯示在 head 裡的 title 元素,使用 `<%= title %>` 顯示
```htmlmixed=
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Joe | <%= title %></title>
</head>
```
執行,在瀏覽器頂端的標題可以看到引用了我們在 app.js 設定的 `title`

<br />
依此類推,將其它的頁面也做一樣的設定,但不同的值
**app.js**
```javascript=
const express = require('express');
const app = express();
app.set('view engine', 'ejs');
app.listen(3000);
app.get('/', (req, res) => {
res.render('index', { title: 'Home'});
});
app.get('/about', (req, res) => {
res.render('about', { title: 'About'});
});
app.get('/blogs/create', (req, res) => {
res.render('create', { title: 'Create a new Blog'});
});
app.use((req, res) => {
res.status(404).render('404', { title: '404'});
});
```
而每個 EJS 模板都與 index.ejs 作法一模一樣
<br />
執行,切換各個頁面,顯示相對應的 `title`



<br />
接著讓我們再輸出更多的內容並做邏輯判斷
在 `'/'` 的 callback function 中新增一個 `blogs` 陣列,裡面包含一些部落格標題及簡介是我們要輸出的內容
```javascript=
app.get('/', (req, res) => {
const blogs = [
{title: 'Nice to meet you', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'The weather is nice today', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'I am so happy', snippet: 'Lorem ipsum dolor sit amet consectetur.'}
];
res.render('index', { title: 'Home', blogs});
});
```
`res.render()` 物件參數中添加 blogs 屬性及值(使用 ES6 的物件屬性值簡寫),這樣就將 `blogs` 傳遞給視圖了。
<br />
現在切換到 index.ejs 將 blogs 顯示出來,這裡我們要先檢查是否有 blogs,若有將內容顯示出來,若沒有則顯示沒有的資訊。
那麼如何確認 blogs 是否有值呢? 在 JavaScript 判斷一個陣列是否有值就檢查它的長度,這裡也這麼做。
我們可以先將 code 以 JS 的方式寫出來,如下
```javascript=
if (blogs.length > 0) {
blogs.forEach(blog => {
// 輸出內容
})
}
```
<br />
要寫在 ejs 裡,則每行都以 `<% %>` 包覆
```javascript=
<% if (blogs.length > 0) { %>
<% blogs.forEach(blog => { %>
// 輸出內容
<% }) %>
<% } %>
```
<br />
至於其中輸出內容的部分如下,可以參考官方文件
```javascript=
<% if (blogs.length > 0) { %>
<% blogs.forEach(blog => { %>
<h3><%= blog.title %></h3>
<p><%= blog.snippet %></p>
<% }) %>
<% } %>
```
<br />
在加上 else 處理 blogs 沒有數據的情況
```javascript=
<% if (blogs.length > 0) { %>
<% blogs.forEach(blog => { %>
<h3><%= blog.title %></h3>
<p><%= blog.snippet %></p>
<% }) %>
<% } else { %>
<p>There are no blogs to display...</p>
<% } %>
```
<br />
index.ejs 全部的 code
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Joe | <%= title %></title>
</head>
<body>
<nav>
<div class="site-title">
<a href="/"><h1>Blog Joe</h1></a>
<p>A Joe Site</p>
</div>
<ul>
<li><a href="/">Blogs</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blogs/create">New Blog</a></li>
</ul>
</nav>
<div class="blogs content">
<h2>All Blogs</h2>
<% if (blogs.length > 0) { %>
<% blogs.forEach(blog => { %>
<h3><%= blog.title %></h3>
<p><%= blog.snippet %></p>
<% }) %>
<% } else { %>
<p>There are no blogs to display...</p>
<% } %>
</div>
</body>
</html>
```
<br />
執行,可以看到在部落格內容的部分顯示 blogs 的數據

<br />
如果將 blogs 的數據註解掉,變成一個空陣列
```javascript=
app.get('/', (req, res) => {
const blogs = [
// {title: 'Nice to meet you', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
// {title: 'The weather is nice today', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
// {title: 'I am so happy', snippet: 'Lorem ipsum dolor sit amet consectetur.'}
];
res.render('index', { title: 'Home', blogs});
});
```
執行便會顯示沒有部落格可以顯示的訊息

<br />
接下來說一說 EJS 的模板從伺服器到顯示在瀏覽器這中間的過程。
EJS 模板透過 EJS view engine 處理其中動態注入的內容、變數、循環、條件...等等,產生出對應的 HTML 文件再發送給瀏覽器,這個過程稱為伺服器端的渲染。
以上即為將數據傳遞到 views 並輸出顯示在瀏覽器上的模板引擎。
<br />
## 部分模板
前面使用到的 index、about、create、404 等模板都有些共同的部分,例如 head、nav 這些標籤是一模一樣,如果某天新增了頁面就要在 nav 新增連結,每個頁面的 nav 都要做修改,這樣當頁面越來越多時很是麻煩、且沒效率,所以我們可以製作部分的模板,並調用它們到每個頁面中,當需要修改時,只要修改該部分的模板就好。
<br />
首先在 views 資料夾建立 partials 資料夾,並在其中新增 head.ejs、nav.ejs、footer.ejs 檔案作為部分模板

接著可以從前面的模板複製相同部分的 head、nav 到這些部分模板裡,如下
**head.ejs**
```htmlmixed=
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Joe | <%= title %></title>
</head>
```
<br />
**nav.ejs**
```htmlmixed=
<nav>
<div class="site-title">
<a href="/"><h1>Blog Joe</h1></a>
<p>A Joe Site</p>
</div>
<ul>
<li><a href="/">Blogs</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blogs/create">New Blog</a></li>
</ul>
</nav>
```
<br />
**footer.ejs** 可以自己編碼
```htmlmixed=
<footer>
Copyright © Blog Joe for learning
</footer>
```
<br />
有了部分模板後,就要在頁面模板中調用、包含它們,語法如下
```javascript
<%- include('./partials/nav.ejs') %>
```
使用 `include()`,參數即部分模板的相對位置,注意這裡使用 `<%-` 減號而不是 `<%=` 等號,否則不能正常轉譯,會直接以字串的形式顯示在網頁中,如下

記得要將原來部分模板取代的部分先刪掉,否則會重複出現,以 index.ejs 舉例
**index.ejs** 刪除 head 及 nav,並包含部分模板進去
```javascript=
<!DOCTYPE html>
<html lang="en">
<%- include('./partials/head.ejs') %>
<body>
<%- include('./partials/nav.ejs') %>
<div class="blogs content">
<h2>All Blogs</h2>
<% if (blogs.length > 0) { %>
<% blogs.forEach(blog => { %>
<h3><%= blog.title %></h3>
<p><%= blog.snippet %></p>
<% }) %>
<% } else { %>
<p>There are no blogs to display...</p>
<% } %>
</div>
<%- include('./partials/footer.ejs') %>
</body>
</html>
```
執行,可以看到 index 首頁仍跟之前一樣並新增了 footer

其它頁面模板也做一樣的處理,這樣之後要在這些相同的元件上做修改時,只要對該部分模板修改即可,所有包含的頁面都會一同更新。
<br />
## 加入 CSS
添加 CSS 讓網頁看起來好看些,要做到這點不難,直接在 head.ejs 裡面編寫 style 即可,如下
**head.ejs**
```htmlmixed=
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Joe | <%= title %></title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif&display=swap');
body{
max-width: 1200px;
margin: 20px auto;
padding: 0 20px;
font-family: 'Noto Serif', serif;
max-width: 1200px;
}
p, h1, h2, h3, a, ul{
margin: 0;
padding: 0;
text-decoration: none;
color: #222;
}
/* nav & footer styles */
nav{
display: flex;
justify-content: space-between;
margin-bottom: 60px;
padding-bottom: 10px;
border-bottom: 1px solid #ddd;
text-transform: uppercase;
}
nav ul{
display: flex;
justify-content: space-between;
align-items: flex-end;
}
nav li{
list-style-type: none;
margin-left: 20px;
}
nav h1{
font-size: 3em;
}
nav p, nav a{
color: #777;
font-weight: 300;
}
footer{
color: #777;
text-align: center;
margin: 80px auto 20px;
}
h2{
margin-bottom: 40px;
}
h3{
text-transform: capitalize;
margin-bottom: 8px;
}
.content{
margin-left: 20px;
}
/* index styles */
/* details styles */
/* create styles */
.create-blog form{
max-width: 400px;
margin: 0 auto;
}
.create-blog input,
.create-blog textarea{
display: block;
width: 100%;
margin: 10px 0;
padding: 8px;
}
.create-blog label{
display: block;
margin-top: 24px;
}
textarea{
height: 120px;
}
.create-blog button{
margin-top: 20px;
background: crimson;
color: white;
padding: 6px;
border: 0;
font-size: 1.2em;
cursor: pointer;
}
</style>
</head>
```
<br />
執行,現在網頁變得好看些了

<br />
但我們會希望 CSS 還是能夠放在單獨的文件使用,這部分在之後的筆記才會學習,以上即為使用 view engine 渲染網頁,並動態地注入數據。