加载中

I built a doubly static site using React (inc react-router) and Webpack. You can see the current demo here on GitHub  or continue reading the following post that explains the steps I took  during this experiment. This post proves the basic concept and there  will be a followup post covering the fine tuning needed to put this into  production.

Why

My blog currently uses Jekyll. Its a great way to build a static site  but for a while now I have been wanting to migrate off Jekyll onto  something more familiar. I don’t use Jekyll for anything other than my  blog so each time I go back to it there is a small learning curve. I  don’t feel the need to join the WordPress cult and Javascript is where  my heart is so some sort of custom node setup was the likely winner.

我使用(包含了 react-router 的)React 和 Webpack 构建了一个全静态网站。你可以在这儿的 GitHub  上看到这个 Demo,或者继续文章接下来的部分,描述我在这次体验过程中所经历步骤。本文展现了基本的概念,后续会有一篇文章涵盖到将此运用于生产环境,我们需要作出那些调整。

为什么

我的博客目前用的是 Jekyll。这是构建一个静态网站的很棒的方式,但如今我已经想要将其从 Jekyll 移植到某些更加熟悉的东西上面。除了博客以外,我并没有将 Jekyll 用在其它的地方,所以每次我回头去维护它,都会有一个小小的学习曲线。我并不觉得有必要加入 WordPress,而 Javascript 则是我心怡的所在,所以一些类型的自定义的节点设置或许更像是能胜任我的需要。

Options

Having narrowed down my search to Javascript there were still plenty of options available. I like the simple approach @code_barbarian took, rendering jade templates from a list of posts as mentioned here. Again though I don’t use jade that often. I have also looked at Harp several times but it never quite got me hooked. Remy Sharp has an interesting article on using Ghost or Harp. I also discovered morpheus  while I was working on this. It looks very interesting but I have ruled  it out as I don’t want to run anything on a server. And just this week I  read Presenting The Most Over Engineered Blog Ever which I’ll be keeping an interested eye on.

Obvious Choice

For me there was an obvious alternative. Recently I have really been  getting into React. I love it and have yet to encounter any major  hurdles. I am totally addicted to Hot Module Reloading as provided by  Webpack and Dan Abramovs React Hot Loader plugin. I am also really interested in exploring the idea of moving css into javascript (or JSS).

Eventually it clicked. React has the renderToStaticMarkup method. I could develop the entire site as a React app and render the result to static html.

选择

尽管我缩窄了对 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。

Requirements

Time to set myself some requirements then.

  • Simple. One of the main goals is to replace Jekyll which is very  simple to use. I am prepared to play around a bit to get the initial  setup right as this is an experiment but ongoing use must be easy.

  • Flexible. The solution must not restrict what I can do with my blog (e.g. url formats, content).

  • A pleasurable build experience. Primarily my blog is written for me  by me. I should enjoy building it as much as using it. Development must  be simple but still have features that help (hot module reloading I’m  looking at you).

  • Doubly Static. The end result of the build step must be doubly  static meaning no further rendering on the server or the client. This is  a simple blog and I want static html files for each route that could be  served from anywhere.

Chosen Approach

  • A single page app built from React components

  • React Router to handle all possible routes for the site

  • Compiled by Webpack

  • All pages and posts listed in a javascript object along with their meta data

需要

是时候预先准备好一些东西了。

  • 简单。一个主要的目标就是替换非常好用的 Jekyll。我已经准备好为了一开始能顺顺利利再忙乎一阵子,因为这是一次尝试,但是未来继续使用起来必须简单容易。

  • 灵活性。解决方案不能限制我使用博客(例如,url 格式,内容)。

  •  愉快的构建体验。我的博客主要是由我自己来为我自己写的。我应该在构建,以及使用它的过程中感到愉悦。开发过程必须简单,但仍然有实用的功能 (热模块加载我看好你哟)。

  •  全静态。构建步骤产生的结果必须全静态的,不用再服务端或者客户端再做进一步的渲染。这是一个简单的博客,每一个路由我都想它们是静态的 html 文件,因而可以被托管到任何地方。

选择方法

  •  来自 React 组件的一个单页面应用

  • 用来处理站点所有请求路由的 React Router

  •  由 Webpack 进行编译

  •  所有的页面和文章被列在一个带有它们的元数据的 JavaScript 对象之中

Getting Started

Time to start putting these ideas into practice and to start with I  just want to create and view a basic index page. For those playing along  at home here is the first commit.

elements/Layout.jsx is my starting point. Its a basic  React component that renders a full html page. There are some caveats to  rendering full pages in React but as long as its first rendered server  side its OK (see this discussion).

So next I need a script to render and serve the page. I’m using the  WebpackDevServer for this so I can take advantage of hot module  replacement. I create my webpack.config.js passing jsx files through the jsx-loader and react-hot-loader transforms and pointing webpack to dev/entry.jsx as an entry point for the bundle it will build. dev/entry.jsx simply renders a Layout component. server.js uses React.renderToString to write the result of creating a Layout component to file in dev/index.html and then starts the WebpackDevServer on localhost port 3000 to serve that file and handle live updates.

开始

是时候将这些想法付诸实践了。一开始,我只是想创建一个基础的主页。对于那些在家玩这个的人,这里有第一次提交的代码

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,提供那个文件的页面服务,并处理动态的更新。

So now if run npm start the following will happen:

  • an elements/Layout.jsx component will be rendered to string and saved in dev/index.html

  • Webpack will create dev/bundle.js from the dev/entry.jsx starting point

  • Webpack will start an express server on port 3000

  • Webpack has setup hot module replacement so any changes I make are updated with out the page having to reload

Another Page

Thats a good development environment to start with but now its time to get this working for multiple pages.

