前端性能优化-pad适配方案

602 阅读2分钟

前言

前段时间做了一个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.csslist.small.css的优先级会降低,同时,它们也不再会阻塞页面的渲染。需要注意的是,优先级降低代表可能会后加载,并非不加载。

image.png

看到这里,我们明白了media属性的用法--它可以根据屏幕尺寸来决定css文件加载的优先级,利用这一属性,我们就可以将不同的尺寸的css适配代码打包成不同的css文件,然后在利用media属性去适配不同的尺寸文件,这样就可以优化加载我们需要的css样式,加快渲染进度!

手动分离css样式

刚开始分离css样式的时候,想着自己实现一个算法,大致可以分为以下几步:

其中最重要的是怎么在css文件中,将不同尺寸的代码找出来,图中已经说的很明白了

image.png

代码如下:

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了,那还犹豫什么,果断用起来啊!

download-1.jpg

在使用postcss-extract-media-query插件结合起来使用,之前,需要先介绍一下gulp.js--基于流的自动化构建工具。怎么说gulp呢,你可以简单的将其理解成为一个文件读写工具,可以连续的读写文件,而且其可以和postcss插件结合起来使用,也就是说gulp负责读取css文件,然后生成文件流,文件流通过postcss-extract-media-query插件,将不同尺寸的@media适配代码分离生成新的流,最终gulp将新的流写入不同的文件,完成上面的整个过程,下面是我们利用gulppostcss-extract-media-query实现该功能的原理图

image.png

代码如下:

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文件

image.png

最后一步--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">
<% } %>

这样就完整实现了我们所说的需求,成功的做到了一个小优化,开心起来吧!