翻译于 2015/08/09 09:15
3 人 顶 此译文
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.
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 则是我心怡的所在,所以一些类型的自定义的节点设置或许更像是能胜任我的需要。
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.
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。
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.
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 对象之中
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
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.
我用来从单个页面创建整个网站的策略,就是使用 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,就可以提供多路由的单页应用服务了。
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))); }); });
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.
到现在为止,你只可以通过 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 混入查找每个路由的标题。
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.
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.
elements/About.jsx 和 elements/Home.jsx 组件都相当垃圾,而我喜欢页面内容已经作为 html 而存在,不用在动手修改时再把它作为组件来重新编写。为此我想要创建 Page.jsx ,它能从 paths.js 所列出的 html 中拉取内容. 那样做足够简单,但是我如何用某种方式加载能在 node 和浏览器上都能用的内容呢? 我可以使用 Webpack 加载器,比如 raw-loader 和 html-loader,但是他们有一个问题。他们在 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。
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.
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 文件进行转换。
这样我就可以轻松的添加任何额外的页面,而不必创建新的组件。
页面都好了,现在是时候加入帖子了,还要有一个显示所有帖子清单的博客索引。
我首先做的事情就是创建一个 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
.
现在需要更新 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 中博客和帖子的路由列表也被更新了。