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:
Resolution | Share | multiple |
---|---|---|
1920x1080 | 44.46% | @1x |
1366x768 | 9.37% | @1x |
1536x864 | 8.24% | @1x |
1440x900 | 7.85% | @1x |
1600x900 | 7.85% | @1x |
2560x1440 | -- | @2x |
3840x2160 | -- | @4x |
4096x2160 | -- | @4x |
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
plan | advantage | shortcoming |
---|---|---|
Media inquiries | Media-based screen configuration | For each set of screens, you need to write a set of styles |
rem+media query | Only need to change the root font, converge the control range | Unit conversion is required for the design draft |
vw/vh | Changes based on changes in the window | Need 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!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。