阅读 769

自适应布局方案与px2rem-loader加强版源码实现

1.核心概念

1.1 设备物理像素

  • 是一个物理概念,是显示器显示的最小物理单位,设备屏幕的物理像素,任何设备的物理像素的数量都是固定的
  • iPhone6的像素分辨率是750*1334

1.2设备独立像素

  • 是一个逻辑概念,是为web开发者创造的,在CSS和javascript中使用的一个抽象的层,用于向CSS中的宽度、高度等提供信息

  • iPhone6的逻辑分辨率是375*667

  • iPhone6:window.screen.width=375,window.screen.height=667

  • px是一个相对单位,相对的是设备像素(device pixel)

    像素,又称画素,是图像显示的基本单位,译自英文“pixel”,pix是英语单词picture的常用简写,加上英语单词“元素”element,就得到pixel,故“像素”表示“图像元素”之意,有时亦被称为pel(picture element) 像素是网页布局的基础。一个像素就是计算机能够显示一种特定颜色的最小区域。当设备尺寸相同但像素变得更密集时,屏幕能显示的画面的过渡更细致,网站看起来更明快。

    每一个CSS声明和几乎所有的javascript属性都使用CSS像素,因此实际上从来用不上设备像素 ,唯一的例外是screen.width/height

1.3 设备像素比

  • DPR(设备像素比) = 设备像素/CSS像素

  • 设备像素比 window.devicePixelRatio

    在早先的移动设备中,并没有DPR的概念。随着技术的发展,移动设备的屏幕像素密度越来越高。从iphone4开始,苹果公司推出了所谓的retina视网膜屏幕。之所以叫做视网膜屏幕,是因为屏幕的PPI(屏幕像素密度)太高,人的视网膜无法分辨出屏幕上的像素点。iphone4的分辨率提高了一倍,但屏幕尺寸却没有变化,这意味着同样大小的屏幕上,像素多了一倍,于是DPR = 2 实际上,此时的CSS像素对应着以后要提到的理想视口,其对应的javascript属性是screen.width/screen.height而对于设备像素比DPR也有对应的javascript属性window.devicePixelRatio 以iphone5为例,iphone5的CSS像素为320px*568px,DPR是2,所以其设备像素为640px*1136px

1.4 移动端适配

  • 一般由设计师按照设备像素(device pixel)为单位制作设计稿
  • 然后由前端工程师参照设备像素比(device pixel ratio)进行换算

1.4.1 rem

  • 参照根元素的字体大小
  • 适配就是让根元素的字体大小根据分辨率进行动态改变
  • px2rem-loader

1.4.2 vw和vh

  • 参照的是viewport视口
  • vw参照的是视口的宽度(1vw=视口宽度/100)
  • vh参照的是视口的高度(1vh=视口高度/100)
  • iPhone6 1vw=3.75px
  • postcss-px-to-viewport

2.px2rem-loader实战

2.1 安装

npm install webpack webpack-cli html-webpack-plugin style-loader css-loader amfe-flexible px2rem-loader --save-dev
复制代码

2.2 src\index.js

import './base.css'
复制代码

2.3 src\base.css

 width:750px;
  height:750px;
  background-color: red;
  font-size:12px;/*px*/
  border: 1px solid #ddd; /*no*/
  box-shadow: 0 2px 0 rgb(0 0 0 / 5%);
}
复制代码

2.4 src\index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>webpack</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
复制代码

2.5 webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    mode:'development',
    devtool: false,
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
    resolveLoader: {
        alias: {
          "px2rem-plus-loader": path.resolve('./loaders/px2rem-plus-loader.js')
        },
        modules: [path.resolve('./loaders'), 'node_modules']
    },
    module: {
        rules: [
          {
            test: /\.css$/,
            use: [{
                loader: 'style-loader'
            }, {
                loader: 'css-loader'
            }, 
            // {
            //     loader: 'px2rem-loader',
            //     options: {
            //         remUni: 75,
            //         remPrecision: 8
            //     }
            // },
            {
              loader: 'px2rem-plus-loader', //path.resolve(__dirname, 'loaders/px2rem-plus-loader.js'),
              options: {
                  remUnit: 75,
                  remPrecision: 8,
                  baseDpr:1,
                  exclude:/antd\.css/
              }
            }
            ]
          },
          {
            test:/\.js$/,
            use: {
                loader: 'babel-loader',
                options: {
                  presets: ['@babel/preset-env','@babel/preset-react'],
                  plugins: ['@babel/transform-runtime']
                }
              },
            exclude: /node_modules/,        
          },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin(
          { template: './src/index.html', 
            title:'index',
            inject: true
          }
        ),
    ]
};

复制代码

2.6 package.json

{
   "scripts": {
    "build": "webpack"
   }
}
复制代码

