CSS 模块和 React

Avatar of Robin Rendle
Robin Rendle

DigitalOcean 为您旅程的每个阶段提供云产品。开始使用 200 美元的免费积分!

在本系列关于 CSS 模块的最后一篇文章中,我将介绍如何借助 Webpack 创建一个静态 React 网站。这个静态网站将有两个模板:一个主页和一个关于页面,以及几个 React 组件来解释它在实践中的工作原理。

文章系列

  1. 什么是 CSS 模块,为什么我们需要它们?
  2. CSS 模块入门
  3. 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 的小捆绑包,然后我们可以将其捆绑到一个模板(如关于或主页)中。这可以通过 reactreact-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.jsAbout.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.htmlbuild/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?我不这么认为,因为——与所有前端技术一样——解决方案取决于问题,而并非所有问题都相同。

文章系列

  1. 什么是 CSS 模块,为什么我们需要它们?
  2. CSS 模块入门
  3. React + CSS 模块 = 😍 (您现在所处位置!)