pug模板語法

 

背景

如果妳在尋找更靈活的網頁寫法,例如將網頁看成是個樣板,以使用者行為產生的變數,來置換樣板的內容,那這個簡單的pug顯然會對妳有所幫助。

  • pug是express套件常用的顯示引擎之一。所謂的引擎事實上就是個編譯器,將樣板內容、套用當時前後端整體的變數環境,編譯成最後使用者看到的html檔案(如wiki: Web_template_system圖示)。

  • 其他選項還包括了EJS、 handlebar、nunjucks等等引擎,因pug有自己的語法因此被認為是比較困難一點,但也正因如此而有較簡潔、直覺的強項,比較符合高階工程師的期待。
  • 目前pug更新到3.0.2版(2021/03)。
  • pug應用狀況
    1. 遠端運轉著大量資訊的伺服器,其複雜度不是一般網頁可以消化,需要切換數個(>101)版本的網頁,才能有正確的呈現。
    2. 網頁常常需要維護、更新
    3. 前端使用者的行為不會太複雜
    4. 工程師可以接受以固定空格(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}
  1. 在pug語法中,縮排是有意義的,相同縮排的物件彼此是平形的,差2格縮排表示有上下從屬關係。
  2. 在pug中可以接受部分js指令,以減號當成前綴。如範例中宣告了字串(注意合併的方式)、序列等
  3. 減號前綴後如果要引用變數(包括pug內臨時宣告、或是經res.render、locals公開到程式間可以使用的公用變數),需加上${...}意即環境變數。
  4. 在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&region=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語法中的條件判斷

  • pug的if能夠允許的形式並不多,且官網也未提及完全相等(==)的比較operator
  • 參考nodejsera的教學做法與嘗試錯誤,發現可以這樣使用

全等範例

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}&region=`;
    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)

一般用法

  • 依據官網的說明,pug語法是允許使用include的。雖然pug沒有函數、副程式,但有include也還是可以模組化,在本文多處引用相同的模組。
  • 官網說明,引用的檔案形式可以是程式、文字。

include指令與其內容的縮排位置

  • 官網沒有說明
    1. include這個指令的縮排位置,是與它原來應該在的位置一樣才行
    2. 如果前述位置正確,include的內容就必須向左靠齊。以便適用安插在本文不同的縮排位置。
    3. 本文與引用檔之間的變數可以通用。不需要另外宣告。

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}&region=`;
            - 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;
  1. 林罡北(2018)如果你是常切版的前端工程師-你一定要知道pug, Medium