1. 程式人生 > >40行程式碼手擼一個靜態文件生成器[譯]

40行程式碼手擼一個靜態文件生成器[譯]

前言

目前有很多優秀的靜態文件生成器,它們的工作原理比你想象的要簡單得多。

原文: Build a static site generator in 40 lines with Node.js

作者: Douglas Matoso

譯者: Simon Ma

日期:2017-09-14

為什麼要造這個輪子

當我計劃建立個人網站時,我的需求很簡單,做一個只有幾個頁面的網站,放置一些關於自己的資訊,我的技能和專案就夠了。

毫無疑問,它應該是純靜態的(不需要後端服務,可託管在任何地方)。

我曾經使用過Jekyll, HugoHexo這些知名的靜態文件生成器,但我認為它們有太多的功能,我不想為我的網站增加這麼多的複雜性。

所以我覺得,針對我的需求,一個簡單的靜態文件生成器就可以滿足。

嗯,手動構建一個簡單的生成器,應該不會那麼難。

正文

需求分析

這個生成器必須滿足以下條件:

  • EJS模板生成HTML檔案。

  • 具有佈局檔案,所有頁面都應該具有相同的頁首,頁尾,導航等。

  • 允許可重用佈局元件。

  • 站點的大致資訊封裝到一個配置檔案中。

  • 從JSON檔案中讀取資料。

    例如:專案列表,這樣我可以輕鬆地迭代和構建專案頁面。

為什麼使用 EJS 模板?

因為 EJS 很簡單,它只是嵌入在 HTML 中的 JavaScript 而已。

專案結構

public/  
 src/  
   assets/  
   data/  
   pages/  
   partials/  
   layout.ejs  
 site.config.js
  • public: 生成站點的位置。
  • src: 原始檔。
  • src/assets: 包含 CSS, JS, 圖片 等
  • src/data: 包含 JSON 資料。
  • src/pages: 根據其中的 EJS 生成 HTML 頁面的模板資料夾。
  • src/layout.ejs: 主要的原頁面模板,包含特殊<%-body%>佔位符,將插入具體的頁面內容。
  • site.config.js: 模板中全域性配置檔案。

生成器

生成器程式碼位於scripts/build.js檔案中,每次想重建站點時,執行npm run build命令即可。

實現方法是將以下指令碼新增到package.jsonscripts塊中:

"build": "node ./scripts/build"

下面是完整的生成器程式碼:

const fse = require('fs-extra')
const path = require('path')
const { promisify } = require('util')
const ejsRenderFile = promisify(require('ejs').renderFile)
const globP = promisify(require('glob'))
const config = require('../site.config')

const srcPath = './src'
const distPath = './public'

// clear destination folder
fse.emptyDirSync(distPath)

// copy assets folder
fse.copy(`${srcPath}/assets`, `${distPath}/assets`)

// read page templates
globP('**/*.ejs', { cwd: `${srcPath}/pages` })
  .then((files) => {
    files.forEach((file) => {
      const fileData = path.parse(file)
      const destPath = path.join(distPath, fileData.dir)

      // create destination directory
      fse.mkdirs(destPath)
        .then(() => {
          // render page
          return ejsRenderFile(`${srcPath}/pages/${file}`, Object.assign({}, config))
        })
        .then((pageContents) => {
          // render layout with page contents
          return ejsRenderFile(`${srcPath}/layout.ejs`, Object.assign({}, config, { body: pageContents }))
        })
        .then((layoutContent) => {
          // save the html file
          fse.writeFile(`${destPath}/${fileData.name}.html`, layoutContent)
        })
        .catch((err) => { console.error(err) })
    })
  })
  .catch((err) => { console.error(err) })

接下來,我將解釋程式碼中的具體組成部分。

依賴

我們只需要三個依賴項:

  • ejs

    把我們的模板編譯成HTML

  • fs-extra

    Node 檔案模組的衍生版,具有更多的功能,並增加了Promise的支援。

  • glob

    遞迴讀取目錄,返回包含與指定模式匹配的所有檔案,型別是陣列。

Promisify

我們使用Node提供的util.promisify將所有回撥函式轉換為基於Promise的函式。

它使我們的程式碼更短,更清晰,更易於閱讀。

const { promisify } = require('util')  
const ejsRenderFile = promisify(require('ejs').renderFile)  
const globP = promisify(require('glob'))

載入配置

在頂部,我們載入站點配置檔案,以稍後將其注入模板渲染中。

const config = require('../site.config')

站點配置檔案本身會載入其他JSON資料,例如:

const projects = require('./src/data/projects')

module.exports = {  
  site: {  
    title: 'NanoGen',  
    description: 'Micro Static Site Generator in Node.js',  
    projects  
  }  
}

清空站點資料夾

我們使用fs-extra提供的emptyDirSync函式清空 生成後的站點資料夾。

fse.emptyDirSync(distPath)

拷貝靜態資源

我們使用fs-extra提供的copy函式,該函式以遞迴方式複製靜態資源 到站點資料夾。

fse.copy(`${srcPath}/assets`, `${distPath}/assets`)

編譯頁面模板

首先我們使用glob(已被 promisify)遞迴讀取src/pages資料夾以查詢.ejs檔案。

它將返回一個匹配給定模式的所有檔案陣列。