3.loader

  • loader 用于对模块的源代码进行转换
  • loader 可以使你在 import 模块时预处理文件
  • loader 可以将文件从不同的语言(如TypeScript)转换为 JavaScript

loaders\px2rem-plus-loader.js

var loaderUtils = require('loader-utils');
//var Px2rem = require('px2rem');
var Px2rem = require('./px2rem');
function loader(source) {
  var options = loaderUtils.getOptions(this);
  if(options.exclude && options.exclude.test(this.resource)){
       return source;
  }
  var px2remIns = new Px2rem(options);
  let targetSource = px2remIns.generateRem(source);
  return targetSource;
}
module.exports = loader;
复制代码

4. 使用自定义loader

webpack.config.js 中的配置

resolveLoader: {
        alias: {
          "px2rem-plus-loader": path.resolve('./loaders/px2rem-plus-loader.js')
        },
        modules: [path.resolve('./loaders'), 'node_modules']
    },
复制代码

5.css

5.1 AST

  • astexplorer

  • JavaScript Parser可以把源代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构

5.2 AST工作流

  • Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
  • Transform(转换) 对抽象语法树进行转换
  • Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码

5.3 px2rem.js

px2rem

/*
 * @Description: 
 * @Author: changqing
 * @Date: 2021-04-29 11:54:54
 * @LastEditTime: 2021-05-02 11:46:46
 * @LastEditors: changqing
 * @Usage: 
 */
'use strict';

var css = require('css');
var extend = require('extend');

var defaultConfig = {
  baseDpr: 2,             // base device pixel ratio (default: 2)
  remUnit: 75,            // rem unit value (default: 75)
  remPrecision: 6,        // rem value precision (default: 6)
  forcePxComment: 'px',   // force px comment (default: `px`)
  keepComment: 'no'       // no transform value comment (default: `no`)
};

var pxRegExp = /\b(\d+(\.\d+)?)px\b/;

function Px2rem(options) {
  this.config = {};
  extend(this.config, defaultConfig, options);
}

// generate @1x, @2x and @3x version stylesheet
Px2rem.prototype.generateThree = function (cssText, dpr) {
  dpr = dpr || 2;
  var self = this;
  var config = self.config;
  var astObj = css.parse(cssText);

  function processRules(rules) {
    for (var i = 0; i < rules.length; i++) {
      var rule = rules[i];
      if (rule.type === 'media') {
        processRules(rule.rules); // recursive invocation while dealing with media queries
        continue;
      } else if (rule.type === 'keyframes') {
        processRules(rule.keyframes); // recursive invocation while dealing with keyframes
        continue;
      } else if (rule.type !== 'rule' && rule.type !== 'keyframe') {
        continue;
      }

      var declarations = rule.declarations;
      for (var j = 0; j < declarations.length; j++) {
        var declaration = declarations[j];
        // need transform: declaration && has 'px'
        if (declaration.type === 'declaration' && pxRegExp.test(declaration.value)) {
          var nextDeclaration = rule.declarations[j + 1];
          if (nextDeclaration && nextDeclaration.type === 'comment') { // next next declaration is comment
            if (nextDeclaration.comment.trim() === config.keepComment) { // no transform
              declarations.splice(j + 1, 1); // delete corresponding comment
              continue;
            } else if (nextDeclaration.comment.trim() === config.forcePxComment) { // force px
              declarations.splice(j + 1, 1); // delete corresponding comment
            }
          }
          declaration.value = self._getCalcValue('px', declaration.value, dpr); // common transform
        }
      }
    }
  }

  processRules(astObj.stylesheet.rules);

  return css.stringify(astObj);
};

