处理 CSS
如前言中提到的,在 Webpack 中处理 CSS,入手的方法和打包 JavaScript 并没有区别—同样也是通过 loader,区别只是前者我们首先会去使用 babel-loader,而对于 CSS,可想而知,就是 css-loader。
为 Webpack 添加“处理 CSS 文件”的一般办法
需要处理 CSS,对 Webpack 来说就得配置新的 loader 来处理所有的 CSS 文件,对应的 loader 可想而知就叫做 css-loader。
新建 CSS 入口文件 & 并在 JavaScript 中引入上一步新建的 CSS 文件。
通过 JavaScript 引入上一步新建的 CSS 文件。 在没有修改 Webpack 入口文件(当前是 app.js)情况下,直接在文件中引入 CSS 文件是最简单的做法(Webpack 中也支持“多入口”的文件引入方式,下文会有说明,目前保持最简单的方式演示,即从 JavaScript 中引入 CSS 的方式)。
随后启动 Webpack Dev Server,在浏览器中打开 http://localhost:1333/ 。
会发现我们写的样式表什么用处都没。
为什么启动 webpack-web-server 之后什么效果都没有,以及如何生效?
为什么在浏览器中打开的页面却没有任何效果? 要回答这个问题,不如想一下我们在“使用 Webapck 处理 CSS”这件事情上,到目前为止究竟干了些什么?
- 首先,第一步我们为 Webpack 编写了“用来处理 CSS 文件”的“规则”;
- 其次,我们新建了 CSS 文件(main.css)、并写了一小段样式进行测试;
- 接着,我们通过 JS 文件引入了 CSS 文件;
- 最后,我们运行了 Webpack,Webpack 在这里成功进行了对 JS、CSS 的打包、编译以及输出。
但我们打开浏览器,期待的“黑底白字”的页面并没有如期而至,仍然是上一步完成的“白底黑字”状态,那么是哪一步使得我们的操作成了彻底的无用功?又或者难道我们完全做错了?
答案是,与其说是“出问题的一步”,不如说是“与传统前端开发有区别的一步”,即“3. 接着,我们通过 JS 文件引入了 CSS 文件”。
一般的前端开发中,导入样式文件是通过 HTML 去引入 CSS,通过 <link>
标签;而我们在 Webpack 里则仅仅是通过 JS 去引入了 CSS,对,Webpack 并不会替我们自动完成“将 CSS 再引入到 HTML 使其生效”这一步。
那么如何让这些“通过 JS 引入的 CSS”,能够经由 Webpack Dev Server 使得目标 HTML 文件也可以成功读取呢?答案是 loader:style-loader(这里为引出 style-loader,才使用“通过 JS 引入 CSS”的例子。通过 Webpack 处理 CSS 不止一个方法,下文会提)。
同时想要引出的,还有 css-loader 和 style-loader 在使用上的一些区别:
- css-loader:是 Webpack 用来读取 CSS 样式表内容的 loader。Webpack 本身可以识别并且打包 JS 的模块,通过 css-loader,则新增了可以阅读 CSS 的能力。
- style-loader: 有了阅读、解析 CSS 的能力,并不代表具有了可以把 CSS 输出给 HTML 的能力,于是就需要 style-loader,来将读取出来的样式信息注入到 HTML 中,来让浏览器“认可”、并且完成解析。
安装 style-loader,并按照文档上,或者简写方式如下修改 Webpack 中针对 CSS 的规则:
Webpack 中的 loader 的执行顺序是从右至左,以上的意思就是,遇到 CSS 样式文件,先使用 css-loader 让 Webpack 有读取样式的能力,接着通过 style-loader 将已经读取到的样式注入到 HTML。
完成后重启 Webpack Dev Server,打开浏览器如期页面背景变成了纯黑(后来我在 main.css 中加了一句新的声明来让字体变成白色方便截图)。
预处理器,以 SCSS 为例
CSS 之后是 SCSS,理解了上面提到的几点,配置 SCSS 也就不那么困难。
sass-loader 的基本使用
总结起来,上文通过试错来获得的“成功配置 Webpack 解析、编译、输出 CSS”这一结果的过程中,会有三点,在之前、之后使用 Webpack 中都会经常遇到:
- Webpack 中使用 css-loader 来读取 CSS 样式表文件;
- Webpack 中使用 style-loader 来将读取的样式文件注入到 HTML 之中(使用 Webpack Dev Server)的时候;
- Webpack 中 loader 的执行是从右至左(基于配置的是否是简写,也可以是由下至上)的顺序。
理解这三点,在面对“需要处理 SCSS 文件”这个需求的时候,即便没有查看过任何教程,其实也可以做出合理的假设:SCSS 是作为扩展形式的“提升开发效率”的工具,不论如何在最后,(上线部署时)被引入到 HTML 文件中之前,都需要、也必须转换成 CSS 文件。
那么我们的处理的过程,对比原先“首先引入 CSS 文件”这一部分,在起始端,处理 CSS 之前,就需要加入对 SCSS 文件的处理机制(处理具体指的是将 SCSS 编译成浏览器“认可”的 CSS)。把整个处理过程想象成一个管道,SCSS -> CSS -> HTML(引入)。然后进一步推测,CSS 的处理是通过 loader,HTML 引入 CSS 也是通过 loader,那么针对 SCSS 处理,应该也是采用 loader?答案是:对,通过 sass-loader。
sass-loader 的安装、以及 Webpack 中针对 SCSS 的一般规则:
其次,我们需要的实实在在的 SCSS 文件来进行测试:
之后我们重新启动 Webpack Dev Server,通过浏览器打开 localhost:1333 查看,结果应该和之前没差别。
更进一步,可以将 SCSS 声明的文字颜色替换成红色,来检查 Webpack Dev Server 的工作效果是否和之前一致。
以上,就是最简单配置 Webpack 对 SCSS 文件的读取、编译、打包的基本配置。
通过 Webpack 配置中的 resolve.alias 选项,解决相对路径过长问题
上面,在 JS 引入 CSS、或者引入 SCSS 这一部分的例子,有经验的开发者可能已经意识会产生的问题:在一个文件(夹)组织十分深的项目中,采用相对路径来来对依赖进行引入,很可能造成让人头疼的结果,比如:
也就是:进、出很多很多层文件目录去找到目标依赖文件这一结果。
Webpack 为我们提供了一个 resolve.alias 选项,具体的作用:通过写入配置,在任何入口文件(entry)内,都可以通过“关键词”的形式,快速“指定”目标文件夹的位置。描述不免抽象,通过我们的配置文件直接举例。
sass-loader 配置中的一些选项
includePath
举例说明,首先在 node_moduels 文件夹下,新建一个 somepackage 目录,在这个目录下新建一个 options.scss 文件,写入简单的样式:
这个时候我们如果想要在 main.scss 中引入新建的这个 options.scss 文件,一般的方式如下,使用 ~ 符号就可以从 node_modules 目录下下找对应路径下的文件(“~”是 Webpack 提供的一个关键字, 使用功能和 resolve.alias 基本一致):
但如果想忽略前缀的 ./node_modules/
,直接写成如下的形式,可以么?
可以,通过 includePaths 属性。includePaths 英语的字面意思就是将路径包括进来,简单理解就是将“预定义”的路径包含到“可以使用 @import
语句”的索引里面来。
重启 Webpack Dev Server,回到的 main.scss 中,将原本的红色 hex 值修改成 options.scss 中定义的 $red 变量,页面效果和之前的样子应该完全一致,没有丝毫变化。同时也证明了通过 includePath 声明而导入的变量也成功被编译。
SourceMap
这里说的 Source Map 针对的是 SCSS 文件 loader,并不是 Webpack 自带配置中的 devtool
属性可以选配的 source map,同样也用例子说明。
- 在上一步新建的 options.css 中写入的变量
$red: red;
,以及 class:.red { color: $red}
; - 存放 SCSS 文件的目录下,新建与上一步同名的文件 options.scss(上一步中 options.scss 是在 node_modules 下的 somepakcage 目录下创建),其中写入变量
$yellow: yellow
,以及对应指定文字颜色的 class:.yellow { color: $yellow; }
; - scss 目录下再次新建一个文件: ext.scss,写入变量
$blue: blue
,以及对应指定文字颜色的 class:.blue { color: $blue;}
; - 在 main.scss 中分别引入上面三步中的提到的文件。
- 在 HTML 中写入对应测试三个 class 的元素
- 分别添加
sourcemap: true
到三个 loader 的配置中; - 重启 Webpack Dev Server。
打开浏览器就可以看到分别为红色、蓝色和黄色的文字,支持 Source Map 的浏览器“检查器”中也可以看到,样式对应的原始 SCSS 文件路径:比如蓝色的 blue 定义,就显示了该类的声明在 ext.scss,其它同理。
Webapck 单独配置 CSS(SCSS)入口
以上演示中,CSS 文件的引入是通过 JS 文件里使用 import 语句(或者 require)来引入,这样的方式很别扭。 自然,Webpack 不可能只支持单一文件,它为我们提供了直接在配置中声明多个“入口”的方法,通过简单地修改 module.entry 就能完成最基本的实现。
不只是 module.entry,作为输出的 module.output 也被做了修改,成为"使用方括号包裹的 [name.ext]
",它的意思是:根据传入设置中写入的“名称”和“文件类型(type)”,进行动态输出;否则,试想一下,如果保持之前的输出名“bundle.js”,那么我们单独传入的 CSS 的文件,也会被输出成 “bundle.js” 文件对不对?
同时,也因为配置了多入口,输出也成为了多个:一个 JS 文件和一个 CSS 文件。原来的情况是:Webpack 将 CSS 一起打包进入 bundle.js,之后通过在 HTML 中动态新建 style 标签的方式进行 CSS 样式的注入。对比现在,我们单独有了一个 CSS 文件,所以我们同时也需要动态更新 HTML 文件,将 CSS 通过 link 标签引入进来。
分离 CSS 至单文件
这一步要做的可能和上一步“Webapck 单独配置 CSS(SCSS)入口”有一些类似,所以先把区别说清楚:
Webapck 单独配置 CSS(SCSS)入口:我们将 “CSS 通过 JS 引入”这一步移除,直接通过 Webpack 的“多入口配置”对 CSS 进行单独单独引入,但问题是按照我们上面那种做法,即使我们引入了不同的 CSS (SCSS),最后通过编译获得的始终只是一个 app.js 文件(css 被包含其中);
分离 CSS(SCSS) 至单文件:通过引入 Webpack 插件,我们可以将引入 CSS(SCSS),分别压缩成不同的目标文件。比如,我们引入了 a.css 和 b.scss,b 文件中又引入了 c.scss 或者 d.scss,我们最终可以获得两个文件:a.css 和 b.css 的压缩完毕打包版本,而不是上一步那样只得到一个 app.css。
明白了需要做什么,搞清楚了之间的区别,接下来就是怎么做才能达成预期效果。很巧的是,Webpack 本身也不提供多种输出方式的选择,就和对 JS、CSS 和 SCSS 入口文件的处理我们依赖 loader 类似,对于输出的处理,我们需要依赖插件。
安装 mini-css-extract-plugin 插件:
使用(这里直接贴一份修改完毕的 webpack.config.js 的 patch,看上去很乱,下面大致解释做了些什么,同时文末会有清晰的版本):
- 做了一些调整,定义 isProd 常量,来存储现在是“开发环境”,还是“生产环境”,修改了 mode 选项的写法;
- 引入 mini-css-extract-plugin;
- 定义了,如果遇到入口文件是 CSS 或者 SCSS,同时也是“生产环境”时,编译、打包之后,不通过 style-loader 将内容动态输出到页面中(不通过 HTML 头部新建 HEAD 标签的形式),转而根据入口文件的“名称(例子中是 app)”,生成单独的 CSS 样式文件。
- 将原先生成的 module.output 中的 [name].[ext] 替换成 [name].js。
重启 Webpack,不过这次需要输入的是部署生产环境的命令。稍作等待后,结果如期而至:app.js 和 app.css 成功生成(如果打开 app.css 会看到一些重复的对 body 的样式声明,因为测试的时候同时在 main.css 和 main.scss 中写了重复的样式,不需要惊讶)。
配置完 CSS、SCSS 后完整的 webpack.config.js
技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。
BEST
Lien(A.K.A 胡椒)