boi剖析 - 基于webpack的css sprites实现方案

本文是58到家前端工程化集成解决方案boi的博文系列之一。boi是基于webpack打造的一站式前端工程化解决方案,现已开源Githubjavascript

做为前端构建工具不可或缺的一个环节,自动生成css sprites图片不只仅可以减小频繁的人工操做,还可以避免多人协做时对同一个sprites图片维护过程当中因我的缘由引发的图片不规范问题。58到家前端工程化解决方案boi的自动css sprites功能基于webpack实现,本文记录一下实现方案的各个细节以及须要注意的地方。css

1. 功能需求

css sprites的功能需求简单说就是将style中引用的散列小图标合并成一张sprites图片。从功能角度来说比较单一,从实现角度来说须要具有如下几点:html

  • 对style文件进行资源依赖分析,可以得出style中引用的图片资源;
  • style文件引用的图片并不是都是图标,其余的好比背景图等资源不该该被sprites合并。因此必须有明确的标识能够区分图标与非图标资源。

对于第一点,webpack自己就具有依赖分析的功能,因此无需自行实现。那么如何设计明确的标识以便区分资源类型呢?前端

2. 用户至上的设计原则

上文提到的资源标识,咱们首先看一下业内的同类产品是如何实现的。以fis为例,请看如下代码:java

li.list-1::before {
  background-image: url('./img/list-1.png?__sprite');
}
li.list-2::before {
  background-image: url('./img/list-2.png?__sprite');
}

fis的css sprites功能要求开发者在style代码中添加__sprite标识,fis经过识别这个标识来区分资源类型。这种模式的优势是能够精确地进行定位,并且对图标文件的路径没有强制要求,能够将图标文件与其余资源文件混合存放。可是,在代码中书写标识,首先须要具体的业务开发人员时刻注意不要遗漏;其次,这种模式实质上是对代码的一种“绑架”,代码中存在与业务无关的内容而且可移植性不高。webpack

做为框架,全部方案都应该遵循用户至上的设计原则git

  • 配置API语义化,一目了然;
  • 减小代码绑架,减小代码中存在与业务无关的内容,以便代码的高可移植性;
  • 提供高级配置API,方便用户进行自定义。

基于以上原则,boi在设计配置API时尽可能作到了语义化,而且style代码中不存在任何与业务无关的内容。如下代码是boi配置css sprites功能的demo:github

boi.spec('style',{
    sprites: true,
    spritesConfig: {
        dir: 'assets/image/icons',
        split: true,
        retina: true,
        postcssSpritesOpts: null
    }
});

