Try   HackMD

Node.js(七) View Engines

tags: Node.js

View Engines

從前面學習到現在,在伺服器響應瀏覽器所用到 HTML 頁面都是靜態的 HTML,內容都是事先就定義好的,現在想要動態的注入數據,例如部落格文章或是個人資料頁面上的用戶資料,而如何很好地在網頁模板上輸出這些數據,我們可以使用稱為 View Engines 視圖引擎或是模板引擎,使用 View Engines 編寫的模板與 HTML 語法相似,且能夠做到將數據動態的注入到網頁中。而 View Engines 也有很多種,例如 Express Handlebars、pug、EJS,而接下來我們將要使用 EJS 繼續後面的學習。

EJS

首先安裝 EJS,只要開啟專案在終端輸入 npm install ejs,這裡的專案使用上一筆記的 app.js

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

安裝 ejs

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

開啟 package.json 確認已安裝 ejs

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


接下來要使用 express 做模板引擎的開發,如果你開啟 EJS 的官網可以看到它是直接引入 ejs 模組使用,不一定要結合 express 使用。

開啟 app.js 註冊 view engine,使用 app.set() 設定一些程序設置。
其中一個設置是 'view engine',告訴 express 使用什麼樣的模板引擎,這裡使用 'ejs'

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' 所以可以不用設置,除非你要將視圖存放在別的目錄。

做完以上設置,接著開始製作模板,將 views 裡的三個 html 檔刪除,因為這裡不再需要純 HTML,取而代之的視圖副檔名為 ejs,所以再創建三個主檔名與之前一樣,但副檔名為 ejs 的檔案

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


然後開啟 index.ejs 開始編碼,目前編寫的內容依舊為純 HTML,包含了導覽功能以及顯示部落格內容的地方,其中一個連結為 /blogs/create 用於之後創建一個新的部落格內容功能的地方

<!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>

試著在瀏覽器上顯示該模板,開啟 app.js,這裡要用到 res.render() 方法渲染視圖取代之前的 res.sendFile()
我們只需如下編碼,express 便會自動在設置視圖目錄中找到檔案,並使用 ejs 模板引擎渲染並發送回瀏覽器

app.get('/', (req, res) => { res.render('index'); });

全部的 code

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}); });

一樣使用 nodemon 執行,在瀏覽器輸入 localhost:3000,會看到以下畫面

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


所以開始編輯 about.ejs,與 index.ejs 差不多,唯一不一樣的地方在顯示部落格內容的地方加了些假字。

<!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>

404.ejs 也只在部落格內容多了找不到頁面的提示,並將 div 的 classname 去除 blogs 改為 not-found

<!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>

完成以上後將它們都渲染出來,這裡不需要 '/about-us' 所以去除了

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'); });

執行,點擊 About 連結


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


在 views 資料夾新增 create.ejs 視圖,與 index.ejs 不同於在部落格內容新增了表單,並將 classname 改為 create-blog,目前該表單並沒有作用,之後再添加功能

<!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>

app.js 添加 '/blogs/create' 的選項

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'); });

執行,點擊 New Blog 連結


做到這裡其實與前面做的差不多,但 EJS 及其它模板引擎真正強大的作用在於之後動態的注入數據及邏輯。

將數據傳遞到 views

現在要開始將數據傳遞到 ejs 模板,使其不僅只有靜態內容,有些像是 PHP 的標籤,但不了解 PHP 也沒關係。

首先開啟 index.ejs,在 body 中編寫以下 code

<% const name = 'Joe' %> <!-- 定義常數 -->

<% 開始,%> 結束,而在裡面則可以編寫一些 code,例如我們可以宣告任何的 JavaScript 類型,就像這裡定義了字串常數 name,但這不會在前端瀏覽器運行,而是在後端伺服器運行,後面會再詳細說明這個過程,現在先理解 code。

定義了常數,那麼要如何讓它輸出顯示出來呢? 如下便會在 p 元素中顯示 name 常數的值,當然不一定要包覆在元素中,但要顯示在網頁中總要放在元素裡讓 css 樣式渲染

<p><%= name %></p> <!-- 顯示常數 -->

index.ejs 全部的 code

<!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>

現在執行看看,連接 localhost:3000

可以看到最上面多了一個 p 元素,內容為我們在伺服器端的設定

以上為一個簡單的示範,實際上並不會這麼做,因為我們更想的是傳遞應用程式中的數據,可能會與 database 進行溝通獲取數據,然後在將數據傳遞給視圖模板輸出,最後顯示在瀏覽器網頁上。

