我的项目背景

随着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端适配策略

通过百度流量研究院,咱们能够得出所需适配的分辨率为:

分辨率份额倍数
1920x108044.46%@1x
1366x7689.37%@1x
1536x8648.24%@1x
1440x9007.85%@1x
1600x9007.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

// Pcx2remconst 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 "#'()/;[\\\]{}]/gconst RE_WORD_END = /[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/gconst RE_BAD_BRACKET = /.[\n"'(/\\]/const RE_HEX_ESCAPE = /[\da-f]/ifunction 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)开发体验,做到降本增效,毕竟咱们是前端工程师,而不仅仅是一个前端开发者,共勉!

参考

  • 术与道 挪动利用UI设计必修课
  • PostCSS 是个什么鬼货色?
  • 如果你不会Postcss,那么你就真的不会Postcss
  • postcss源码
  • 谈谈PostCSS
  • 深刻PostCSS Web设计