前言
没想到吧,这个系列还能出4,这篇会介绍Webpack中Css的相关内容,一些JavaScript的内容留到下个文章了。
CSS工程化
为什么要进行css工程化
在前端项目开发中,CSS是不可或缺的一部分,在前端项目不断变得庞大,复杂的今天,传统的CSS在开发中也暴露了一些问题,这驱使前端开发者不断寻找CSS工程化的最佳实践
传统开发会使用一个link标签引入CSS样式
1 | <link rel="stylesheet" href="index.css"> |
这种开发模式有下面几个弊端
- 类名冲突:当页面复杂时,如果层级太浅,很容易导致类名冲突,但是过深的层级会导致代码的编写,阅读和复用变得困难,在打包时也难以压缩代码体积
- 重复样式:在页面中,有一些重复的值(比如主题色)会重复出现在代码中,难以维护和复用
- CSS文件细分问题:在大型项目里,我们希望把CSS按照不同的模块来拆分,这样更有利于维护,比如一个轮播图模块,我们希望依赖的css样式只关心轮播图,这就要求css可以按照不同的模块,功能来拆分,css会比过去拆分的更细。
怎么解决上面的问题
这篇文章会介绍下面列出的方法,这里只是进行一个大致的介绍
类名冲突
命名约定
即提供一种命名的标准,来解决冲突,常见的标准有:
- BEM
- OOCSS
- AMCSS
- SMACSS
CSS in JS
这种方式用js对象来表示样式,然后把样式直接应用到元素的style中,这样一来,css变成了一个一个的对象,就可以利用js的一些特性对css进行操作
- 通过一个函数返回一个样式对象
- 把公共的样式提取到公共模块中返回
- 应用js的各种特性操作对象,比如:混合、提取、拆分
React-Native中就使用了这个方式
css module
非常好用的css模块化方案,这个方案会根据css文件的路径和类名生成一个新类名,在页面上应用这个新类名就可以避免重名了
解决重复样式
CSS in JS
既然是在JS里写CSS,自然可以解决重复样式,因为所有的公共部分都可以提取出来,然后需要时通过函数混合就行
预处理语言
预编译语言,你可以理解成CSS的高级版,它支持变量,函数等语法,然后在打包成项目时,会通过编译器转化成正常的CSS
常见的预处理语言有
- scss
- sass
- less
解决CSS文件细分问题
这部分可以使用构建工具解决,比如webpack
在webpack中使用一些loader或者plugin用来处理,打包,合并,压缩CSS文件
在Webpack处理CSS
使用css-loader和style-loader插入样式
安装css-loader
和style-loader
1 | npm install css-loader style-loader -D |
修改webpack.config.js
1 | const path = require("path"); |
这里我们只修改了css的处理方式,改为使用css-loader和style-loader来处理,运行下面的命令
1 | npm run build |
打包成功
1 | E:\Workspaces\Project\WebPro\WebpackTest>npm run build |
查看打包的页面,可以看到页面中多了一个style元素
这样打包就成功了
简单的css-loader和style-loader解析
我们来看看这两个loader的作用吧,先看css-loader
The
css-loader
interprets@import
andurl()
likeimport/require()
and will resolve them.
从介绍中我们可以看出,css-loader会解析@import和url这样的语法,举个例子你就知道了
1 | .red{ |
经过css-loader转换后变成js代码:
1 | module.exports = `.red{ |
再例如:
1 | .red{ |
经过css-loader转换后变成:
1 | var import1 = require("./bg.png"); |
webpack会把上面的代码转化成AST,然后进行依赖查找,这样Webpack会把依赖./bg.png
添加到模块列表,最后把代码转化为
1 | var import1 = __webpack_require__("./src/bg.png"); |
再例如:
1 | @import "./reset.css"; |
在css-loader处理后会变成
1 | var import1 = require("./reset.css"); |
webpack进行处理后
1 | var import1 = __webpack_require__("./reset.css"); |
上面给出的css-loader导出结果是经过简化过的,实际上css-loader会导出更复杂的结果,但是核心思想还是这个
总结一下,css-loader做了两件事
将css文件的内容作为字符串导出
将css中的其他依赖作为require导入,以便webpack分析依赖
Inject CSS into the DOM.
style-loader会把经过css-loader导出的结果经过处理后插入到页面上
例如:
1 | .red{ |
经过css-loader转换后变成js代码:
1 | module.exports = `.red{ |
经过style-loader转换后变成:
1 | module.exports = `.red{ |
style-loader的处理结果也是简化过的,实际上处理远比比这个复杂
使用mini-css-extract-plugin提取css文件
目前,css代码被css-loader转换后,交给的是style-loader进行处理。
style-loader使用的方式是用一段js代码,将样式加入到style元素中。
而实际的开发中,我们往往希望依赖的样式最终形成一个css文件。
这时候我们可以使用mini-css-extract-plugin
这个库
该库提供了1个plugin和1个loader
- plugin:负责生成css文件
- loader:负责记录要生成的css文件的内容,同时导出开启css-module后的样式对象
安装mini-css-extract-plugin
1 | npm install mini-css-extract-plugin -D |
使用
1 | const MiniCssExtractPlugin = require("mini-css-extract-plugin") |
完整的webpack.config.js
1 | const path = require("path"); |
运行命令
1 | npm run build |
打包成功
如果你出现了下面的问题
这是webpack版本的问题,安装mini-css-extract-plugin
时我看到有这个东西
嘛,于是我Webpack更新到5.13.0问题就解决了
index.css的内容
1 | :root, body { |
打包结果
html/index.html
1 |
|
可以看到,打包后的html通过link标签引入css文件,而要引入哪个模块的css文件,可以在HtmlWebpackPlugin的chunks里进行配置
开启本地服务器
页面正常运行
命名约定
这里我们使用BEM来举例
BEM是一套针对css类样式的命名方法。
其他命名方法还有:OOCSS、AMCSS、SMACSS等等
BEM全称是:Block Element Modifier
三个部分的具体含义为:
- Block:页面中的大区域,表示最顶级的划分,例如:轮播图(
banner
)、布局(layout
)、文章(article
)等等 - element:区域中的组成部分,例如:轮播图中的横幅图片(
banner__img
)、轮播图中的容器(banner__container
)、布局中的头部(layout__header
)、文章中的标题(article_title
) - modifier:可选。通常表示状态,例如:处于展开状态的布局左边栏(
layout__left_expand
)、处于选中状态的轮播图小圆点(banner__dot_selected
)
例如:banner__dot_selected
,可以表示:轮播图中,处于选中状态的小圆点
另外,在某些大型工程中,如果使用BEM命名法,还可能会增加一个前缀,来表示类名的用途,常见的前缀有:
- l: layout,表示这个样式是用于布局的
- c: component,表示这个样式是一个组件,即一个功能区域
- u: util,表示这个样式是一个通用的、工具性质的样式
- j: javascript,表示这个样式没有实际意义,是专门提供给js获取元素使用的
CSS in JS
css in js简单来说就是:用一个JS对象来描述样式
比如下面的对象就描述了一组样式
1 | const styles = { |
由于这种描述样式的方式根本就不存在类名,自然不会有类名冲突
至于如何把样式应用到界面上,不是它所关心的事情,你可以用任何技术、任何框架、任何方式将它应用到界面。
举个例子
修改index.html
1 |
|
修改index.js
1 | import css from "./style/index.css"; |
在util/index.js
中添加下面的代码
1 | function applyStyles(dom, ...styles) { |
这样我们就可以把用对象描述的css应用到页面上了
你也可以使用jquery,代码会更简单
1 | const styles = { |
css module
css module 遵循以下思路解决类名冲突问题:
- css的类名冲突往往发生在大型项目中
- 大型项目往往会使用构建工具(webpack等)搭建工程
- 构建工具允许将css样式切分为更加精细的模块
- 同JS的变量一样,每个css模块文件中难以出现冲突的类名,冲突的类名往往发生在不同的css模块文件中
- 只需要保证构建工具在合并样式代码后不会出现类名冲突即可
因此,css module引入了局部作用域这个概念,让css样式只能在对应的模块中生效
我们来试一下,修改webpack.config.js
1 | module.exports = { |
新建component1.css
1 | .box { |
新建component2.css
1 | .box { |
修改index.html
1 |
|
我们假设component1和component2是两个不同的组件,我们这两个组件的样式是隔离的
在index.js中导入
1 | import "./style/index.css"; |
重新打包,查看打包结果
可以看到,最终的代码文件中,不再使用box这个类名,而是分别变成了两个值,这就是css-module的实现局部作用域的方法,css-loader会将样式中的类名进行转换,根据模块路径和类名生成一个hash,然后把这个类名进行重命名,因此,不同的css模块,哪怕具有相同的类名,转换后的hash值也不一样。
但是问题就来了,源代码的类名和最终生成的类名是不一样的,而开发者只知道自己写的源代码中的类名,并不知道最终的类名是什么,那我们必须要想办法把最终的类名应用到元素上
css-loader会导出一个包含了描述原类名和最终类名对应关系的对象(导出的对象.locals就是我们要的对象了)
在index.js
中的打印一下导出结果(因为MiniCssExtractPlugin会把css-loader导出的对象.locals重新导出,所以这里可以打印)
1 | import index from "./style/index.css"; |
我们修改一下index.js
1 | import index from "./style/index.css"; |
这样就可以应用了
全局类名
某些类名是全局的、静态的,不需要进行转换,仅需要在类名位置使用一个特殊的语法即可:
1 | :global(.main){ |
使用了global的类名不会进行转换,相反的,没有使用global的类名,表示默认使用了local
1 | :local(.main) { |
使用了local的类名表示局部类名,是可能会造成冲突的类名,会被css module进行转换
配置类名
如果你想控制最后输出的类名,可以配置localIdentName
这个选项
1 | module.exports = { |
打包后代码就会变成这样
1 | .box-36382 { |
tips
- css module往往配合构建工具使用
- css module仅处理顶级类名(第一个类名),尽量不要书写嵌套的类名,也没有这个必要
- css module仅处理类名,不处理其他选择器
- css module还会处理id选择器
- 使用了css module后,只要能做到让类名望文知意即可,不需要遵守其他任何的命名规范
ps
事实上,如果你在一些框架里使用css-module,就会方便许多,比如React
预处理语言
编写css时,受限于css语言本身,常常难以处理一些问题:
- 重复的样式值:例如常用颜色、常用尺寸
- 重复的代码段:例如绝对定位居中、清除浮动
- 重复的嵌套书写
由此,预处理语言出现了,我们可以直接书写预处理语言来书写代码,虽然预处理语言的代码不能被浏览器直接识别,但通过一个编译器,就可以把预处理语言转化成css
这里我们介绍less和scss
less官网:http://lesscss.org/
less中文文档1(非官方):http://lesscss.cn/
less中文文档2(非官方):https://less.bootcss.com/
sass官网:https://sass-lang.com/
sass中文文档1(非官方):https://www.sass.hk/
sass中文文档2(非官方):https://sass.bootcss.com/
安装less和scss
首先安装less和scss
1 | npm install sass less -D |
然后新建两个文件less-style.less
和scss-style.scss
less-style.less
1 | @base-color : #c6538c; |
scss-style.scss
1 | $base-color : #c6538c; |
转化less
1 | lessc ./src/style/less-style.less ./src/style/less-style.css |
转化scss
1 | sass ./src/style/scss-style.scss ./src/style/scss-style.css |
转化结果
最简单的用法就是这样了,但我们希望能在开发时直接书写预处理语言的文件,最后打包时webpack帮我们转化成css,我们就要进行相应的配置了
在webpack中使用
我们要先安装相应的loader
1 | npm install less-loader sass-loader -D |
在webpack.config.js
中导入
1 | const cssLoaderConfig = { |
打包结果
使用就是这么简单,具体的语法可以去看官方文档,还是挺详细的,而且不需要英文,康康代码就能看懂
PostCSS
官网地址:https://postcss.org/
github地址:https://github.com/postcss/postcss
Api:http://api.postcss.org/
PostCSS是一个代码编译器,可以把你写的代码转化成最终的css代码
听起来是不是有点像sass这样的预处理语言,他们的功能其实有相同之处,不过PostCSS是一个编译器,只负责把源代码转化成AST语法树,然后调用插件处理AST树,最后再把AST语法树转化回代码,简单来说就是,PostCSS只负责代码转化,具体的功能需要单独的插件来使用,你可以自由选择插件,甚至可以自己写一个,sass如同定义,是一个语言,虽然最后要被编译成CSS才能执行
放个图吧
这就是PostCSS的工作流,Parser负责把CSS转化成AST,Stringifier负责把AST重新转化为CSS
安装
PostCss是基于node编写的,因此可以使用npm安装
安装postcss和cli工具
1 | npm install postcss postcss-cli -D |
postcss库提供了对应的Api用于转换代码,postcss-cli允许我们通过命令行调用postcss中的api来完成编译
1 | postcss 源文件路径 -o 输出路径 |
比如
1 | postcss ./src/style/postcss-style.pcss -o ./src/style/postcss-style.css |
输出结果
配置文件
单纯使用postcss没有意义,因为postcss转化代码需要插件,在执行上面的命令时,postcss给了我们一个提示
1 | You did not set any plugins, parser, or stringifier. Right now, PostCSS does nothing. Pick plugins for your case on https://www.postcss.parts |
所以我们要弄点插件进去,那么要弄插件自然要先有个配置文件,postcss的配置文件默认名称是postcss.config.js
我们新建一个postcss.config.js
文件,加入一些配置
1 | module.exports = { |
使用插件
postcss-preset-env
过去使用postcss的时候,往往会使用大量的插件,它们各自解决一些问题
这样导致的结果是安装插件、配置插件都特别的繁琐
于是出现了这么一个插件postcss-preset-env
,它称之为postcss预设环境
,大意就是它整合了很多的常用插件到一起,并帮你完成了基本的配置,你只需要安装它一个插件,就相当于安装了很多插件了。
安装插件
1 | cnpm install postcss-preset-env -D |
安装好该插件后,在postcss配置中加入下面的配置
1 | module.exports = { |
自动补全浏览器前缀
某些新的css样式需要在旧版本浏览器中使用厂商前缀方可实现
例如
1 | ::placeholder { |
该功能在不同的旧版本浏览器中需要书写为
1 | ::-webkit-input-placeholder { |
要完成这件事情,需要使用autoprefixer
库。
而postcss-preset-env
内部包含了该库,自动有了该功能。
我们再次运行一次命令,可以看到输出的文件已经补全了前缀
如果需要调整兼容的浏览器范围,可以通过下面的方式进行配置
方式1:在postcss-preset-env的配置中加入browsers
1 | module.exports = { |
方式2:添加 .browserslistrc 文件
这个方式比较推荐,因为这个文件,在以后需要做其他的浏览器兼容性处理时,其他插件也会读取这个文件,比如babel也会读取这个文件
创建文件.browserslistrc
,填写配置内容
1 | last 2 version |
方式3:在package.json的配置中加入browserslist
这个方式也是比较推荐的,理由同上
1 | "browserslist": [ |
browserslist
是一个多行的(数组形式的)标准字符串。
它的书写规范多而繁琐,详情见:https://github.com/browserslist/browserslist
一般情况下,大部分网站都使用下面的格式进行书写
1 | last 2 version |
last 2 version
: 浏览器的兼容最近期的两个版本> 1% in CN
: 匹配中国大于1%的人使用的浏览器,in CN
可省略not ie <= 8
: 排除掉版本号小于等于8的IE浏览器
默认情况下,匹配的结果求的是并集
你可以通过网站:https://browserl.ist/ 对配置结果覆盖的浏览器进行查询,查询时,多行之间使用英文逗号分割
browserlist的数据来自于CanIUse网站,由于数据并非实时的,所以不会特别准确
未来的CSS语法
CSS的某些前沿语法正在制定过程中,没有形成真正的标准,如果希望使用这部分语法,为了浏览器兼容性,需要进行编译。过去,完成该语法编译的是cssnext
库,不过有了postcss-preset-env
后,它自动包含了该功能。
你可以通过postcss-preset-env
的stage
配置,告知postcss-preset-env
需要对哪个阶段的css语法进行兼容处理,它的默认值为2
1 | "postcss-preset-env": { |
一共有5个阶段可配置:
- Stage 0: Aspirational - 只是一个早期草案,极其不稳定
- Stage 1: Experimental - 仍然极其不稳定,但是提议已被W3C公认
- Stage 2: Allowable - 虽然还是不稳定,但已经可以使用了
- Stage 3: Embraced - 比较稳定,可能将来会发生一些小的变化,它即将成为最终的标准
- Stage 4: Standardized - 所有主流浏览器都应该支持的W3C标准
下面我们来试一下
1 | :root { |
编译后
1 | :root{ |
编译后,仍然可以看到原语法,因为某些新语法的存在并不会影响浏览器的渲染,尽管浏览器可能不认识
如果不希望在结果中看到新语法,可以配置postcss-preset-env
的preserve
为false
postcss-apply
安装
1 | npm install postcss-apply -D |
该插件可以支持在css中书写属性集
类似于LESS中的混入,可以利用CSS的新语法定义一个CSS代码片段,然后在需要的时候应用它
1 | :root { |
编译后
1 | .item{ |
postcss-color-function
安装
1 | npm install postcss-color-function -D |
该插件支持在源码中使用一些颜色函数
1 | body { |
编译后
1 | body { |
postcss-import
该插件可以让你在postcss
文件中导入其他样式代码,通过该插件可以将它们合并
安装
1 | npm install postcss-import -D |
应用
1 | module.exports = { |
在Webpack中使用
安装postcss-loader
1 | npm install postcss-loader -D |
配置
1 | module.exports = { |
我们把postcss-loader
加到css-loader
前,postcss-loader
会调用插件处理源代码,然后再把生成的css代码传给css-loader
用文档的原话:
After setting up your
postcss.config.js
, addpostcss-loader
to yourwebpack.config.js
. You can use it standalone or in conjunction withcss-loader
(recommended).Use it before
css-loader
andstyle-loader
, but after other preprocessor loaders like e.gsass|less|stylus-loader
, if you use any (since webpack loaders evaluate right to left/bottom to top).
所以如果你要给在sass里用,把postcss-loader
放到sass-loader
前面就可以了
1 | const path = require("path"); |
后记
啊,CSS的部分就这样告一段落了,下一篇我们看看针对JS的基本配置,不咕的话,明天或者后天写完