在最后这篇有关 CSS Modules 的教程里,我们会介绍如何运用 Webpack 开发一个React的静态站点。这个静态站点包含两个页面模板:主页和关于我们页面,通过一些React组件来实现。
- 第一部分:什么是CSS Modules?为什么要使用它?
- 第二部分:如何上手CSS Modules
- 第三部分:在React中使用CSS Modules
在上一篇教程中,我们通过一个简单的示例项目学习了如何在Webpack中导入文件依赖,并在构建时生成唯一的类名。本篇教程中的示例需要在上一篇的基础上进行,并且需要你稍稍有一点React的基础。
在之前的demo中,我们的代码还完全没有框架结构,只是简单地用JS的模板字符输出。接下来我们会举一些更实际的例子,我们会试着写一些组件,并学习更多有关Webpack的知识。
假如你实在是懒得动手写上一篇教程的代码的话,可以在webpack-css-modules-example下载上篇教程的示例代码再继续下面的教程:
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插件,它的文档里提到:
本插件会根据你配置的路由在相应的目录下生成index.html文件,通过webpack来渲染。
这听起来可能还是很抽象,我们还是来看具体的例子吧,先修改webpack.config.js文件,在其中的module.exports里添加如下内容:
module.exports = {
entry: {
'main': './src/',
},
output: {
path: 'build',
filename: 'bundle.js',
libraryTarget: 'umd' // this is super important
},
...
}
其中添加的libraryTarget配置项是静态站点生成器插件正常工作所必须的。生成的文件同样会保存在/build路径下。
同样我们还需要在webpack的plugins配置里添加StaticSiteGeneratorPlugin,并传入路由作为参数:
plugins: [
new ExtractTextPlugin('styles.css'),
new StaticSiteGeneratorPlugin('main', locals.routes),
]
现在我们完整的配置文件看起来是这个样子:
var path = require('path');
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: path.resolve(__dirname, 'src'),
}, {
test: /\.css/,
loader: ExtractTextPlugin.extract('css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'),
include: path.resolve(__dirname, 'src'),
}],
},
plugins: [
new ExtractTextPlugin("styles.css"),
new StaticSiteGeneratorPlugin('main', locals.routes),
]
};
接下来,再将我们的src/index.js文件修改为:
// Exported static site renderer:
module.exports = function render(locals, callback) {
callback(null, '<html>Hello!</html>');
};
我们先试着在主页输出Hello!来测试一下插件能否正常工作。
直接在命令行运行:
webpack
如果一切正常的话,你就可以在build文件夹里看到一个index.html文件,内容就和我们刚才要输出的一样。测试一切正常后,我们就开始下一步吧,先在webpack.config.js里修改路由:
var locals = {
routes: [
'/',
'/about'
]
};
再次运行 webpack 命令后,我们就又能生成一个新文件build/about/index.html,内容也和刚才的build/index.html一样,因为我们还没有为两个页面配置不同的内容。
我们先将路由配置存放在一个独立的文件里,在项目根目录创建./data.js文件:
module.exports = {
routes: [
'/',
'/about'
]
}
之后将webpack.config.js里的locals变量替换:
var data = require('./data.js');
最后再给插件传入新的参数:
plugins: [
new ExtractTextPlugin('styles.css'),
new StaticSiteGeneratorPlugin('main', data.routes, data),
]
安装React
我们需要写出很多个模块,然后把它们打包到一起。这也正是React能帮我们做到的,首先通过命令行安装相关依赖:
npm i -D react react-dom babel-preset-react
然后修改我们的.babelrc文件:
{
"presets": ["es2015", "react"]
}
新建文件夹/src/templates,之后在其中创建Main.js文件。这个文件用来存放页面模板的通用部分:
import React from 'react'
import Head from './Head'
export default class Main extends React.Component {
render() {
return (
<html>
<Head title='React and CSS Modules' />
<body>
{/* This is where our content for various pages will go */}
</body>
</html>
)
}
}
有两点需要注意的:首先,你可能还不太熟悉JSX的语法,这里需要解释的是body中的{}里的内容是注释,<Head />不是原生的HTML标签,而是代表React中的一个组件,我们可以给组件传入一个title参数。要注意这并不代表HTML标签的某个属性,而是React中的props值。
现在再来创建/src/components/Head.js文件:
import React from 'react'
export default class Head extends React.Component {
render() {
return (
<head>
<title>{this.props.title}</title>
</head>
)
}
}
虽然我们也可以直接把所有的代码全部都写在Main.js里,可把不同的部件分开是有好处的,组件化的代码可以很轻松地被复用,比方我们可以把它和另一个Footer.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, '<!DOCTYPE html>' + html)
}
上面这段代码的意思是把所有Main.js里的内容通过ReactDOM渲染出来并返回。这时我们再运行一次webpack命令就能够看到渲染的结果,但此时about页面和主页还是相同的内容,下一步我们要为它们配置不同的内容:
配置路由
我们需要为不同的路由设置不同的内容,About页面和Home页面都需要显示各自的内容。我们可以使用react-router来实现这一需求:
本教程里使用的是2.0版本的react-router,和之前的版本可能会有出入。
首先还是安装,react-router是独立于react的一个库:
npm i -D react-router
然后在/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
)
之后为我们的About页面创建单独的模板:
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>
)
}
}
再然后是Home页面的模板:
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>
Main.js里包含网页的框架内容(例如<head>等)。Home.js和About.js里包含了可以放在<body>元素中的内容。
接下来我们需要创建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);
})
}
如果你觉得一时间无法消化上面这段代码,可以学习一下react-router入门教程。
因为我们已经删掉了index.js文件,所以也要对webpack.config.js配置文件做出修改:
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}用来传递About和Home两个模板中的内容。我们再运行一遍webpack命令之后就能看到效果啦。
引入CSS Modules
接下来我们会一起实现一个Button模块。在这篇教程里我们还会沿用之前Webpack的CSS loader,不过你也可以使用react-css-modules.
我们的Button模块目录结构大概是下面这个样子:
/components
/Button
Button.js
styles.css
首先创建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中的.red的样式。Webpack会在构建时自动生成相应的类名。
我们先创建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>
)
}
}
在Head.js里加入生成css文件的链接:
import React from 'react'
export default class Head extends React.Component {
render() {
return (
<head>
<title>{this.props.title}</title>
<link rel="stylesheet" href="./styles.css"/>
</head>
)
}
}
最后再运行一下webpack命令,就可以看到生成的结果啦!

