前言
前段时间做了一个pad适配的需求,需要使用@media属性去适配不同尺寸的h5页面,这里就衍生了一个问题--适配代码被打包成成了一个css文件。这就导致,无论我在哪个尺寸的设备中渲染h5页面,都要完全加载整个css文件,才可以继续渲染页面,造成了一定的性能浪费,且css适配代码很乱,不够优雅!
link标签的media属性
黄天不负有心人,经过我的一番搜索,了解到link标签有个media属性,可以解决这个问题,类似下面这样:
<link rel="stylesheet" href="navigator.small.css" media="(max-width: 500px)" />
举个例子说明一下media
属性的用法:如下图所示,我们引入了4个css文件,并设定后两个css文件的media属性为(max-width: 500px)
。
这就意味着,当页面的宽度大于500px
时,navigator.small.css
和 list.small.css
的优先级会降低,同时,它们也不再会阻塞页面的渲染。需要注意的是,优先级降低代表可能会后加载,并非不加载。
看到这里,我们明白了media属性的用法--它可以根据屏幕尺寸来决定css文件加载的优先级,利用这一属性,我们就可以将不同的尺寸的css适配代码打包成不同的css文件,然后在利用media属性去适配不同的尺寸文件,这样就可以优化加载我们需要的css样式,加快渲染进度!
手动分离css样式
刚开始分离css样式的时候,想着自己实现一个算法,大致可以分为以下几步:
其中最重要的是怎么在css文件中,将不同尺寸的代码找出来,图中已经说的很明白了
代码如下:
const path = require('path')
const fs = require('fs')
const less = require('less')
const fsExtra = require('fs-extra')
const walkSync = (currentDirPath, callback) => {
fs.readdirSync(currentDirPath).forEach(function (name) {
const filePath = path.join(currentDirPath, name)
const stat = fs.statSync(filePath)
if (stat.isFile()) {
callback(filePath, stat)
} else if (stat.isDirectory()) {
walkSync(filePath, callback)
}
})
}
let modulesMap = {}
let sourceModules = []
let mark = false
function compile() {
if (mark) {
return
}
if (modulesMap.length !== 0 || sourceModules.length !== 0) {
modulesMap = {}
sourceModules = []
}
const promises = []
const files = ['src/components', 'src/pages']
files.forEach(dir => {
walkSync(dir, function (filePath, stat) {
if (filePath.endsWith('-pad.less')) {
// eslint-disable-next-line
const task = new Promise(resolve => {
sourceModules.push(filePath)
fs.readFile(filePath, function (error, data) {
if (error) {
console.error(error)
} else {
data = data.toString()
less.render(data, function (e, output) {
if (e) {
console.error(filePath)
} else {
sortStyleBySize(output.css, modulesMap)
resolve()
}
})
}
})
})
promises.push(task)
}
})
})
const linkInfo = []
// 将适配代码输出到不同的css文件中
return Promise.all(promises).then(() => {
for (const i in modulesMap) {
linkInfo.push({
rel: 'stylesheet',
media: i.slice(7),
href: `${i}.css`
})
// 可以考虑策略模式
fsExtra.outputFile(path.resolve(__dirname, `./src/assets/css/${i}.css`), modulesMap[i].join(''))
}
sourceModules.forEach(item => {
console.log(path.resolve(__dirname, item))
fs.watch(path.resolve(__dirname, item), compile)
})
mark = false
return linkInfo
})
}
// compile()
function sortStyleBySize(data, modules) {
const medias = findMark(data, '@media') // 找到所有'@media'的位置
const length = medias.length
for (let i = 0; i < length; i++) {
let module
if (i !== length - 1) {
module = data.slice(medias[i], medias[i + 1])
} else {
module = data.slice(medias[i])
}
let mark = module.match(/@media([\s\S]*?)\{/g)[0]
mark = mark.substring(0, mark.length - 1).trim()
const moduleKeys = Object.keys(modules)
if (moduleKeys.includes(mark)) {
modules[mark].push(module)
} else {
const styleModules = []
styleModules.push(module)
modules[mark] = styleModules
}
}
}
function findMark(data, mark) {
let location = data.indexOf(mark)
const result = []
while (location !== -1) {
result.push(location) // 保存结果
location = data.indexOf(mark, location + 1)
}
return result
}
module.exports = compile
以上代码亲测有效,但是没有覆盖100%用例,所以可能存在瑕疵,抱着学习的心态看看就可以了,接下来,才是重头戏!
利用gulp、postcss-extract-media-query插件完成css样式分离
在实际做项目的过程当中,我发现了一个神仙插件postcss-extract-media-query
,它可以帮助我们将css中的@media代码根据不同尺寸提取出来,相当于将我上面手动分离css样式的逻辑帮我实现了,简直是太nice了,那还犹豫什么,果断用起来啊!
在使用postcss-extract-media-query
插件结合起来使用,之前,需要先介绍一下gulp.js--基于流的自动化构建工具。怎么说gulp呢,你可以简单的将其理解成为一个文件读写工具,可以连续的读写文件,而且其可以和postcss
插件结合起来使用,也就是说gulp负责读取css文件,然后生成文件流,文件流通过postcss-extract-media-query
插件,将不同尺寸的@media适配代码分离生成新的流,最终gulp将新的流写入不同的文件,完成上面的整个过程,下面是我们利用gulp
和postcss-extract-media-query
实现该功能的原理图
代码如下:
const path = require('path')
const gulp = require('gulp')
const fsExtra = require('fs-extra')
const del = require('del')
const less = require('gulp-less')
const postcss = require('gulp-postcss')
const hash = require('gulp-hash-filename')
const cleanCSS = require('gulp-clean-css')
// 确保.temp目录下存在css文件夹
const tempDir = './.temp/css'
fsExtra.ensureDir(path.join(__dirname, tempDir))
const distDir = './src/static/css'
const entries = ['./src/pages/**/*.less', './src/components/**/*.less']
// 编译less并提取不同@media下的内容
function compile() {
const processors = [
require('cssnano')({
preset: 'default'
}),
require('postcss-extract-media-query')({
output: {
path: path.join(__dirname, tempDir),
name: '[query].[ext]'
},
queries: {
'screen and (width: 640px)': '640',
'screen and (width: 840px)': '840'
}
})
]
return gulp
.src(entries)
.pipe(
less({
paths: [path.join(__dirname, 'less', 'includes')]
})
)
.pipe(postcss(processors))
}
// 重命名文件
function rename() {
const stream = gulp.src(path.join(__dirname, `${tempDir}/*.css`))
return stream
.pipe(cleanCSS({ compatibility: 'ie8' }))
.pipe(
hash({
format: '{name}-{hash:8}{ext}'
})
)
.pipe(gulp.dest(path.join(__dirname, distDir)))
}
exports.default = gulp.series(compile, rename)
下面是实测生成的css文件
最后一步--htmlWebpackPlugin插件
生成不同尺寸的css文件后,我们还需药做最后一件事情,那就是将不同尺寸的css文件通过link标签的media属性引入到html文件中,该怎么做呢?如果你使用的是vue框架,那这是一件很简单的事情,我们可以使用htmlWebpackPlugin插件
具体用法如下:
在 vue.config.js 中配置 webpack-html-plugin 插件配置,引入 media 模块
const mediaLinkList = require('./mediaModules')
module.exports = {
chainWebpack: config => {
config.plugin('html').tap(args => {
args[0].template = './public/index.html'
args[0].mediaLinkList = mediaLinkList
return args
})
}
}
在 public/index.html 下引入模版,将适配代码通过 link 引入 html
<% for(var i=0; i < htmlWebpackPlugin.options.mediaLinkList.length; i++){ %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.mediaLinkList[i].href %>"
media="<%= htmlWebpackPlugin.options.mediaLinkList[i].media %>" type="text/css">
<% } %>
这样就完整实现了我们所说的需求,成功的做到了一个小优化,开心起来吧!