At this point I’m going to setup some basic css styling. I’ll explain  what I’ve done but this approach will definitely be changed later on  before this is ready for production. First I update webpack.config.js to pass css files though the css-loader and style-loaders. I can then just require elements/style.css and /bower_components/pure/pure.css in entry.jsx to have them injected into the page by Webpack.

因此现在如果运行 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 将它们注入页面中。

My strategy for creating the entire site from a single page will revolve around React Router. First I create elements/Routes.jsx as my main router. It uses elements/Layout.jsx as the handler at the top level and then has home and about routes pointing to new elements/Home.jsx and elements/About.jsx elements as handlers.  At this stage the new elements only render simple headings but are enough to see the router working.

elements/Layout.jsx gets updated now to render a new elements/LayoutNav.jsx  element so we can move between the home and about pages (with the help  of react-routers Link element). It also renders react-routers  RouteHandler element as its main content which will be either elements/Home.jsx or elements/About.jsx.

I also update dev/entry.jsx to run the elements/Routes.jsx router (passing in the current history location as the route) and rendering the handler return from it instead of rendering elements/Layout.jsx. Similarly I update server.js write the handler resulting from running the elements/Routes.jsx router with the current route set to ‘/‘ into dev/index.html.

So now running npm start will serve our multi route single page app.

Commit 2

我用来从单个页面创建整个网站的策略,就是使用 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,就可以提供多路由的单页应用服务了。

第二次提交

A Little Fix

At this point the app works but only if you load localhost:3000.  Reloading the browser on localhost:3000/about will fail because Webpacks  express server doesn’t know about other routes. This is quickly fixed  with the following update to server.js

server.use('/', function(req, res) {   Router.run(Routes, req.path,  function (Handler) {     res.send(React.renderToString(React.createElement(Handler, null)));   }); });

Commit 3

Dynamic Page Titles

Its all well and good having two pages we can move between but the  html page title doesn’t update. I need a strategy to store data for each  page and  dynamically display it. The key to this is paths.js.  It contains an object with keys for each route and some helper methods  for extracting data from this object (at this point just titleForPath()). elements/Layout.jsx is now updated to lookup the title for the current route which it determines thanks to the react-router Router.State mixin.

Commit 4

小修

到现在为止,你只可以通过 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 混入查找每个路由的标题。

第四次提交

Rendering Rethink

The elements/About.jsx and elements/Home.jsx  components are pretty rubbish and I am likely to already have existing  page contents as html that I would like to reuse without rewriting it as  a component. To do that I want to create Page.jsx that pulls content  from a html file listed in paths.js. Thats easy enough to do but how do I  load the content in a way that works in both node and the browser? I  can use Webpack loaders like raw-loader and html-loader but they present  a problem. They work fine within the Webpack context but they don’t  work outside of it. My current strategy for the intial server side  render would no longer work. Time for a rethink.

Dual Webpack Configs

The solution was actually quite simple and cleaned up server.js a lot. First I updated webpack.config.js to return an array of two configs. The first named browser is unchanged, so still points to the dev/entry.jsx entry point and compiles to dev/bundle.js. The second named server and targeting node points to a new entry point dev/page.jsx and compiles to dev/bundlePage.js. The function exported from dev/page.jsx returns the html for the path of a given request and is callable from node. server.js no longer needs any knowledge of my components (or React for that matter) and can use dev/page.jsx to get the html for any path it needs.

Commit 5

对渲染的重新思考

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。

提交 5

On The Same Page

Now thats sorted I can get back to making a reusable page element to pull in html content. I remove elements/Home.jsx and elements/About.jsx and replace them with elements/Page.jsx (also updating elements/Routes.jsx). This new component again makes use of react-routers Router.State mixin to pull the page title (heading) and html content via paths.js. The html content is inserted into the page using the dangerouslySetInnerHTML method. Note that the pageForPath() method of paths.js uses require.context('./pages', false, /^\.\/.*\.html$/); so that Webpack nows to transform html files in pages/ directory.

At this point I can easily add any additional pages I want without the need to create new components.

Commit 6

Whats A Blog Without Posts

Pages are done so now its time to add posts and a blog index page to list all posts.

The first thing I did was to create elements/PathsMixin.js to make it easier to access data from paths.js and remove the need to repeat logic. It depends on getCurrentPathname and getCurrentParams from Router.State  so it sets them as required in the contextTypes property (meaning React  will throw a warning if you try to use the PathsMinixin without  Router.State). This cool because things like var title = paths.titleForPath(this.getPathname()); can now become var title = this.getPathMeta('title’); within components using this mixin. Mixins for the win.

在相同的页面之上

现在我可以回头再去创建一个可以重复使用的页面元素来拉取 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 文件进行转换。

这样我就可以轻松的添加任何额外的页面,而不必创建新的组件。

提交 6

有博客没帖子可不行

页面都好了,现在是时候加入帖子了,还要有一个显示所有帖子清单的博客索引。

我首先做的事情就是创建一个 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 was updated to list the posts in a similar way  to how it lists pages but with posts having some extra data such as md,  published and preview. It also gains a postforPath method that utilises a new require context to load and transform markdown files is the posts/ directory.

elements/Post.jsx is very similar to elements/Page.jsx  but displays the transformed markdown for a post as well as the date it  was published. The display of the date was a great opportunity to  create a reusable component elements/Moment.jsx to format and render the date. This is where components really shine.

elements/Blog.jsx is a custom component that grabs data via the elements/PathsMixin.js  and loops of each post to create a list. Nothing too exciting (and  please ignore how I have done the styling) but it shows just how quickly  a new feature can be added. elements/Routes.jsx gets updated for the blog and post routes as does elements/LayoutNav.jsx.

Commit 7

现在需要更新 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 中博客和帖子的路由列表也被更新了。

返回顶部
顶部