在本系列关于 CSS 模块的最后一篇文章中,我将介绍如何借助 Webpack 创建一个静态 React 网站。这个静态网站将有两个模板:一个主页和一个关于页面,以及几个 React 组件来解释它在实践中的工作原理。
文章系列
- 什么是 CSS 模块,为什么我们需要它们?
- CSS 模块入门
- React + CSS 模块 = 😍 (您现在所处位置!)
在上一篇文章中,我们使用 Webpack 设置了一个快速项目,展示了如何将依赖项导入文件,以及如何使用构建过程生成在 CSS 和 HTML 中生成的唯一类名。以下示例在很大程度上依赖于该教程,因此绝对值得首先完成这些之前的示例。此外,本文假设您熟悉 React 的基础知识。
在之前的演示中,当我们结束时,代码库存在一些问题。我们依赖 JavaScript 来渲染标记,并且我们并不完全清楚应该如何构建项目。在这篇文章中,我们将研究一个更真实的示例,在该示例中,我们将尝试使用我们新的 Webpack 知识创建一些组件。
要快速了解,您可以查看我创建的 css-modules-react 存储库,它只是一个演示项目,可以让我们回到上次演示结束的地方。从那里,您可以继续下面的教程。
Webpack 的静态站点生成器
要生成静态标记,我们需要为 Webpack 安装 一个插件,它可以帮助我们生成静态标记。
npm i -D static-site-generator-webpack-plugin
现在我们需要将我们的插件添加到 webpack.config.js
中并添加我们的路由。路由就像主页的 /
或关于页面的 /about
。路由告诉插件要创建哪些静态文件。
var StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin');
var locals = {
routes: [
'/',
]
};
由于我们想要提供静态标记,并且我们希望在此阶段避免服务器端代码,因此我们可以使用我们的 StaticSiteGeneratorPlugin。正如此插件的文档中提到的,它提供
一系列要呈现的路径,并且通过执行您自己的自定义、Webpack 编译的渲染函数,将在您的输出目录中呈现一组匹配的 index.html 文件。
如果这听起来很吓人,别担心!在我们 webpack.config.js
中,我们现在可以更新我们的 module.exports
对象。
module.exports = {
entry: {
'main': './src/',
},
output: {
path: 'build',
filename: 'bundle.js',
libraryTarget: 'umd' // this is super important
},
// ...
}
我们设置 libraryTarget
,因为这是 nodejs 和静态站点插件正常工作的要求。我们还添加了一条路径,以便所有内容都将生成到我们的 /build
目录中。
仍在 webpack.config.js
文件中,我们需要在底部添加 StaticSiteGeneratorPlugin
,如下所示,传入我们想要生成的路由
plugins: [
new ExtractTextPlugin('styles.css'),
new StaticSiteGeneratorPlugin('main', locals.routes),
]
我们完整的 webpack.config.js
现在应该如下所示
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin')
var locals = {
routes: [
'/',
]
}
module.exports = {
entry: './src',
output: {
path: 'build',
filename: 'bundle.js',
libraryTarget: 'umd' // this is super important
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
include: __dirname + '/src',
},
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'),
include: __dirname + '/src'
}
],
},
plugins: [
new StaticSiteGeneratorPlugin('main', locals.routes),
new ExtractTextPlugin("styles.css"),
]
};
在我们空的 src/index.js
文件中,我们可以添加以下内容
// Exported static site renderer:
module.exports = function render(locals, callback) {
callback(null, 'Hello!');
};
现在我们只想将 Hello!
打印到我们网站的主页上。最终,我们将把它发展成一个更真实的网站。
在我们的 package.json
中,我们在之前的教程中讨论过,我们已经有了基本命令 webpack
,我们可以用它来运行
npm start
如果我们查看我们的 build
目录,那么我们应该会找到一个包含我们内容的 index.html
文件。太棒了!我们可以确认静态站点插件正在工作。现在要测试这一切是否有效,我们可以回到我们的 webpack.config.js
中并更新我们的路由
var locals = {
routes: [
'/',
'/about'
]
};
通过重新运行我们的 npm start
命令,我们创建了一个新文件:build/about/index.html
。但是,它将与 build/index.html
一样包含“Hello!”,因为我们向这两个文件发送了相同的内容。要解决此问题,我们需要使用路由器,但首先,我们需要设置 React。
在这样做之前,我们应该将我们的路由移动到一个单独的文件中,以保持整洁。所以在 ./data.js
中,我们可以编写
module.exports = {
routes: [
'/',
'/about'
]
}
然后,我们将在 webpack.config.js
中需要该数据并删除我们的 locals
变量
var data = require('./data.js');
在该文件的下方,我们将更新我们的 StaticSiteGeneratorPlugin
plugins: [
new ExtractTextPlugin('styles.css'),
new StaticSiteGeneratorPlugin('main', data.routes, data),
]
安装 React
我们想制作许多 HTML 和 CSS 的小捆绑包,然后我们可以将其捆绑到一个模板(如关于或主页)中。这可以通过 react
和 react-dom
来完成,我们需要安装它们
npm i -D react react-dom babel-preset-react
然后我们需要更新我们的 .babelrc
文件
{
"presets": ["es2016", "react"]
}
现在在一个新文件夹 /src/templates
中,我们需要创建一个 Main.js
文件。这将是我们所有标记所在的位置,并且将是我们所有模板的共享资产(如 中的所有内容以及我们网站的 <footer>
)所在的位置
import React from 'react'
import Head from '../components/Head'
export default class Main extends React.Component {
render() {
return (
{ /* This is where our content for various pages will go */ }
)
}
}
这里有两点需要注意:首先,如果您不熟悉 React 使用的 JSX 语法,那么了解 body
元素内部的文本是 注释 很有帮助。您可能还注意到那个奇怪的元素——它不是标准的 HTML 元素——它是一个 React 组件,我们在这里所做的是通过它的 title
属性传递数据。虽然它不是属性,但在 React 世界中,它被称为 props。
现在我们需要创建一个 src/components/Head.js
文件。
import React from 'react'
export default class Head extends React.Component {
render() {
return (
)
}
}
我们可以将 Head.js
中的所有代码放入 Main.js
中,但将代码分解成更小的部分很有帮助:如果我们想要一个页脚,我们将使用 src/components/Footer.js
创建一个新组件,然后将其导入到我们的 Main.js
文件中。
现在,在 src/index.js
中,我们可以用新的 React 代码替换所有内容
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import Main from './templates/Main.js'
module.exports = function render(locals, callback) {
var html = ReactDOMServer.renderToStaticMarkup(React.createElement(Main, locals))
callback(null, '' + html)
}
这将从 Main.js
导入我们所有的标记(随后将导入 Head
React 组件),然后它将使用 React DOM 呈现所有这些内容。如果我们再次运行 npm start
并查看此阶段的 build/index.html
,那么我们会发现 React 已添加了我们的 Main.js
React 组件以及 Head 组件,然后将其全部渲染成静态标记。
但该内容仍在为我们的关于页面和主页生成。让我们引入我们的路由器来解决此问题。
设置我们的路由器
我们需要将某些代码片段传递到某些路由:在关于页面上,我们需要关于页面的内容,同样在主页、博客或我们可能希望拥有的任何其他页面上。换句话说,我们需要一些软件来管理内容:路由器。为此,我们可以让 react-router
为我们完成所有繁重的工作。
在我们开始之前,值得注意的是,在本教程中,我们将使用 React Router 的 2.0 版本,并且与之前的版本相比,有一些 变化。
首先我们需要安装它,因为 React Router 默认情况下不会与 React 捆绑在一起,所以我们需要跳到命令行
npm i -D react-router</code>
在 /src
目录中,我们可以创建一个 routes.js
文件并添加以下内容
import React from 'react'
import {Route, Redirect} from 'react-router'
import Main from './templates/Main.js'
import Home from './templates/Home.js'
import About from './templates/About.js'
module.exports = (
// Router code will go here
)
我们想要多个页面:一个用于主页,另一个用于关于页面,因此我们可以快速继续创建一个 src/templates/About.js
文件
import React from 'react'
export default class About extends React.Component {
render() {
return (
<div>
<h1>About page</h1>
<p>This is an about page</p>
</div>
)
}
}
以及一个 src/templates/Home.js
文件
import React from 'react'
export default class Home extends React.Component {
render() {
return (
<div>
<h1>Home page</h1>
<p>This is a home page</p>
</div>
)
}
}
现在我们可以返回到 routes.js
并进入 module.exports
<Route component={Main}>
<Route path='/' component={Home}/>
<Route path='/about' component={About}/>
</Route>
我们的 src/templates/Main.js
文件包含所有周围的标记(如)。然后,Home.js
和 About.js
React 组件可以放置在 Main.js
的元素内部。
接下来我们需要一个 src/router.js
文件。这将有效地替换 src/index.js
,因此您可以继续删除该文件并在 router.js
中编写以下内容
import React from 'react'
import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server'
import {Router, RouterContext, match, createMemoryHistory} from 'react-router'
import Routes from './routes'
import Main from './templates/Main'
module.exports = function(locals, callback){
const history = createMemoryHistory();
const location = history.createLocation(locals.path);
return match({
routes: Routes,
location: location
}, function(error, redirectLocation, renderProps) {
var html = ReactDOMServer.renderToStaticMarkup(
<RouterContext {...renderProps} />
);
return callback(null, html);
})
}
如果您不熟悉这里发生的事情,最好查看 Brad Westfall 的 React Router 入门教程。
因为我们已删除 index.js
文件并将其替换为我们的路由器,所以我们需要返回到我们的 webpack.config.js
并修复 entry
键的值
module.exports = {
entry: './src/router',
// other stuff...
}
最后,我们只需要转到 src/templates/Main.js
export default class Main extends React.Component {
render() {
return (
<html>
<Head title='React and CSS Modules' />
<body>
{this.props.children}
</body>
</html>
)
}
}
{this.props.children}
是所有来自其他模板的代码将被放置的位置。所以现在我们可以再次运行 npm start
,我们应该会看到两个文件正在生成:build/index.html
和 build/about/index.html
,每个文件都有各自的内容。
重新实现 CSS 模块
由于是 CSS 的 Hello World,我们将创建一个 Button 模块。虽然我将坚持使用 Webpack 的 CSS 加载器和我之前教程中使用的内容,但有一些 替代方案。
这是我们希望在此项目中使用的文件结构
/components
/Button
Button.js
styles.css
接下来,我们将把这个自定义的 React 组件导入到我们的其中一个模板中。为此,我们可以创建一个新文件:src/components/Button/Button.js
import React from 'react'
import btn from './styles.css'
export default class CoolButton extends React.Component {
render() {
return (
<button className={btn.red}>{this.props.text}</button>
)
}
}
正如我们在上一篇教程中学到的,{btn.red}
类名会深入到 styles.css
中的 CSS,并找到 .red
类,然后 Webpack 将生成我们乱七八糟的 CSS 模块类名。
现在,我们可以在 src/components/Button/styles.css
中添加一些简单的样式。
.red {
font-size: 25px;
background-color: red;
color: white;
}
最后,我们可以将 Button 组件添加到模板页面中,例如 src/templates/Home.js
。
import React from 'react'
import CoolButton from '../components/Button/Button'
export default class Home extends React.Component {
render() {
return (
<div>
<h1>Home page</h1>
<p>This is a home page</p>
<CoolButton text='A super cool button' />
</div>
)
}
}
再执行一次 npm start
,我们就完成了!一个静态的 React 网站,我们可以快速添加新的模板和组件,并且我们还拥有 CSS Modules 的额外好处,因此我们的类现在看起来像这样。

