在最后这篇有关 CSS Modules 的教程里,我们会介绍如何运用 Webpack 开发一个React的静态站点。这个静态站点包含两个页面模板:主页和关于我们页面,通过一些React组件来实现。

在上一篇教程中,我们通过一个简单的示例项目学习了如何在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