[TOC]
## 概況
### 背景: 編輯-發布-開發分離
在這種情形中,編輯能否完成工作就不依賴于網站——脫稿又少了 個借口。這時候網站出錯的概率太小了——你不需要一個緩存服務器、HTTP服務器,由于沒有動態生成的內容,你也不需要守護進程。這些內容都是靜態文件,你可以將他們放在任何可以提供靜態文件托管的地方——CloudFront、S3等等。或者你再相信自己的服務器,Nginx可是全球第二好(第一還沒出現)的靜態文件服務器。
開發人員只在需要的時候去修改網站的一些內容。
So,你可能會擔心如果這時候修改的東西有問題了怎么辦。
1. 使用這種模式就意味著你需要有測試來覆蓋這些構建工具、生成工具。
2. 相比于自己的代碼,別人的CMS更可靠?
需要注意的是如果你上一次構建成功,你生成的文件都是正常的,那么你只需要回滾開發相關的代碼即可。舊的代碼仍然可以工作得很好。
其次,由于生成的是靜態文件,查錯的成本就比較低。
最后,重新放上之前的靜態文件。
> 動態網頁是下一個要解決的難題。我們從數據庫中讀取數據,再用動態去渲染出一個靜態頁面,并且緩存服務器來緩存這個頁面。既然我們都可以用Varnish、Squid這樣的軟件來緩存頁面——表明它們可以是靜態的,為什么不考慮直接使用靜態網頁呢?
為了實現之前說到的`編輯-發布-開發分離`的CMS,我還是花了兩天的時間打造了一個面向普通用戶的編輯器。效果截圖如下所示:

