from: http://www.jianshu.com/p/6ce8619dc674
从css谈模块化
模块化是现今我们随处都可以听到的一个名词,什么是模块化?为什么我们需要模块化?这是本系列文章我们要弄明白的一个问题。我们也借这部分内容,顺带回顾一下前端的发展历程。
说实话,模块化这个主题有点大,我一时也不知道从哪里讲起比较合适,通常来说,前端的工作内容主要涉及三个方面:html、css、js(javascript),其他的像as(actionscript,flash的脚本语言)、jsp、smarty等等模版类的语法标记我们在此就先略去了,因为不是特别重要。那我们所说的模块化也可以分别当成这三条线去看,如html的模块化、css的模块化,以及js的模块化,这三者我们称为(web)前端模块化,搞清楚这几者的关系,我们接下来要了解的事情已经很清晰了。
我们单独来讲,我想先从css,因为这是我认为最容易入手且非常鲜明的一块内容。
背景
起初的.css是长什么样的?
/* index.css */
body {
margin: 0;
padding: 0;
font-size: 18px;
}
.box {
background: #333;
color: #fff;
}
.box .list {
margin-left: 10px;
}
.box .list .item {
border-bottom: 1px solid #ccc;
}
.box .list .item:last-child {
border-bottom: 0;
}
.box .list .item a {
text-decoration: none;
color: #fff;
}
.box .list .item span {
color: red;
}
.box .list .item a ... {
...
}
是不是很熟悉?一个简单的手打的列表模块。
这里存在的问题是:
- 按照这个顺序写下去,选择器会越写越长,造成书写累赘;
- 越来越长的选择器容易使我们混淆dom的空间顺序,想象如果有好几个平级的选择器(如.box .list .item a与.box .list .item span),我们可能一时看不出这两者的关系,是父子还是兄弟元素?
- 维护困难,假设我们需要重构这个box,在.box和.list之间加入一层.wrap,在.item与a和span之间加入一层.block,那简直就是个灾难,我们要谨慎地找到确切的位置,然后再找到所有匹配的、长长的选择器,在合适的位置全部做修改;
- 我们很难从做到复用,假设我们在另外一个页面也需要这个box,那我们就需要把所有跟box相关的部分复制粘贴一份,而当这个box需要修改的时候,我们可能要重新找出所有用到这个box的地方,然后又是复制粘贴一份——当然,有人说这个问题是可以通过一定的方法解决的,我们后面再来讨论这个问题;
……
(欢迎补充css的开发痛点)
事实上我们手打css遇到的问题可以大致归纳为以下几点:
- 选择器繁琐冗长;
- 命名冲突;
- 层级结构不清晰;
- 代码难以复用;
问题很多,怎么去解决?由于css的发展很缓慢,在工具上也一直没有进展,导致这些问题,比如上述的第一点,几乎无解。所以问题往往要依赖“规范”来解决。我们先来看代码复用。
复用
要实现代码复用很简单,我们只需要提供一个公共css库,来存放我们的公共样式以及公共模块即可:
/* common.css */
body {
background: #fff;
color: #333;
font-size: 16px;
}
.box ... {
background: #333;
color: #fff;
...
}
.another-box ... {
...
}
然后我们在其他的css文件中引用这个common.css,这样就实现了代码的复用,只要是想全局共享的样式和模块,只要在这里添加进去就可以了。
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
<link rel="stylesheet" type="text/css" href="./style/common.css">
<link rel="stylesheet" type="text/css" href="./style/index.css">
</head>
<body>
<div class="box">
...
</div>
<div class="another-box">
...
</div>
</body>
</html>
很完美,
——至少目前为止是这样没错。
这里我们来探究几种情况:
- 假设我们这个项目非常大,大概有20个页面这么多,那么我们每做一个页面就会往common里面补充3~4个公共样式/模块,那么在这个项目开发完成以后,common的体积可能要比其他css的体积都大;
- 假设有几个这样的页面,他们本身内容非常少,比如404页面,可能只需要用到少量的公共样式,但是由于考虑到维护问题,我们还是要引入common(单独写样式会使得该页面在common更新的时候无法同步得到更新),这就使得一个页面变得很“重”;
- 由于common越写越大,它所占用的命名就越多,那么我们在引入common的时候,即使我们页面还什么都没有,但已经默认被占用了很多的命名,使得我们在某个页面的可用命名变少,而且是越来越少;
- 我们在common中书写公共模块,在具体页面的私有css里书写私有模块,假设现在我们需要全局添加一个公共模块.nice-box,我们发现,这个模块名已经在index.css中被占用了,于是我们试着把名字改成.handsome-box,却又发现这个名字在about.css中被占用了,哦买噶的!
……
瞬间整个人都不好了,内心充满了绝望,心想还是转行吧,垃圾语言毁我一生。
我们分析一下,上面这些情况其实重点只有两个:一、冗余;二、污染。冗余是难免的,为了维护牺牲一部分灵活性也是可以接受的,不过我们需要用一些方式来减少这样的冗余,避免让它成为负担;而污染,却是亟待解决的问题,这颗定时炸弹随着项目的增大最终会变成一场无法挽回的灾难!这时候就体现出了命名规范的重要性了。
这就要求我们用一套合理的规范来约束和组织我们的代码。
规范
编程规范使得我们的项目在一定程度上是可维护的。比如针对类名污染制定了命名规范,针对选择器指定了书写选择器所要遵循的规范,等等。这些规范都在一定程度上约束了css的书写,使得项目不至于混乱。网易的NEC是其中一种比较完整的解决方案。有兴趣的童鞋可以搜索了解,笔者本身受nec影响较大,所以以下内容有一定程度的雷同。
现在我们来试着制定一套css编程规范,来解决以上提到的问题。
我们规定页面由且只由几种基本结构体构成:框架、模块,以及元件。其他零散的元素,除了是作为模块的辅助类,否则不能独立于这三者存在。
框架
框架是指构成页面的基础结构,它是一个页面的筋骨。我们假设有个页面index.html,它的整体最外围表现为一个class为.g-index的div,然后它由页头(.g-hd)、主体(.g-bd)、页脚(.g-ft)三个部分组成:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
</head>
<body>
<div class="g-index">
<div class="g-hd"></div>
<div class="g-bd"></div>
<div class="g-ft"></div>
</div>
</body>
</html>
这样我们就大概能描绘出一个页面的基本轮廓了。紧接着我们来给它补充一些模块。
模块
模块是页面上数量最多,同时也是最重要的部分,它是代码复用的主体部分,是一个个按照功能划分的区域,如导航栏、轮播图、登录窗口、信息列表等等,模块之间相互独立,分布在页面上,嵌在框架的各个位置上,组成一个丰富多彩的页面。
还是以index.html为例,我们假设页头有个导航栏模块(.m-nav),主体有个新闻列表模块(.m-news),页脚有个版权声明模块(.m-copy_right):
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
</head>
<body>
<div class="g-index">
<div class="g-hd">
<div class="m-nav">
nav
</div>
</div>
<div class="g-bd">
<div class="m-news">
news
</div>
</div>
<div class="g-ft">
<div class="m-copy_right">
copy_right
</div>
</div>
</div>
</body>
</html>
元件
元件是独立的、可重复使用的,并且在某些情况下可以作为模块的组成部分的一种细颗粒。比如一个按钮,一个logo等等。某种意义上说,它其实可以等同于模块,因为它们两者的区别只是规模不同而已。模块更强调一个功能完整的整体,而元件则更强调独立性。
我们假设这个页面还需要在页头放个logo(.u-logo),在导航栏中放置一个登录按钮(.u-login_btn):
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
</head>
<body>
<div class="g-index">
<div class="g-hd">
<img class="u-logo" alt="logo">
<div class="m-nav">
nav
<a href="/logoin" class="u-login_btn">登录</a>
</div>
</div>
<div class="g-bd">
<div class="m-news">
news
</div>
</div>
<div class="g-ft">
<div class="m-copy_right">
copy_right
</div>
</div>
</div>
</body>
</html>
三种基本结构体介绍完,我们回来总结一下。你一定发现了一个现象,在搭建框架的时候,我给框架元素命名用g-
开头,给模块命名使用m-
,给元件命名使用u-
。这是命名规范的一部分,我们使用这三个前缀给相应结构体命名,就是为了更好地标志一个结构体,更好地展示它的功用,这也是我们常说的 语义化 ,同时也能实现隔离作用,起到类似命名空间的效果。
- 框架的命名以
g-
开头,一般与页面同名,比如index.html,那框架就是最外层就是.g-index,about.html就是.g-about,以此类推,其他常用的内部结构有.g-hd(header)、.g-bd(body)、.g-ft(footer)、.g-sd(side)、.g-mn(main)等等; - 模块命名以
m-
开头,一般以相对应的用途来命名,比如导航栏m-nav
、新闻m-news
、版权m-copy_right
等等,一般来说模块名是唯一的,而且模块本身应该是可移植、可复用的; - 元件命名以
u-
开头,一般以自身含义来命名,比如u-logo
表示一个logo,u-btn
表示一个按钮。
那么除却框架、模块、元件的相关命名内容之外,命名规范还有以下几点内容:
- 命名尽量以缩写的方式,言简意赅地表达,比如用
bd
表达body
,用nav
表达navigator
等,使用长长的单词显得多余又臃肿; - 前缀与名称之间用
-
连接,而名称之间的若干单词以_
连接,组合单词除外,如side-menu
; z-
开头表示状态,如z-active
、z-succ
、z-disabled
等等;- 可以根据需要定制其他开头,但是请尽量将分类控制在少数,因为太多的分类反而造成困惑和不必要的分类开销,其实gmuz就已经可以满足日常开发了。
重构common
有了命名规范,我们可以对common进行一次改写:
/* common.css */
body {
background: #fff;
color: #333;
font-size: 16px;
}
.m-nav { ... }
.m-news { ... }
.m-copy_right { ... }
ok,现在我们一定程度上缓解了“污染”的问题,至少按照命名规范,我们的common构成由原来笼统的一类,变成了现在gmuz四类,变得更加可管理且“没那么容易冲突”了,但是这还远没有解决“污染”。
以下为了方便表述,我们把common.css称为“common”,把对应页面的css,比如index.html -> index.css、about.html -> about.css,称为“页面css”。
这里有个问题需要细致思考一下:模块的属性。理论上讲,一个模块应该是公有或者私有的,假设一个模块它基本只可能在某个页面用,或者我们不打算在其他页面用到它,我们可以说这个模块是这个页面的私有模块,比如文章页里的文章列表模块(m-article_list),以及组成这个模块的列表单元元件(u-article_item),我们基本可以确定这两者不会在其他页面被复用到了,那么它们其实是已经默认私有的属性,没必要放在common里,直接放在article.css就可以了。这样也可以人为地减少common的体积。那么问题来了,如果模块既可以存放在common,又可以存放在页面css,那么我们后续在common中添加公共模块的时候,如何避免模块名已经在页面css中被占用的情况?(即上文对common的设计提问的第4点)
我曾经跟一位同事针对“后续添加公共模块可能与其他页面的私有模块命名冲突”的问题进行探讨,最后我们得出两种解决方案:
- 默认由common管理所有模块,所有模块默认为公共模块,不允许私有模块;
- 为公共模块单独使用一种前缀
cm-
来做区分,所有m-
前缀的模块都是私有模块。
第一种方案会使得common体积非常大,而且会一直增大,不可取;第二种方案显式地声明模块属性,以此来避免冲突,可取。
于是乎又变成了:
/* common.css */
body {
background: #fff;
color: #333;
font-size: 16px;
}
.cm-nav { ... }
.cm-news { ... }
.cm-copy_right { ... }
而我们的私有模块是这样的:
/* index.css */
.g-index {
background: #fff;
color: #333;
font-size: 16px;
}
.m-nav { ... }
.m-news { ... }
.m-copy_right { ... }
这样子处理之后,我们的公共模块和私有模块之间的命名冲突就解决了,而且也不会出现“一个还什么都没有页面引用了common之后,许多的类名就被占用了”的情况,因为common绝大部分内容都是cm
模块,而页面自己的css里只能拥有私有的m
模块。
显然这种方案是可行的,但是我们会多一种前缀,而且还略丑。但就当时的手打css技术来说,我们没有其他更好的解决方案,这个问题就到这里暂算了结。我们解决了问题,但是方法还不够好,后续等我们提及css预处理语言的时候,我们会提出一些更好的解决方案。
好,接下来我们思考这样一个问题:假设我们已经写完了index页面,接着写about页面,这时候我们发现,原本在index中的一个模块m-news
,我们将它归为私有模块,而现在在about中居然也需要用到这一个模块,于是乎,我们重新回到index页面,把m-news
模块从index.css转移到了common.css当中,并改名为cm-news
,然后回到index页面,把与m-news
相关的内容(html、js)都修改成cm-news
。这还是在我们能够意识到的情况下做的,如果页面多了起来,我们根本没有印象哪个页面是不是也有这样一个模块,要不要把它提升为公共模块。一个月之后,这个项目一个星期前已经搞定了,现在需要进行后续的开发,加多一个contact页面,然后我们又发现,这页面里用到了一个原本我们在about页面里把它划为私有模块的m-loc
,于是乎,我们又走了一遍提升公共模块的流程。。。
为什么会出现这样的问题?根本原因在于,我们无法事先规划好所有的模块,无法在一开始就对一个模块的属性清晰地划分。这个问题也基本算是无解。矛盾在于,我们对模块进行了私有和公有的属性划分,却无法事先掌握所有的模块属性,只能走一步算一步,错了就回来再改改。
解决这问题的办法是,取消对模块的属性划分,所有模块都默认为公共模块,可以随时取用。但是这样就倒退回了我们之前的那种情况,所有的模块都是m-*
,且都扎堆在common里,导致common的体积过大,所以这个问题只能到这里为止了。
模块
如何界定一个模块?或者说,怎么样才能把一部分代码划分为一个模块?划分的依据是什么?这是我们接下去要探讨的问题。
设计原则
我们说模块是一个功能相对独立且完整的结构体,其实这应该是 组件 的概念,我们这里只从css的范围内来探讨模块化,那么模块的定义就可以缩窄到:一个(组)样式相对独立且完整的类。比如:
/* copy_right */
.m-copy_right {
color: #ccc;
background: #666;
font-size: 14px;
text-align: center;
padding: 20px 0;
line-height: 1.8;
}
/* nav */
.m-nav {
color: #ccc;
background: #666;
font-size: 14px;
}
.m-nav .u-logo { ... }
.m-nav .list { ... }
.m-nav .list .item { ... }
原则上来讲,一个css模块应该遵循以下几点要求:
- 只对外暴露一个类名;
/**
* 正确示范,所有模块相关的代码都挂在模块的选择器名下
*/
.m-nav { ... }
.m-nav .list { ... }
.m-nav .list .item { ... }
/**
* 错误示范,暴露了.m-nav和.list两个类名,污染了空间
*/
.m-nav { ... }
.list { ... }
.list .item { ... }
- 不影响周围布局:一般情况下,尽量不要使用一个脱离文档流的布局(既使用了float:left/right,position:absolute/fixed的布局),尽量不要使用外边距(margin)。这是为了使得模块更加稳定、具备更高的可塑性;
/**
* 正确示范,在common中定义一个模块,在页面css中对模块进行定位和偏移
*/
/* common */
.u-logo {
width: 100px;
height: 100px;
}
.cm-news {
width: 200px;
height: 100px;
}
/* index */
.u-logo {
position: absolute;
left: 20px;
top: 20px;
}
.cm-news {
margin-top: 50px;
}
/**
* 错误示范,在common中定义一个模块并固定它的位置
*/
/* common */
.u-logo {
width: 100px;
height: 100px;
position: absolute;
left: 20px;
top: 20px;
}
.cm-news {
width: 200px;
height: 100px;
margin-top: 50px;
}
- 模块尽量设计为方便复用的量级,避免大而全,求精巧;
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
</head>
<body>
<div class="g-index">
<div class="g-bd">
<!-- 正确的示范 -->
<!-- 创建一个大的内容块article_box,而不是一个大模块 -->
<div class="article_box">
<div class="hd">
最新文章
</div>
<div class="bd">
<div class="list">
<!-- 这里我们把每一个项作为可复用的私有模块 -->
<div class="m-list_item">
<img class="cover" />
<div class="info">
<div class="title">
<a href="#">文章标题</a>
</div>
<div class="desc">文章简介</div>
</div>
</div>
</div>
</div>
<div class="ft">
<!-- 这里我们直接引入了一个公共分页模块 -->
<div class="cm-page">
<a href="#" class="pg">1</a>
<a href="#" class="pg">2</a>
<a href="#" class="pg">3</a>
<a href="#" class="pg">4</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
</head>
<body>
<div class="g-index">
<div class="g-bd">
<!-- 错误的示范 -->
<!-- 创建一个庞大且不可复用的私有模块m-article_box -->
<div class="m-article_box">
<div class="hd">
最新文章
</div>
<div class="bd">
<div class="list">
<div class="item">
<img class="cover" />
<div class="info">
<div class="title">
<a href="#">文章标题</a>
</div>
<div class="desc">文章简介</div>
</div>
</div>
</div>
</div>
<div class="ft">
<div class="page">
<a href="#" class="pg">1</a>
<a href="#" class="pg">2</a>
<a href="#" class="pg">3</a>
<a href="#" class="pg">4</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
值得注意的是,这里的原则第三点,并不只是出于css模块化的考虑,事实上这更适用于 组件化 的设计思路,这在后面讲组件化的时候我们会提到。
继承
css的继承也是很简单的,一般来说是有这么几种方式:
- 在css中并写两个类,如
.cm-nav, .m-nav
,我们知道,这相当于让两个(组)类共享一套样式,然后我们再单独对.m-nav进行补充,实现继承和定制; - 在class属性里并写两个类,如
<img class="u-logo logo">
,这样我们只需要在页面css中单独对.logo类进行补充,就可以实现定制; - 在页面css中直接对类进行引用
,然后补充样式,实现定制,如.cm-nav { margin-bottom: 20px; }
;
……
(还有许多黑魔法,欢迎补充)
第一种在我们这套模式里是不可取的,因为我们的公共模块都是放在common里,不可能每继承一次就上去补一个类;
第二种可取,但是需要多一个近似的类名,不提倡;
第三种又简单又靠谱。
/* common.css */
body {
background: #fff;
color: #333;
font-size: 16px;
}
.cm-nav {
width: 100%;
height: 50px;
color: #fff;
background: #333;
}
我们在页面css可以这样用:
/* index.css */
.g-index {
background: #fff;
color: #333;
font-size: 16px;
}
.cm-nav {
width: 1000px; /* 样式覆盖 */
margin: auto; /* 样式增加 */
}
状态
我们在上面讲前缀的时候,提到过一个前缀z-
,我们说它可以用来表示状态。一个模块是可以有 状态 的,当然,这里说的不是状态好状态差的意思(模块还成精了~),这里指的是有多种表现形式,我们举例来说,一个弹窗模块m-dialog
,它应该至少具备两种状态:显示和隐藏(关闭)。我们用关键字 active 来表示这两种状态,添加z-active
类表示显示,不加表示隐藏。如下:
/* index.css */
.m-dialog {
display: none;
}
.m-dialog.z-active {
display: block;
}
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
</head>
<body>
<div class="g-index">
<div class="g-bd">
<div class="m-dialog">
这是一个未激活的弹窗,你看不到!
</div>
<div class="m-dialog z-active">
这是一个已激活的弹窗,你看得到!
</div>
</div>
</div>
</body>
</html>
弹窗一个比较有代表性的例子,另一个典型的例子是按钮,用过bootstrap的人都知道,按钮btn
只需要相对应添加几个状态类,就可以有不同的配色方案,应付不同的场景需要,这其实就是我们的z-
的含义。z-
是很常用的,我们应该把我们的模块设计得尽量满足多种可预见的需求,而不是每次都在页面去定制和覆盖基本样式。
总结
到目前为止,我们已经定制了一套基于规范的css模块化生产方式,虽然并不完美,但这却也是一套简单的、零工具、零成本的解决方案,适用于任何项目。
可以看出,我们这里所谓的模块化,其实是规范化的子集,通过制定了一套规范,才产生了模块。所以css的模块化过程其实是css规范化的过程。
事实上,由于css本身并不是一门语言,不具备语言的特性,我们只有借助其他方式和工具,才能达到模块化的目的。而目前为止,我们还只是停留在规范的约束方式上,内容看起来比较low :),没关系,下一节我们会开始介绍【css预处理语言的模块化实践】。要知道,对于早已习惯sass编程的我,也是闷着一大口气好不容易才写到了这里的。。。
想必你也留意到了文中多处提到“手打css”这一说法,其实这只是对传统css编程方式的一种戏称,说实话有哪种编程不是手打的,难不成用脚么?哈哈。但是说实话,有了css预处理,模块化才能算得上真正意义的模块化,模块化的意义才凸显出来,因为我们所有的思考与努力,机关算尽,最终的目的都只有一个——提高工作效率。
预告
css预处理语言的出现是一个十分重要的阶段,它直接推进了css的发展,改变了长久以来css的编程方式。
我们来瞥一眼sass:
// index.scss
@import './common/normalize';
@import './common/common';
@import './common/mixin';
.g-index {
@include m-nav;
@include m-news;
@include m-copy_right;
.g-hd {
.m-nav {
width: 100px;
margin: auto;
}
}
.g-bd {
.m-news {
margin-top: 20px;
}
}
.g-ft {
.m-copy_right {
width: 100px;
margin: auto;
}
}
}
sass的选择器嵌套写法是不是亮瞎双眼?文件直接导入是不是很方便?还有变量?还能写函数???
css预处理语言的模块化实践
编写css是前端工作中,一项普通而又频繁的劳动,由于css并不是一门语言,所以在程序设计上显得有些简陋。对于小型项目来说,css的量还不至于庞大,问题没有凸显,而如果要开发和持续维护一个较为大型的项目,那就需要对css进行管理和规范了,否则会发生不可挽回的后果(吓唬谁呢??)。
背景
上一节【从css谈模块化】我们通过规范的约束,将css的编写方式进行了优化和改进,形成一种可持续发展的路线。但还是遗留了一些问题:冗余。虽然我们通过定义公共模块和私有模块,来委婉地分担common的体积,但common的体积还是太大了,而且从设计上考虑,我们应该尽量多地提炼公共模块,以便更好地实现复用。最理想的情况是,所有的模块都寄存在一个公共的库里,哪里需要用到就从库中直接调过来。这个美好的愿望不是不可实现的,借助预处理语言,我们可以很轻易地完成这事情。
预处理语言是一种类css的语言,我们知道css本身不是语言,而预处理语言的诞生,就是为填补这一部分语言功能。它实现了变量、函数、混合的定义,以及文件的引用、合并、压缩功能,使得css也能面向对象,应付复杂庞大的业务。
目前流行的预处理语言主要有两种:less和sass。作为学习,两者都可以入门一下,而作为工作,尽量熟悉一种。我比较常用sass,所以以下内容都是以sass为基本语言做介绍,两者在特性上有很多相似的地方,所以大家不必担心实现上有什么千差万别。
sass
基本语法可以到官网(英语)或者w3cplus sass guide(中文)查看学习,我们这里只简单地过一遍,讲一些我们需要用到的内容,不会面面俱到。
sass有两种后缀名文件:一种后缀名为sass,不使用大括号和分号;另一种就是我们这里使用的scss文件,这种和我们平时写的css文件格式差不多,使用大括号和分号。而本教程中所说的所有sass文件都指后缀名为scss的文件。在此也建议使用后缀名为scss的文件,以避免sass后缀名的严格格式要求报错。 ——摘自w3cplus sass guide
1、嵌套(非常重要的特性)
sass的嵌套包括两种:一种是选择器的嵌套;另一种是属性的嵌套。我们一般说起或用到的都是选择器的嵌套。 ——摘自w3cplus sass guide
选择器嵌套 所谓选择器嵌套指的是在一个选择器中嵌套另一个选择器来实现继承,从而增强了sass文件的结构性和可读性。在选择器嵌套中,可以使用&表示父元素选择器。 ——摘自w3cplus sass guide
// index.scss
.g-index {
...
.g-hd {
...
.m-nav { ... }
}
.g-bd {
...
.m-news { ... }
}
.g-ft {
...
.m-copy_right { ... }
}
.m-dialog {
display: none;
&.z-active { // 留意此处&的用法
display: block;
}
}
}
编译后:
/* index.css */
.g-index { ... }
.g-index .g-hd { ... }
.g-index .g-hd .m-nav { ... }
.g-index .g-bd { ... }
.g-index .g-bd .m-news { ... }
.g-index .g-ft { ... }
.g-index .g-ft .m-copy_right { ... }
.g-index .m-dialog {
display: none;
}
.g-index .m-dialog.z-active { // 留意此处&的编译结果
display: block;
}
是不是爽歪歪?再也不用一遍一遍地去复制和修改一大堆的选择器,也不需要去整理它们之间的关系,只需要嵌套一下,所有的关系就如同直接看dom一样简单明了了!解放双手,解放双眼,同时还提高效率。值得留意的是,我们书写sass的时候,应该尽量保持sass的嵌套顺序与dom一致,注意,是嵌套顺序一致,而不是层次一致,因为并不是dom里的所有元素都需要写样式。
我们再来提一个场景,说明sass的嵌套写法利于维护,假设g-bd
下原本有个模块m-article_box
,现在我们要把m-article_box
从g-bd
迁移到g-hd
中(当然这个需求有些不合理~),我们来看原始代码:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
</head>
<body>
<div class="g-index">
<div class="g-bd">
<div class="m-article_box">
<div class="hd">
最新文章
</div>
<div class="bd">
<div class="list">
<div class="item">
<img class="cover" />
<div class="info">
<div class="title">
<a href="#">文章标题</a>
</div>
<div class="desc">文章简介</div>
</div>
</div>
</div>
</div>
<div class="ft">
<div class="page">
<a href="#" class="pg">1</a>
<a href="#" class="pg">2</a>
<a href="#" class="pg">3</a>
<a href="#" class="pg">4</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
.g-bd { ... }
.g-bd .m-article_box { ... }
.g-bd .m-article_box .hd { ... }
.g-bd .m-article_box .bd { ... }
.g-bd .m-article_box .bd .list { ... }
.g-bd .m-article_box .bd .list .item { ... }
.g-bd .m-article_box .bd .list .item .cover { ... }
.g-bd .m-article_box .bd .list .item .info { ... }
.g-bd .m-article_box .bd .list .item .info .title { ... }
.g-bd .m-article_box .bd .list .item .info .desc { ... }
.g-bd .m-article_box .ft { ... }
.g-bd .m-article_box .ft .page { ... }
.g-bd .m-article_box .ft .page .pg { ... }
按照css的方式,我们就要把所有跟m-article_box
有关的部分,从g-bd
全部复制到g-hd
里去。这还是在模块的书写符合规范的情况下,如果这个模块书写不符合规范,没有把全部结构都挂在m-article_box
类下,那真的就是灾难了~而现在使用sass的话,我们只需要把m-article_box
的区块整个从g-bd
剪切到g-hd
就完事了(这里为了突出修改的工作量大,我特地把整个模块结构都写全了——才不是为了凑字数。。。):
// 修改前
.g-hd { ... }
.g-bd {
.m-article_box {
.hd { ... }
.bd {
.list {
.item {
.cover {
...
}
.info {
.title {
...
}
.desc {
...
}
}
}
}
}
.ft {
.page {
.pg { ... }
}
}
}
}
// 修改后
.g-hd {
.m-article_box {
.hd { ... }
.bd {
.list {
.item {
.cover {
...
}
.info {
.title {
...
}
.desc {
...
}
}
}
}
}
.ft {
.page {
.pg { ... }
}
}
}
}
.g-bd { ... }
非常方便,而且不容易出错。
2、变量(variable)
咱们直接上代码:
// index.scss
$fontSize: 16px;
$grey: #ccc;
.m-nav {
font-size: $fontSize;
color: $grey;
}
编译结果:
/* index.css */
.m-nav {
font-size: 16px;
color: #ccc;
}
写过代码的人都熟悉 参数 的用法吧,太简单太直白了不想说太多,自己意会吧。
3、函数(function)
// pixels to rems
@function rem($px) {
@return $px / 640 * 16rem;
}
太简单了直白了不想说太多,自己意会吧。
4、混合(mixin)
混合,顾名思义,就是混合的意思。。。也就是我们可以事先定义一段代码块,在需要使用到的地方,直接引用(include),而在引用之前,这段代码都不会出现在编译文件中,也就是不会生成任何内容。
这也是非常重要的一个特性!我们知道common的体积非常大,而体积大的根本原因是它存放了许许多多的模块。我们设想一下,如果将每一个模块都打包成mixin,那common不就减肥成功了?!多年的顽疾终于看到希望,没有比这更让人惊喜的了!我们这就上车:
/* common.css */
.m-nav { ... }
.m-news { ... }
.m-copy_right { ... }
改造后
// common.scss
@mixin m-nav {
.m-nav { ... }
}
@mixin m-news {
.m-news { ... }
}
@mixin m-copy_right {
.m-copy_right { ... }
}
// index.scss
.g-index {
@include m-nav;
@include m-news;
@include m-copy_right;
}
5、import
这个属性很眼熟?没错,事实上,css本身就有这个属性实现,我们可以在css文件中直接使用import
来引入其他文件。那么css的import
和sass的import
有什么区别?从含义和用法上来说,没有区别,区别在于工作原理。css的import是阻塞的,而sass的import在编译后,其实是合并文件,最后只产出一个css文件,而css则没有合并,该多少个文件就还是多少个文件。
注意:
- 只有import一个.sass/.scss文件的时候,才可以省去后缀名,如果是直接import一个.css文件,要补全文件名;
- import之后的分号
;
不要漏写,会报错; - sass如果import的是一个.css文件的话,那它的作用就跟css原生的import作用一样,只有import一个sass文件的时候,才是合并文件。
如下:
// index.scss
@import 'common';
@import 'a.css';
编译结果:
/* index.scss */
.m-nav { ... }
.m-news { ... }
.m-copy_right { ... }
@import url('a.css');
css的import之所以没有被普遍使用是有原因的。我们可以大概猜到它的工作原理:a.css import b.css以后,当浏览器加载到页面中的a.css的时候,已经准备按照a.css的内容来渲染页面了,刚解析到第一行,发现a.css居然还import了一个b.css,于是它不得不先放下a.css(既阻塞a.css),去加载b.css,直到b.css加载完,并且优先解析它,然后才开始回来解析a.css——鬼知道b.css会不会又import了c.css……这直接导致了渲染工作滞后,引发性能问题。
说实话我还不如直接用两个link标签去同步加载a.css和b.css,效率会高一些。
所以css的import基本是被抛弃了的属性。
sass的import主要的好处就是把文件合并了,减少了请求。原本需要link好几个css文件的页面,现在只需要一个。
模块化
终于要开始干点正事了,首先我们来回顾一下,上一节我们以规范为基础构建的模块化项目,遗留了一些什么问题。
- 冗余 体积庞大的common;
- 使用
cm-
模块区别m-
模块,使得后期开发过程中,m-
模块向cm-
模块转变过程比较繁琐;
……
好像,问题也不是特别多,我们一个一个解决。
为了方便,在这里我们把每个页面所对应的scss文件叫做 页面scss;把变量、函数、混合等(没有被引用或者执行的情况下)编译后不产生实际内容的代码叫做 定义类代码 ,那么相对应的其他内容就是 实际内容代码。
1、mixin.scss
我们知道,一方面,在common中过多地添加模块最终会导致common的体积过大,使得资源冗余,另一方面,为了方便维护,我们又希望尽量多地把模块公有化。
这是一对矛盾,仅靠css本身是无法解决的,但sass可以!如果我们使用mixin来代替直接书写模块,由于mixin并不直接生成代码,而是通过主动引用,才能生成对应内容,那么理论上,common就可以无限多地存放模块而不必占用一点空间!
(注意,这里说的是理论上,实际应用中,文件太过庞大的话,免不了还是要受到命名冲突的限制的,不过这问题不大。)
说干就干,我们把common中的模块全部打包成mixin:
/* common.css */
.m-nav { ... }
.m-news { ... }
.m-copy_right { ... }
改造后
// common.scss
@mixin m-nav {
.m-nav { ... }
}
@mixin m-news {
.m-news { ... }
}
@mixin m-copy_right {
.m-copy_right { ... }
}
调用方式如下:
// index.scss
@import 'common'; // 记得先引入common
.g-index {
@include m-nav;
@include m-news;
@include m-copy_right;
}
原本我们会在每个需要用到公共模块的页面中,先引用common,然后再引用页面css,而现在,我们只需要在页面scss中直接@import common;
就可以了。
使用common:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
<link rel="stylesheet" type="text/css" href="./style/common.css">
<link rel="stylesheet" type="text/css" href="./style/index.css">
</head>
<body>
...
</body>
</html>
改造后:
// index.scss
@import 'common';
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
<link rel="stylesheet" type="text/css" href="./style/index.css">
</head>
<body>
...
</body>
</html>
很完美,
——至少目前为止是这样。
我们思考一个问题,common除了存放模块之外,还有没有其他内容?答案是肯定的,大家一定知道有个东西叫做css reset
(或者normalize.css),这肯定是全局的;另外,如果是做后台管理系统的话,可能还会有bootstrap
。当然,还有一些自定义的全局的样式,比如常见的.clearfix
,等等。
这些东西目前我们也堆积在common当中,而且合情合理,因为它们都是全局的样式。但是对比起mixin来说,这些实际内容代码显得很少量,有种被淹没的感觉,使得整个common看上去就像只有mixin。但是这些实际内容代码的作用却又非常重要。为了使common的构成更加直观,我们把mixin全部都抽离出来,单独存放一个叫做mixin.scss的文件中,然后在common引用它,这样,mixin的管理更加的规范,而且common的结构也更加清晰了。
抽离mixin还有另外一个重要原因,后面会讲到的,我们希望mixin作为一个纯粹定义类代码文件,随处可以引用而不会生成多余的代码。
原本我们会在每个需要用到公共模块的页面中,先引用common,然后再引用页面css,而现在,我们只需要在页面scss中直接@import mixin;
就可以了。
使用mixin:
// index.scss
@import 'common'; // 引入common,如果有需要,common里一样可以引入mixin
@import 'mixin'; // 引入mixin
.g-index {
...
}
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>index</title>
<link rel="stylesheet" type="text/css" href="./style/index.css">
</head>
<body>
...
</body>
</html>
2、common.scss
好,抽离了mixin之后,我们现在来重新看回common,common里应该是些什么样的内容。上面的内容我们稍稍提到了一点,我们来展开一下。
2.1、css reset(normalize)
我们知道浏览器千差万别,各浏览器的默认样式也是不尽相同,最常见的比如body的默认内边距,p标签的默认内边距,以及ul/ol等等。这些不统一的默认样式经常让我们感到头疼,所以就有人提出一开始写样式就先把它们消除的想法,于是就催生了后来非常流行的reset.css
。
起初的reset.css很简单,大概是这样的:
html, body, h1, h2, h3, h4, h5, h6, div, dl, dt, dd, ul, ol, li, p {
margin: 0;
padding: 0;
}
没错,就是把几乎所有会用到的标签都给去了内边距和外边距,简单粗暴,这样所有的标签就都统一了,而且在不同的浏览器下也是统一的。
其他的部分每个人有各自的补充,比如有人会把h1~h6
的所有字号给定义一遍,以保证在不同浏览器下他们有统一的大小;有人会给a
标签设置统一的字体颜色和hover效果,诸如此类等等。
很好,没毛病。我们把这些统称为css reset
,然后再统一封装到一个叫做reset.css
的文件中,然后每个页面都引用。
这种方式一直以来都挺实用,而且大家也都这么用,没出过什么问题。只是后来有人提出,这种方式太过粗暴(居然还心疼浏览器了)。。。而且会降低页面渲染的性能,最重要的是,这使得我们原本设计出来的表达各种含义的标签儿们,变得毫无特点了。。。
说的好有道理,如果你家里所有人名字不一样但是都长一个样,还有啥意思。
于是,就出现了normalize.css
,normalize的目的同样是为了统一各个浏览器下各不相同的默认样式,不过它并不是简单粗暴地全部抹平,而是根据规范,来人为地把那些不符合规范的默认样式“扶正”,从而达到统一各个浏览器默认样式,同时保留各个标签原有特点的目的。
我们不能说reset与normalize这两种思想孰好孰坏,只能说各有各的特点和作用,它们的存在都是为了解决同样的问题。
2.2、插件
一般来说,一个ui插件都会至少包括一个css文件,像bootstrap、datepicker等等。假设我们项目中需要以bootstrap为基础框架,实现快速开发,那么这时候我们就需要在项目中全局引入bootstrap.min.css,当然,还有bootstrap.min.js。说到全局暴露,我们第一时间想到的是common,没错,我们可以在common中引入。
有人问,插件的.css文件怎么import?额,改一下扩展名为.scss就可以了,scss是兼容原生css语法的~
所以最终,我们的common大概是这样子的:
// common.scss
@import './reset';
@import './bootstrap.min';
@import './mixin';
事实上,如果我们不需要使用到 mixin.scss 中的变量和mixin的话,我们可以不引用它。
那么我们的页面scss应该是这样的:
// index.scss
@import './common';
@import './mixin';
.g-index {
...
}
干净,整洁。
3、mixin编写规范
每添加一个新角色,我们就要及时给它设置规范,好让它按照我们的期望工作别添乱。我们接下来来归纳一下mixin的书写规范。
场景一:项目里有mixin.scss、a.scss(假设这是某个功能文件)、index.scss三个文件,mixin中定义了一个变量$fontSize: 16px;
,a中定义了一个变量$color: #ccc;
,我们在index中同时引用这两个文件,那么我们在index中是可以直接使用$fontSize
和$color
这两个变量的——我的意思是,尽管在index中我们并没有看到这两个变量的声明和定义,但它们就这么存在了。
这是好事还是坏事呢?直觉告诉我,这可能有问题。没错,这是不是跟我们之前讨论过的 污染 很像?只不过我们之前是引用了common之后,index什么都还没写就已经被占用了很多模块名,而现在是因为引用了其他文件,而占用了index的很多变量名。另外,就维护的角度来看,这也是有问题的,如果我不事先告诉你,或者你不事先看过一遍mixin和a,你知道index中的$color
是哪里来的吗?假设我们需要字体大小,你知道去哪个文件修改吗?另外,你怎么保证同时引用mixin与a的时候,他们之间有没有可能存在同名的变量?那谁覆盖谁呢?这些问题看起来很小,但是当你项目规模大的时候,这可能是无法挽回的灾难(吓唬谁呢???)。
场景二:假设我们的项目有一个主题色,边框、tab背景、导航栏背景,以及字体颜色等等,都是这个主题色,为了方便使用,不想总是用取色器去取值,于是我们在mixin中定义了一个全局变量$color: #ff9900
,然后就可以愉快地到处使用了!
整个网站开发完了,一个月后,设计师突然过来跟你说:“老板说,这个主题色要改改,有点土,咱们换个大红。”,于是你一脸不情愿然而内心却窃喜地打开mixin,把$color的值改成了大红,然后得瑟地对设计师说:“幸好我早有准备,搞定了,你看看吧。”,保存,打开页面一看,设计师和你的脸都绿了,页面怎么这么丑,有些字原本是主题色,但背景是红色,而现在一改,整块都变成红的,内容都看不清了,有些边框原本就是红色的,但是字体是原本的主题色,然而现在一改,边框跟字体都变成红的了。
设计师:“不不不,我只是想把背景颜色改一下。”
你:“你不是说改主题色吗?那就是所有的地方啊。”
设计师:“不用,改背景就好了。”
你:“不行啊。。。”
设计师:“为什么不行,不就是改个背景颜色吗?怎么设置的就怎么改回来呀。”
你:“不是你想的那么简单。。。”
……
好吧我就是吓唬你的,你要是特能折腾那么这些都不叫事儿。
所以我们需要对(全局)变量进行管理,就像我们当初管理mixin那样,不能想在哪里定义就在哪里定义,也不能动不动就修改一个全局变量:
- 全局变量只在mixin中定义,其他scss文件定义的变量(无论是暴露到全局还是局部)都只看作局部变量,不在当前文件以外的地方使用(即便是在能引用到的情况下,也避免使用);
- 需要使用全局变量的地方直接import mixin;
- 一般来说,定义全局变量应该慎重,全局变量的数量应该尽量少;
- 尽可能不改动,如果需求变动,除非是对用途十分确定的情况,否则请新增一个全局变量来逐步替换需要修改的地方;
- 不要使用太过笼统的名词来作为全局变量,比如
color
,建议直接是用色值的描述,比如$orange: #ff9900
,这使得我们在维护上更方便扩展,如果色值需要修改,但是又不是所有的地方都需要修改,那么我们可以新定义一个变量来扩展它,比如$red: red
。
这些点说起来都有点飘忽,事实上也确实很难说明白为什么要这么做,毕竟都是经验总结,所以大家不妨先熟悉使用sass一段时间之后,再来细细思考这些问题。
注意,以上讲的这些都不是死规定,在某些时候,这个规范是需要根据实际项目而做调整的,就比如我们之后要讲到的SPA。十全十美的项目是不存在的,也不存在能适用所有项目的开发模式,因地制宜才能更好地解决问题。而且我们目前提到的问题都不是致命的,致命的问题在上一节我们制定规范的时候已经避开了。
调用模块
问题,在哪里调用模块?
答,页面scss。
在页面scss中调用模块是一个好习惯,它使得我们在每个页面所用到的模块既是一致的又是互相隔离的,不像在common中直接引用模块那样,使得一个页面scss还没有内容的时候就已经被很多模块名污染了。
再提个问题,在页面scss的哪里调用模块?
例一,根类外:
// index.scss
@import './common';
@import './mixin';
@include m-nav;
@include m-news;
@include m-copy_right;
.g-index {
...
}
例二,根类内:
// index.scss
@import './common';
@import './mixin';
.g-index {
@include m-nav;
@include m-news;
@include m-copy_right;
...
}
目前为止,这两种方式都是可以的,至于我为什么用“目前为止”这个词,那是因为我们后面将要讲到的SPA,如果用例一的方式是有问题的。所以我比较鼓励使用例二的方式。当然,我说了,目前为止例一也是没问题的。
性能优化
目前为止,我们的模块化工作已经算是完成了,其实已经可以收工了。不过我们还是可以稍微做一下优化。
1、缓存
我们需要考虑一个问题:缓存。
缓存是我们web开发中最常见的情况之一,很多时候我们都需要跟缓存打交道,特别是在做性能优化的时候。
一般来说,静态资源在被加载到浏览器之后,浏览器会把它本地缓存下来,以便下次请求同个资源的时候可以快速响应,不需要再去远程服务器加载。
我们就css来说,假设我们按照原来的方式,使用多个link去加载reset、bootstrap、common、index这几个文件的话,这几个文件都会被缓存下来,以使得下次再访问这个页面,这个页面的加载速度会快很多。
如果是从index页面跳转到about页面呢?你会发现也很快,因为about页面的全局css(reset、bootstrap、common)和index页面是一样的,而它们在你访问index的时候,已经加载过了,得益于缓存的作用,之后的页面打开都快。
我们现在的方式是,一个页面所用到的所有css文件都被合并成一个,也就不存在相同的文件可以利用缓存这样的优势了。
那我们有办法改进吗?有的!我们只需要把common独立出来,那么common就可以做为被缓存的公共文件了。最终我们从一个页面只引用一个文件变成了一个页面引用两个文件,即common和页面css:
// common.scss
@import './reset';
@import './bootstrap.min';
@import './mixin';
// index.scss
@import './mixin';
.g-index {
...
}
注意,不同于之前,我们这里的index.scss不再引入common.scss,所以我们最终是得到了两个css文件,而common.css是在所有页面中通过link标签引入的。
如此一来,我们就实现了既能够合并文件,减少请求数,又可以利用缓存,提高加载速度。
2、压缩
代码压缩是优化工作中最基本的一步,css的压缩空间是很大的,尤其是我们这种 垂直的书写方式 ,压缩起来是相当高效的。
在sass中这很简单,sass在编译的时候提供了几种模式,其中的compressed
模式是最高效的压缩模式,记得在编译打包的时候选择compressed模式就行了。
总结
总的来说,预处理语言在使我们编程更加美好的同时,也使得规范更加的完善。在css本身无法实现的情况下,我们通过工具来完成了模块化开发。
我不会讲如何去安装和配置sass环境,因为这些w3cplus sass guide有详细的介绍了,建议使用nodejs的方式,不会捣鼓nodejs/npm的前端不是好前端。
最后,我们回到一开始提到的问题——为什么要模块化?现在我们可以先从css的工作来回答,从某种意义上讲,模块化提高了我们编程能力和解决问题的能力,使得构建一个庞大而可扩展可维护的项目成为可能,使得我们能够以架构的思维和眼光去搭建整个项目。
下一节本想就着这一篇的内容直接说SPA的,但是发现SPA已经是组件化的内容了,也就是说它不单单是css的内容,需要更多的知识点来搭台,所以还是先放着