使用 React 和 Webpack 構建靜態網站
使用 React 和 Webpack 構建靜態網站
我使用(包含了 react-router 的)React 和 Webpack 構建了一個全靜態網站。你可以在這兒的 GitHub 上看到這個 Demo,或者繼續文章接下來的部分,描述我在這次體驗過程中所經歷步驟。本文展現了基本的概念,后續會有一篇文章涵蓋到將此運用于生產環境,我們需要作出那些調整。
為什么
我的博客目前用的是 Jekyll。這是構建一個靜態網站的很棒的方式,但如今我已經想要將其從 Jekyll 移植到某些更加熟悉的東西上面。除了博客以外,我并沒有將 Jekyll 用在其它的地方,所以每次我回頭去維護它,都會有一個小小的學習曲線。我并不覺得有必要加入 WordPress,而 Javascript 則是我心怡的所在,所以一些類型的自定義的節點設置或許更像是能勝任我的需要。
選擇
盡管我縮窄了對 JavaScript 的研究范圍,但還是需要對許多的東西做出選擇。我喜歡 @code_barbarian 所采用的簡單方式,這兒提到的一個文章列表中是渲染 jade 模板。不過我并不經常使用 jade。我也幾次關注過 Harp,但它從沒讓我看上它。Remy Sharp 寫了一篇有關于使用 Ghost或者 Harp 的有趣文章。我研究過 morpheus , 有次我需要用到它。它看起來非常有趣,但我也沒有看上它,因為我不想在服務器上運行任何東西。而就在這個星期我讀了展現最過于工程師文化的博客,對此我一直保持中關注。
明顯的選擇
對我而言其實有一個很明顯的選擇。最近我真的跟 React 走得很近。我喜歡它,并且還沒有遇到任何重大的障礙。我完全沉迷于由 Webpack 和 Dan Abramovs 的 React Hot Loader 插件所提供的熱模塊加載功能。我也由衷地對于探討將 css 移到 JavaScript (或者說 JSS)中去很感興趣。
最終就敲定了它。React 擁有 renderToStaticMarkup 方法。我可以將整個站點作為一個 React 應用來開發,并將結果渲染成為靜態的html。
需要
是時候預先準備好一些東西了。
-
簡單。一個主要的目標就是替換非常好用的 Jekyll。我已經準備好為了一開始能順順利利再忙乎一陣子,因為這是一次嘗試,但是未來繼續使用起來必須簡單容易。
-
靈活性。解決方案不能限制我使用博客(例如,url 格式,內容)。
-
愉快的構建體驗。我的博客主要是由我自己來為我自己寫的。我應該在構建,以及使用它的過程中感到愉悅。開發過程必須簡單,但仍然有實用的功能 (熱模塊加載我看好你喲)。
-
全靜態。構建步驟產生的結果必須全靜態的,不用再服務端或者客戶端再做進一步的渲染。這是一個簡單的博客,每一個路由我都想它們是靜態的 html 文件,因而可以被托管到任何地方。
選擇方法
-
來自 React 組件的一個單頁面應用
-
用來處理站點所有請求路由的 React Router
-
由 Webpack 進行編譯
-
所有的頁面和文章被列在一個帶有它們的元數據的 JavaScript 對象之中
開始
是時候將這些想法付諸實踐了。一開始,我只是想創建一個基礎的主頁。對于那些在家玩這個的人,這里有第一次提交的代碼。
elements/Layout.jsx 是我的起點。這是一個基礎的 React 組件,能渲染出一個完整的 htm l頁面。盡管在 React 中渲染整個全部的頁面的過程中有一些注意事項,但這不過是第一次渲染,所以對服務端而言還好(見這里的討論)。
那么接下來我就需要一個腳本來渲染頁面并提供頁面服務。我使用的是 WebpackDevServer,用這個的話我就可以利用熱模塊加載的好處. 我創建了 webpack.config.js 將 jsx 文件傳入 jsx-loader 和 react-hot-loader 轉換器,并將 webpack 指向 dev/entry.jsx,作為它將會構建的程序包的入口點。dev/entry.jsx 簡單的渲染了一個布局組件。server.js 使用了 React.renderToString 來將創建布局組件過程的結果寫到 dev/index.html 的文件中,然后在 localhost 的 3000 端口啟動WebpackDevServer,提供那個文件的頁面服務,并處理動態的更新。
因此現在如果運行 npm start,將會發生下面的事情:
-
一個 elements/Layout.jsx 組件會被渲染成一個字符串,并被保存到 dev/index.html
-
Webpack 將從 dev/entry.jsx 起點創建 dev/bundle.js
-
Webpack 在端口 3000 上啟動一個 express 服務器
-
Webpack 設置好了熱模塊替換,因此不需要頁面重新被載入,我所作出的任何修改就都能被更新
另外的頁面
起步階段,這是一個好的開發環境,而現在也是時候使其在多頁面上起作用了.
再次我準備設置一些基礎的 css 樣式。我會解釋在這里做了什么,但這種方式稍后會在準備放到生產環境之前被改變。首先我會更新 webpack.config.js,通過 css-loader 和 style-loader 來講 css 文件傳入。然后我可以在 entry.jsx 引入 elements/style.css 和 /bower_components/pure/pure.css,以通過 Webpack 將它們注入頁面中。
我用來從單個頁面創建整個網站的策略,就是使用 React Router 解決問題。 首先是創建 elements/Routes.jsx 作為主路由。它使用 elements/Layout.jsx 作為頂級的處理器讓 home 和 about 路由指向作為新的處理器的 elements/Home.jsx 和 elements/About.jsx. 在這個階段,新的元素之渲染簡單的頭部,但足以看到路由起作用了。
elements/Layout.jsx 會獲得更新,這樣就可以去渲染一個新的 elements/LayoutNav.jsx 元素,這樣我們就可以在home和about頁面之間移動了 (在 react-routers Link 元素的幫助之下)。它也會渲染 react-routers 的 RouteHandler 元素,把它作為主要的內容,可以是 elements/Home.jsx 或者 elements/About.jsx。
我也更新了 dev/entry.jsx 來運行 elements/Routes.jsx 路由 (作為路由傳入當前的歷史位置) ,并對從它那里,而不是從 renderingelements/Layout.jsx 返回的處理器。同樣的,我也更新了 server.js , 編寫處理器的代碼,將當前路由設置到的“/”, 從 elements/Routes.jsx 導向 dev/index.html.
現在運行npm start,就可以提供多路由的單頁應用服務了。
小修正
到現在為止,你只可以通過 localhost:3000 來訪問。由于 webpacks express 服務不知道其他路由的存在,因此如果你通過 ocalhost:3000/about 來加載會訪問失敗。不過我們可以通過在 server.js 文件中應用下面的修改來快速修復。
server.use('/', function(req, res) { Router.run(Routes, req.path, function (Handler){ res.send( React.renderToString(React.createElement(Handler, null))); }); });
動態頁面標題
我們可以在兩個頁面之間切換,這非常好,但是頁面的標題始終沒有改變。我們需要一種方法來存儲每個頁面的獨有的數據并動態的展示它。這就是 paths.js 解決的問題。它包含一個擁有每個路由的鍵和一些輔助方法(用于訪問這個對象中的數據,目前只有 titleForPath() 這個方法)的對象。更改elements/Layout.jsx,使其根據 react-router 的 Router.State 混入查找每個路由的標題。
對渲染的重新思考
elements/About.jsx 和 elements/Home.jsx 組件都相當垃圾,而我喜歡頁面內容已經作為 html 而存在,不用在動手修改時再把它作為組件來重新編寫。為此我想要創建 Page.jsx ,它能從 paths.js 所列出的 html 中拉取內容. 那樣做足夠簡單,但是我如何用某種方式加載能在 node 和瀏覽器上都能用的內容呢? 我可以使用 Webpack 加載器,比如 raw-loader 和 html-loader,但是他們有一個問題。他們在 Webpack 的環境中能運行的很好,但在他之外的地方就不怎么行了。當前我用在服務端渲染的最開始的辦法就不再有用了。是時候重新考慮了。
雙 Webpack 配置
解決的方案相當簡單,并且能讓 server.js 干凈許多。首先我更新了 webpack.config.js,使其返回兩個配置的數組。第一個叫做 browser 的沒有變化,所以仍然指向 dev/entry.jsx 入口端點而被編譯成 dev/bundle.js。第二個叫做 server,被指向一個新的入口端點 dev/page.jsx 并被編譯成 dev/bundlePage.js。從 dev/page.jsx 導出的函數返回對應于給定請求的 html,可以從 node 處被調用到. server.js 不在需要知道有關我的組件的任何信息(或者 React 也是如此) 并且可以使用 dev/page.jsx 獲取它所需要的任何路徑的 html。
在相同的頁面之上
現在我可以回頭再去創建一個可以重復使用的頁面元素來拉取 html 內容。我移除了 elements/Home.jsx 和 elements/About.jsx,并使用 elements/Page.jsx 替換他們(也要更新 elements/Routes.jsx)。這個新的組件再次使用了 react-routersRouter.State 的混合去通過 paths.js 拉取到頁面的標題(頭)以及 html 的內容。使用 dangerouslySetInnerHTML 方法,html 被插入到了頁面中。注意 path.js 的 pageForPath() 方法使用了 require.context('./pages', false, /^\.\/.*\.html$/);因此 Webpack 現在要在 pages/ 目錄對 html 文件進行轉換。
這樣我就可以輕松的添加任何額外的頁面,而不必創建新的組件。
有博客沒帖子可不行
頁面都好了,現在是時候加入帖子了,還要有一個顯示所有帖子清單的博客索引。
我首先做的事情就是創建一個 elements/PathsMixin.js,以使得從 paths.js 訪問數據變更更加容易,并且去掉需要重復編寫的邏輯。這依賴于 Router.State 的 getCurrentPathname 和getCurrentParams,因此要在 contextTypes 屬性里面把它們設置為必需的 (意思就是如果你嘗試使用沒有 Router.State 的 PathsMinixin,React 就會拋出錯誤)。這很酷,因為在組件中使用這個,像 var title = paths.titleForPath(this.getPathname());現在就可以寫成 var title = this.getPathMeta('title’);了,在組件中使用 . Mixin 更棒。
現在需要更新 paths.js, 以頁面那樣相同的方式來列出所有有帖子,不過還需要一些額外數據,比如對應的 Markdown 格式的 .md 文件、是否發布及預覽等。另外,添加了一個新的方法 postforPath, 它利用了 require context 里信息來加載并轉換 posts/ 目錄下的所有 markdown 文件。
elements/Post.js 基本類似于 elements/Page.jsx,只不過它顯示將帖子的 markdown 轉換后的內容,同時顯示發布的時間。顯示時間的部分,是個好機會來創建一個可重用的組件elements/Moment.jsx,專門格式化和顯示時間。
elements/Blog.jsx 是一個自定義的組件,從 elements/PathsMixin.js 獲得數據后循環每一個帖子來創建列表。沒有什么令人興奮的東西(請無視我的實現方式),但卻顯示了如何快速添加一個新的功能。elements/Routes.jsx 和 elements/LayoutNav.jsx 中博客和帖子的路由列表也被更新了。
為生產環境構建
到現在為止,index.html 自從初始化時由dev/bundle.js 加載之后, 一直使用 React 來渲染頁面上的改動。在生產環境中我想要網站更加靜態,這將由 build.js 來完成。訪問靜態博客所用到的每一個文件都會被放到 public/ 目錄下。build.js 的實現很簡單,首先拷貝所有樣式表(style sheets)到路徑public/assets/ 下,然后循環地為 paths.js 中指定的每一個 page 和 post 生成出一個 html 文件。
因為我不想在生產環境下使用 React 來更新我創建的頁面內容,也就是第三個訪問入口 dev/staticPage.jsx,它調用了 React.renderToStaticMarkup 而非 React.renderToString。雖然知道會有方法來 將新的入口點添加 到 webpack.config.js 中為 node 而設的 “server”配置項下, 但我不清楚該怎么弄。 于是就添加一個新的名為“static” 配置項, 用于將dev/staticPage.jsx 編譯成 dev/bundleStaticPage.js。 如果有誰知道不用添加這第三個 “static” 配置項而能生成出dev/bundleStaticPage.js 的方法, 請不吝指教。 非常感謝 Eric Eldredge 提交的 pull request,webpack.config.js 現在就只需要兩個配置項了。我犯的錯誤是在該使用對象地方卻用了數組來定義入口點(entry points) ; 如果使用對象,就可以使用該對象的鍵作為變量值[name]來配置輸出文件名。
為了方便我創建了 publicServer.js,它是為 public/ 目錄做的一個簡單快速服務。現在我可以使用 npm run-script build-static 命令將產品的版本信息創建到 public/,并且可以 localhost:4000 訪問它。
結論
我認為這是一個成功。盡管我還需要整理一下我的樣式策略,而且還有一堆功能需要實現。但我最初的目標已經達到。React,React Router 和 Webpack 已經成為一個創建靜態網站的組合工具。我會跟進另一個提交,在這個方向上微調,并將它遷移到 bradenver.com。任何反饋都是受歡迎的,所以請使用下面的評論或者 GitHub 的 issue 向我反饋。
我們的翻譯工作遵照 CC 協議,如果我們的工作有侵犯到您的權益,請及時聯系我們