CSS 模块并没有唯一的处理 JavaScript 模板、CSS 文件或构建步骤的方法来使其工作。在这篇文章中,它是关于 CSS 模块系列的一部分,我们将探讨一种方法。这篇文章的目的是让一个 CSS 模块项目能够运行起来。
文章系列
- 什么是 CSS 模块,为什么我们需要它们?
- CSS 模块入门 (您现在在这里!)
- React + CSS 模块 = 😍
在我参与的项目中,有一个要求是 CSS 永远不应该依赖于客户端 JavaScript 来工作,因此构建步骤需要在部署之前将所有内容处理成可用的 HTML 和 CSS。我们将使用 webpack,一个构建系统和模块打包器。在下一篇文章中,我们将重点介绍如何使下面的代码适合于一个向浏览器渲染静态 HTML 的真实项目。
让我们开始吧!
安装 webpack
在 安装 NPM 和 node 之后,我们需要在某个地方设置一个空目录并运行以下命令
npm init --y
这将创建一个 package.json
文件并用一些默认值填充它。这是我们的依赖项清单——当其他人npm install
此项目时,下载和安装内容的说明。
webpack 将处理我们的构建过程。它将监视我们的 CSS、JavaScript 和 HTML,并在两者之间执行所有操作。但是 webpack 是什么?Maxime Fabre 想知道 webpack 是构建系统还是模块打包器
好吧,它两者都是——我说的不是它同时执行这两者,而是它将两者结合起来。webpack 不会构建您的资产,然后单独捆绑您的模块,它将您的资产视为模块本身……可以导入、修改、操作,最终可以打包到您的最终包中。
如果这听起来很奇怪,别担心。还记得 Sass、Gulp 和 npm 都是不熟悉和可怕的时候吗?我们会弄清楚的。
让我们通过创建一个 JavaScript 文件来定义一个依赖项,以便我们可以导入该代码块,来确保 webpack 正确地“捆绑”模块。首先,我们需要全局安装 webpack,这将使我们能够在终端中使用webpack
命令
npm install webpack -g
完成后,我们需要在我们的项目中本地安装 webpack,如下所示
npm i -D webpack
现在我们需要在/src
目录中创建一个index.js
文件。通常我喜欢创建一个目录,其中包含所有静态资产(例如图像、字体、CSS 文件和标记)。我编写的任何代码通常都位于/src
目录中,而由机器编写或在特定过程中解释的任何代码都应位于/build
目录中。我的想法是,删除/build
目录应该完全没问题,并且不会遇到任何问题,因为我们只需运行一个命令,它就会处理/src
目录中的内容并完全重建/build
目录。在这种情况下,我们希望 webpack 查看/src
中的所有内容,执行某个过程,然后将该代码移动到/build
中。
在/src
目录中,我们还可以添加一个空alert.js
文件(我们将在稍后返回)。我们还需要一个webpack.config.js
文件,该文件位于项目的根目录下,/src
目录之外,因此我们的项目结构现在应该如下所示
package.json
webpack.config.js
/node_modules
/src
index.js
alert.js
在webpack.config.js
(一个用于配置 webpack 的文件)中,我们可以添加以下内容
module.exports = {
entry: './src',
output: {
path: 'build',
filename: 'bundle.js',
},
};
从现在开始,每当我们运行webpack
命令时,webpack 将查看/src
中的所有资产以构建依赖项树。
回到我们的src/index.js
文件,我们可以添加这个
require("./alert.js");
在我们的alert.js
文件中,我们可以编写这个
alert("LOUD NOISES");
现在让我们在根目录中创建一个index.html
文件,并在关闭之前添加一个脚本标签中的捆绑包
CSS 模块演示
<script src="build/bundle.js"></script>
该bundle.js
将由 webpack 生成。要生成它,我们只需运行webpack
命令。为了使这更容易,我们可以使用构建脚本更新我们的package.json
文件。这是您应该在该文件中找到的内容
"scripts": {
"test": "echo 'Error: no test specified' && exit 1"
},
这些是npm
给我们的默认值,但我们可以用以下代码替换上面的代码,以创建我们自己的命令行脚本,该脚本将为我们运行 webpack 并打开一个浏览器窗口
"scripts": {
"start": "webpack && open index.html"
},
因此,每当我们运行npm start
时,我们都会自动运行webpack
命令并在浏览器中打开我们的索引文件。让我们现在这样做,看看会发生什么。

万岁,有些东西正在工作!这证明了我们的index.js
文件正在从alert.js
导入我们的代码,并且 webpack 正在正确地捆绑所有内容。如果我们现在删除alert.js
文件,我们将在再次运行npm start
时发现错误

