本文涉及的全部代碼能夠在 docsite 的開源代碼倉庫 github.com/txd-team/do… 中找到,若是對你有所幫助,歡迎 Star 關注咱們。javascript
諸如github pages的靜態託管服務的興起,靜態生成+託管對託管環境要求低、維護簡單、可配合版本控制,但又靈活多變,這一系列的優勢,使得靜態站點生成器在近年有了極大的發展,涌現出一系列優秀的靜態站點生成器。css
筆者負責整個部門的開源站點搭建,要想提升開發效率,沒有一個稱手的工具是不行的。搭建站點的工具須要知足以下要求:html
考察了一系列的開源靜態站點搭建工具,總有這樣或者那樣的功能不知足需求,因而就着手打造一款靜態站點搭建工具。因主要用於靜態站點的搭建,且支持markdown文檔,筆者爲該工具起名爲docsite。前端
從總體上來講,docsite須要可以支持站點項目的初始化、本地開發和本地構建。而對於前端同窗來講,採用NodeJS實現一個命令行工具,不失爲一個有效的方法。爲此,docsite須要對應實現至少三個命令,docsite init
,docsite start
,docsite build
。java
docsite init
須要實現項目的初始化,將內置模板拷貝到當前的工做目錄,並安裝好相關的依賴。docsite start
須要實現一個本地的開發環境,在相關代碼、markdown文件變化時,可以從新編譯。docsite build
須要實現資源的構建,生成最終可用的代碼。起初,採用的方案是react+hashRouter的純js渲染邏輯。這種的優勢在於簡單,在實際項目開發中docsite和站點項目的交互簡單。但缺點也很明顯,hashRouter是經過hash值來區分不一樣的頁面的,Google搜索引擎對於#
後面的標記是會忽略的,即便採用hashBang(#!
開頭的hash路由),Google爬蟲可以識別這種標記。好比www.example.com/ajax.html#!key=value
這樣的一個地址,谷歌爬蟲將其識別爲www.example.com/ajax.html?_escaped_fragment_=key=value
。但要想爬蟲收錄該地址,服務端必須爲後者的URL形式返回一份具體的內容,而對於無後端的靜態站點來講,顯然是不現實的。node
那browserRouter可不能夠呢?browserRouter的url形式和普通的url形式同樣,惟一須要解決的是url變化後刷新頁面時的404問題。目前主流的靜態託管都提供了自定義404頁面的功能,即在訪問站點的某個地址出現404響應碼時,可以以自定義的404頁面做爲響應返回給客戶端。react
彷佛看到了一線生機,然而,現實是殘酷的。雖然利用這一機制可以實現頁面刷新時的空白問題,可是404響應碼對於搜索引擎而言並不友好,直接影響頁面的收錄。webpack
那麼,前端路由這條路是走不通了,只能走多頁的形式。除此之外,靜態站點大部分託管在github pages上。目前,國內訪問速度仍是比較慢的,純js渲染的站點,須要先加載完js資源後,再進行頁面的渲染。在加載js的過程當中,整個頁面是一片空白,影響使用體驗。另外,爲了讓其餘人更方便的尋找到你的站點,對SEO的支持就顯得尤其重要。而國內的搜索引擎百度對js渲染的內容的抓取能力簡直就是弱雞。考慮到國內大多數的開發者並無法順暢地使用Google搜索引擎,對於百度搜索引擎的支持就顯得十分必要。git
react有一系列的優點:es6
但爲了實現SEO和減小白屏時間,就這麼不甘心地放棄React帶來的這些便利性嗎?
爲了解決上述問題,同時還能使用React,只好搬出最後一件利器了,ReactDOMServer.render
,借用服務端渲染的概念,在生成最終的多頁中插入渲染出的html字符串,同時保留js文件的引入,從而實現原有的一些交互邏輯。爲實現html的生成,咱們須要藉助模板引擎,本項目中採用了ejs。
肯定好技術方案後,首先須要規劃下站點的目錄結構。採用ES6+React的技術方案,同時須要支持SEO和國際化,最終肯定下來的模板目錄結構以下:
.
├── .babelrc
├── .docsite
├── .eslintrc
├── .gitignore
├── README.md
├── blog
│ ├── en-us
│ └── zh-cn
├── docs
│ ├── en-us
│ └── zh-cn
├── gulpfile.js
├── img
├── package-lock.json
├── package.json
├── redirect.ejs
├── site_config
│ ├── blog.js
│ ├── community.jsx
│ ├── docs.js
│ ├── home.jsx
│ └── site.js
├── src
│ ├── components
│ ├── markdown.scss
│ ├── pages
│ │ ├── blog
│ │ ├── blogDetail
│ │ ├── community
│ │ ├── documentation
│ │ └── home
│ ├── reset.scss
│ └── variables.scss
├── template.ejs
├── utils
│ └── index.js
└── webpack.config.js
複製代碼
現從上至下對主要的文件、文件夾做說明。
.docsite
空文件,用做判斷當前項目是否已初始化過。
template.ejs
全部生成的html頁面的模板,修改對全部頁面(除重定向頁面)生效。
redirect.ejs
重定向頁面模板,可在其中配置重定向邏輯。默認會根據這個模板在項目根目錄下生成index.html
和404.html
(用於某些靜態託管站點的自定義404頁面的功能)。
blog
存放博客的markdown文檔及相關圖片資源的目錄,分爲中、英文兩個目錄。
docs
存放說明文檔的markdown文檔及相關圖片資源的目錄,分爲中、英文兩個目錄。
img
存放非markdown使用的一些站點的圖片,其中system中存放一些業務無關的圖片。
site_config
存放整個站點的中英文配置數據,其中site.js
配置全局的一些數據,其他的文件用於對應pages
目錄下不一樣頁面的語言包配置。
src
存放源碼的位置,其中,markdown.scss
爲markdown文檔的樣式文件,variable.scss
爲一些公共scss變量,components
爲公共組件,pages
爲對應站點的不一樣頁面,utils中
存放一些公共方法。
國際化分爲兩部分,分別爲markdown文檔的國際化和站點其他部分的國際化。
markdown文檔主要分爲說明文檔和博客文檔,按照不一樣的語言版本分別放入zh-cn
和en-us
目錄。
經過在site_config
目錄中配置不一樣頁面對應的語言包,根據不一樣的語言版本去讀取不一樣的語言文案,從而實現國際化。
webpack對jsx、scss代碼改動的監聽佔用一個進程。那麼markdown文件和ejs模板的改動該如何處理呢,開啓另外一個獨立的進程?不須要,NodeJS能夠開啓子進程,在該進程中實現對markdown文檔和模板的監聽。那麼文件監聽如何實現呢?
其實Node.js 標準庫中提供 fs.watch 和 fs.watchFile 兩個方法用於處理文件監控。可是fs.watch 和 fs.watchFile 存在如下問題:
rename
爲此,須要一款專門用於文件監控的庫來彌補這些缺點,而chokidar就是完成這項任務不二人選。其使用方法很簡單。咱們只須要監聽文件的添加、修改、刪除就能夠了。
const watcher = chokidar.watch('file, dir, glob, or array', {
ignored: /(^|[\/\\])\../,
persistent: true
});
watcher
.on('add', path => log(`File ${path} has been added`))
.on('change', path => log(`File ${path} has been changed`))
.on('unlink', path => log(`File ${path} has been removed`));
複製代碼
在文件添加、修改、刪除時,執行對應的命令就能夠了。
對於markdown文件,除了基本的語法,咱們還但願可以放置一些額外數據,用來描述markdown文件的內容,好比title
,keywords
,description
等,在生成html頁面時,能夠將這些數據注入其中,利於搜索引擎收錄頁面。爲此,咱們須要作些約定。
markdown文檔的頂部---
(至少三個-
)之間的數據會被認爲是元數據,一個key佔用一行,其基本形式以下:
---
title: demo title
keywords: keywords1,keywords2,keywords3
description: some description
---
複製代碼
經過簡單的字符串匹配,咱們就可以輕鬆地獲取到這些元數據。
在獲取到markdown的內容後,如何將markdown語法轉換爲html字符串呢?這下輪到markdown-it
登場了。它是目前擴展性和活躍度最好的markdown parser了。使用方法也很簡單:
const Mkit = require('markdown-it');
const hljs = require('highlight.js'); // 用於實現代碼高亮
const md = new Mkit({
html: true,
linkify: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value;
} catch(err) {
console.log(err)
}
}
return ''; // use external default escaping
}
})
.use(plugin1)
.use(plugin2);
複製代碼
若是基本語法的解析不知足要求,還可使用生態中的插件,插件名以markdown-it-
開頭,進一步完善markdown-it
的功能。
最終,一份markdown文件會被解析成一個json文件,好比/blog/zh-cn/demo.md
文檔中內容以下:
---
title: demo title
keywords: keywords1,keywords2,keywords3
description: some description
---
## the title
複製代碼
那麼通過解析後,則會在/zh-cn/blog/
下生成一個demo.json
文件,內容以下:
{
"title": "demo title",
"keywords": "keywords1,keywords2,keywords3",
"description": "some description",
"__html": "<h2>the title</h2>",
"filename": "demo.md",
}
複製代碼
通過markdown解析後的html字符串,默認帶有一些class。接下來就是爲這些class指定樣式了,其實這些前人早就爲咱們作好了。github.com/sindresorhu…提供了github風格的展現效果。另外,對於代碼高亮,highlightjs.org/static/demo…有多種豐富的配色供咱們選擇。
前面提到過,爲使用react,同時又要支持SEO,須要將react代碼轉換成html字符串。藉助於react-dom/server
提供的服務端渲染功能,咱們可以輕鬆地實現react到html的轉換,可是有一些事項須要注意。
在前端代碼中,咱們使用了大量的ES6/7語法,jsx語法,css資源,圖片資源,最終經過webpack配合各類loader打包成一個文件最後運行在瀏覽器環境中。可是在nodejs環境下,不支持import、jsx這種語法,而且沒法識別對css、image資源後綴的模塊引用,那麼要怎麼處理這些靜態資源呢?咱們須要藉助相關的工具、插件來使得Node.js解析器可以加載並執行這類代碼。爲此,須要做以下環境配置。
// Provide custom regenerator runtime and core-js
require('babel-polyfill');
// Javascript required hook
require('babel-register')({
extensions: ['.es6', '.es', '.jsx', '.js'],
presets: ['es2015', 'react', 'stage-0'],
plugins: ['transform-decorators-legacy'],
});
// Css required hook
require('css-modules-require-hook')({
extensions: ['.scss', '.css'],
preprocessCss: (data, filename) =>
require('node-sass').renderSync({
data,
file: filename
}).css,
camelCase: true,
generateScopedName: '[name]__[local]__[hash:base64:8]'
});
// Image required hook
require('asset-require-hook')({
extensions: ['jpeg', 'jpg', 'png', 'gif', 'webp'],
limit: 8000
});
複製代碼
代碼中會使用一些瀏覽器環境下獨有的對象,這樣在node環境中,就須要模擬下瀏覽器中的這些對象,不然就會報錯。固然jsdom
就是爲此而生的,其使用方法以下:
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
const dom = new JSDOM('<!doctype html><html><body><head><link/><style></style><script></script></head><script></script></body></html>');
const {window} = dom;
const copyProps = (src, target) => {
const props = Object.getOwnPropertyNames(src)
.filter(prop => typeof target[prop] === 'undefined')
.map(prop => Object.getOwnPropertyDescriptor(src, prop));
Object.defineProperties(target, props);
}
global.window = window;
global.document = window.document;
global.HTMLElement=window.HTMLElement;
global.navigator = {
userAgent: 'node.js',
};
copyProps(window, global);
複製代碼
將window下的全部對象所有複製到node環境下的global對象,從而實如今node環境下對瀏覽器環境的模擬。
在constructor
、componentWillMount
、render
等服務端渲染會調用的生命週期方法中,不要出現未定義的或者沒法識別的變量和方法,包括其依賴的組件,不然會出現錯誤。
每個獨立的頁面都須要生成一份html文件,所以,咱們須要一款模板引擎。docsite採用了ejs做爲模板引擎進行渲染。這個模板的內容以下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="keywords" content="<%= keywords %>" />
<meta name="description" content="<%= description %>" />
<!-- 網頁標籤標題 -->
<title><%= title %></title>
<link rel="shortcut icon" href="<%= rootPath %>/img/docsite.ico"/>
<link rel="stylesheet" href="<%= rootPath %>/build/<%= page %>.css" />
</head>
<body>
<div id="root"><%- __html %></div>
<script src="https://f.alicdn.com/react/15.4.1/react-with-addons.min.js"></script>
<script src="https://f.alicdn.com/react/15.4.1/react-dom.min.js"></script>
<script> window.rootPath = '<%= rootPath %>'; </script>
<script src="<%= rootPath %>/build/<%= page %>.js"></script>
</body>
</html>
複製代碼
docsite在構建過程當中,會向其中注入一些變量。其中keywords
、description
、title
是在markdown文件中定義的元數據。rootPath
是站點的根路徑,這個在後面會有具體描述。page
就是對應不一樣頁面的資源,其命名同pages
目錄下的一級文件夾的名稱。__html
爲注入的html字符串,包括react轉換而來的和markdown轉換而來的。
markdown文件對應的html頁面,包括頁面組件的內容和markdown文件轉換成的html字符串。頁面組件優先獲取從props注入的html字符串(由docsite在構建時注入,構建出具體的html文件)。同時,爲保證不一樣markdown文件公用一個react頁面組件,在實際的瀏覽器環境中,經過請求工具加載構建生成的json文件,從而獲取到markdown文件對應的html字符串。
直接經過ReactDOMServer.render渲染出來,生成文件便可。
爲每一個頁面,包括markdown文件均生成一份html,不只解決了搜索引擎收錄頁面的問題,並且不須要加載完js文件就能夠展示頁面,一舉解決了js文件加載慢致使的長時間白屏問題。
因爲整個站點支持國際化,因此對於每一個可訪問路徑,都須要以/zh-cn
或/en-us
開頭,爲此,全部可訪問的頁面對應的html文件均在這兩個文件夾下。
當站點部署在一些靜態託管站點時,其根路徑並非/
。好比github pages,其根路徑通常爲/repertory_name/
,若是須要部署到多個平臺,那麼修改資源的訪問地址將是個噩夢。爲此,docsite將根路徑抽取出來,放置在site_config/site.js
中的rootPath
字段進行配置,配置規則以下:
/
,則設置爲''
空字符串便可。/
,則設置爲具體的根路徑,注意需以/
開頭,但不能有尾/
。站點內的引用地址均以/
開頭,在最終的處理中,和模板中全局注入的window.rootPath
進行拼接,從而獲得最終的訪問地址。
有時,一個markdown文件須要引用另外一個markdown文件,若是讓用戶去指定在站點上線後的實際線上地址,顯然是不現實的。可能更習慣的方式是直接按照文件間的相對目錄關係進行指定。這些路徑的轉換不須要在markdown轉換成html字符串中進行。markdown文件路徑和頁面路徑有以下的對應關係:
/docs/zh-cn/dir/demo.md
<=> /zh-cn/docs/dir/demo.html
所以,很容易根據這一轉換規則推斷出markdown文件對應的實際訪問路徑。再結合rootPath
,最終獲取到實際的頁面訪問地址。
一方面,當分享給別人站點地址的時候,可能須要作一次語言版本的跳轉,好比從https://txd-team.github.io/docsite-doc-v1/
跳轉到https://txd-team.github.io/docsite-doc-v1/zh-cn/
。又或者用戶訪問站點的時候,訪問了站點內不存在的一個頁面,這時就須要一個404.html
頁面來進行重定向到正常的頁面。
docsite默認會在項目根目錄下根據模板redirect.ejs
生成index.html
和404.html
(用於某些靜態站點託管平臺自定義404頁面的功能)。redirect.ejs
中配置了訪問到根目錄時的跳轉邏輯。 以下所示:
<script>
window.rootPath = '<%= rootPath %>';
window.defaultLanguage = '<%= defaultLanguage %>';
var lang = Cookies.get('docsite_language');
if (!lang) {
lang = '<%= defaultLanguage %>';
}
window.location = window.rootPath + '/' + lang + '/docs/installation.html';
</script>
複製代碼
docsite內置模板默認包含首頁、文檔頁、博客列表頁、博客詳情頁、社區頁,分別對應src/pages
目錄下的home
、documentation
、blog
、blogDetail
、community
。對於js和css資源,docsite在構建時,會將src/pages
目錄下的文件夾名稱做爲js和css資源的名稱,在build
目錄中生成對應的js和css文件,並經過ejs生成html頁面時注入到頁面中去。
目前,docsite已發佈正式版本,服務了部門多個開源站點的搭建,收到了良好的反饋。歡迎有建站需求的朋友使用,說明文檔詳見 txd-team.github.io/docsite-doc…。
歡迎關注阿里巴巴 TXD 團隊微信公衆號喲,更多內容(mei zi)等你來撩~