PC端高倍屏适配方案实践

编程入门 行业动态 更新时间:2024-10-10 17:25:37

PC端高倍屏适<a href=https://www.elefans.com/category/jswz/34/1765773.html style=配方案实践"/>

PC端高倍屏适配方案实践

项目背景

随着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

// 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", // 只换算pxkeepComment: "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];// 转化pxif (declaration.type === "declaration" &&pxRegExp.test(declaration.value)) {let nextDeclaration = declarations[j + 1];if (nextDeclaration && nextDeclaration.type === "comment") {if (nextDeclarationment.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 (nextDeclarationment.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 = inputthis.root = new Root()this.current = this.rootthis.spaces = ''this.semicolon = falsethis.customProperty = falsethis.createTokenizer()this.root.source = { input, start: { offset: 0, line: 1, column: 1 } }}createTokenizer() {this.tokenizer = tokenizer(this.input)}parse() {let tokenwhile (!this.tokenizer.endOfFile()) {token = this.tokenizer.nextToken()switch (token[0]) {case 'space':this.spaces += token[1]breakcase ';':this.freeSemicolon(token)breakcase '}':this.end(token)breakcase 'comment':thisment(token)breakcase 'at-word':this.atrule(token)breakcase '{':this.emptyRule(token)breakdefault: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 = falsethis.current.raws.after = (this.current.raws.after || '') + this.spacesthis.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.spacesthis.spaces = ''if (node.type !== 'comment') this.semicolon = false}raw(node, prop, tokens) {let token, typelet length = tokens.lengthlet value = ''let clean = truelet next, prevlet pattern = /^([#.|])?(\w)+/ifor (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 valueif (!detect) detect = own// Already hadif (own) {value = node.raws[own]if (typeof value !== 'undefined') return value}let parent = node.parentif (detect === 'before') {// Hack for first rule in CSSif (!parent || (parent.type === 'root' && parent.first === node)) {return ''}// `root` nodes in `document` should use only their own rawsif (parent && parent.type === 'document') {return ''}}// Floating child without parentif (!parent) return DEFAULT_RAW[detect]// Detect style by other nodeslet 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] = valuereturn value}beforeAfter(node, detect) {let valueif (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.parentlet depth = 0while (buf && buf.type !== 'root') {depth += 1buf = 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]/ifunction tokenizer(input, options = {}) {let css = input.css.valueOf()let ignore = options.ignoreErrorslet code, next, quote, content, escapelet escaped, escapePos, prev, n, currentTokenlet length = css.lengthlet pos = 0let 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) returnlet ignoreUnclosed = opts ? opts.ignoreUnclosed : falsecode = css.charCodeAt(pos)switch (code) {case NEWLINE:case SPACE:case TAB:case CR:case FEED: {next = posdo {next += 1code = css.charCodeAt(next)} while (code === SPACE ||code === NEWLINE ||code === TAB ||code === CR ||code === FEED)currentToken = ['space', css.slice(pos, next)]pos = next - 1break}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 = falsenext = css.indexOf(')', next + 1)if (next === -1) {if (ignore || ignoreUnclosed) {next = posbreak} else {unclosed('bracket')}}escapePos = nextwhile (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 = posdo {escaped = falsenext = css.indexOf(quote, next + 1)if (next === -1) {if (ignore || ignoreUnclosed) {next = pos + 1break} else {unclosed('string')}}escapePos = nextwhile (css.charCodeAt(escapePos - 1) === BACKSLASH) {escapePos -= 1escaped = !escaped}} while (escaped)currentToken = ['string', css.slice(pos, next + 1), pos, next]pos = nextbreak}case AT: {RE_AT_END.lastIndex = pos + 1RE_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 = nextbreak}case BACKSLASH: {next = posescape = truewhile (css.charCodeAt(next + 1) === BACKSLASH) {next += 1escape = !escape}code = css.charCodeAt(next + 1)if (escape &&code !== SLASH &&code !== SPACE &&code !== NEWLINE &&code !== TAB &&code !== CR &&code !== FEED) {next += 1if (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 = nextbreak}default: {if (code === SLASH && css.charCodeAt(pos + 1) === ASTERISK) {next = css.indexOf('*/', pos + 2) + 1if (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 + 1RE_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] = falsethis[my] = truefor (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 = undefinedreturn this}toString(stringifier = stringify) {if (stringifier.stringify) stringifier = stringifier.stringifylet 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 = thislet foundSelf = falsefor (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 undefinedlet index = this.parent.index(this)return this.parent.nodes[index + 1]}prev() {if (!this.parent) return undefinedlet 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 = thiswhile (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.beforedelete this.raws.afterif (!keepBetween) delete this.raws.between}toJSON(_, inputs) {let fixed = {}let emitInputs = inputs == nullinputs = inputs || new Map()let inputsNextIndex = 0for (let name in this) {if (!Object.prototype.hasOwnProperty.call(this, name)) {// istanbul ignore nextcontinue}if (name === 'parent' || name === 'proxyCache') continuelet 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 = inputsNextIndexinputs.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.columnlet line = this.source.start.linefor (let i = 0; i < index; i++) {if (string[i] === '\n') {column = 1line += 1} else {column += 1}}return { line, column }}positionBy(opts) {let pos = this.source.startif (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 truenode[prop] = valueif (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 = thisif (error.stack && this.source && /\n\s{4}at /.test(error.stack)) {let s = this.sourceerror.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] = falselet next = thiswhile ((next = next.parent)) {next[isClean] = false}}}get proxyOf() {return this}
}

总结

对于UI设计稿的高保真还原是作为前端工程师最最基本的基本功,但对于现代前端而言,我们不只要考虑到解决方案,还要具备工程化的思维,提升DX(Develop Experience)开发体验,做到降本增效,毕竟我们是前端工程师,而不仅仅是一个前端开发者,共勉!

参考

  • 术与道 移动应用UI设计必修课
  • PostCSS 是个什么鬼东西?
  • 如果你不会Postcss,那么你就真的不会Postcss
  • postcss源码
  • 谈谈PostCSS
  • 深入PostCSS Web设计

更多推荐

PC端高倍屏适配方案实践

本文发布于:2024-03-13 01:14:45,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1732852.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:配方   PC   端高倍屏适

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!