背景
如果妳在尋找更靈活的網頁寫法,例如將網頁看成是個樣板,以使用者行為產生的變數,來置換樣板的內容,那這個簡單的pug顯然會對妳有所幫助。
- pug是express套件常用的顯示引擎之一。所謂的引擎事實上就是個編譯器,將樣板內容、套用當時前後端整體的變數環境,編譯成最後使用者看到的html檔案(如wiki: Web_template_system圖示)。
- 其他選項還包括了EJS、 handlebar、nunjucks等等引擎,因pug有自己的語法因此被認為是比較困難一點,但也正因如此而有較簡潔、直覺的強項,比較符合高階工程師的期待。
- 目前pug更新到3.0.2版(2021/03)。
- pug應用狀況
- 遠端運轉著大量資訊的伺服器,其複雜度不是一般網頁可以消化,需要切換數個(>101)版本的網頁,才能有正確的呈現。
- 網頁常常需要維護、更新
- 前端使用者的行為不會太複雜
- 工程師可以接受以固定空格(tab)來定義從屬關係,即python縮排語法
- pug的中文簡要說明可以參考前端筆記,完整的說明可以詳見官網pug.org。英文的互動說明可以參考LHViet88@2018 Bibooki
- 典型的index.pug檔案
- var user = {description: 'foo bar baz'}
- var authorised = false
#user
if user.description
h2.green Description
p.description= user.description
else if authorised
h2.blue Description
p.description.
User has no description,
why not add one...
else
h2.red Description
p.description User has no description
其效果為
<div id="user">
<h2 class="green">Description</h2>
<p class="description">foo bar baz</p>
</div>
實例
- http://125.229.149.182:3000這個實際範例是按照NASA GMAO空氣品質預報網頁進行反組譯的成果。圖像為東亞~台灣未來10天之WRF-CMAQ預報(每天早晨更新)。
- 網頁的邏輯詳見NASA GMAO空品預報服務網頁。目前完成region與field 等2項之反組譯。
pug中的變數
- 因為pug是在整體js環境中,除了自己本身宣告的變數之外,也可以取用或改變公用的變數(不必另外宣告),如以下views/index.pug的片段。
index.pub片段
div(id="menu-container" class="fluid hide_mobile")
ul(id="menu")
li: form(action="/" method="POST")
- var root = "/";
- var tauH = `tau=${reqT}`;
- var fcsD = `fcst=${reqF}`;
- var regR = `region=${reqR}`;
- var fieD = `field=${reqD}`;
- var specs = ["O3", "NO2", "CO", "SO2", "PM2.5"];
- urlH = `${root}?${tauH}&${regR}&${fcsD}&field=`;
div(class="category-wrapper")
p() FIELDS
div(class="category-wrapper")
p() Surface
ul
each spName in specs
- url = `${urlH}${spName}`
li: a(href=url) #{spName}
- 在pug語法中,縮排是有意義的,相同縮排的物件彼此是平形的,差2格縮排表示有上下從屬關係。
- 在pug中可以接受部分js指令,以減號當成前綴。如範例中宣告了字串(注意合併的方式)、序列等
- 減號前綴後如果要引用變數(包括pug內臨時宣告、或是經res.render、locals公開到程式間可以使用的公用變數),需加上
${...}
意即環境變數。 - 在pug語法括號
(...)
內如要引用變數,直接寫變數名稱即可,而在括號外部,則需要加上#{...}
,如li: a(href=url) #{spName}
- pug變數如何傳回變數?
- 基本上是透過html上使用者的行為。
- 當使用者點選物件、觸發了post方法,在伺服器action的內容,讓伺服器可以藉此解讀傳回變數的內容。具體而言即為express.Router().get所傳回的reqire.query內容。
- 如以下html內容,使用者如果點擊後續物件,將會在browser命令列上出現以/為首的url文字串。
html form段落
<form action="/" method="POST">
<form action="/" method="POST" name="control_form" class="animate">
- 使用者觸發事件將會回傳一個網址,如在layout.pug中指定之
url=http://125.229.149.182:3000/?tau=undefined&fcst=undefined&field=O3®ion=se_china
- 此字串經routers/index.js將其中的內容記錄成為變數的值,再利用res.render()將其宣告成公用變數,如下:
網頁主控js
- routes/index.js
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Composition Forecast Maps ' + req.query.region + req.query.field,
reqD: req.query.field,
reqR: req.query.region,
reqF: req.query.fcst,
reqT: req.query.tou,
reqC: req.query.control_form}
);
});
module.exports = router;
- 範例程式將title、require所查詢到的變數內容等等對照表,再次提交(render)到respond中,讓pug程式可以使用
pug程式使用變數
- 如views/layouts.pug。此檔案為整個網頁的平面配置。
- 範例中冒號右邊的title,內容即為router/index.js所提交的title。
doctype html
html
head
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1")
link(rel="shortcut icon" href="https://www.sinotech-eng.com/assets/front/images/favicon/favicon.svg" type="image/svg")
title= title
...
pug語法中的條件判斷
全等範例
div(class="category-wrapper")
p() REGIONS
select(name="region" onchange="window.location.href=this.value")
- var fieD = `field=${reqD}`;
- var regns = ["eastern_asia", "se_china", "taiwan"];
- urlH = `${root}?${tauH}&${fcsD}&${fieD}®ion=`;
each rgName in regns
- url = `${urlH}${rgName}`
if reqR == rgName
option(value=url, selected="selected") #{rgName}
else
option(value=url) #{rgName}
- 這個範例是讓下拉選單能夠有正確的預設值,就是使用者剛剛做出的選擇,可以停留在選單上。
- 因為
if ... else
算是pug的語法範圍,類似前述(...)
內範圍,因此可以不必加上前綴${...}
或#{...}
。 - 其他地方,還是按照前述規則。
未定義範例
if ! reqR
- var reqR = "eastern_asia";
if ! reqD
- var reqD = "PM2.5";
- 這個範例判斷使用者是否給定完整的(reqR,reqD)內容,如果是,則引用正確的檔案(設定為gif檔),如果否,則給定內定檔。
- 因為沒有and指令,還蠻困擾的。只能用負面表列,先暫時宣告並給定一組內定值。
大小
- 雖然官網中沒有大小判斷的範例,但js一般的判別,在pug中也是可行的,如整數,
- 序號大或等於1才會出現箭頭向左的物件和連結
if selr >= 1
- var d = regns[selr-1];
- url = `${urlH}${d}`;
a(href=url)
div(class="arrow-left")
- 字串(前月當天的日期,比序列第1個還大,才會出現箭頭向上的物件連結)
if lastM >= days[0]
- url = `${urlH}${lastM}`;
a(href=url)
div(class="arrow-up")
pug中的迴圈
each
- 這對選項很多的下拉選單是項福音
- var regns = ["eastern_asia", "se_china", "taiwan"];
select(name="region" onchange="window.location.href=this.value")
each rgName in regns
- url = `${urlH}${rgName}`
if reqR == rgName
option(value=url, selected="selected") #{rgName}
else
option(value=url) #{rgName}
while
- 選項個數、內容序列都是變數、每次可能會新增或減少。如下面範例,日期每天會增加
select(name="fcst" onchange="window.location.href=this.value")
- var i = 0;
while i < ndays
- let d = days[i];
- url = `${urlH}${d}`;
if reqF == d
option(value=url, selected="selected") #{reqF}
else
option(value=url) #{d}
- i++
限制
- pug的迴圈不如if,跟js的落差還蠻大的,很多功能沒有,例如push。
- 所以如果要組成一個不特定長度的序列,還是在js中先做好再傳遞到pug.
外部檔案的引用(include)
一般用法
include指令與其內容的縮排位置
- 官網沒有說明
- include這個指令的縮排位置,是與它原來應該在的位置一樣才行
- 如果前述位置正確,include的內容就必須向左靠齊。以便適用安插在本文不同的縮排位置。
- 本文與引用檔之間的變數可以通用。不需要另外宣告。
todo’s
gif vs png’s
- 以href(url)呼叫gif檔,無法控制播放與速度。
- 每張圖之間如果使用href(url)呼叫,會出現閃屏,干擾還蠻大的。
- 使用hanis雖然有控制鍵,但圖片要放到接近原尺寸(排擠左側選單)才不會有模糊化。不能使用shrinkfit。
選單
- 左側選單似太佔空間,應考慮移到上幅。
內容
- 2019公版模式成果展示
- 預報成果展示
- LGHAP日均值
source code
views/index.pug
extends layout
block content
div(class="slicknav_menu")
a(href="#" aria-haspopup="true" role="button" tabindex="0" class="slicknav_btn slicknav_collapsed" style="outline: none;")
span(class="slicknav_menutxt") MENU
span(class="slicknav_icon")
span(class="slicknav_icon-bar")
span(class="slicknav_icon-bar")
span(class="slicknav_icon-bar")
ul(class="slicknav_nav slicknav_hidden" aria-hidden="true" role="menu" style="display: none;")
a(name="top" id="top")
div(class="gridContainer clearfix")
div(id="top-box" class="fluid hide_mobile")
div(class="top-nasa-logo")
img(src="https://www.sinotech-eng.com/assets/front/images/favicon/favicon.svg" alt="SES logo")
div(class="top-gmao-logo")
a(href="https://gmao.gsfc.nasa.gov/")
img(src="https://fluid.nccs.nasa.gov/static/img/GMAO-logo.png" alt="GMAO logo")
div(class="top-box-text")
a(href="https://sinotec2.github.io/Focus-on-Air-Quality/GridModels/ForecastSystem/") Focus on Air Quality at GitHub
div(id="top-links-box" class="top-links-text fluid")
a(href="/cf/") Home
a(href="/gram/cf_no2/?region=nam") Datagrams
a(href="/cf/classic_geos_cf/?region=nam" style="color: #b7a98b;") Surface Concentrations
a(href="/cf/totcol_geos_cf/?region=nam" style="") Total Column
div(id="menu-container" class="fluid hide_mobile")
ul(id="menu")
li: form(action="/" method="POST")
if ! reqR
- var reqR = "eastern_asia";
if ! reqD
- var reqD = "PM2.5";
- var root = "/";
- var tauH = `tau=${reqT}`;
- let ndays = days.length - 10;
- var fcsD = `fcst=${reqF}`;
- var regR = `region=${reqR}`;
- var fieD = `field=${reqD}`;
- var specs = ["O3", "NO2", "CO", "SO2", "PM2.5"];
- urlH = `${root}?${tauH}&${regR}&${fcsD}&field=`;
div(class="category-wrapper")
p() FIELDS
div(class="category-wrapper")
p() Surface
ul
each spName in specs
- url = `${urlH}${spName}`
li: a(href=url) #{spName}
div(class="category-wrapper")
p() REGIONS
- urlH = `${root}?${tauH}&${fcsD}&${fieD}®ion=`;
- var regns = ["eastern_asia", "se_china", "taiwan"];
- var nregn = regns.length
- var selr = regns.indexOf(reqR);
if selr >= 1
- var d = regns[selr-1];
- url = `${urlH}${d}`;
a(href=url)
div(class="arrow-left")
select(name="region" onchange="window.location.href=this.value")
each rgName in regns
- url = `${urlH}${rgName}`
if reqR == rgName
option(value=url, selected="selected") #{rgName}
else
option(value=url) #{rgName}
if selr <= nregn - 2
- var d = regns[selr+1];
- url = `${urlH}${d}`;
a(href=url)
div(class="arrow-right")
div(class="category-wrapper")
p() FORECAST INITIAL DATE
- urlH = `${root}?${tauH}&${regR}&${fieD}&fcst=`;
- var lastM = Mths[0];
- var nextM = Mths[1];
- var seli = days.indexOf(reqF);
if lastM >= days[0]
- url = `${urlH}${lastM}`;
a(href=url)
div(class="arrow-up")
if seli >= 1
- var d = days[seli-1];
- url = `${urlH}${d}`;
a(href=url)
div(class="arrow-left")
select(name="fcst" onchange="window.location.href=this.value")
- var i = 0;
while i < ndays
- let d = days[i];
- url = `${urlH}${d}`;
if reqF == d
option(value=url, selected="selected") #{reqF}
else
option(value=url) #{d}
- i++
if seli <= ndays - 2
- var d = days[seli+1];
- url = `${urlH}${d}`;
a(href=url)
div(class="arrow-right")
if nextM <= days[ndays-1]
- url = `${urlH}${nextM}`;
a(href=url)
div(class="arrow-down")
div(class="category-wrapper")
p() FORECAST LEAD HOUR
- nhrs = hrs.length
- urlH = `${root}?${fcsD}&${regR}&${fieD}&tou=`;
- var selh = hrs.indexOf(reqT);
if selh >= 1
- var d = hrs[selh-1];
- url = `${urlH}${d}`;
a(href=url)
div(class="arrow-left")
select(name="tau" onchange="window.location.href=this.value")
- var i = 0;
while i < nhrs
- let d = hrs[i];
- url = `${urlH}${d}`;
if reqT == d
option(value=url, selected="selected") #{reqT}
else
option(value=url) #{d}
- i++
if selh <= nhrs - 2
- var d = hrs[selh+1];
- url = `${urlH}${d}`;
a(href=url)
div(class="arrow-right")
div(id="content-container" class="fluid")
h1=title
- var url = `cf/classic_geos_cf/latest_${reqD}_${reqR}`;
img(src=url alt="" class="map-wrapper")
form(action="/" method="POST" name="control_form" class="animate")
a(href="")
input(value=" ANIMATE " type="button")
a(href="")
input(value=" DOWNLOAD MOVIE " type="button")
div(id="footer" class="fluid")
div(class="footer-text-box")
div(class="footer-links")
a(href="https://gmao.gsfc.nasa.gov") GMAO Homepage
a(href="/about/") About FLUID
a(href="https://gmao.gsfc.nasa.gov/contact.php") Contact FLUID
a(href="mailto:Steven.Pawson-1@nasa.gov") NASA Official: Steven Pawson
a(href="mailto:james.gass@nasa.gov") Web Curator: James Gass
a(href="http://www.nasa.gov/about/highlights/HP_Privacy.html") Privacy Policy
div(class="footer-mobile-gmao-logo")
img(src="/static/img/GMAO-logo.png" alt="GMAO logo")
div(class="footer-mobile-nasa-logo")
img(src="/static/img/nasa-logo.png" alt="NASA logo")
routes/index.js
var express = require('express');
var router = express.Router();
/* GET home page. */
/* https://stackoverflow.com/questions/4413590/javascript-get-array-of-dates-between-2-dates */
var getDaysArray = function(start, end) {
for(var arr=[],dt=new Date(start); dt<=new Date(end); dt.setDate(dt.getDate()+1)){
arr.push(new Date(dt));
}
return arr;
};
/* https://stackoverflow.com/questions/563406/how-to-add-days-to-date */
Date.prototype.addDays = function(numdays) {
var date = new Date(this.valueOf());
date.setDate(date.getDate() + numdays);
return date;
}
router.get('/', function(req, res, next) {
const date = new Date();
let day = ('0'+date.getDate()).slice(-2);
let month = ('0'+ ( date.getMonth() + 1 )).slice(-2);
let year = date.getFullYear();
let hour = ('0'+date.getHours()).slice(-2);
// This arrangement can be altered based on how we want the date's format to appear.
var reqFt;
var reqTt;
const begd = '2022-08-01'
const endd = new Date().addDays(10);
var daytmp = getDaysArray(new Date(begd),new Date());
var ndayst = daytmp.length;
var daytmp = getDaysArray(new Date(begd),endd);
var daylist = daytmp.map( v => v.toISOString().slice(0,10) );
var todayDate = daylist[ndayst-1];
if ( !req.query.fcst ) { reqFt = todayDate }
else { reqFt = req.query.fcst }
var inow = daylist.indexOf(reqFt);
if ( inow == -1 ) { var reqFt = todayDate }
if ( inow >= 0 ) { var current = [daytmp[inow],daytmp[inow]] }
else { var current = [daytmp[ndayst-1],daytmp[ndayst-1]] }
current[0].setMonth(current[0].getMonth()-1);
const lastM = current[0].toISOString().slice(0,10);
current[1].setMonth(current[1].getMonth()+2);
const nextM = current[1].toISOString().slice(0,10);
var seli = daylist.indexOf(reqFt);
var hrlist = [];
var i = 0;
for(var da = 0; da < 10; da++){
for(var hr = 0; hr < 24; hr+=3){
let hh = ('0' + hr).slice(-2);
let hhh = ('00' + i).slice(-3);
hrlist.push(daylist[seli+da]+'T'+hh+'_'+hhh);
i++;
};
};
if ( !req.query.tou ) { reqTt = reqFt+'T00_000' }
else { reqTt = req.query.tou }
res.render('index', { title: req.query.field+' Forecast Map over ' + req.query.region +' area since '+ reqFt,
reqD: req.query.field,
reqR: req.query.region,
reqF: reqFt,
days: daylist,
hrs: hrlist,
Mths: [lastM,nextM],
reqT: reqTt,
reqC: req.query.control_form}
);
});
/*
locals: { {regQD: req.query.field};
{regQR: req.query.region};
};
console.log(req.query.region);
console.log(req.query.field);
console.log(req.query.tau);
console.log(req.query.fcst);
console.log(req.query.control_form);
locals: { {regQD: req.query.field; (typeof regQD !== 'undefined') ? regQD : "NO2";};
{regQR: req.query.region; (typeof regQR !== 'undefined') ? regQR : "eastern_asia";};
*/
var locals = { myrouter : require('express').Router() };
module.exports = router;