一、问题引出
在使用一些图片图标、样例图时,我们往往将其设计为一个具有一定宽高的背景盒子:
width: 20px;
height: 20px;
background-image: url("/icons/home.png");
复制代码
如果项目中存在大量的这种小图,就会引发一个问题:
大量的小图引起了大量的请求,不仅对服务器造成了比较大的压力,对用户而言,图片的显示效果也不佳(图片会一个一个蹦出来)。
在没有字体图标、SVG图标之前,web网页的图标大多数都是以图片的形式存在。
二、雪碧图概念
为了解决这个问题,我们可以将所有小图放置在一张大图上:
如上图所示,大图上放置了若干个小图,以后在使用这些小图时,直接引用这张大图,然后定位到小图的位置就可以了:
上图中,A区域可以任意移动和改变大小,同时它也是真正的可视区域,现在,让我们移动A区域,并调整其大小,使其选中第二排第一张图:
这样,我们就成功将这个小图给显示出来了。
在CSS中,我们通过宽高来改变A区域的尺寸,如:
width: 30px;
height: 30px;
复制代码
因为图片是从左上角开始摆放,所以我们实际上只会看到左上角的一小部分图片,现在我们可以让背景发生一定的偏移:
background-position: -30px -120px;
复制代码
三、问题的解决
雪碧图的概念是非常简单的,就像我们拿放大镜在书本上移动,书本相当于这个大图,而每个文字相当于一个小图,要想看清每个文字,我们必须不断移动放大镜的位置。
要按照上述概念实现雪碧图,我们需要做这些工作:
- 准备好所有小图
- 准备一张大图,将小图放置在大图上,并且记录好每个图片的尺寸和位于大图的位置
- 准备一个与小图尺寸相同的元素,设置背景图为大图,并且根据小图所处的位置进行偏移,直至小图完全显示出来
诚然雪碧图的概念是简单的,但是实际操作起来却是非常繁琐的,当需要增加小图时,需要不断维护这张大图,并且需要仔细定义这些小图的位置、大小,一旦发生变化,引用的地方也要同步修改。
这些重复且繁琐的工作会使开发人员的工作变得乏味、无趣,有没有好的办法来解决这一问题呢?
四、理想的雪碧图使用
因为将小图画到大图上是枯燥且麻烦的,因此,理想状态下,我们最好压根不用维护这些大图,而只用维护小图:
- sprites
- page1
- icon1.png
- icon2.png
- icon3.png
- page2
- icon1.png
- icon2.png
- page1
如上所示,sprites目录下放置了所有的小图,而page1目录相当于这个大图,未来我们需要将该目录下所有小图组合,生成一个page1.png的大图。
这样,每次新增小图时,我们将准备好的小图直接复制到指定的大图目录下即可,这个步骤是非常简单的。
除了手动维护大图外,手动记录小图的尺寸、位置都是非常麻烦、无趣的事情,因此,在使用雪碧图时,只需要指定两个重要参数即可:
- 大图
- 小图
也就是说,开发人员只要按照sprites目录下的结构来写雪碧图就可以,而不用关心其他问题。使用的方法看起来像这样:
smallImage('page1/icon1.png')
复制代码
这个语法最终会被处理为一串css:
width: 15px;
height: 15px;
background-repeat: no-repeat;
background-image: url("page1.png");
background-position: -100px -200px;
复制代码
怎么样?是不是很炫酷?接下来,让我们一步一步来实现吧。
五、方案的实现
理想的状态下,开发人员在使用图片时,最好不要有雪碧图的概念,之所以制作雪碧图,纯粹是为了减少页面的请求量,与开发人员是无关的,开发人员不应为此承担多余的工作量。如果不能很好的解决雪碧图的使用,反而会给开发人员造成负担,以及造成后期维护上的困难。
完成步骤四,至少需要解决这些问题:
- 程序自动读取雪碧图目录下的文件,并将所有图片组合成一张大图
- 在生成大图的同时,将雪碧图上每个小图的尺寸、位置记录下来
- 提供一个工具函数,参数为路径如
/page1/icon1.png
,返回值为一连串的css
有了要解决的问题,我们马上找到了一款与符合上述需求的webpack插件:webpack-spritesmith。该插件可以读取目录下的图片文件并将其组合起来,同时生成一个对应的样式文件,并且支持重写。
核心代码如下:
const { resolve, join, basename } = require('path')
const fs = require('fs')
const SpritesmithPlugin = require('webpack-spritesmith')
const BASE_PATH = resolve('./src/assets/sprite')
const SOURCE_PATH = join(BASE_PATH, 'src')
const plugins = []
// 1. 读取assets/sprite/src下的所有子目录
// 2. 每个子目录生成一张雪碧图
// 3. 生成的雪碧图与变量文件放置在assets/sprite目录下
// 4. 使用时需要按需引入.scss文件
// 读取源目录,每个子目录生成一个插件对象
const files = fs.readdirSync(SOURCE_PATH, {
withFileTypes: true
})
files.forEach(file => {
if (!file.isDirectory()) return
plugins.push(createPlugin(join(SOURCE_PATH, file.name)))
})
function createPlugin(path) {
const imageName = basename(path)
const cssPath = join(BASE_PATH, imageName) + '.scss'
const imagePath = join(BASE_PATH, imageName) + '.png'
const templateHandler = createTemplateHandler(imageName)
const plugin = new SpritesmithPlugin({
src: {
cwd: path,
glob: '*.png'
},
target: {
image: imagePath,
css: [
[
cssPath,
{
format: 'templateHandler'
}
]
]
},
customTemplates: {
templateHandler
}
})
// 样式生成
function createTemplateHandler(imageName) {
return function(data) {
const items = []
const image = basename(data.spritesheet.image) // 大图名称,如:icon.png
data.items.forEach(item => {
const smallImageName = basename(item.source_image) // 小图名称,如:edit.png
items.push(`
'${smallImageName}': (
width: ${item.width}px,
height: ${item.height}px,
background-repeat: no-repeat,
background-image: url("~@/assets/sprite/${image}"),
background-position: ${item.offset_x}px ${item.offset_y}px
)`
)
})
return `
$sprite-${getFileBasename(image)}: (
${items.join()}
);
`
}
}
return plugin
}
/**
* 获取文件名
* @param {String} filename 文件名
*/
function getFileBasename(filename) {
if (!filename) return null
const sepIdx = filename.lastIndexOf('.')
if (sepIdx < 0) return filename
return filename.substring(0, sepIdx)
}
module.exports = plugins
复制代码
之所以创建多个插件对象,是因为该插件单个对象只能处理一张雪碧图,因此我们需要实例化多个插件对象用来处理多个雪碧图的情况。
然后,将导出的插件对象列表传到webpack的创建配置中即可:
plugins: [
...spritesPlugins
]
复制代码
以上代码会读取sprite/src
下的目录,每个目录视为一张雪碧图,然后将该目录下的所有小图组合成一张大图,并在sprite
目录下生成对应的图片和样式文件,最后的结构如下所示:
- sprite
- src
- icons
- examples
- icons.png 自动生成的雪碧图
- icons.scss 自动生成的样式文件,里面包含了每个小图的样式
- examples.png
- examples.scss
- src
每个样式文件中存储了一个sass map对象,key为小图的名称,value为样式,如:
$sprite-icons: (
'home.png': {
width: 20px,
height: 20px,
background-repeat: no-repeat,
background-image: url("~@/assets/sprite/icons.png"),
background-position: -40px -120px
},
// ...
)
复制代码
然后,我们编写一个sass mixin,来统一处理雪碧图的使用:
@import "@/assets/sprite/icons.scss";
// FIXME 不得已的处理方案,不知道sass中有没有可以通过变量名直接获取变量的方法
// 如果有,请改善它
$sprites: (
icons: $sprite-icons
);
@mixin sprite($path) {
$sep-index: str-index($path, '/');
@if $sep-index {
$dir: str-slice($string: $path, $start-at: 0, $end-at: $sep-index - 1);
$name: str-slice($string: $path, $start-at: $sep-index + 1, $end-at: -1);
$sprite: map-get($map: $sprites, $key: $dir);
@if $sprite {
$css: map-get($sprite, $name);
@debug $sprite;
@if $css {
@each $key, $value in $css {
#{$key}: $value;
}
} @else {
display: none;
}
}
}
};
复制代码
使用时,只需要这样就可以:
@import "@/styles/mixins/sprite.scss";
.icon-home {
@include sprite('icons/home.png');
}
复制代码
是不是很方便呢?
六、如何维护和使用雪碧图?
增加雪碧图
增加雪碧图需要在sprite/src
目录下新建一个目录,并将所有小图放入该目录。
注意:项目需要重新启动才可以触发该目录的雪碧图生成,切记!(后续会考虑解决此问题)
重启项目后,还需要在styles/mixins/sprite.scss
中引入,并为$sprites
map新增一项,如:
$sprites: (
icons: $sprite-icons,
examples: $sprite-examples
)
复制代码
注:map的key需要与雪碧图目录的名称相同。
新增小图
新增小图非常方便,将小图直接放入指定的雪碧图目录即可。(会自动重新编译,生成新的雪碧图)
使用
引入雪碧图sass mixin并使用即可:
@import "@/styles/mixins/sprite.scss";
.icon-home {
@include sprite('examples/bg.png');
}
复制代码
总结
尽管我们使用一个看似很复杂的过程完成了雪碧图的制作,但是思路是非常清晰的:
要让开发人员像使用普通图片一样使用雪碧图。
从这一点出发,不断倒推,最后得出了上述的解决方案。
对于一个项目的架构人员而言,所有的开发者都是Ta的用户,要想“用户”开发得舒适,必须为他们解决掉最棘手的问题,这也算是一种用户体验方面的设计吧。