蜗牛上树

【React】Webpack入门 以及 css模块化

蜗牛上树 · 2017-05-23翻译 · 303阅读 原文链接

这是讲CSS模块化系列文章的最后一篇,我将探索如何通过Webpack来建一个静态的React网站。这个静态网站将包含模板:一个主页和一个包含几个React组件的页面。我们来看看他们是怎么运作的。

系列文章

之前的文章中我们用Webpack快速搭建了一个工程来展示了所有依赖是如何被导入到一个文件的以及在打包过程中如何将一个唯一的classname同时在csshtml中生成the basics of React.

在之前的演示中,我们知道通过js来渲染我们标签,但是我们不清楚如何构建我们项目。在这篇文章中我们将用一个更真实的例子演示如何通过我们新学的Webpack知识来构建我们的项目。

我把之前演示的代码放到github上了 css-modules-react ,你可以通过这个代码开始下面的例子.

#配置webpack

生成静态标签需要安装一个Webpack的 插件

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

现在我们需要将插件配置到 webpack.config.js 然后配置路由. 比如homepage页面的路径为/ , 又比如about页面的路径为/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,是为了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, '<html>Hello!</html>');
};

现在我们只是在主页上打印出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是一模一样的,因为我们把相同内容转给了这两个文件. 为了解决这个问题我们需要一个路由器, 但是在此之前我们需要把React装好.

在安装之前,我们应该把我们的路由独立成一个文件,让代码更为整洁. 所以我们新建一个文件 ./data.js, 然后我们可以写:

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

然后我们引入data.js,然后删除locals变量:

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

修改 StaticSiteGeneratorPlugin 配置:

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

#安装React

我们想要生成许多包含HTML和CSS组件的js文件(比如 About.js , Homepage.js)。这些可以通过 reactreact-dom来实现,所以我们安装如下:

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

然后我们需要更新.babelrc 文件:

{
  "presets": ["es2016", "react"]
}

新建一个文件夹/src/templates,在文件夹中创建Main.js,里面存放所有标签以及也可以作为标签被其他组件调用(就像<head><footer>那样使用):

import React from 'react'
import Head from '../components/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 /> 标签有点怪—它不是标准的HTML标签—它是一个React组件以及通过title属性可以传递数据. 它并不是一个属性,在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>
    )
  }
}

我可以把Head.js的代码放到 Main.js中,不这样做是有助于把代码分成小块;如果我们需要一个footer,那么我们就创建一个新组件 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, '<!DOCTYPE html>' + html)
}

引用Main.js (Main.js 随后将引用Head React组件) ,然后它们通过 React DOM都会被渲染. 如果我们运行npm start 命令,然后查看 build/index.html , 我们会发现Main.js组件连同Head.js组件都被添加进来了,然后它被渲染成了静态标签.

但是About和Homepage内容还是一样,让我们用路由器来解决这个问题。

#设置我们的路由器

我们需要传递特定的代码到特定的路径:对于About页面我们需要关于About页面的内容,同样的,主页,博客页面或者其他页面需要与自己相关的内容。换句话说我们需要一个软件来管理这些内容:路由器。我们可以将这些繁重的工作交给 react-router来实现。

在我们开始之前值得注意的是,本次教程我们将使用 React-Router2.0版本,比起之前的版本,它有 一些变化 .

首先我们需要安装它,因为它没有被默认打包到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
)

We want multiple pages: one for the homepage and another for the About page so we can quickly go ahead and make a src/templates/About.js file:

我们需要多个网页: 所以还需要快速创建一个 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 包含了所有组件 (类似于 <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不太了解,你可以最好先看看这个 Brad Westfall 介绍 React Router.

由于我们删除了 index.js 文件,又用router替换了它,所以我们需要回到 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模块化

首先我们将创建一个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}表示classname来自于 styles.css.redclass,然后Webpack将生成全局唯一的classname。

我们在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模块化,所以ClassName看起来是这样的:

你可以找到本次教程全部代码 点这里 . 如果你发现到任何问题请提交issue.

当然我们可以对这个工程进行改进, 比如我们可以 添加 Browsersync 来让Webpack自动打包,我们就不用一直输入npm命令了. 我们也可以添加Sass, PostCSS 等加载器或者插件来帮助我们, 但是为了简洁起见我没有用这些.

#总结

到现在我们完成了什么?好吧,尽管看起来做了许多工作,但是我们有了一个模块化环境来写代码。我们可以像这样添加许多组件:

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

如果我们有一个 .large 的class在Head组件,它不会和来自Button组件的 .large的class冲突。并且,我们可以通过引入 src/globals.css在任何一个组件继续使用全局样式, 或者仅仅时作为一个独立css文件添加到<head>.

整个项目是非常简洁的,但是很多情况下,结合css模块、React和Webpack就没必要了。这取决于你项目的大小。如果使用上面那种方式仅仅为了做一个网页那就太疯狂了。

然而,如果每天都有许多人来维护css代码,那么css模块化是非常有帮助的,它可以避免很多错误。但是对于设计师来说,他们可能就会很少去维护css代码,因为在此之前他们需要学习怎么来写JavaScript。

是不是意味着我们在未来项目中都会使用css模块化?我不这样认为,因为对于所有前端技术来说,解决方案是取决于遇到的问题,而且不是所有问题都长得一样。

相关文章