// generate rem version stylesheet
Px2rem.prototype.generateRem = function (cssText) {
  var self = this;
  var config = self.config;
  var astObj = css.parse(cssText);

  function processRules(rules, noDealPx) { // FIXME: keyframes do not support `force px` comment
    for (var i = 0; i < rules.length; i++) {
      var rule = rules[i];
      if (rule.type === 'media') {
        processRules(rule.rules); // recursive invocation while dealing with media queries
        continue;
      } else if (rule.type === 'keyframes') {
        processRules(rule.keyframes, true); // recursive invocation while dealing with keyframes
        continue;
      } else if (rule.type !== 'rule' && rule.type !== 'keyframe') {
        continue;
      }

      if (!noDealPx) {
        // generate 3 new rules which has [data-dpr]
        var newRules = [];
        for (var dpr = 1; dpr <= 3; dpr++) {
          var newRule = {};
          newRule.type = rule.type;
          newRule.selectors = rule.selectors.map(function (sel) {
            return '[data-dpr="' + dpr + '"] ' + sel;
          });
          newRule.declarations = [];
          newRules.push(newRule);
        }
      }

      var declarations = rule.declarations;
      for (var j = 0; j < declarations.length; j++) {
        var declaration = declarations[j];
        // need transform: declaration && has 'px'
        if (declaration.type === 'declaration' && pxRegExp.test(declaration.value)) {
          var nextDeclaration = declarations[j + 1];
          if (nextDeclaration && nextDeclaration.type === 'comment') { // next next declaration is comment
            if (nextDeclaration.comment.trim() === config.forcePxComment) { // force px
              // do not transform `0px`
              if (declaration.value === '0px') {
                declaration.value = '0';
                declarations.splice(j + 1, 1); // delete corresponding comment
                continue;
              }
              if (!noDealPx) {
                // generate 3 new declarations and put them in the new rules which has [data-dpr]
                for (var dpr = 1; dpr <= 3; dpr++) {
                  var newDeclaration = {};
                  extend(true, newDeclaration, declaration);
                  newDeclaration.value = self._getCalcValue('px', newDeclaration.value, dpr);
                  newRules[dpr - 1].declarations.push(newDeclaration);
                }
                declarations.splice(j, 2); // delete this rule and corresponding comment
                j--;
              } else { // FIXME: keyframes do not support `force px` comment
                declaration.value = self._getCalcValue('rem', declaration.value); // common transform
                declarations.splice(j + 1, 1); // delete corresponding comment
              }
            } else if (nextDeclaration.comment.trim() === config.keepComment) { // no transform
              declarations.splice(j + 1, 1); // delete corresponding comment
            } else {
              declaration.value = self._getCalcValue('rem', declaration.value); // common transform
            }
          } else {
            declaration.value = self._getCalcValue('rem', declaration.value); // common transform
          }
        }
      }

      // if the origin rule has no declarations, delete it
      if (!rules[i].declarations.length) {
        rules.splice(i, 1);
        i--;
      }

      if (!noDealPx) {
        // add the new rules which contain declarations that are forced to use px
        if (newRules[0].declarations.length) {
          rules.splice(i + 1, 0, newRules[0], newRules[1], newRules[2]);
          i += 3; // skip the added new rules
        }
      }
    }
  }

  processRules(astObj.stylesheet.rules);

  return css.stringify(astObj);
};

// get calculated value of px or rem
Px2rem.prototype._getCalcValue = function (type, value, dpr) {
  var config = this.config;
  var pxGlobalRegExp = new RegExp(pxRegExp.source, 'g');

  function getValue(val) {
    val = parseFloat(val.toFixed(config.remPrecision)); // control decimal precision of the calculated value
    return val == 0 ? val : val + type;
  }

  return value.replace(pxGlobalRegExp, function ($0, $1) {
    return type === 'px' ? getValue($1 * dpr / config.baseDpr) : getValue($1 / config.remUnit);
  });
};

module.exports = Px2rem;


复制代码

5.4 usePx2rem.js

let Px2rem = require('./px2rem');
let px2rem = new Px2rem({
    remUnit: 75,
    remPrecision: 8
});
let cssText = `
#root{
    width:750px;
    height:750px;
    font-size:12px;/*px*/
    border: 1px solid #ddd; /*no*/
}
`;
let newCSS = px2rem.generateRem(cssText);
console.log(newCSS);

// #root {
//   width: 10rem;
//   height: 10rem;
//   border: 1px solid #ddd;
// }

// [data-dpr="1"] #root {
//   font-size: 6px;
// }

// [data-dpr="2"] #root {
//   font-size: 12px;
// }

// [data-dpr="3"] #root {
//   font-size: 18px;
// }
复制代码

6. px2rem-loader.js

  • loader-utils是一个webpack工具类

  • px2rem-loader

  • 直接写px,编译后会直接转化成rem

  • 在px后面添加/no/,不会转化px,会原样输出 一般border需用这个

  • 在px后面添加/px/,会根据dpr的不同,生成三套代码 一般字体需用这个

7. lib-flexible

import './base.css';
import 'amfe-flexible/index.js';
复制代码

8. 第三方框架样式问题

  • 如果第三方组件已经是为移动端做了适配,px2rem又转成了rem就导致其样式变的很小

8.1 index.js

import './base.css'
//import 'amfe-flexible/index.js';
import 'lib-flexible/flexible.js'
import React from 'react';
import ReactDOM from 'react-dom';
import 'antd/dist/antd.css';
import {Button} from 'antd';
ReactDOM.render(<div>
    我是<Button type="primary">按钮</Button>
</div>, document.getElementById('root'));
复制代码

8.2 webpack.config.js

{
    loader: 'px2rem-plus-loader', //path.resolve(__dirname, 'loaders/px2rem-plus-loader.js'),
     options: {
                  remUnit: 75,
                  remPrecision: 8,
                  baseDpr:1,
                  exclude:/antd\.css/
              }
      }
复制代码

px2rem-plus-loader 增加 exclude参数排除不需要处理的css文件

仓库地址

文章分类
前端
文章标签