globP('**/*.ejs', { cwd: `${srcPath}/pages` })  
  .then((files) => {

對於找到的每個模板檔案,我們使用Nodepath.parse函式來分隔檔案路徑的各個組成部分(例如目錄,名稱和副檔名)。

然後,我們在站點目錄中使用fs-extra提供的mkdirs函式建立與之對應的資料夾。

files.forEach((file) => {  
  const fileData = path.parse(file)  
  const destPath = path.join(distPath, fileData.dir)

 // create destination directory  
  fse.mkdirs(destPath)

然後,我們使用EJS編譯檔案,並將配置資料作為資料引數。

由於我們使用的是已 promisify 的ejs.renderFile函式,因此我們可以返回呼叫結果,並在下一個promise鏈中處理結果。

.then(() => {  
  // render page  
  return ejsRenderFile(`${srcPath}/pages/${file}`, Object.assign({}, config))  
})

在下一個then塊中,我們得到了已編譯好的頁面內容。

現在,我們編譯佈局檔案,將頁面內容作為body屬性傳遞進去。

.then((pageContents) => {  
  // render layout with page contents  
  return ejsRenderFile(`${srcPath}/layout.ejs`, Object.assign({}, config, { body: pageContents }))  
})

最後,我們得到了生成好的編譯結果(佈局+頁面內容的 HTML),然後將其儲存到對應的HTML檔案中。

.then((layoutContent) => {  
  // save the html file  
  fse.writeFile(`${destPath}/${fileData.name}.html`, layoutContent)  
})

除錯伺服器

為了使檢視結果更容易,我們在package.jsonscripts中新增一個簡單的靜態伺服器。

"serve": "serve ./public"

執行 npm run serve 命令,開啟http://localhost:5000就看到結果了。

進一步探索

Markdown

大多數靜態文件生成器都支援以Markdown格式編寫內容。

並且,它們還支援以YAML格式在頂部新增一些元資料,如下所示:

---  
title: Hello World  
date: 2013/7/13 20:46:25  
---

只需要一些修改,我們就可以支援相同的功能了。

首先,我們必須增加兩個依賴:

  • marked

    markdown編譯為HTML

  • front-matter

    markdown中提取元資料(front matter)。

然後,我們將glob的匹配模式更新為包括.md檔案,並保留.ejs,以支援渲染複雜頁面。

如果想要部署一些純 HTML 頁面,還需包含.html

globP('**/*.@(md|ejs|html)', { cwd: `${srcPath}/pages` })

對於每個檔案,我們都必須載入檔案內容,以便可以在頂部提取到元資料。

.then(() => {  
  // read page file  
  return fse.readFile(`${srcPath}/pages/${file}`, 'utf-8')  
})

我們將載入後的內容傳遞給front-matter

它將返回一個物件,其中attribute屬性便是提取後的元資料。

然後,我們使用此資料擴充站點配置。

.then((data) => {  
  // extract front matter  
  const pageData = frontMatter(data)  
  const templateConfig = Object.assign({}, config, { page: pageData.attributes })

現在,我們根據副檔名將頁面內容編譯為 HTML。

如果是.md,則利用marked函式編譯;

如果是.ejs,我們繼續使用EJS編譯;

如果是.html,便無需編譯。

let pageContent  

switch (fileData.ext) {  
  case '.md':  
    pageContent = marked(pageData.body)  
    break  
  case '.ejs':  
    pageContent = ejs.render(pageData.body, templateConfig)  
    break  
  default:  
    pageContent = pageData.body  
}

最後,我們像以前一樣渲染布局。

增加元資料,最明顯的一個意義是,我們可以為每個頁面設定單獨的標題,如下所示:

---  
title: Another Page  
---

並讓佈局動態地渲染這些資料:

<title><%= page.title ? `${page.title} | ` : '' %><%= site.title %></title>

如此一來,每個頁面將具有唯一的<title>標籤。

多種佈局的支援

另一個有趣的探索是,在特定的頁面中使用不同的佈局。

比如專門為站點首頁設定一個獨一無二的佈局:

---  
layout: minimal  
---

我們需要有單獨的佈局檔案,我將它們放在src/layouts資料夾中:

src/layouts/  
   default.ejs  
   mininal.ejs

如果front matter出現了佈局屬性,我們將利用layouts資料夾中同名模板檔案進行渲染; 如果未設定,則利用預設模板渲染。

const layout = pageData.attributes.layout || 'default'

return ejsRenderFile(`${srcPath}/layouts/${layout}.ejs`, 
  Object.assign({}, templateConfig, { body: pageContent })
)

即使添加了這些新特性,構建指令碼也才只有60行。

下一步

如果你想更進一步,可以新增一些不難的附加功能:

  • 可熱過載的除錯伺服器

    你可以使用像live-server (內建自動重新載入) 或 chokidar (觀察檔案修改以自動觸發構建指令碼)這樣的模組去完成。

  • 自動部署

    新增指令碼以將站點部署到GitHub Pages等常見的託管服務,或僅通過SSH(使用scprsync等命令)將檔案上傳到你自己的伺服器上。

  • 支援 CSS/JS 前處理器

    在靜態檔案被複制到站點檔案前,增加一些前處理器(SASS 編譯為 CSS,ES6 編譯為 ES5 等)。

  • 更好的日誌列印

    新增一些 console.log 日誌輸出 來更好地分析發生了什麼。

    你可以使用chalk包來完善這件事。

反饋? 有什麼建議嗎? 請隨時發表評論或與我聯絡!


結束語

這個文章的完整示例可以在這裡找到:https://github.com/doug2k1/nanogen/tree/legacy。

一段時間後,我決定將專案轉換為CLI模組,以使其更易於使用,它位於上面連結的master分支中。

譯者:

今日本想寫一篇ants(一個高效能的goroutine池)原始碼解析,奈何環境太吵,靜不下心,遂罷。

這是一篇我前些日子無意間看到的文章,雖然是17年的文章,在讀完之後仍對我產生了一些思考。

希望這篇文章對你有所幫助。

轉載本站文章請註明作者和出處 一個壞掉的番茄,請勿用於任何商業用途