如何打造一款靜態開源站點搭建工具

image.png | left | 827x362

如何打造一款靜態開源站點搭建工具

本文涉及的全部代碼能夠在 docsite 的開源代碼倉庫 github.com/txd-team/do… 中找到,若是對你有所幫助,歡迎 Star 關注咱們。javascript

背景

諸如github pages的靜態託管服務的興起,靜態生成+託管對託管環境要求低、維護簡單、可配合版本控制,但又靈活多變,這一系列的優勢,使得靜態站點生成器在近年有了極大的發展,涌現出一系列優秀的靜態站點生成器。css

筆者負責整個部門的開源站點搭建,要想提升開發效率,沒有一個稱手的工具是不行的。搭建站點的工具須要知足以下要求:html

  • 簡單易於上手
  • 同時支持PC端和移動端
  • 支持中英文國際化
  • 支持SEO
  • 支持markdown文檔
  • 支持開源站點常見的首頁、文檔頁、博客列表頁、博客詳情頁、社區頁
  • 支持站點的風格的自定義,包括站點主題風格、文檔代碼高亮風格等的自定義
  • 支持自定義頁面

考察了一系列的開源靜態站點搭建工具,總有這樣或者那樣的功能不知足需求,因而就着手打造一款靜態站點搭建工具。因主要用於靜態站點的搭建,且支持markdown文檔,筆者爲該工具起名爲docsite。前端

技術方案選型

docsite工具

從總體上來講,docsite須要可以支持站點項目的初始化、本地開發和本地構建。而對於前端同窗來講,採用NodeJS實現一個命令行工具,不失爲一個有效的方法。爲此,docsite須要對應實現至少三個命令,docsite initdocsite startdocsite buildjava

  • 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

image | left

彷佛看到了一線生機,然而,現實是殘酷的。雖然利用這一機制可以實現頁面刷新時的空白問題,可是404響應碼對於搜索引擎而言並不友好,直接影響頁面的收錄。webpack

那麼,前端路由這條路是走不通了,只能走多頁的形式。除此之外,靜態站點大部分託管在github pages上。目前,國內訪問速度仍是比較慢的,純js渲染的站點,須要先加載完js資源後,再進行頁面的渲染。在加載js的過程當中,整個頁面是一片空白,影響使用體驗。另外,爲了讓其餘人更方便的尋找到你的站點,對SEO的支持就顯得尤其重要。而國內的搜索引擎百度對js渲染的內容的抓取能力簡直就是弱雞。考慮到國內大多數的開發者並無法順暢地使用Google搜索引擎,對於百度搜索引擎的支持就顯得十分必要。git

react有一系列的優點:es6

  • 豐富的生命週期方法
  • 統一的事件綁定
  • 經過操做數據來操做DOM
  • ...

但爲了實現SEO和減小白屏時間,就這麼不甘心地放棄React帶來的這些便利性嗎?

image | left

爲了解決上述問題,同時還能使用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.html404.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文檔的國際化

markdown文檔主要分爲說明文檔和博客文檔,按照不一樣的語言版本分別放入zh-cnen-us目錄。

  • 站點其他部分的國際化

經過在site_config目錄中配置不一樣頁面對應的語言包,根據不一樣的語言版本去讀取不一樣的語言文案,從而實現國際化。

文件變動監聽

webpack對jsx、scss代碼改動的監聽佔用一個進程。那麼markdown文件和ejs模板的改動該如何處理呢,開啓另外一個獨立的進程?不須要,NodeJS能夠開啓子進程,在該進程中實現對markdown文檔和模板的監聽。那麼文件監聽如何實現呢?

其實Node.js 標準庫中提供 fs.watch 和 fs.watchFile 兩個方法用於處理文件監控。可是fs.watch 和 fs.watchFile 存在如下問題:

  • OS X 系統環境不報告文件名變化
  • OS X 系統中使用Sublime等編輯器時,不報告任何事件
  • 常常會報告兩次事件
  • 多數事件通知爲rename
  • 不可以簡單地遞歸監控文件樹
  • 致使高CPU使用率
  • 還有其餘大量的問題

爲此,須要一款專門用於文件監控的庫來彌補這些缺點,而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文件,除了基本的語法,咱們還但願可以放置一些額外數據,用來描述markdown文件的內容,好比titlekeywordsdescription等,在生成html頁面時,能夠將這些數據注入其中,利於搜索引擎收錄頁面。爲此,咱們須要作些約定。

markdown文檔的頂部---(至少三個-)之間的數據會被認爲是元數據,一個key佔用一行,其基本形式以下:

---
title: demo title
keywords: keywords1,keywords2,keywords3
description: some description
---
複製代碼

經過簡單的字符串匹配,咱們就可以輕鬆地獲取到這些元數據。