Echeveria Editor
作為一個普通用戶,這是一個很簡單的軟件。除了Electron + Node.js + React作了一個140M左右的軟件,盡管打包完只有40M左右 ,但是還是會把用戶嚇跑的。不過作為一個快速構建的原型已經很不錯了——構建速度很快、并且運行良好。
盡管這個界面看上去還是稍微復雜了一下,還在試著想辦法將鏈接名和日期去掉——問題是為什么會有這兩個東西?
#### 從Schema到數據庫
我們在我們數據庫中定義好了Schema——對一個數據庫的結構描述。在《[編輯-發布-開發分離](https://www.phodal.com/blog/editing-publishing-coding-seperate/)?》一文中我們說到了echeveria-content的一個數據文件如下所示:
~~~
{
"title": "白米粥",
"author": "白米粥",
"url": "baimizhou",
"date": "2015-10-21",
"description": "# Blog post \n > This is an example blog post \n Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
"blogpost": "# Blog post \n > This is an example blog post \n Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \n Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
}
~~~
比起之前的直接生成靜態頁面這里的數據就是更有意思地一步了,我們從數據庫讀取數據就是為了生成一個JSON文件。何不直接以JSON的形式存儲文件呢?
我們都定義了這每篇文章的基本元素:
1. title
2. author
3. date
4. description
5. content
6. url
即使我們使用NoSQL我們也很難逃離這種模式。我們定義這些數據,為了在使用的時候更方便。存儲這些數據只是這個過程中的一部分,下部分就是取出這些數據并對他們進行過濾,取出我們需要的數據。
Web的骨架就是這么簡單,當然APP也是如此。難的地方在于存儲怎樣的數據,返回怎樣的數據。不同的網站存儲著不同的數據,如淘寶存儲的是商品的信息,Google存儲著各種網站的數據——人們需要不同的方式去存儲這些數據,為了更好地存儲衍生了更多的數據存儲方案——于是有了GFS、Haystack等等。運營型網站想盡辦法為最后一公里努力著,成長型的網站一直在想著怎樣更好的返回數據,從更好的用戶體驗到機器學習。而數據則是這個過程中不變的東西。
盡管,我已經想了很多辦法去盡可能減少元素——在最開始的版本里只有標題和內容。然而為了滿足我們在數據庫中定義的結構,不得不造出來這么多對于一般用戶不友好的字段。如鏈接名是為了存儲的文件名而存在的,即這個鏈接名在最后會變成文件名:
~~~
repo.write('master', 'contents/' + data.url + '.json', stringifyData, 'Robot: add article ' + data.title, options, function (err, data) {
if(data.commit){
that.setState({message: "上傳成功" + JSON.stringify(data)});
that.refs.snackbar.show();
that.setState({
sending: 0
});
}
});
~~~
然后,上面的數據就會變成一個對象存儲到“數據庫”中。
今天 ,仍然有很多人用Word、Excel來存儲數據。因為對于他們來說,這些軟件更為直接,他們簡單地操作一下就可以對數據進行排序、篩選。數據以怎樣的形式存儲并不重要,重要的是他們都以文件的形式存儲著。
#### git作為NoSQL數據庫
在控制臺中運行一下?`man git`你會得到下面的結果:

Man Git
這個答案看起來很有意思——不過這看上去似乎無關主題。
不同的數據庫會以不同的形式存儲到文件中去。blob是git中最為基本的存儲單位,我們的每個content都是一個blob。redis可以以rdb文件的形式存儲到文件系統中。完成一個CMS,我們并不需要那么多的查詢功能。
> 這些上千年的組織機構,只想讓人們知道他們想要說的東西。
我們使用NoSQL是因為:
1. 不使用關系模型
2. 在集群中運行良好
3. 開源
4. 無模式
5. 數據交換格式
我想其中只有兩點對于我來說是比較重要的`集群`與`數據格式`。但是集群和數據格式都不是我們要考慮的問題。。。
我們也不存在數據格式的問題、開源的問題,什么問題都沒有。。除了,我們之前說到的查詢——但是這是可以解決的問題,我們甚至可以返回不同的歷史版本的。在這一點上git做得很好,他不會像WordPress那樣存儲多個版本。
#### git + JSON文件
JSON文件 + Nginx就可以變成這樣一個合理的API,甚至是運行方式。我們可以對其進行增、刪、改、查,盡管就當前來說查需要一個額外的軟件來執行,但是為了實現一個用得比較少的功能,而去花費大把的時間可能就是在浪費。
git的“API”提供了豐富的增、刪、改功能——你需要commit就可以了。我們所要做的就是:
1. git commit
2. git push Carrot使用了下面的方案來搭建他們的靜態內容的CMS。

Carrot
在這個方案里內容是用Contentful來發布他們的內容。而在我司[ThoughtWorks](https://www.thoughtworks.com/)的官網里則采用了Github來管理這些內容。于是如果讓我們寫一個基于Github的CMS,那么架構變成了這樣:

Github 編輯-發布-開發
或許你也用過Hexo / Jekyll / Octopress這樣的靜態博客,他們的原理都是類似的。我們有一個代碼庫用于生成靜態頁面,然后這些靜態頁面會被PUSH到Github Pages上。
從我們設計系統的角度來說,我們會在Github上有三個代碼庫:
1. Content。用于存放編輯器生成的JSON文件,這樣我們就可以GET這些資源,并用Backbone / Angular / React 這些前端框架來搭建SPA。
2. Code。開發者在這里存放他們的代碼,如主題、靜態文件生成器、資源文件等等。
3. Builder。在這里它是運行于Travis CI上的一些腳本文件,用于Clone代碼,并執行Code中的腳本。
以及一些額外的服務,當且僅當你有一些額外的功能需求的時候。
1. Extend Service。當我們需要搜索服務時,我們就需要這樣的一些服務。如我正考慮使用Python的whoosh來完成這個功能,這時候我計劃用Flask框架,但是只是計劃中——因為沒有合適的中間件。
2. Editor。相比于前面的那些知識這一步適合更重要,也就是為什么生成的格式是JSON而不是Markdown的原理。對于非程序員來說,要熟練掌握Markdown不是一件容易的事。于是,一個考慮中的方案就是使用 Electron + Node.js來生成API,最后通過GitHub API V3來實現上傳。
So,這一個過程是如何進行的。
### 用戶場景
整個過程的Pipeline如下所示:
1. 編輯使用他們的編輯器來編輯的內容并點擊發布,然后這個內容就可以通過GitHub API上傳到Content這個Repo里。
2. 這時候需要有一個WebHooks監測到了Content代碼庫的變化,便運行Builder這個代碼庫的Travis CI。
3. 這個Builder腳本首先,會設置一些基本的git配置。然后clone Content和Code的代碼,接著運行構建命令,生成新的內容。
4. 然后Builder Commit內容,并PUSH內容。
這里還依賴于WebHook這個東西——還沒想到一個合適的解決方案。下面,我們對里面的內容進行一些拆解,Content里面由于是JSON就不多解釋了。
## 步驟
### Step 1: 構建工具
Github與Travis之間,可以做一個自動部署的工具。相信已經有很多人在Github上玩過這樣的東西——先在Github上生成Token,然后用travis加密:
~~~
travis encrypt-file ssh_key --add
~~~
加密后的Key就會保存到`.travis.yml`文件里,然后就可以在Travis CI上push你的代碼到Github上了。
接著,你需要創建個deploy腳本,并且在`after_success`執行它:
~~~
after_success:
- test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && bash deploy.sh
~~~
在這個腳本里,你所需要做的就是clone content和code中的代碼,并執行code中的生成腳本,生成新的內容后,提交代碼。
~~~
#!/bin/bash
set -o errexit -o nounset
rev=$(git rev-parse --short HEAD)
cd stage/
git init
git config user.name "Robot"
git config user.email "robot@phodal.com"
git remote add upstream "https://$GH_TOKEN@github.com/phodal-archive/echeveria-deploy.git"
git fetch upstream
git reset upstream/gh-pages
git clone https://github.com/phodal-archive/echeveria-deploy code
git clone https://github.com/phodal-archive/echeveria-content content
pwd
cp -a content/contents code/content
cd code
npm install
npm install grunt-cli -g
grunt
mv dest/* ../
cd ../
rm -rf code
rm -rf content
touch .
if [ ! -f CNAME ]; then
echo "deploy.baimizhou.net" > CNAME
fi
git add -A .
git commit -m "rebuild pages at ${rev}"
git push -q upstream HEAD:gh-pages
~~~
這就是這個builder做的事情——其中最主要的一個任務是`grunt`,它所做的就是:
~~~
grunt.registerTask('default', ['clean', 'assemble', 'copy']);
~~~
### Step 2: 靜態頁面生成
Assemble是一個使用Node.js,Grunt.js,Gulp,Yeoman 等來實現的靜態網頁生成系統。這樣的生成器有很多,Zurb Foundation, Zurb Ink, Less.js / lesscss.org, Topcoat, Web Experience Toolkit等組織都使用這個工具來生成。這個工具似乎上個Release在一年多以前,現在正在開始0.6。雖然,這并不重要,但是還是順便一說。
我們所要做的就是在我們的`Gruntfile.js`中寫相應的生成代碼。
~~~
assemble: {
options: {
flatten: true,
partials: ['templates/includes/*.hbs'],
layoutdir: 'templates/layouts',
data: 'content/blogs.json',
layout: 'default.hbs'
},
site: {
files: {'dest/': ['templates/*.hbs']}
},
blogs: {
options: {
flatten: true,
layoutdir: 'templates/layouts',
data: 'content/*.json',
partials: ['templates/includes/*.hbs'],
pages: pages
},
files: [
{ dest: './dest/blog/', src: '!*' }
]
}
}
~~~
配置中的site用于生成頁面相關的內容,blogs則可以根據json文件的文件名生成對就的html文件存儲到blog目錄中。
生成后的目錄結果如下圖所示:
~~~
.
├── about.html
├── blog
│?? ├── blog-posts.html
│?? └── blogs.html
├── blog.html
├── css
│?? ├── images
│?? │?? └── banner.jpg
│?? └── style.css
├── index.html
└── js
├── jquery.min.js
└── script.js
7 directories, 30 files
~~~
這里的靜態文件內容就是最后我們要發布的內容。
還需要做的一件事情就是:
~~~
grunt.registerTask('dev', ['default', 'connect:server', 'watch:site']);
~~~
用于開發階段這樣的代碼就夠了,這個和你使用WebPack + React 似乎相差不了多少。