what is mask-image
mask-image CSS属性用于设置元素上遮罩层的图像。
MDN是这样来表述mask-image的。关于它的兼容性,你可以在这里看到Can i use mask-image。
- Chrome全系支持
- Firefox v53起
- Safari v4起
到目前为止,我们想要正常地使用该CSS属性,需要做一下兼容,用-webkit-mask-image:
.icon { width: 24px; height: 24px; mask-repeat: no-repeat; mask-size: cover; background: black; mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M368 368L144 144M368 144L144 368'/%3E%3C/svg%3E"); -webkit-mask-size: cover; -webkit-mask-repeat: no-repeat; -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M368 368L144 144M368 144L144 368'/%3E%3C/svg%3E"); }复制代码
why mask-image
看一下市面上的icon解决方案:
icon图片 使用img标签,或者作为背景图片
- 优点:直接且方便,不需要任何额外的支持,兼容性最好
- 缺点:多次http请求,且没有任何灵活性,无法满足多场景下的的需求
svg symbol 使用use标签,动态添加图标
- 优点:比较灵活,支持多色
- 缺点:需要把图标写入到模版,过于影响开发体验
iconfont 基于base64和雪碧图
- 优点:合并为一张图,仅一次http请求;兼容性好;相对灵活
- 缺点:编码为base64之后,图标磁盘占用变大了;base64相对于原始svg,显著影响页面渲染速度;
而使用了mask-image之后,上述问题都不复存在。mask-image十分灵活,它支持一个特别炫酷的特性,就是渐变色(本质是背景叠加),它的表现原理类似于PS中针对图层的“混合选项”=>“渐变叠加”。
我甚至以为,mask-image这个属性就是依据PS的“颜色叠加”、“渐变叠加”来实现的,因为二者的作用,十分相似。
有了mask-image,你可以制造出各种意想不到的效果:
下面基于mask-image分别封装一个小程序组件和React组件。
小程序组件
<!--index.wxml--><wxs src="./index.wxs" module="wxs"></wxs><view class="icon_wrap" style="{{wxs.getWrapStyles({size,bordered,filled,round,borderColor,fillColor})}}" wx:if="{{visibleWrap}}"> <viewclass="icon"style="{{wxs.getStyles({size,color,src})}}" ></view></view><view class="icon" style="{{wxs.getStyles({size,color,src})}}" wx:else></view>复制代码
// index.wxsfunction getWrapStyles (props){ var styles = ''if (props.bordered) styles += ';border:2rpx solid ' + props.borderColor if (props.filled) styles += ';background:' + props.fillColor if (props.round) styles += ';border-radius:50%' styles += ';width:' + props.size * 1.5 + 'px' styles += ';height:' + props.size * 1.5 + 'px'return styles }function getStyles (props){ var styles = '' styles += ';width:' + props.size + 'px' styles += ';height:' + props.size + 'px' styles += ';background:' + props.color styles += ';mask-image:url("' + props.src + '")' styles += ';-webkit-mask-image:url("' + props.src + '")'return styles }module.exports = { getWrapStyles: getWrapStyles, getStyles: getStyles }复制代码
// index.tsComponent({ options: { //@ts-ignorepureDataPattern: /^(type)$/ }, properties: { icon: <{ type: ObjectConstructor value: { outline: string; filled: string } }>{ type: Object, value: {} }, type: <{ type: StringConstructor value: 'outline' | 'filled' }>{ type: String, value: 'outline' }, size: { type: Number, value: 20 }, color: { type: String, value: '#000000' }, visibleWrap: { type: Boolean, value: false }, bordered: { type: Boolean, value: false }, filled: { type: Boolean, value: false }, round: { type: Boolean, value: false }, borderColor: { type: String, value: 'whitesmoke' }, fillColor: { type: String, value: 'whitesmoke' } }, observers: { type (v) { this.getSrc(this.data.icon[v]) }, icon (v) { if (!v) returnif (!this.data.type) returnthis.getSrc(v[this.data.type]) } }, data: { src: '', height: 20, width: 20 }, methods: { getSrc (svg) { if (!svg) returnthis.setData({ src: 'data:image/svg+xml,' + svg.replace(/</g, '%3C').replace(/>/g, '%3E') }) } } })复制代码
// index.less.icon_wrap { display: flex; justify-content: center; align-items: center; box-sizing: border-box; }.icon { vertical-align: middle; display: inline-block; background: black; mask-repeat: no-repeat; mask-size: cover; }复制代码
// icon.tsexport const cube = { outline: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path d='M448 341.37V170.61A32 32 0 00432.11 143l-152-88.46a47.94 47.94 0 00-48.24 0L79.89 143A32 32 0 0064 170.61v170.76A32 32 0 0079.89 369l152 88.46a48 48 0 0048.24 0l152-88.46A32 32 0 00448 341.37z' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M69 153.99l187 110 187-110M256 463.99v-200'/></svg>`, filled: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path d='M440.9 136.3a4 4 0 000-6.91L288.16 40.65a64.14 64.14 0 00-64.33 0L71.12 129.39a4 4 0 000 6.91L254 243.88a4 4 0 004.06 0zM54 163.51a4 4 0 00-6 3.49v173.89a48 48 0 0023.84 41.39L234 479.51a4 4 0 006-3.46V274.3a4 4 0 00-2-3.46zM272 275v201a4 4 0 006 3.46l162.15-97.23A48 48 0 00464 340.89V167a4 4 0 00-6-3.45l-184 108a4 4 0 00-2 3.45z'/></svg>`}复制代码
在应用中使用:
- 导入图标
import { cube } from 'icon'Page({data:{icon:{ cube } } })复制代码
- 使用组件
<Iconicon="{{icon.cube}}"color="black"size="{{20}}"type="filled"></Icon>复制代码
通过直接传入svg使用,这样对图标进行按需加载,利于缩减包大小和后续扩展项目。上述这种方式有利有弊:牺牲了运行时内存,但换取的是包大小和渲染效率。
上面的小程序组件经过很少的改动(wxs => computed)就可以用作Vue组件。
React组件
// index.tsximport { useState, useEffect } from 'react'import { getWrapStyles, getStyles, getSrc } from './utils'import styles from './index.less'interface IProps { icon: { outline: string; filled?: string } type?: 'outline' | 'filled' size?: number color?: string visibleWrap?: boolean bordered?: boolean filled?: boolean round?: boolean borderColor?: string fillColor?: string }const Index = (props: IProps) => { const { icon, type = 'outline', size = 20, color = 'black', visibleWrap, bordered, filled, round, borderColor = 'whitesmoke', fillColor = 'whitesmoke' } = props const [ state_src, setStateSrc ] = useState('') useEffect( () => { if (!icon) returnif (!type) return setStateSrc(getSrc(icon[type])) }, [ icon, type ] ) return ( <div className={styles._local}> {visibleWrap ? ( <divclassName='icon_wrap'style={getWrapStyles({size, bordered, filled, round, borderColor, fillColor })} ><divclassName='icon'style={getStyles({ size, color, src: state_src })} /></div> ) : ( <div className='icon' style={getStyles({ size, color, src: state_src })} /> )} </div> ) }export default Index复制代码
// index.less._local { display: flex; :global {.icon_wrap { display: flex; justify-content: center; align-items: center; box-sizing: border-box; }.icon { vertical-align: middle; display: inline-block; background: black; mask-repeat: no-repeat; mask-size: cover; } } }复制代码
// utils.tsimport { CSSProperties } from 'react'export const getWrapStyles = ({ size, bordered, filled, round, borderColor, fillColor }): CSSProperties => { const styles: CSSProperties = {} if (bordered) styles.border = '2rpx solid ' + borderColor if (filled) styles.background = fillColor if (round) styles.borderRadius = '50%' styles.width = size * 1.5 + 'px' styles.height = size * 1.5 + 'px'return styles }export const getStyles = ({ size, color, src }): CSSProperties => { let styles: CSSProperties = {} styles.width = size + 'px' styles.height = size + 'px' styles.background = color styles.WebkitMaskImage = `url("${src}")`return styles }export const getSrc = (svg: string) => { if (!svg) returnreturn 'data:image/svg+xml,' + svg.replace(/</g, '%3C').replace(/>/g, '%3E') }复制代码
注意,目前这种方案仅针对svg,对于其他类型的图标并不适用,不过也足以应对大部分场景了。
最后,如果觉得文章对你有用,点赞,评论,让更多的人看到,谢谢。