# 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 ![](https://i.imgur.com/030lvXM.png) 安裝 ejs ![](https://i.imgur.com/YEuOX7Y.png) 開啟 package.json 確認已安裝 ejs ![](https://i.imgur.com/BVFpIAl.png) <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 的檔案 ![](https://i.imgur.com/mk874N9.png) <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`,會看到以下畫面 ![](https://i.imgur.com/bfXPpen.png) <br /> 點擊 About 或 New Blog 連結會出現如下畫面,因為刪除了 html 檔所以找不到 ![](https://i.imgur.com/z9PEhPB.png) <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 連結 ![](https://i.imgur.com/woLGDLW.png) <br /> 點擊 New Blog 連結或是輸入其它的 URL,顯示 404 頁面 ![](https://i.imgur.com/whJuRM0.png) <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 連結 ![](https://i.imgur.com/RM4pJKL.png) <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` ![](https://i.imgur.com/WgU8quM.png) 可以看到最上面多了一個 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` ![](https://i.imgur.com/wKRLMus.png) <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` ![](https://i.imgur.com/jmOeaHd.png) ![](https://i.imgur.com/t0iDsRL.png) ![](https://i.imgur.com/C0kboNE.png) <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 的數據 ![](https://i.imgur.com/qml4Kz4.png) <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}); }); ``` 執行便會顯示沒有部落格可以顯示的訊息 ![](https://i.imgur.com/7hoZIJ7.png) <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 檔案作為部分模板 ![](https://i.imgur.com/wWHMuPi.png) 接著可以從前面的模板複製相同部分的 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 &copy; Blog Joe for learning </footer> ``` <br /> 有了部分模板後,就要在頁面模板中調用、包含它們,語法如下 ```javascript <%- include('./partials/nav.ejs') %> ``` 使用 `include()`,參數即部分模板的相對位置,注意這裡使用 `<%-` 減號而不是 `<%=` 等號,否則不能正常轉譯,會直接以字串的形式顯示在網頁中,如下 ![](https://i.imgur.com/QSBAQx4.png) 記得要將原來部分模板取代的部分先刪掉,否則會重複出現,以 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 ![](https://i.imgur.com/rnyuh7C.png) 其它頁面模板也做一樣的處理,這樣之後要在這些相同的元件上做修改時,只要對該部分模板修改即可,所有包含的頁面都會一同更新。 <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 /> 執行,現在網頁變得好看些了 ![](https://i.imgur.com/MempvHI.png) <br /> 但我們會希望 CSS 還是能夠放在單獨的文件使用,這部分在之後的筆記才會學習,以上即為使用 view engine 渲染網頁,並動態地注入數據。