如果 webpack 找不到导入的模块,它将显示此错误。但是现在我们已经确认所有这些都正常工作,我们可以删除index.js
文件中的require
语句,并继续学习 Webpack 的下一步。
添加我们的第一个加载器
webpack 中的加载器非常重要。Maxime Fabre 在 主题 上这样说道
加载器是小型插件,基本上表示“当您遇到此类文件时,对其执行此操作”。
在 Maxime 的教程中,他添加了 Babel 加载器,这是一个非常好的起点,因为 Babel 允许我们使用 ES2015 和 JavaScript 语言的最新改进。因此,我们不再使用前面用于require
另一个模块的 Common.js 函数,而是可以使用import
。使用 Babel,我们还可以使用类、箭头函数和 许多其他很酷的功能
像 Babel 这样的工具允许我们今天编写新的 ES2015 代码,并执行称为转译(类似于预处理)的任务,将代码转换为具有更大浏览器支持的早期版本的 JavaScript。这类似于 Sass 的工作原理;最初以 Sass 语法编写代码,然后预处理器编译为标准 CSS。
以下将安装 webpack Babel 加载器以及我们运行 Babel 所需的依赖项
npm i -D babel-loader babel-core babel-preset-env
在项目根目录下的.babelrc
文件中,我们可以配置预设以让其他人知道我们将使用哪个 JavaScript 语法
{
"presets": ["babel-preset-env"]
}
现在我们希望对所有.js
文件运行 Babel,但仅限于我们编写的文件,我们以后安装的任何其他依赖项可能都有自己的语法,我们不想弄乱该代码。这就是 webpack 加载器发挥作用的地方。我们可以打开webpack.config.js
文件并将该代码替换为此
module.exports = {
entry: './src',
output: {
path: 'build',
filename: 'bundle.js',
},
module: {
loaders: [
{
test: /\.js/,
loader: 'babel-loader',
include: __dirname + '/src',
}
],
}
};
loaders
数组内的test
键/值对是我们告诉 webpack 我们想要对哪种类型的文件执行操作的方式,而include
则准确地告诉它我们希望在项目中的哪个位置执行该操作。
让我们测试 Babel 是否与 webpack 协同工作。在一个新文件(src/robot.js
)中,让我们编写以下内容
const greetings = (text, person) => {
return `${text}, ${person}. I read you but I’m sorry, I’m afraid I can’t do that.`;
}
export default greetings;
此 JavaScript 文件使用了一些 ES2015 特定的功能,例如export
、const
和let
、箭头函数和模板文字。
现在我们可以将该模块import
到我们的src/index.js
文件中,如下所示
import greetings from './robot.js'
document.write(greetings("Affirmative", "Dave"));
最后,我们需要做的就是再次运行npm start
,我们的浏览器应该弹出文本:“肯定的,戴夫。我读懂了,但很抱歉,恐怕我不能那样做。”这仅仅证实了 Babel 正在按预期工作。
万岁!那还不是 CSS 模块,尽管我们肯定更近了一步。但在继续之前,让我们删除src/robot.js
和src/index.js
中的所有代码。
加载样式
现在我们已经让我们的模板几乎可以工作了,我们需要添加两个加载器:css-loader 和 style-loader,我们将安装它们
npm i -D css-loader style-loader
css-loader 获取 CSS 文件并读取其所有依赖项,而 style-loader 将直接将这些样式嵌入到标记中。让我们通过在src/app.css
中编写一些 CSS 来测试它
.element {
background-color: blue;
color: white;
font-size: 20px;
padding: 20px;
}
然后我们可以将该样式表import
到我们的src/index.js
文件中
import styles from './app.css'
let element = `
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur laudantium recusandae itaque libero velit minus ex reiciendis veniam. Eligendi modi sint delectus beatae nemo provident ratione maiores, voluptatibus a tempore!
`
document.write(element);
等等,等等!我们刚刚是不是让一个样式表成为了 JavaScript 文件的依赖项?没错,我们确实这么做了。但在它正常工作之前,在我们了解到这为什么有用之前,我们需要再次重新配置我们的 webpack.config.js
文件。
module.exports = {
entry: './src',
output: {
path: 'build',
filename: 'bundle.js',
},
module: {
loaders: [
{
test: /\.js/,
loader: 'babel',
include: __dirname + '/src',
},
{
test: /\.css/,
loaders: ['style', 'css'],
include: __dirname + '/src'
}
],
}
};
运行 npm start
将得到类似这样的结果。

因此,如果我们在文档上“检查元素”,我们会发现 style-loader 已将该文件放入
<style>
标签中,位于文档的 <head>
部分。

