共计 17006 个字符,预计需要花费 43 分钟才能阅读完成。
我的项目背景
随着 PC 端屏幕的倒退,PC 端也逐渐呈现了更高倍数的屏幕,绝对于手机端的 Retina 屏,PC 端也呈现了多倍数适配的要求,本文次要是 PC 端高倍屏幕适配计划的一个实际总结,心愿能给对 PC 端有适配高倍屏幕需要的同学有一些思路的启发和借鉴
原理剖析
随着屏幕技术的倒退,越来越多的 PC 设施装备了大尺寸高清屏幕,对于之前只须要在 PC 端实现的 web 利用就须要思考相似手机端的挪动利用相干的适配准则了,咱们先来看一下手机端的高清屏幕的一个原理,对于纸媒时代来说,咱们罕用 DPI(Dot Per Inch) 即网点密度来形容打印品的打印精度,而对于手机挪动设施,在 iPhone4s 时,苹果提出了一个所谓 Retina 屏幕的概念,即通过单位屏幕上像素密度的不同来实现更高密度的图像信息形容,即雷同尺寸的屏幕但像素密度却不雷同,通过逻辑像素与物理像素进行比例换算从而达到高清屏的显示,也就是 PPI(Pixcels Per Inch) 不同,如上图所示,对于同一个细节形容通过更多的像素点来进行刻画,就能够使信息出现更多细节,画面也就更加细腻,基于此,咱们来看一下手机端常见的一个适配计划
对于 UI 设计来说,在挪动端设计过程中,咱们经常须要思考 iOS 和 Android 的设计,除了根本的交互操作的区别外,这两者的设计适配计划也是 UI 面试中经常被问及的问题,对于 UI 设计来说,咱们对于同一个利用来说总心愿同一面对用户触达的感知应该是基本一致,除了零碎特定的交互及展现格调,应尽可能抹平平台的差别,因此一般来说咱们通常会在 750×1334(iOS @2x) 和 720X1280(Android @2x) 进行适配,对于 PC 端的 Web 来说只须要设计一个尺寸而后模仿实现 Retina 的需要即可,基于此,咱们须要调研一下所需思考的 PC 端适配策略
通过百度流量研究院,咱们能够得出所需适配的分辨率为:
分辨率 | 份额 | 倍数 |
---|---|---|
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 |
最终通过产品的调研计划,咱们决定以 1366×768 作为主屏设计,接着咱们通过栅格化的布局对各屏幕的兼容性做解决
计划选型
对于多终端分辨率的适配咱们罕用的计划有
计划 | 长处 | 毛病 |
---|---|---|
媒体查问 | 基于媒体的 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)开发体验,做到降本增效,毕竟咱们是前端工程师,而不仅仅是一个前端开发者,共勉!
参考
- 术与道 挪动利用 UI 设计必修课
- PostCSS 是个什么鬼货色?
- 如果你不会 Postcss,那么你就真的不会 Postcss
- postcss 源码
- 谈谈 PostCSS
- 深刻 PostCSS Web 设计