轉換爲html字符串

在獲取到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文檔顯示樣式及代碼高亮

通過markdown解析後的html字符串,默認帶有一些class。接下來就是爲這些class指定樣式了,其實這些前人早就爲咱們作好了。github.com/sindresorhu…提供了github風格的展現效果。另外,對於代碼高亮,highlightjs.org/static/demo…有多種豐富的配色供咱們選擇。

react轉換爲html

前面提到過,爲使用react,同時又要支持SEO,須要將react代碼轉換成html字符串。藉助於react-dom/server提供的服務端渲染功能,咱們可以輕鬆地實現react到html的轉換,可是有一些事項須要注意。

在前端代碼中,咱們使用了大量的ES6/7語法,jsx語法,css資源,圖片資源,最終經過webpack配合各類loader打包成一個文件最後運行在瀏覽器環境中。可是在nodejs環境下,不支持import、jsx這種語法,而且沒法識別對css、image資源後綴的模塊引用,那麼要怎麼處理這些靜態資源呢?咱們須要藉助相關的工具、插件來使得Node.js解析器可以加載並執行這類代碼。爲此,須要做以下環境配置。

  1. 首先引入babel-polyfill這個庫來提供regenerator運行時和core-js來模擬全功能ES6環境。
  2. 引入babel-register,這是一個require鉤子,會自動對require命令所加載的js文件進行實時轉碼。
  3. 引入css-modules-require-hook,一樣是鉤子,只針對樣式文件。
  4. 引入asset-require-hook,來識別圖片資源,對小於8K的圖片轉換成base64字符串,大於8k的圖片轉換成路徑引用。
// 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環境下對瀏覽器環境的模擬。

其餘

constructorcomponentWillMountrender等服務端渲染會調用的生命週期方法中,不要出現未定義的或者沒法識別的變量和方法,包括其依賴的組件,不然會出現錯誤。

html文件生成

每個獨立的頁面都須要生成一份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在構建過程當中,會向其中注入一些變量。其中keywordsdescriptiontitle是在markdown文件中定義的元數據。rootPath是站點的根路徑,這個在後面會有具體描述。page就是對應不一樣頁面的資源,其命名同pages目錄下的一級文件夾的名稱。__html爲注入的html字符串,包括react轉換而來的和markdown轉換而來的。

__html的注入

  • markdown文件對應的html頁面

markdown文件對應的html頁面,包括頁面組件的內容和markdown文件轉換成的html字符串。頁面組件優先獲取從props注入的html字符串(由docsite在構建時注入,構建出具體的html文件)。同時,爲保證不一樣markdown文件公用一個react頁面組件,在實際的瀏覽器環境中,經過請求工具加載構建生成的json文件,從而獲取到markdown文件對應的html字符串。

  • 其他頁面組件對應的html頁面

直接經過ReactDOMServer.render渲染出來,生成文件便可。

SEO及性能

爲每一個頁面,包括markdown文件均生成一份html,不只解決了搜索引擎收錄頁面的問題,並且不須要加載完js文件就能夠展示頁面,一舉解決了js文件加載慢致使的長時間白屏問題。

路徑處理

路徑規則

因爲整個站點支持國際化,因此對於每一個可訪問路徑,都須要以/zh-cn/en-us開頭,爲此,全部可訪問的頁面對應的html文件均在這兩個文件夾下。

路徑前綴

當站點部署在一些靜態託管站點時,其根路徑並非/。好比github pages,其根路徑通常爲/repertory_name/,若是須要部署到多個平臺,那麼修改資源的訪問地址將是個噩夢。爲此,docsite將根路徑抽取出來,放置在site_config/site.js中的rootPath字段進行配置,配置規則以下:

  • 當部署根路徑爲/,則設置爲''空字符串便可。
  • 當部署根路徑不爲/,則設置爲具體的根路徑,注意需以/開頭,但不能有尾/

站點內的引用地址均以/開頭,在最終的處理中,和模板中全局注入的window.rootPath進行拼接,從而獲得最終的訪問地址。

markdown文件內的相互引用

有時,一個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.html404.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目錄下的homedocumentationblogblogDetailcommunity。對於js和css資源,docsite在構建時,會將src/pages目錄下的文件夾名稱做爲js和css資源的名稱,在build目錄中生成對應的js和css文件,並經過ejs生成html頁面時注入到頁面中去。

結語

目前,docsite已發佈正式版本,服務了部門多個開源站點的搭建,收到了良好的反饋。歡迎有建站需求的朋友使用,說明文檔詳見 txd-team.github.io/docsite-doc…

歡迎關注阿里巴巴 TXD 團隊微信公衆號喲,更多內容(mei zi)等你來撩~

image.png | left | 747x722
相關文章
相關標籤/搜索