该篇为翻译文章:原文为:CSS Modules and React

该系列文章目录:

第一部分: CSS Modules是什么东西,我们为什么需要它?-译文(原文)
第二部分: 如何使用CSS Modules-译文(原文)
第三部分: CSS Modules和React (当前)

这篇文章是CSS Modules系列文章的最后一篇,我将演示用React来做一个静态网站。当然,我们需要借助于Webpack工具。这个例子中,我们会做两个页面,一个首页 ,一个关于页。我们将通过写几个React组件来看一下CSS Modules在React中是如何应用的。

在上一篇文章当中,我们说了如何使用Webpack对各文件之间的依赖进行管理,以及如何在CSSHTML当中生成惟一的class名。这篇文章用到的东西除比较依赖上一篇中讲到的这些东西之外,当然还要会用React

上次写的Demo当中,我们在文章最后遗留了一个问题,就是我们是使用JS来生成的HTML,这样我们的项目就是非常的乱。如果使用React,那么这个问题就不再是问题。

为了方便大家学习和练习,大家可以在我的GitHub上把 css-modules-react 项目下载下来,这个项目就是上篇文章没写完的东西,下载下来并把里面的npm包安装好后,就可以在此基础上往下进行学习。

Webpack静态页面生成器

Webpack当中如果想生成静态的HTML,我们需要安装一个插件static-site-generator-webpack-plugin

npm i -D static-site-generator-webpack-plugin

现在,我们需要在webpack.config.js当中来配制插件并添加路由。路由的分配应该是这样,/是首页,/about是关于页面。插件正是根据路由来判断该创建哪个页面。

var StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin');
var locals = {
  routes: [
    '/',
  ]
};

我们需要在不受后台代码影响的情况下来生成静态HTML文件,就需要用好static-site-generator-webpack-plugin插件。

如何使用呢?现在我们需要来修改一下webpack.config.js文件当中的module.exports对象:

module.exports = {
  entry:  {
    'main': './src/',
  },
  output: {
    path: 'build',
    filename: 'bundle.js',
    libraryTarget: 'umd' // 这个属性非常非常重要!!
  },
  ...
}

这里将libraryTarget属性设置成umd非常重要(umd是Universal Module Definition的缩写,也就是’通用模块定义’的意思)。我们还设置了path/build,这样所有生成的东西都会在这个文件夹下面。
然后,我们在webpack.config.js文件当中添加并使用static-site-generator-webpack-plugin插件,使用时传入路由以便插件根据路由进行生成:

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, '<html>Hello!</html>');
};

现在,我们只是想在首页打出Hello!。而最终的页面将会比较复杂。

package.json中,我们设置了命令来运行webpack:

npm start

npm start后,我们打开build目录,应该会看到一个index.html文件,并且里面有我们上面写下的内容。这说明static-site-generator-webpack-plugin插件起作用了。
随后,我们来修改webpack.config.js当中的routes选项来让‘关于页面’都起作用:

var locals = {
  routes: [
    '/',
    '/about'
  ]
};

这时再次运行npm start命令,就会生成build/about/index.html文件。但是,这个文件当中的内容也是Hello!,也就是跟/build/index.html文件一样,因为我们在两个文件当中传入了一样的内容。为了解决这个问题,我们需要用到router,但首先我们需要进行React的一些配制。

但在配制之前,我们需要将我们的路由拆分到另外一个文件,这样我们的项目看起来更有条理。所以,我们在./data.js当中添加:

module.exports = {
  routes: [
    '/',
    '/about'
  ]
}

webpack.config.js中,我们把locals变量删掉,然后将data导入:

var data = require('./data.js');

最后,我们需要修改static-site-generator-webpack-plugin插件的配制 :

plugins: [
  new ExtractTextPlugin('styles.css'),
  new StaticSiteGeneratorPlugin('main', data.routes, data),
]

安装React

运行以下命令来进行安装reactreact-dom等:

npm i -D react react-dom babel-preset-react

然后我们需要修改.babelrc文件:

{
  "presets": ["es2016", "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>
    )
  }
}

如果你对React使用的JSX的语法不熟悉的话,上面的代码中body内的文字是一段注释。另外,上面的<head></head>元素并不是HTML的标准元素,它是一个React组件,我们通过给它添加title属性来达到传送数据的目的。其实这个title在这里并不叫属性,它在React当中叫做props

下面我们来看一下Head组件,它是src/components/Head.js文件:

import React from 'react'

export default class Head extends React.Component {
  render() {
    return (
      <head>
        <title>{this.props.title}</title>
      </head>
    )
  }
}

现在在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中的内容导入,然后使用React DOM将其渲染出来。现在运行npm start,然后再查看build/index.html文件会发现React已经将我们所有的HTML内容都解析出来了。

但是,上面的内容依然会同时生成到首页和关于页。接下来我们通过使用路由来解决这个问题。

配制Router-路由

我们需要将路由和内容进行同步,关于页需要的是关于的内容,首页也有自己的内容,如果有其它页面,它们都会有自己的内容。我们需要react-router来完成这些。

首先,我们需要安装react-router:

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 )

我们需要多个页面,首页和关于页。首先,我们来看关于页src/templates/About.js:

import React from 'react'
import Head from '../components/Head'

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'
import Head from '../components/Head'

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文件中包含了所有的必要HTML标签,比如<head>Home.jsAbout.js现在是React组件,可以作为内容而替换到Main.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不是特别熟悉,那么可以查看 Brad Westfall的Leveling Up With React: React Router

因为我们删掉了index.js文件,取而代之的是router.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.htmlbuild/about/index.html,每个文件的内容都是它们应该有的。

将CSS Modules加入其中

我们将来写一个按钮模块来进行说明。为了这一系列文章的完整性,我将继续使用上篇文章当中讲到的WebpackCSS loader来进行演示,我这样说的原因是因为有一个替代品出现了。

现在,我们需要这样的一个目录结构:

/components
  /Button
    Button.js
    styles.css

现在,我们需要在一个模板当中应用Button模块,在这之前我们先来创建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}会通过找到style.css当中的.red类,然后,Webpack会根据它生成一堆很长的class名。

现在,我们在src/components/Button/styles.css文件当中添加一些简单的样式:

.red {
  font-size: 25px;
  background-color: red;
  color: white;
}

最后,我们在模板当中运行Button组件,比如在src/templates/Home.js当中使用:

import React from 'react'
import Head from '../components/Head'
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,我们就会看到最终效果啦!:

cssmodules-react1

你可以将我已经完成的 React and CSS Modules下载下来进行查看学习。如果你发现了什么错误,欢迎到GitHub提出来。

结语

上面我们只是以一个Button模块为例,实际上,我们可以添加许多其它的组件,比如这样:

/components
  Head.js
  /Button
    Button.js
    styles.css
  /Input
    Input.js
    style.css
  /Title
    Title.js
    style.css

因此,即使我们在标题组件和按钮组件当中都有.large这个class,它们也不会有任何冲突。当然,我们仍然可以使用一些全局的样式,比如我们可以创建一个src/globals.css,然后在各组件当中引入它就行了。

那么是不是在我们以后的项目当中都要使用CSS Modules呢?当然不是,因为不同的项目的复杂程度不一样,要选择最合适的前端解决方案。没有好不好,只有合不合适。