Node.js
從前面學習到現在,在伺服器響應瀏覽器所用到 HTML 頁面都是靜態的 HTML,內容都是事先就定義好的,現在想要動態的注入數據,例如部落格文章或是個人資料頁面上的用戶資料,而如何很好地在網頁模板上輸出這些數據,我們可以使用稱為 View Engines 視圖引擎或是模板引擎,使用 View Engines 編寫的模板與 HTML 語法相似,且能夠做到將數據動態的注入到網頁中。而 View Engines 也有很多種,例如 Express Handlebars、pug、EJS,而接下來我們將要使用 EJS 繼續後面的學習。
首先安裝 EJS,只要開啟專案在終端輸入 npm install ejs
,這裡的專案使用上一筆記的 app.js
安裝 ejs
開啟 package.json 確認已安裝 ejs
接下來要使用 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 的檔案
然後開啟 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
,會看到以下畫面
點擊 About 或 New Blog 連結會出現如下畫面,因為刪除了 html 檔所以找不到
所以開始編輯 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 及其它模板引擎真正強大的作用在於之後動態的注入數據及邏輯。
現在要開始將數據傳遞到 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 © 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 讓網頁看起來好看些,要做到這點不難,直接在 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 渲染網頁,並動態地注入數據。