40行程式碼手擼一個靜態文件生成器[譯]
前言
目前有很多優秀的靜態文件生成器,它們的工作原理比你想象的要簡單得多。
原文: Build a static site generator in 40 lines with Node.js
作者: Douglas Matoso
譯者: Simon Ma
日期:2017-09-14
為什麼要造這個輪子
當我計劃建立個人網站時,我的需求很簡單,做一個只有幾個頁面的網站,放置一些關於自己的資訊,我的技能和專案就夠了。
毫無疑問,它應該是純靜態的(不需要後端服務,可託管在任何地方)。
我曾經使用過Jekyll
, Hugo
和Hexo
這些知名的靜態文件生成器,但我認為它們有太多的功能,我不想為我的網站增加這麼多的複雜性。
所以我覺得,針對我的需求,一個簡單的靜態文件生成器就可以滿足。
嗯,手動構建一個簡單的生成器,應該不會那麼難。
正文
需求分析
這個生成器必須滿足以下條件:
從
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.json
的scripts
塊中:
"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) => {
對於找到的每個模板檔案,我們使用Node
的path.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.json
的scripts
中新增一個簡單的靜態伺服器。
"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
(使用scp
或rsync
等命令)將檔案上傳到你自己的伺服器上。支援 CSS/JS 前處理器
在靜態檔案被複制到站點檔案前,增加一些前處理器(SASS 編譯為 CSS,ES6 編譯為 ES5 等)。
更好的日誌列印
新增一些
console.log
日誌輸出 來更好地分析發生了什麼。你可以使用
chalk
包來完善這件事。
反饋? 有什麼建議嗎? 請隨時發表評論或與我聯絡!
結束語
這個文章的完整示例可以在這裡找到:https://github.com/doug2k1/nanogen/tree/legacy。
一段時間後,我決定將專案轉換為CLI
模組,以使其更易於使用,它位於上面連結的master
分支中。
譯者:
今日本想寫一篇ants(一個高效能的goroutine
池)原始碼解析,奈何環境太吵,靜不下心,遂罷。
這是一篇我前些日子無意間看到的文章,雖然是17
年的文章,在讀完之後仍對我產生了一些思考。
希望這篇文章對你有所幫助。
轉載本站文章請註明作者和出處 一個壞掉的番茄,請勿用於任何商業用途