美文网首页
PC端高倍屏适配方案实践

PC端高倍屏适配方案实践

作者: 维李设论 | 来源:发表于2021-10-29 22:21 被阅读0次
    前端 | PC端高倍屏适配方案实践.png

    项目背景

    随着PC端屏幕的发展,PC端也逐步出现了更高倍数的屏幕,相对于手机端的Retina屏,PC端也出现了多倍数适配的要求,本文主要是PC端高倍屏幕适配方案的一个实践总结,希望能给对PC端有适配高倍屏幕需求的同学有一些思路的启发和借鉴

    原理分析

    图片

    随着屏幕技术的发展,越来越多的PC设备配备了大尺寸高清屏幕,对于之前只需要在PC端实现的web应用就需要考虑类似手机端的移动应用相关的适配原则了,我们先来看一下手机端的高清屏幕的一个原理,对于纸媒时代来说,我们常用DPI(Dot Per Inch)即网点密度来描述打印品的打印精度,而对于手机移动设备,在iPhone4s时,苹果提出了一个所谓Retina屏幕的概念,即通过单位屏幕上像素密度的不同来实现更高密度的图像信息描述,即相同尺寸的屏幕但像素密度却不相同,通过逻辑像素与物理像素进行比例换算从而达到高清屏的显示,也就是PPI(Pixcels Per Inch)不同,如上图所示,对于同一个细节描述通过更多的像素点来进行刻画,就可以使信息呈现更多细节,画面也就更加细腻,基于此,我们来看一下手机端常见的一个适配方案

    图片

    对于UI设计来说,在移动端设计过程中,我们常常需要考虑iOS和Android的设计,除了基本的交互操作的区别外,这两者的设计适配方案也是UI面试中常常被问及的问题,对于UI设计来说,我们对于同一个应用来说总希望同一面对用户触达的感知应该是基本一致,除了系统特定的交互及展示风格,应尽可能抹平平台的差异,因而一般来说我们通常会在750x1334(iOS @2x)和720X1280(Android @2x)进行适配,对于PC端的Web来说只需要设计一个尺寸然后模拟实现Retina的需求即可,基于此,我们需要调研一下所需考虑的PC端适配策略

    图片

    通过百度流量研究院,我们可以得出所需适配的分辨率为:

    分辨率 份额 倍数
    1920x1080 44.46% @1x
    1366x768 9.37% @1x
    1536x864 8.24% @1x
    1440x900 7.85% @1x
    1600x900 7.85% @1x
    2560x1440 -- @2x
    3840x2160 -- @4x
    4096x2160 -- @4x

    最终通过产品的调研方案,我们决定以1366x768作为主屏设计,接着我们通过栅格化的布局对各屏幕的兼容性做处理

    方案选型

    对于多终端分辨率的适配我们常用的方案有

    方案 优点 缺点
    媒体查询 基于媒体的screen进行配置 对于每套屏幕都需要写一套样式
    rem+媒体查询 只需要变化根字体,收敛控制范围 需要对设计稿进行单位转换
    vw/vh 基于视窗的变化而变化 需要转化设计稿单位,并且浏览器兼容性不如rem
    图片 图片

    最终考虑到兼容性,我们决定使用rem+媒体查询的方案来进行高倍屏的适配,但是如果完全基于rem进行单位改写,对于设计稿向开发视图改变需要有一定的计算量,这时,我们就想到了使用前端工程化进行统一的魔改来提升DX(Develop Experience)

    案例实践

    图片

    我们使用PostCSS来对CSS代码进行转化,为了灵活配置及项目使用,参考px2rem实现了一个pc端px2rem的类,然后实现一个自定义的postcss的插件

    图片

    Pcx2rem

    // Pcx2rem
    const css = require("css");
    const extend = require("extend");
    
    const pxRegExp = /\b(\d+(\.\d+)?)px\b/;
    
    class Pcx2rem {
      constructor(config) {
        this.config = {};
        this.config = extend(
          this.config,
          {
            baseDpr: 1, // 设备像素比
            remUnit: 10, // 自定义rem单位
            remPrecision: 6, // 精度
            forcePxComment: "px", // 只换算px
            keepComment: "no", // 是否保留单位
            ignoreEntry: null, //  忽略规则实例载体
          },
          config
        );
      }
      generateRem(cssText) {
        const self = this;
        const config = self.config;
        const astObj = css.parse(cssText);
    
        function processRules(rules, noDealPx) {
          for (let i = 0; i < rules.length; i++) {
            let rule = rules[I];
            if (rule.type === "media") {
              processRules(rule.rules);
              continue;
            } else if (rule.type === "keyframes") {
              processRules(rule.keyframes, true);
              continue;
            } else if (rule.type !== "rule" && rule.type !== "keyframe") {
              continue;
            }
    
            // 处理 px 到 rem 的转化
            let declarations = rule.declarations;
            for (let j = 0; j < declarations.length; j++) {
              let declaration = declarations[j];
              // 转化px
              if (
                declaration.type === "declaration" &&
                pxRegExp.test(declaration.value)
              ) {
                let nextDeclaration = declarations[j + 1];
                if (nextDeclaration && nextDeclaration.type === "comment") {
                  if (nextDeclaration.comment.trim() === config.forcePxComment) {
                    // 不转化`0px`
                    if (declaration.value === "0px") {
                      declaration.value = "0";
                      declarations.splice(j + 1, 1);
                      continue;
                    }
                    declaration.value = self._getCalcValue(
                      "rem",
                      declaration.value
                    );
                    declarations.splice(j + 1, 1);
                  } else if (
                    nextDeclaration.comment.trim() === config.keepComment
                  ) {
                    declarations.splice(j + 1, 1);
                  } else {
                    declaration.value = self._getCalcValue(
                      "rem",
                      declaration.value
                    );
                  }
                } else {
                  declaration.value = self._getCalcValue("rem", declaration.value);
                }
              }
            }
    
            if (!rules[i].declarations.length) {
              rules.splice(i, 1);
              I--;
            }
          }
        }
    
        processRules(astObj.stylesheet.rules);
    
        return css.stringify(astObj);
      }
      _getCalcValue(type, value, dpr) {
        const config = this.config;
    
        // 验证是否符合  忽略规则
        if (config.ignoreEntry && config.ignoreEntry.test(value)) {
          return config.ignoreEntry.getRealPx(value);
        }
    
        const pxGlobalRegExp = new RegExp(pxRegExp.source, "g");
    
        function getValue(val) {
          val = parseFloat(val.toFixed(config.remPrecision)); // 精度控制
          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 = Pcx2rem;
    

    postCssPlugins

    const postcss = require("postcss");
    const Pcx2rem = require("./libs/Pcx2rem");
    const PxIgnore = require("./libs/PxIgnore");
    
    const postcss_pcx2rem = postcss.plugin("postcss-pcx2rem", function (options) {
      return function (css, result) {
        // 配置参数 合入 忽略策略方法
        options.ignoreEntry = new PxIgnore();
        // new 一个Pcx2rem的实例
        const pcx2rem = new Pcx2rem(options);
        const oldCssText = css.toString();
        const newCssText = pcx2rem.generateRem(oldCssText);
        result.root = postcss.parse(newCssText);
      };
    });
    
    module.exports = {
      "postcss-pcx2rem": postcss_pcx2rem,
    };
    

    vue.config.js

    // vue-cli3 内嵌了postcss,只需要在对应config出进行书写即可
    const {postCssPlugins} = require('./build');
    
    module.exports = {
        ...
        css: {
            loaderOptions: {
                postcss: {
                    plugins: [
                        postCssPlugins['postcss-pcx2rem']({
                            baseDpr: 1,
                            // html基础fontSize 设计稿尺寸 屏幕尺寸
                            remUnit: (10 * 1366) / 1920,
                            remPrecision: 6,
                            forcePxComment: "px",
                            keepComment: "no"
                        })
                    ]
                }
            }
        }
        ...
    }
    

    源码解析

    图片

    对于PostCSS而言,有很多人分析为后处理器,其本质其实是一个CSS语法转换器,或者说是编译器的前端,不同于scss/less等预处理器,其并不是将自定义语言DSL转换过来的。从上图中可以看出PostCss的处理方式是通过Parser将 CSS 解析,然后经过插件,最后Stringifier后输出新的CSS,其采用流式处理的方法,提供nextToken(),及back方法等,下面我们来逐一看一下其中的核心模块

    parser

    parser的实现大体可以分为两种:一种是通过写文件的方式进行ast转换,常见的如Rework analyzer;另外一种便是postcss使用的方法,词法分析后进行分词转ast,babel以及csstree等都是这种处理方案

    class Parser {
      constructor(input) {
        this.input = input
    
        this.root = new Root()
        this.current = this.root
        this.spaces = ''
        this.semicolon = false
        this.customProperty = false
    
        this.createTokenizer()
        this.root.source = { input, start: { offset: 0, line: 1, column: 1 } }
      }
    
      createTokenizer() {
        this.tokenizer = tokenizer(this.input)
      }
    
      parse() {
        let token
        while (!this.tokenizer.endOfFile()) {
          token = this.tokenizer.nextToken()
    
          switch (token[0]) {
            case 'space':
              this.spaces += token[1]
              break
    
            case ';':
              this.freeSemicolon(token)
              break
    
            case '}':
              this.end(token)
              break
    
            case 'comment':
              this.comment(token)
              break
    
            case 'at-word':
              this.atrule(token)
              break
    
            case '{':
              this.emptyRule(token)
              break
    
            default:
              this.other(token)
              break
          }
        }
        this.endFile()
      }
    
      comment(token) {
        // 注释
      }
    
      emptyRule(token) {
        // 清空token
      }
    
      other(start) {
        // 其余情况处理
      }
    
      rule(tokens) {
        // 匹配token
      }
    
      decl(tokens, customProperty) {
        // 对token描述
      }
    
      atrule(token) {
        // 规则校验
      }
    
      end(token) {
        if (this.current.nodes && this.current.nodes.length) {
          this.current.raws.semicolon = this.semicolon
        }
        this.semicolon = false
    
        this.current.raws.after = (this.current.raws.after || '') + this.spaces
        this.spaces = ''
    
        if (this.current.parent) {
          this.current.source.end = this.getPosition(token[2])
          this.current = this.current.parent
        } else {
          this.unexpectedClose(token)
        }
      }
    
      endFile() {
        if (this.current.parent) this.unclosedBlock()
        if (this.current.nodes && this.current.nodes.length) {
          this.current.raws.semicolon = this.semicolon
        }
        this.current.raws.after = (this.current.raws.after || '') + this.spaces
      }
    
      init(node, offset) {
        this.current.push(node)
        node.source = {
          start: this.getPosition(offset),
          input: this.input
        }
        node.raws.before = this.spaces
        this.spaces = ''
        if (node.type !== 'comment') this.semicolon = false
      }
    
      raw(node, prop, tokens) {
        let token, type
        let length = tokens.length
        let value = ''
        let clean = true
        let next, prev
        let pattern = /^([#.|])?(\w)+/I
    
        for (let i = 0; i < length; i += 1) {
          token = tokens[I]
          type = token[0]
    
          if (type === 'comment' && node.type === 'rule') {
            prev = tokens[i - 1]
            next = tokens[i + 1]
    
            if (
              prev[0] !== 'space' &&
              next[0] !== 'space' &&
              pattern.test(prev[1]) &&
              pattern.test(next[1])
            ) {
              value += token[1]
            } else {
              clean = false
            }
    
            continue
          }
    
          if (type === 'comment' || (type === 'space' && i === length - 1)) {
            clean = false
          } else {
            value += token[1]
          }
        }
        if (!clean) {
          let raw = tokens.reduce((all, i) => all + i[1], '')
          node.raws[prop] = { value, raw }
        }
        node[prop] = value
      }
    }
    

    stringifier

    用于格式化输出CSS文本

    const DEFAULT_RAW = {
      colon: ': ',
      indent: '    ',
      beforeDecl: '\n',
      beforeRule: '\n',
      beforeOpen: ' ',
      beforeClose: '\n',
      beforeComment: '\n',
      after: '\n',
      emptyBody: '',
      commentLeft: ' ',
      commentRight: ' ',
      semicolon: false
    }
    
    function capitalize(str) {
      return str[0].toUpperCase() + str.slice(1)
    }
    
    class Stringifier {
      constructor(builder) {
        this.builder = builder
      }
    
      stringify(node, semicolon) {
        /* istanbul ignore if */
        if (!this[node.type]) {
          throw new Error(
            'Unknown AST node type ' +
              node.type +
              '. ' +
              'Maybe you need to change PostCSS stringifier.'
          )
        }
        this[node.type](node, semicolon)
      }
    
      raw(node, own, detect) {
        let value
        if (!detect) detect = own
    
        // Already had
        if (own) {
          value = node.raws[own]
          if (typeof value !== 'undefined') return value
        }
    
        let parent = node.parent
    
        if (detect === 'before') {
          // Hack for first rule in CSS
          if (!parent || (parent.type === 'root' && parent.first === node)) {
            return ''
          }
    
          // `root` nodes in `document` should use only their own raws
          if (parent && parent.type === 'document') {
            return ''
          }
        }
    
        // Floating child without parent
        if (!parent) return DEFAULT_RAW[detect]
    
        // Detect style by other nodes
        let root = node.root()
        if (!root.rawCache) root.rawCache = {}
        if (typeof root.rawCache[detect] !== 'undefined') {
          return root.rawCache[detect]
        }
    
        if (detect === 'before' || detect === 'after') {
          return this.beforeAfter(node, detect)
        } else {
          let method = 'raw' + capitalize(detect)
          if (this[method]) {
            value = this[method](root, node)
          } else {
            root.walk(i => {
              value = i.raws[own]
              if (typeof value !== 'undefined') return false
            })
          }
        }
    
        if (typeof value === 'undefined') value = DEFAULT_RAW[detect]
    
        root.rawCache[detect] = value
        return value
      }
    
      beforeAfter(node, detect) {
        let value
        if (node.type === 'decl') {
          value = this.raw(node, null, 'beforeDecl')
        } else if (node.type === 'comment') {
          value = this.raw(node, null, 'beforeComment')
        } else if (detect === 'before') {
          value = this.raw(node, null, 'beforeRule')
        } else {
          value = this.raw(node, null, 'beforeClose')
        }
    
        let buf = node.parent
        let depth = 0
        while (buf && buf.type !== 'root') {
          depth += 1
          buf = buf.parent
        }
    
        if (value.includes('\n')) {
          let indent = this.raw(node, null, 'indent')
          if (indent.length) {
            for (let step = 0; step < depth; step++) value += indent
          }
        }
    
        return value
      }
    }
    

    tokenize

    postcss定义的转换格式如下

    .className {
        color: #fff;
    }
    

    会被token为如下的格式

    [
        ["word", ".className", 1, 1, 1, 10]
        ["space", " "]
        ["{", "{", 1, 12]
        ["space", " "]
        ["word", "color", 1, 14, 1, 18]
        [":", ":", 1, 19]
        ["space", " "]
        ["word", "#FFF" , 1, 21, 1, 23]
        [";", ";", 1, 24]
        ["space", " "]
        ["}", "}", 1, 26]
    ]
    
    const SINGLE_QUOTE = "'".charCodeAt(0)
    const DOUBLE_QUOTE = '"'.charCodeAt(0)
    const BACKSLASH = '\\'.charCodeAt(0)
    const SLASH = '/'.charCodeAt(0)
    const NEWLINE = '\n'.charCodeAt(0)
    const SPACE = ' '.charCodeAt(0)
    const FEED = '\f'.charCodeAt(0)
    const TAB = '\t'.charCodeAt(0)
    const CR = '\r'.charCodeAt(0)
    const OPEN_SQUARE = '['.charCodeAt(0)
    const CLOSE_SQUARE = ']'.charCodeAt(0)
    const OPEN_PARENTHESES = '('.charCodeAt(0)
    const CLOSE_PARENTHESES = ')'.charCodeAt(0)
    const OPEN_CURLY = '{'.charCodeAt(0)
    const CLOSE_CURLY = '}'.charCodeAt(0)
    const SEMICOLON = ';'.charCodeAt(0)
    const ASTERISK = '*'.charCodeAt(0)
    const COLON = ':'.charCodeAt(0)
    const AT = '@'.charCodeAt(0)
    
    const RE_AT_END = /[\t\n\f\r "#'()/;[\\\]{}]/g
    const RE_WORD_END = /[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g
    const RE_BAD_BRACKET = /.[\n"'(/\\]/
    const RE_HEX_ESCAPE = /[\da-f]/I
    
    function tokenizer(input, options = {}) {
      let css = input.css.valueOf()
      let ignore = options.ignoreErrors
    
      let code, next, quote, content, escape
      let escaped, escapePos, prev, n, currentToken
    
      let length = css.length
      let pos = 0
      let buffer = []
      let returned = []
    
      function position() {
        return pos
      }
    
      function unclosed(what) {
        throw input.error('Unclosed ' + what, pos)
      }
    
      function endOfFile() {
        return returned.length === 0 && pos >= length
      }
    
      function nextToken(opts) {
        if (returned.length) return returned.pop()
        if (pos >= length) return
    
        let ignoreUnclosed = opts ? opts.ignoreUnclosed : false
    
        code = css.charCodeAt(pos)
    
        switch (code) {
          case NEWLINE:
          case SPACE:
          case TAB:
          case CR:
          case FEED: {
            next = pos
            do {
              next += 1
              code = css.charCodeAt(next)
            } while (
              code === SPACE ||
              code === NEWLINE ||
              code === TAB ||
              code === CR ||
              code === FEED
            )
    
            currentToken = ['space', css.slice(pos, next)]
            pos = next - 1
            break
          }
    
          case OPEN_SQUARE:
          case CLOSE_SQUARE:
          case OPEN_CURLY:
          case CLOSE_CURLY:
          case COLON:
          case SEMICOLON:
          case CLOSE_PARENTHESES: {
            let controlChar = String.fromCharCode(code)
            currentToken = [controlChar, controlChar, pos]
            break
          }
    
          case OPEN_PARENTHESES: {
            prev = buffer.length ? buffer.pop()[1] : ''
            n = css.charCodeAt(pos + 1)
            if (
              prev === 'url' &&
              n !== SINGLE_QUOTE &&
              n !== DOUBLE_QUOTE &&
              n !== SPACE &&
              n !== NEWLINE &&
              n !== TAB &&
              n !== FEED &&
              n !== CR
            ) {
              next = pos
              do {
                escaped = false
                next = css.indexOf(')', next + 1)
                if (next === -1) {
                  if (ignore || ignoreUnclosed) {
                    next = pos
                    break
                  } else {
                    unclosed('bracket')
                  }
                }
                escapePos = next
                while (css.charCodeAt(escapePos - 1) === BACKSLASH) {
                  escapePos -= 1
                  escaped = !escaped
                }
              } while (escaped)
    
              currentToken = ['brackets', css.slice(pos, next + 1), pos, next]
    
              pos = next
            } else {
              next = css.indexOf(')', pos + 1)
              content = css.slice(pos, next + 1)
    
              if (next === -1 || RE_BAD_BRACKET.test(content)) {
                currentToken = ['(', '(', pos]
              } else {
                currentToken = ['brackets', content, pos, next]
                pos = next
              }
            }
    
            break
          }
    
          case SINGLE_QUOTE:
          case DOUBLE_QUOTE: {
            quote = code === SINGLE_QUOTE ? "'" : '"'
            next = pos
            do {
              escaped = false
              next = css.indexOf(quote, next + 1)
              if (next === -1) {
                if (ignore || ignoreUnclosed) {
                  next = pos + 1
                  break
                } else {
                  unclosed('string')
                }
              }
              escapePos = next
              while (css.charCodeAt(escapePos - 1) === BACKSLASH) {
                escapePos -= 1
                escaped = !escaped
              }
            } while (escaped)
    
            currentToken = ['string', css.slice(pos, next + 1), pos, next]
            pos = next
            break
          }
    
          case AT: {
            RE_AT_END.lastIndex = pos + 1
            RE_AT_END.test(css)
            if (RE_AT_END.lastIndex === 0) {
              next = css.length - 1
            } else {
              next = RE_AT_END.lastIndex - 2
            }
    
            currentToken = ['at-word', css.slice(pos, next + 1), pos, next]
    
            pos = next
            break
          }
    
          case BACKSLASH: {
            next = pos
            escape = true
            while (css.charCodeAt(next + 1) === BACKSLASH) {
              next += 1
              escape = !escape
            }
            code = css.charCodeAt(next + 1)
            if (
              escape &&
              code !== SLASH &&
              code !== SPACE &&
              code !== NEWLINE &&
              code !== TAB &&
              code !== CR &&
              code !== FEED
            ) {
              next += 1
              if (RE_HEX_ESCAPE.test(css.charAt(next))) {
                while (RE_HEX_ESCAPE.test(css.charAt(next + 1))) {
                  next += 1
                }
                if (css.charCodeAt(next + 1) === SPACE) {
                  next += 1
                }
              }
            }
    
            currentToken = ['word', css.slice(pos, next + 1), pos, next]
    
            pos = next
            break
          }
    
          default: {
            if (code === SLASH && css.charCodeAt(pos + 1) === ASTERISK) {
              next = css.indexOf('*/', pos + 2) + 1
              if (next === 0) {
                if (ignore || ignoreUnclosed) {
                  next = css.length
                } else {
                  unclosed('comment')
                }
              }
    
              currentToken = ['comment', css.slice(pos, next + 1), pos, next]
              pos = next
            } else {
              RE_WORD_END.lastIndex = pos + 1
              RE_WORD_END.test(css)
              if (RE_WORD_END.lastIndex === 0) {
                next = css.length - 1
              } else {
                next = RE_WORD_END.lastIndex - 2
              }
    
              currentToken = ['word', css.slice(pos, next + 1), pos, next]
              buffer.push(currentToken)
              pos = next
            }
    
            break
          }
        }
    
        pos++
        return currentToken
      }
    
      function back(token) {
        returned.push(token)
      }
    
      return {
        back,
        nextToken,
        endOfFile,
        position
      }
    }
    

    processor

    插件处理机制

    class Processor {
        constructor(plugins = []) {
            this.plugins = this.normalize(plugins)
        }
        use(plugin) {
    
        }
        process(css, opts = {}) {
    
        }
        normalize(plugins) {
            // 格式化插件
        }
    }
    

    node

    对转换的ast节点的处理

    class Node {
      constructor(defaults = {}) {
        this.raws = {}
        this[isClean] = false
        this[my] = true
    
        for (let name in defaults) {
          if (name === 'nodes') {
            this.nodes = []
            for (let node of defaults[name]) {
              if (typeof node.clone === 'function') {
                this.append(node.clone())
              } else {
                this.append(node)
              }
            }
          } else {
            this[name] = defaults[name]
          }
        }
      }
    
      remove() {
        if (this.parent) {
          this.parent.removeChild(this)
        }
        this.parent = undefined
        return this
      }
    
      toString(stringifier = stringify) {
        if (stringifier.stringify) stringifier = stringifier.stringify
        let result = ''
        stringifier(this, i => {
          result += I
        })
        return result
      }
    
      assign(overrides = {}) {
        for (let name in overrides) {
          this[name] = overrides[name]
        }
        return this
      }
    
      clone(overrides = {}) {
        let cloned = cloneNode(this)
        for (let name in overrides) {
          cloned[name] = overrides[name]
        }
        return cloned
      }
    
      cloneBefore(overrides = {}) {
        let cloned = this.clone(overrides)
        this.parent.insertBefore(this, cloned)
        return cloned
      }
    
      cloneAfter(overrides = {}) {
        let cloned = this.clone(overrides)
        this.parent.insertAfter(this, cloned)
        return cloned
      }
    
      replaceWith(...nodes) {
        if (this.parent) {
          let bookmark = this
          let foundSelf = false
          for (let node of nodes) {
            if (node === this) {
              foundSelf = true
            } else if (foundSelf) {
              this.parent.insertAfter(bookmark, node)
              bookmark = node
            } else {
              this.parent.insertBefore(bookmark, node)
            }
          }
    
          if (!foundSelf) {
            this.remove()
          }
        }
    
        return this
      }
    
      next() {
        if (!this.parent) return undefined
        let index = this.parent.index(this)
        return this.parent.nodes[index + 1]
      }
    
      prev() {
        if (!this.parent) return undefined
        let index = this.parent.index(this)
        return this.parent.nodes[index - 1]
      }
    
      before(add) {
        this.parent.insertBefore(this, add)
        return this
      }
    
      after(add) {
        this.parent.insertAfter(this, add)
        return this
      }
    
      root() {
        let result = this
        while (result.parent && result.parent.type !== 'document') {
          result = result.parent
        }
        return result
      }
    
      raw(prop, defaultType) {
        let str = new Stringifier()
        return str.raw(this, prop, defaultType)
      }
    
      cleanRaws(keepBetween) {
        delete this.raws.before
        delete this.raws.after
        if (!keepBetween) delete this.raws.between
      }
    
      toJSON(_, inputs) {
        let fixed = {}
        let emitInputs = inputs == null
        inputs = inputs || new Map()
        let inputsNextIndex = 0
    
        for (let name in this) {
          if (!Object.prototype.hasOwnProperty.call(this, name)) {
            // istanbul ignore next
            continue
          }
          if (name === 'parent' || name === 'proxyCache') continue
          let value = this[name]
    
          if (Array.isArray(value)) {
            fixed[name] = value.map(i => {
              if (typeof i === 'object' && i.toJSON) {
                return i.toJSON(null, inputs)
              } else {
                return I
              }
            })
          } else if (typeof value === 'object' && value.toJSON) {
            fixed[name] = value.toJSON(null, inputs)
          } else if (name === 'source') {
            let inputId = inputs.get(value.input)
            if (inputId == null) {
              inputId = inputsNextIndex
              inputs.set(value.input, inputsNextIndex)
              inputsNextIndex++
            }
            fixed[name] = {
              inputId,
              start: value.start,
              end: value.end
            }
          } else {
            fixed[name] = value
          }
        }
    
        if (emitInputs) {
          fixed.inputs = [...inputs.keys()].map(input => input.toJSON())
        }
    
        return fixed
      }
    
      positionInside(index) {
        let string = this.toString()
        let column = this.source.start.column
        let line = this.source.start.line
    
        for (let i = 0; i < index; i++) {
          if (string[i] === '\n') {
            column = 1
            line += 1
          } else {
            column += 1
          }
        }
    
        return { line, column }
      }
    
      positionBy(opts) {
        let pos = this.source.start
        if (opts.index) {
          pos = this.positionInside(opts.index)
        } else if (opts.word) {
          let index = this.toString().indexOf(opts.word)
          if (index !== -1) pos = this.positionInside(index)
        }
        return pos
      }
    
      getProxyProcessor() {
        return {
          set(node, prop, value) {
            if (node[prop] === value) return true
            node[prop] = value
            if (
              prop === 'prop' ||
              prop === 'value' ||
              prop === 'name' ||
              prop === 'params' ||
              prop === 'important' ||
              prop === 'text'
            ) {
              node.markDirty()
            }
            return true
          },
    
          get(node, prop) {
            if (prop === 'proxyOf') {
              return node
            } else if (prop === 'root') {
              return () => node.root().toProxy()
            } else {
              return node[prop]
            }
          }
        }
      }
    
      toProxy() {
        if (!this.proxyCache) {
          this.proxyCache = new Proxy(this, this.getProxyProcessor())
        }
        return this.proxyCache
      }
    
      addToError(error) {
        error.postcssNode = this
        if (error.stack && this.source && /\n\s{4}at /.test(error.stack)) {
          let s = this.source
          error.stack = error.stack.replace(
            /\n\s{4}at /,
            `$&${s.input.from}:${s.start.line}:${s.start.column}$&`
          )
        }
        return error
      }
    
      markDirty() {
        if (this[isClean]) {
          this[isClean] = false
          let next = this
          while ((next = next.parent)) {
            next[isClean] = false
          }
        }
      }
    
      get proxyOf() {
        return this
      }
    }
    

    总结

    对于UI设计稿的高保真还原是作为前端工程师最最基本的基本功,但对于现代前端而言,我们不只要考虑到解决方案,还要具备工程化的思维,提升DX(Develop Experience)开发体验,做到降本增效,毕竟我们是前端工程师,而不仅仅是一个前端开发者,共勉!

    参考

    相关文章

      网友评论

          本文标题:PC端高倍屏适配方案实践

          本文链接:https://www.haomeiwen.com/subject/pgywaltx.html