使用 React 和 Webpack 构建静态网站

nf83 9年前

使用 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。

提交 5

在相同的页面之上

现在我可以回头再去创建一个可以重复使用的页面元素来拉取 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, 以页面那样相同的方式来列出所有有帖子,不过还需要一些额外数据,比如对应的 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 访问它。

Commit 8

结论

我认为这是一个成功。尽管我还需要整理一下我的样式策略,而且还有一堆功能需要实现。但我最初的目标已经达到。React,React Router 和 Webpack 已经成为一个创建静态网站的组合工具。我会跟进另一个提交,在这个方向上微调,并将它迁移到 bradenver.com。任何反馈都是受欢迎的,所以请使用下面的评论或者 GitHub 的 issue 向我反馈。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们