Project background

Along with the development of the PC screen, PC also gradually appeared higher multiples of the screen, relative to the Retina screen on mobile phones, PC, there is more than a multiple adaptation, this article mainly PC high screen adaptation scheme of a practice, the hope can give is adapted for PC demand high screen students have some ideas of inspiration and reference

The principle of analysis

With the development of screen technology, more and more PC devices are equipped with large-size HD screens. For web applications that only need to be implemented on PC before, it is necessary to consider adaptation principles related to mobile applications on mobile phones. Let’s first take a look at a principle of hd screens on mobile phones. DPI(Dot Per Inch) is commonly used to describe the printing accuracy of printed products. For mobile phones and mobile devices, In iPhone4s, Apple proposed a concept called Retina screen, that is, to achieve higher density image information description through the difference of pixel density on the unit screen. That is, screens of the same size have different pixel densities. The display of high-definition screen can be achieved by converting logical pixels to physical pixels in proportion, that is, PPI(Pixcels Per Inch) is different. As shown in the figure above, the same detailed description can be described by more pixels, so that the information can present more details. The picture is more delicate, based on this, we have a look at a common adaptation of mobile phone

For UI design, in the process of mobile terminal design, we often need to consider the design of iOS and Android. In addition to the difference of basic interactive operation, the design adaptation scheme of the two is also a question that is often asked in UI interview. For UI design, For the same application, we always hope that the perception of user touch should be basically the same. In addition to the specific interaction and presentation style of the system, we should try to erase the differences of flat platform. So in general, we usually adapt between 750×1334(iOS @2x) and 720X1280(Android and 720X1280(Android) @2x). For THE PC side of the Web, we just need to design a size and simulate the Retina needs. Based on this, We need to investigate the PC adaptation strategies we need to consider

Through Baidu Traffic Research Institute, we can conclude that the resolution required for adaptation is:

The resolution of the share A multiple
1920×1080 44.46% @1x
1366×768 9.37% @1x
1536×864 8.24% @1x
1440×900 7.85% @1x
1600×900 7.85% @1x
2560×1440 @2x
3840×2160 @4x
4096×2160 @4x

Finally, through the product research program, we decided to take 1366×768 as the main screen design, and then we processed the compatibility of all screens through the raster layout

Scheme selection

For multi-terminal resolution adaptation we commonly used schemes are

plan advantages disadvantages
Media queries Screen is configured based on media You need to write a set of styles for each set of screens
Rem + media query Only need to change the root font, convergence control range Unit conversion is required for the design draft
vw/vh Changes as the window changes Conversion of design units is required, and browser compatibility is not as good as REM

Finally, considering compatibility, we decided to use the rem+ media query scheme for high-power screen adaptation. However, if the unit rewriting is completely based on REM, there will be a certain amount of calculation for changing the design draft to the development view. At this point, We came up with the idea of using front-end engineering to do a unified magic change to improve the DX(Develop Experience)

Case study

We use PostCSS to transform the CSS code, in order to flexibly configure and project use, reference pX2REM to achieve a PC 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.// Device pixel ratio
        remUnit: 10.// Customize the REM unit
        remPrecision: 6./ / precision
        forcePxComment: "px".// Convert only px
        keepComment: "no".// Whether to keep the unit
        ignoreEntry: null.// Ignore the rule instance carrier
      },
      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;
        }

        // Process the conversion from PX to REM
        let declarations = rule.declarations;
        for (let j = 0; j < declarations.length; j++) {
          let declaration = declarations[j];
          / / conversion of 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) {
                // Do not convert '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;

    // Verify that the ignore rule is met
    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)); // Precision control
      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;
Copy the code

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) {
    // Configure parameters to join the ignore policy method
    options.ignoreEntry = new PxIgnore();
    // new an instance of 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,
};
Copy the code

vue.config.js

// Vue-cli3 is embedded with postCSS, just need to write in the corresponding config
const {postCssPlugins} = require('./build');

module.exports = {
    ...
    css: {
        loaderOptions: {
            postcss: {
                plugins: [
                    postCssPlugins['postcss-pcx2rem'] ({baseDpr: 1.// HTML base fontSize Design size screen size
                        remUnit: (10 * 1366) / 1920.remPrecision: 6.forcePxComment: "px".keepComment: "no"})]}}}... }Copy the code

The source code parsing

For PostCSS, which many people analyze as a post-processor, its essence is actually a CSS syntax converter, or a front-end compiler, unlike SCSS/LESS and other preprocessors, it does not convert custom language DSL. From the figure above, we can see that PostCss is processed by parses CSS through Parser, then passes the plug-in, and finally outputs the new CSS after the Stringifier. It adopts the stream processing method and provides nextToken(), back method, etc. Here we take a look at the core modules one by one

parser

Parser can be implemented in two ways: one is to convert ast by writing files, such as Rework Analyzer. The other is the method used by PostCSS, ast, Babel, csSTree and so on after lexical analysis

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) {
    / / comment
  }

  emptyRule(token) {
    / / clear the token
  }

  other(start) {
    // Handle other cases
  }

  rule(tokens) {
    / / match the token
  }

  decl(tokens, customProperty) {
    // Describe the token
  }

  atrule(token) {
    // Verify the rule
  }

  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
  }
}
Copy the code

stringifier

Used to format 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 (typeofvalue ! = ='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 (typeofroot.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 (typeofvalue ! = ='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
  }
}
Copy the code

tokenize

Postcss defines the conversion format as follows

.className {
    color: #fff;
}
Copy the code

Will be token 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]]Copy the code
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 = The '*'.charCodeAt(0)
const COLON = ':'.charCodeAt(0)
const AT = The '@'.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 = posdo {
            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 -= 1escaped = ! 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 -= 1escaped = ! 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
  }
}
Copy the code

processor

Plug-in handling mechanism

class Processor {
    constructor(plugins = []) {
        this.plugins = this.normalize(plugins)
    }
    use(plugin){}process(css, opts = {}){}normalize(plugins) {
        // Format the plug-in}}Copy the code

node

Processing of transformed 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(a)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}}Copy the code

conclusion

The high-fidelity restoration of UI design draft is the most basic basic skill for front-end engineers. However, for modern front-end, we should not only consider solutions, but also have engineering thinking, improve DX (Develop Experience) development Experience, and achieve cost reduction and efficiency increase. After all, we are front-end engineers. Not just a front-end developer, mutual encouragement!

reference

  • UI design for art and Tao Mobile application is a required course
  • What the hell is PostCSS?
  • If you can’t Postcss, then you really can’t Postcss
  • Postcss source
  • Talk about PostCSS
  • Dive into PostCSS Web design