与sprites功能相关的配置项细节以下:web

  • sprites - Boolean,是否开启自动sprites功能,默认false。只有在spritestrue时,spritesConfig才会生效;
  • spritesConfig - Object,功能配置细节:
    • dir - String,图标文件的目录路径,默认为undefined。boi以路径做为区分图标与非图标资源的标识,也就是说参与自动sprites的图标文件必须存放于独立的目录下,好比'assets/image/icons'
    • split - Boolean,是否识别子目录而且每一个子目录分别编译为sprites图片,默认为true。好比上述代码对应的项目中存在图标目录'assets/image/icons',在此目录下又存在两个子目录'assets/image/icons/index''assets/image/icons/admin',分别存在index页面和admin页面的图标文件。若是配置split:true,boi将会编译输出两个sprites图片sprite.index.pngsprite.admin.png;若是配置split:false,boi只会编译输出一个sprites图片文件sprite.icons.png
    • retina - Boolean,是否识别分辨率标识,默认为true。分辨率标识指的是相似@2x的文件名标识,好比存在两个图标文件logo.pnglogo@2x.png而且style文件中对两张图标都有引用,以下:
    @media screen and (max-width:780px){
        .logo{
            background-image: url(../assets/icons/logo.png)
        }
    }
    @media screen and (min-width:781px and max-width:900px){
        .logo{
            background-image: url(../assets/icons/logo@2x.png)
        }
    }
    若是配置`retina:true`,boi将把两种分辨率的图片分别合并为一张sprites图片,不然会编译到同一张sprites图片里。详细内容能够参考[boi-example-css-sprites](https://github.com/boijs/boi-example-css-sprites)。
    • postcssSpritesOpts - Object,默认为null。boi使用postcss-sprites做为实现css sprites的技术选型。postcssSpritesOpts是提供给用户自定义postcss-sprites相关功能的,这个配置项通常状况下是不须要用户操做的。若是遇到上文提到的配置项不能知足的应用场景,用户能够经过此API直接对postcss-sprites进行配置。

3. 技术选型

boi实现css sprites功能的技术选型以下:前端工程化

4. 实现方案

上文第二节中提到了boi实现sprites功能的设计原则和工做模式。用户在配置API中指定图标文件的路径
,boi以此路径做为区分图标与非图标文件的标识;而且支持识别分辨率标识进行单独编译。

在配置postcss时,要注意如下几点:

  1. 使用less/sass等css预编译器时postcss的执行时机问题;
  2. 经过路径进行图标文件合法性过滤;
  3. 以子目录名称和分辨率标识为基础的sprites图片命名规则。

下文将分别介绍boi针对上述问题的具体解决方案。

4.1 与css预编译器综合使用

postcss并不是只支持原始的css语法,同时也支持less和sass等预编译语法。webpack根据loader的前后顺序从右至左依次进行编译,好比:

{
    test: /\.less$/,
    loader: 'css!less'
}

webpack对less文件的编译顺序为:less->css->style。那么在使用postcss时应该在哪一步执行呢?

虽然postcss支持less和sass,笔者也并不推荐直接使用postcss去编译less和sass。一方面是由于postcss支持的预编译器类型有限;另外一方面即便postcss支持全部预编译语言,考虑到用户配置预编译器的多样性,若是对不一样编译器分派不一样的postcss插件势必会形成boi框架体积的臃肿。

基于上述的考虑,postcss-loader的位置就已经肯定了:在预编译loader以后,css-loader以前。以下:

{
    test: /\.less$/,
    loader: 'css!postcss!less'
}

之因此在css-loader以前还有另一个缘由, postcss-sprites将散列的图标合并成sprites以后首先要将生成的sprites图片存放于一个临时目录内,而后在经过css-loader进行资源依赖解析并编译到统一的dest目录中。因此中间有一个暂存的过程,必须经过css-loader进行依赖解析才能获得最终的结果。

4.2 合法性过滤

boi经过路径进行图标合法性标识,首先根据用户的配置建立验证正则:

const REG_SPRITES_NAME = new RegExp([
    path.posix.normalize(spritesConfig.dir).replace(/^\.*/, '').replace(/\//, '\\/'),
    '\\/\.+\\.',
    _.isArray(config.image.extType) ? '(' + config.image.extType.join('|') +')' : config.image.extType,
    '\$'
].join(''), 'i');

而后配置postcss-sprites的filterBy钩子函数进行合法性验证:

filterBy: (image) => {
    if (!REG_SPRITES_NAME.test(image.url)) {
        return Promise.reject();
    }
    return Promise.resolve();
}

4.3 分组规则

分组的依据有两个:目录名称和分辨率标识。首先须要根据用户的配置建立目录名称验证和分辨率标识验证的正则:

// 合法的散列图path
const REG_SPRITES_PATH = new RegExp([
    path.posix.normalize(spritesConfig.dir).replace(/^\.*/, '').replace(/\//, '\\/'),
    '\\/(.*?)\\/.*'
].join(''), 'i');
// 合法的retina标识
const REG_SPRITES_RETINA = new RegExp([
    '@(\\d+)x\\.',
    _.isArray(config.image.extType) ? '(' + config.image.extType.join('|') +')' : config.image.extType,
].join(''), 'i');

而后经过postcss-sprites的groupBy钩子函数进行分组规则制定:

groupBy: (image) => {
    let groups = null;
    let groupName = undefined;

    if (spritesConfig && spritesConfig.split) {
        groups = REG_SPRITES_PATH.exec(image.url);
        groupName = groups ? groups[1] : 'icons';
    } else {
        groupName = 'icons';
    }
    if (spritesConfig && spritesConfig.retina) {
        image.retina = true;
        image.ratio = 1;
        let ratio = REG_SPRITES_RETINA.exec(image.url);
        if (ratio) {
            ratio = ratio[1];
            while (ratio > 10) {
                ratio = ratio / 10;
            }
            image.ratio = ratio;
            image.groups = image.groups.filter((group) => {
                return ('@' + ratio + 'x') !== group;
            });
            groupName += '@' + ratio + 'x';
        }
    }
    return Promise.resolve(groupName);
}

上述代码包括如下逻辑:

  • 若是用户配置split:true,boi会对子目录进行正则验证,若是存在子目录将会单独分组;若不存子目录子默认分组名称为'icons'
  • 若是用户配置retina:true,boi会验证图标文件名是否包含分辨率标识,若是存在则将groupName加上相似'@2x'的后缀。

各位可能注意到上述代码中如下的部分比较怪异:

image.groups = image.groups.filter((group) => {
    return ('@' + ratio + 'x') !== group;
});

postcss-sprites识别到图标存在分辨率标识会生成单独的分组名称,若是不进行上述过滤的话,最终生成的sprites图片名称相似sprites.@2x.icons.png。以上过滤是为了将@2x分组删除,以便编译后的文件名更具语义化,好比sprites.icons@2x.png

5. 开源代码

各位能够结合源码/lib/config/genConfig/mp/style.js理解本文的内容。

相关文章
相关标签/搜索