您可以在 React 和 CSS 模块 仓库中找到上面演示的完整版本。如果您在上面的代码中发现任何错误,请务必提交问题。
当然,我们可以改进这个项目,例如,我们可以 将 Browsersync 添加到我们的 Webpack 工作流程中,这样我们就不用一直 npm install
了。我们还可以添加 Sass、PostCSS 和许多加载器和插件来提供帮助,但为了简洁起见,我决定暂时不将它们包含在项目中。
总结
我们在这里完成了什么?好吧,虽然这看起来像是做了很多工作,但我们现在拥有了一个模块化的代码编写环境。我们可以添加任意数量的组件。
/components
Head.js
/Button
Button.js
styles.css
/Input
Input.js
style.css
/Title
Title.js
style.css
因此,如果我们在标题组件的样式中有一个 .large
类,那么它不会与 Button 组件中的 .large
样式冲突。此外,我们仍然可以通过将诸如 src/globals.css
之类文件导入到每个组件中,或者简单地将单独的 CSS 文件添加到 . 中来使用全局样式。
通过使用 React 创建静态网站,我们失去了 React 默认提供的许多神奇特性,包括状态管理,但仍然可以使用此系统提供两种类型的网站:您可以按照我上面展示的方式创建静态网站,然后在之后逐步使用 React 的强大功能增强所有内容。
此工作流程简洁明了,但在许多情况下,这种 CSS Modules、React 和 Webpack 的组合将是完全过度的。根据 Web 项目的大小和范围,花费时间实施此解决方案几乎是疯狂的——例如,如果它只是一个网页。
但是,如果每天有很多人向代码库中添加 CSS,那么如果 CSS Modules 可以防止由于级联而导致的任何错误,那将非常有用。但这可能会导致设计师无法访问代码库,因为他们现在也必须学习如何编写 JavaScript。为了使此方法正常工作,还需要支持许多依赖项。
这是否意味着我们将来都会使用 CSS Modules?我不这么认为,因为——与所有前端技术一样——解决方案取决于问题,而并非所有问题都相同。
文章系列
- 什么是 CSS 模块,为什么我们需要它们?
- CSS 模块入门
- React + CSS 模块 = 😍 (您现在所处位置!)
很棒的系列。如果您正在将 CSS Modules 与 React 一起使用,我强烈建议您查看 react-css-modules。
非常好的演练。如果您想更进一步(PostCSS、Redux 等),我喜欢这个入门项目:https://github.com/koistya/react-static-boilerplate
您好,我能做的最好的事情就是向您推荐 http://www.smartranking.fr,他们在这一方面是最好的。
如果您以这种风格编写 CSS Modules,ES CSS Modules 也可能很有用!https://github.com/jacobp100/es-css-modules
很棒的文章!正是我想要的 :D
解释得很好,非常感谢!
静态文件生成使部署变得容易,因为您只需要上传文件即可完成,无需担心 git 钩子或根据您的主机/设置而不同的特殊部署。我开始非常喜欢这种方法,在很多情况下,这已经足够了。
静态文件生成对于我的 universal-dev-toolkit 来说可能非常有用。我已在其中添加了哈希文件、browsersync、(s)css-modules、服务器端渲染和一堆其他好东西,因此添加此功能应该不会太难!
这很酷!我一定会仔细看看这个项目。
非常感谢您撰写这一系列文章/教程,Robin,但我发现这篇文章中有一些问题,这让我有点困扰。我只是想强调一下。
您上面提供的
.babelrc
文件的代码示例中包含"presets": ["es2016", "react"]
,但在第 2 部分中,我们之前使用的是"presets": ["es2015"]
,因此如果您按照第 2 部分继续操作,则会导致错误。此外,您上面提供的
router.js
示例中有一个错别字。应该是
(我花了一段时间才弄清楚这一点!)无论如何,希望您有机会更新这些内容。
谢谢!
谢谢 Chuck!我已经更新了这篇文章。
Yo!感谢您对 CSS Modules 的如此温和的介绍。学习基础知识很棒,但我仍然觉得缺少第四部分……眨眼;)
考虑更复杂的场景,例如 CSS 特异性示例,将非常有帮助。此外,不要忘记更新此部分中的
.babelrc
示例代码,该代码仍然包含对es2016
而不是es2015
的引用!