19
头图

Background of the project

With the development of PC-side screens, PC-side screens with higher multiples have gradually appeared. Compared with the Retina screen on mobile phones, the PC-side also has multiple adaptation requirements. This article mainly focuses on the PC-side high-power screen adaptation scheme. A practical summary, hoping to inspire and learn some ideas for students who need to adapt to high-power screens on the PC side

Principle analysis

图片

With the development of screen technology, more and more PC devices are equipped with large-size high-definition screens. For web applications that only need to be implemented on the PC side before, it is necessary to consider the adaptation principles related to mobile applications similar to the mobile phone. Take a look at a principle of the high-definition screen on the mobile phone. For the paper media era, we often use DPI (Dot Per Inch), or dot density, to describe the printing accuracy of printed products. For mobile phone mobile devices, in the iPhone4s, Apple proposed a so-called The concept of Retina screen is to achieve higher-density image information description through the difference in pixel density on the unit screen, that is, the same size screen but the pixel density is different, and the ratio of logical pixels to physical pixels is converted to achieve high-definition screens. The display, that is, PPI (Pixcels Per Inch) is different. As shown in the figure above, for the same detail description through more pixels to describe, you can make the information show more details, and the picture will be more delicate. Based on this, Let's take a look at a common adaptation scheme on the mobile phone

图片

For UI design, in the mobile design process, we often need to consider the design of iOS and Android. In addition to the difference between basic interactive operations, the design adaptation of the two is also a question that is often asked in UI interviews. For UI design, we always hope that the same user’s perception of contact with the same application should be basically the same. In addition to the specific interaction and display style of the system, platform differences should be smoothed out as much as possible, so generally Say that we usually adapt between 750x1334 (iOS @2x) and 720X1280 (Android @2x). For the PC-side Web, we only need to design a size and then simulate the needs of Retina. Based on this, we need to investigate PC-side adaptation strategies to be considered

图片

Through Baidu Traffic Research Institute , we can get the required resolution:

ResolutionSharemultiple
1920x108044.46%@1x
1366x7689.37%@1x
1536x8648.24%@1x
1440x9007.85%@1x
1600x9007.85%@1x
2560x1440--@2x
3840x2160--@4x
4096x2160--@4x

Finally, through the product research plan, we decided to use 1366x768 as the main screen design, and then we processed the compatibility of each screen through the rasterized layout

Scheme selection

For the adaptation of multi-terminal resolution, our commonly used solutions are

planadvantageshortcoming
Media inquiriesMedia-based screen configurationFor each set of screens, you need to write a set of styles
rem+media queryOnly need to change the root font, converge the control rangeUnit conversion is required for the design draft
vw/vhChanges based on changes in the windowNeed to convert the design draft unit, and the browser compatibility is not as good as rem

图片

图片

Finally, considering compatibility, we decided to use the rem+media query solution to adapt the high-power screen. However, if the unit is rewritten based on rem, there will be a certain amount of calculation to change the design draft to the development view. At this time, we will I thought of using front-end engineering for a unified magic change to improve DX (Develop Experience)

Case practice

图片

We use PostCSS to transform the CSS code. For flexible configuration and project use, refer to px2rem to implement a PC-side px2rem class, and then implement a custom postcss plug-in

图片

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"
                    })
                ]
            }
        }
    }
    ...
}

Source code analysis

图片

For PostCSS, many people analyze it as a post processor. Its essence is actually a CSS syntax converter, or the front end of a compiler. Unlike preprocessors such as scss/less, it is not a custom language DSL. Converted. It can be seen from the above figure that the processing method of PostCss is to parse the CSS through Parser, then pass the plug-in, and finally output the new CSS after the Stringifier. It uses the streaming method, providing nextToken(), and the back method, etc. Below we Let’s take a look at the core modules one by one

parser

The implementation of parser can be roughly divided into two types: one is to perform ast conversion by writing files, such as Rework analyzer ; the other is the method used by postcss, after lexical analysis, word segmentation is performed to ast, babel and csstree, etc. are all such solutions

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

Used to format the output CSS text

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

The conversion format defined by postcss is as follows

.className {
    color: #fff;
}

Will be tokenized in the following format

[
    ["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

Plug-in handling mechanism

class Processor {
    constructor(plugins = []) {
        this.plugins = this.normalize(plugins)
    }
    use(plugin) {

    }
    process(css, opts = {}) {

    }
    normalize(plugins) {
        // 格式化插件
    }
}

node

Processing of converted ast nodes

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
  }
}

Summarize

The high-fidelity restoration of UI design drafts is the most basic basic skill as a front-end engineer, but for modern front-ends, we not only need to consider solutions, but also have engineering thinking to enhance the DX (Develop Experience) development experience. To reduce costs and increase efficiency, after all, we are front-end engineers, not just front-end developers, encourage each other!

refer to


维李设论
1.1k 声望4k 粉丝

专注大前端领域发展