到这一步差不多就结束啦,你可以在React and CSS Modules找到本教程的源码。
目前项目的代码还有很多可以改进的地方,比如加入Browsersync,这样我们就不用一遍又一遍地手动生成。还可以加入Sass, PostCSS一类的CSS预处理器。如果你喜欢探索可以亲自尝试一下,为了保证教程不至于太过复杂,我们就见好就收啦。
总结
截至目前我们达成了什么呢?模块化看起来好像把代码分得支离破碎。可我们可以按照这样的方式添加许多组件:
/components
Head.js
/Button
Button.js
styles.css
/Input
Input.js
style.css
/Title
Title.js
style.css
在这种方式下,假如Head模块里有一个.large的样式类,它不会与Button模块里的同名.large类相互冲突。与此同时,也不影响我们添加全局的CSS样式文件。
通过创建这个小demo,我们见识了许多React带来的功能特性。你完全可以在这个demo的基础上逐步开发出一个完整的静态站点。
整个项目的开发工作流还是相当简洁的,当然是否采用CSS Modules, React以及Webpack的这套解决方案,还要根据你的项目规模和形式而定。
加入一个项目组有很多人都在贡献CSS代码的话,采用CSS Modules可以很好地防止代码之间相互的干扰。但对于UI设计师来说,这可能就为他们接触CSS代码造成了更多的障碍,因为在这种情况下必须掌握JavaScript才行。同样CSS Modules也需要很多的工具依赖才能实现。
CSS Modules只是提供了一个前端代码组织,命名冲突的解决方案。并不代表唯一的真理,只是在解决某些具体问题时可以采用它。
欢迎在评论区交流讨论。
原文链接:https://css-tricks.com/css-modules-part-3-react/,作者:Robin Rendle