让我们总结一下刚刚发生了什么。我们创建了一个 JavaScript 文件,它请求另一个 CSS 文件,然后该代码被嵌入到网页中。因此,在一个更真实的例子中,我们可以创建一个 buttons.js
文件,并使 buttons.css
成为它的依赖项,然后将该 JavaScript 导入到另一个文件中,该文件组织我们的模板并输出一些 HTML。这应该会使我们的代码变得非常模块化且易于阅读!
就我个人而言,为了保持代码整洁,我更倾向于使用单独的 CSS 文件,而不是将所有代码内联。为此,我们需要使用一个名为 webpack 插件 的 extract text,它
将入口块中的每个 require('style.css') 移动到一个单独的 css 输出文件中。因此,您的样式不再内联到 JavaScript 中,而是位于一个单独的 css 捆绑文件中 (styles.css)。如果您的样式表总量很大,它会更快,因为样式表捆绑包与 JavaScript 捆绑包并行加载。
我们必须使用 npm 安装它。
npm i -D extract-text-webpack-plugin
现在我们可以再次更新我们的 webpack.config.js
文件,通过 require 它并将我们的 CSS 加载器放入其中。
var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './src',
output: {
path: 'build',
filename: 'bundle.js',
},
module: {
loaders: [
{
test: /\.js/,
loader: 'babel',
include: __dirname + '/src',
},
{
test: /\.css/,
loader: ExtractTextPlugin.extract("css")
}
],
},
plugins: [
new ExtractTextPlugin("styles.css")
]
};
ExtractTextPlugin
现在将为我们创建一个 styles.css
文件!
您可能已经注意到,我们完全去掉了 style-loader。这是因为我们不再希望这些样式注入到我们的标记中。因此,现在如果我们打开 /build
目录,应该会发现已创建了一个包含所有代码的 styles.css
文件。然后在我们的 index.html
文件中,我们现在可以在 <head>
中添加我们的样式表。
<link rel="stylesheet" href="build/styles.css">
再次运行 npm start
,然后bam! – 我们的样式神奇地回到了它们所属的页面上。
现在我们的 CSS 和 HTML 在页面上正常工作了,我们如何操作类名以获得局部作用域的所有好处?我们只需像这样更新我们的 webpack.config.js
文件即可。
{
test: /\.css/,
loader: ExtractTextPlugin.extract('css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'),
}
这会将生成的随机文本添加到类名的末尾。CSS Modules 实际上就是这样,一个散列值,它会更改类名,可以通过 webpack 中的 CSS 加载器添加这些类。
接下来,我们必须使用 styles.element
类更新我们的 index.js
文件。
import styles from './app.css'
let element = `
<div class="${styles.element}">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur laudantium recusandae itaque libero velit minus ex reiciendis veniam. Eligendi modi sint delectus beatae nemo provident ratione maiores, voluptatibus a tempore!
</div>
`
document.write(element);
看看发生了什么!再运行一次 npm start
,我们的代码现在已经被 webpack 处理,所以局部作用域不再是问题,因为注入到网页中的类现在看起来像这样。
<div class="app__element___1MmQg">
...
</div>
我们还没有真正完成,因为还有很多问题没有得到解答。我们如何在开发环境中编写这样的代码?我们如何绕过我们用来将标记注入页面的那个讨厌的 document.write
规则?我们应该如何组织我们的模块和文件?让 CSS Modules 运行起来只是工作的一半,接下来我们需要考虑如何将代码库从另一个系统移植到其中。
在下一个教程中,我们将了解 React 如何帮助我们生成整洁的小模块,此外,我们还将了解如何从多个模板生成静态标记,以及如何向我们的项目添加 Sass 和 PostCSS 等其他功能。
文章系列
- 什么是 CSS 模块,为什么我们需要它们?
- CSS 模块入门 (您现在在这里!)
- React + CSS 模块 = 😍
呵呵——有时候我确实感觉是这样。
在现实环境中,您在一个页面上通常会看到多少个这样的“模块”?这些附加的散列值会对 gzip 压缩产生多大负面影响?
非常感谢您撰写这篇文章!这真是太及时了;我今天计划使用 CSS Modules 启动一个新项目,所以醒来看到这篇文章发布真是太完美了。
不过我在使用 webpack 时遇到一个错误。我已经按照“加载样式”部分的操作步骤操作,所以我的 src 目录中包含
index.js
和robot.js
文件,甚至直接复制粘贴了这两个文件以及 webpack.config.js 中的示例,以确保我没有在那里弄错什么,但 webpack 仍然给我这个错误。看起来它不喜欢
import
语法。我搜索了很多资料,也尝试了一些在 S/O 或 GitHub 问题中看到的各种修复方法,但似乎都没有奏效 :( 在尝试对 webpack.config.js 文件进行了一些不同的编辑后,这些编辑最终都没有奏效,我以为可能是我的 package.json 文件出了问题(对于遇到此错误的其他用户来说也是如此),但我通过了一个在线验证器,没有发现任何问题。这是完整的仓库,如果有人想看一看。还有其他人遇到这个问题吗?:(
几个小时后,终于解决了。出于某种原因,在加载器的
include
字段中,它不喜欢我使用__dirname + '/src'
,所以我尝试在 webpack 配置文件顶部添加var path = require('path');
,并在include
字段中使用path.resolve('src')
而不是尝试使用__dirname + '/src'
。不知道为什么会出现这种情况,但不管怎么说,我松了一口气 :D嗯。我再次运行了演示,但无法重现该错误。对于所有这些麻烦,我表示歉意!
没关系 :) 看起来它并不是特定于本教程的;我怀疑我的这边可能出了点其他问题,或者其他一些奇怪的事情。
再次感谢您撰写这篇精彩的文章 :D
我也遇到过这个问题,花了我几个小时。感谢您的解决方案。
感谢您撰写这篇文章,Robin,我一直想做 CSS 局部作用域。只是很可惜没有(简单)的原生方法可以实现,我们不得不采用新的流程。也就是说,我喜欢跟随您的教程,期待第 3 部分!
但我认为在高级别上全局作用域也很有用——例如,确保
.button
类始终应用相同的品牌颜色。在这种方法中,是否可以引用一个静态的global.css
文件以及一个生成的scoped.css
文件?一种混合方法。然后我可以写<a class="button ${styles.big}">
并知道“big”增强始终对该元素/模块唯一,但所有按钮都将全局使用相同的颜色。这样理解对吗?当然可以。尽管我认为有两个 CSS 文件会非常混乱,并且理想情况下,如果我们正在实现一个限制级联的解决方案,那么我们应该始终在任何地方都使用它。
这里没有什么能阻止我们导入
.button
类到模板中,然后使用<a class="{button} {button.big}">
,并将所有这些样式保留在button.css
中。我肯定会担心另一种选择,即使用两个 CSS 文件,仅仅因为它随着时间的推移难以维护。很棒的文章,解释得很详细。谢谢!
感谢这篇文章,伙计!干得漂亮,期待下一篇文章。
我有一个关于这个主题的问题。我想知道使用样式作为 Blob 链接与在 head 中使用标准的“style”标签相比,有什么好处和区别。因为有时 Webpack 会生成类似这样的内容。
我也在 Stack Overflow 上问过这个问题:http://stackoverflow.com/questions/36678077/blob-based-link-stylesheet-vs-standard-style-tag
有人知道吗?;)
很棒的文章,但您的一些 npm 命令虽然有效,但并不一致。例如,从
npm install
切换到npm i
。我不知道这是个别名。好吧,有些人可能会认为这是不言而喻的,但您还使用了npm i -D
,这让我在 Google 上搜索了一段时间。我仍然不知道-D
标志是什么。如果它是--save
或--save-dev
的别名,我花这么多时间搜索它会很沮丧。如何根据不同的路由动态加载 css 依赖项?
第 3 部分在哪里?
1
呃……应该是“加 1”。我认为 Markdown 吃掉了加号;)
Robin,我无法表达我对这篇写作的赞赏。我喜欢您对每个步骤的简洁解释,甚至包括(看似)微不足道的细节。我一直在尝试弄清楚 webpack,这是我第一次真正理解“加载器”这个词。我希望你能写一系列关于 Webpack 的文章!(那个 module.exports = merge(common, …) 是什么??还有 TARGET 和 PATHS??我希望我能知道!)
期待下一篇文章!
第 3 部分什么时候发布?我迫切地等待着它。
您会在下一期中介绍如何配置热模块替换吗?
在我参与的项目中,有一个要求是CSS永远不能依赖于客户端JavaScript才能工作,因此构建步骤需要在部署之前将所有内容处理成可工作的HTML和CSS。
对于新手来说,您能否详细说明一下,为什么使用预处理器可以成功地处理避免“客户端Javascript”的必要性,我猜想这个要求是为了安全原因?当然,浏览器(客户端)仍然会运行脚本?谢谢。
嗨,Sarah——我对这个问题有点困惑,但我会尽力回答。像Sass或LESS这样的预处理器不需要浏览器中的JavaScript,因为这些代码根本不会发送给它们。这些文件是“构建步骤”的一部分,这样开发人员就可以用Sass编写代码,但最终会被转换或编译成CSS——因此,像styles.css这样的文件才是实际发送给客户端的文件。
简而言之:预处理器仅供开发人员使用,与JavaScript本身关系不大。
CSS Modules与此非常相似:它们不需要浏览器中的JavaScript,尽管我在这里谈到的方法确实需要JavaScript,但这仅仅是因为我们正在使用Webpack。如果您想阅读更多关于比第3部分更现实的示例,它会更深入地探讨。
我回答了你的问题吗?如果你愿意,我很乐意详细阐述CSS Modules或预处理器。