但現在先跳過數據庫的部分,現在僅先從我們負責處理程序的應用程式 app.js 傳遞數據給模板,這部分要用到 res.render() 的第二個參數,該參數是一個傳遞給視圖變數的物件,如下

app.get('/', (req, res) => { res.render('index', { title: 'Home'}); });

然後在 index.ejs 就可以接收上面傳遞的 title 屬性,我們將它顯示在 head 裡的 title 元素,使用 <%= title %> 顯示

<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


依此類推,將其它的頁面也做一樣的設定,但不同的值

app.js

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 作法一模一樣

執行,切換各個頁面,顯示相對應的 title




接著讓我們再輸出更多的內容並做邏輯判斷

'/' 的 callback function 中新增一個 blogs 陣列,裡面包含一些部落格標題及簡介是我們要輸出的內容

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 傳遞給視圖了。

現在切換到 index.ejs 將 blogs 顯示出來,這裡我們要先檢查是否有 blogs,若有將內容顯示出來,若沒有則顯示沒有的資訊。

那麼如何確認 blogs 是否有值呢? 在 JavaScript 判斷一個陣列是否有值就檢查它的長度,這裡也這麼做。

我們可以先將 code 以 JS 的方式寫出來,如下

if (blogs.length > 0) { blogs.forEach(blog => { // 輸出內容 }) }

要寫在 ejs 裡,則每行都以 <% %> 包覆

<% if (blogs.length > 0) { %> <% blogs.forEach(blog => { %> // 輸出內容 <% }) %> <% } %>

至於其中輸出內容的部分如下,可以參考官方文件

<% if (blogs.length > 0) { %> <% blogs.forEach(blog => { %> <h3><%= blog.title %></h3> <p><%= blog.snippet %></p> <% }) %> <% } %>

在加上 else 處理 blogs 沒有數據的情況

<% if (blogs.length > 0) { %> <% blogs.forEach(blog => { %> <h3><%= blog.title %></h3> <p><%= blog.snippet %></p> <% }) %> <% } else { %> <p>There are no blogs to display...</p> <% } %>

index.ejs 全部的 code

<!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>

執行,可以看到在部落格內容的部分顯示 blogs 的數據


如果將 blogs 的數據註解掉,變成一個空陣列

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}); });

執行便會顯示沒有部落格可以顯示的訊息


接下來說一說 EJS 的模板從伺服器到顯示在瀏覽器這中間的過程。
EJS 模板透過 EJS view engine 處理其中動態注入的內容、變數、循環、條件等等,產生出對應的 HTML 文件再發送給瀏覽器,這個過程稱為伺服器端的渲染。

以上即為將數據傳遞到 views 並輸出顯示在瀏覽器上的模板引擎。

部分模板

前面使用到的 index、about、create、404 等模板都有些共同的部分,例如 head、nav 這些標籤是一模一樣,如果某天新增了頁面就要在 nav 新增連結,每個頁面的 nav 都要做修改,這樣當頁面越來越多時很是麻煩、且沒效率,所以我們可以製作部分的模板,並調用它們到每個頁面中,當需要修改時,只要修改該部分的模板就好。

首先在 views 資料夾建立 partials 資料夾,並在其中新增 head.ejs、nav.ejs、footer.ejs 檔案作為部分模板

接著可以從前面的模板複製相同部分的 head、nav 到這些部分模板裡,如下

head.ejs

<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Blog Joe | <%= title %></title> </head>

nav.ejs

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

footer.ejs 可以自己編碼

<footer> Copyright &copy; Blog Joe for learning </footer>

有了部分模板後,就要在頁面模板中調用、包含它們,語法如下

<%- include('./partials/nav.ejs') %>

使用 include(),參數即部分模板的相對位置,注意這裡使用 <%- 減號而不是 <%= 等號,否則不能正常轉譯,會直接以字串的形式顯示在網頁中,如下

記得要將原來部分模板取代的部分先刪掉,否則會重複出現,以 index.ejs 舉例
index.ejs 刪除 head 及 nav,並包含部分模板進去

<!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

其它頁面模板也做一樣的處理,這樣之後要在這些相同的元件上做修改時,只要對該部分模板修改即可,所有包含的頁面都會一同更新。

加入 CSS

添加 CSS 讓網頁看起來好看些,要做到這點不難,直接在 head.ejs 裡面編寫 style 即可,如下

head.ejs

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

執行,現在網頁變得好看些了


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