一款 PostCSS 插件,将固定尺寸的视图转为可跟随宽度变化而等比例伸缩的视图。To adapt different displays by one mobile viewport.
⚠️ Warning
使用 vw 或 rem 生成的伸缩视图,且没有限制最大宽度的,将不能触发浏览器的缩放功能(可以通过快捷键同时按下 Cmd/Ctrl 和 +/- 触发),不能满足针对缩放的可访问性标准,因此存在可访问性问题。查看一个关于 vw 伸缩视图的可访问性实验。
不同设备上的界面一致,不等于用户体验一致,使用 vw(或 rem)做移动端适配,是一种粗暴的、技术先于设计的适配方法,是一条技术捷径,请考虑站在用户的角度、利用专业知识,使用响应式设计开发页面,使得用户在大尺寸设备上看到更丰富的内容,在小尺寸设备上看到更简洁的内容。
postcss-mobile-forever 是一款 PostCSS 插件,用于将固定尺寸转为伸缩尺寸,得到一个能够等比例缩放的视图,并提供一揽子限制最大宽度的办法。mobile-forever 可以配合 scale-view 使用,前者用于编译阶段,后者用于运行时。mobile-forever 有 4 种模式,适用不同的场景:
px
->vw
实现@media
实现min(vw, px)
实现<html>
的 font-size: vw
和 @media
结合 rem
实现后面的“配置参数”一节将介绍如何激活这些模式。mobile-forever 默认会矫正 fixed
定位的元素(例如将宽屏右下角的“Top”按钮矫正回中央视图区域),并支持逻辑属性的转换。
下面是一个移动端模版列表,这些模版使用了 mobile-forever 进行配置,在宽屏上展示良好,下面的每一项都包含了在线演示链接和模版源码,通过源码可以参考 mobile-forever 的配置方法:
cd example/react/
npm install
npm run start
npm 安装最新版本(基于 postcss@^8.0.0)(yarn 则是 yarn add -D postcss postcss-mobile-forever
):
npm install --save-dev postcss postcss-mobile-forever
npm 安装最新的兼容版本(基于 postcss@^6.0.0)(yarn 则是 yarn add -D postcss-mobile-forever@legacy
):
npm install postcss-mobile-forever@legacy --save-dev
查看兼容版本的 mobile-forever 文档,目前兼容版本不支持逻辑属性的处理。
postcss.config.js
有好几种配置格式,下面是其中一种配置方法:
import mobile from 'postcss-mobile-forever' // <---- 这里
import autoprefixer from 'autoprefixer'
// 省略……
{
postcss: {
plugins: [
autoprefixer(),
mobile({ // <---- 这里
appSelector: '#app',
viewportWidth: 375,
maxDisplayWidth: 580,
}),
]
}
}
// 省略……
https://github.com/webpack-contrib/postcss-loader/issues/172
如果项目原本是基于 rem+js 做的移动端适配,可以参考文档“迁移”,迁移到 vw 移动端适配或 mobile-forever 提供的其它适配办法。
一大波配置参数正在靠近,不必焦虑,尽在掌握,深入之前可以先尝试最基础的配置参数。下方是一个基础配置(max-vw-mode),表示了应用正在基于 750px
的宽度开发,经过 mobile-forever 转换后,浏览器中,应用视图将被限制在 600px
宽度以内进行等比例缩放,当宽度大于 600px
,视图将不改变,并且根元素 #app
的应用视图始终处于浏览器窗口的中央区域:
{
"viewportWidth": 750,
"appSelector": "#app",
"maxDisplayWidth": 600
}
下面的每一项都是可选的,设定 viewportWidth
后激活 vw-mode,设定 maxDisplayWidth
后激活 max-vw-mode,设定 basicRemWidth
或指定 mobileUnit
的值为 rem
后,激活 rem-mode,设定 enableMediaQuery
后激活 mq-mode。前 3 种模式的属性设定是递进的,后一个模式需要同时设定前一个模式的所需属性。
激活 max-vw-mode、rem-mode、mq-mode 后,样式文件中至少要包含空的应用最外层选择器,例如 #app {}
,激活 rem-mode
后,样式文件还要至少包含空的 <html>
选择器,例如 html {}
。
Name | Type | Default | Desc |
---|---|---|---|
viewportWidth | number|(file: string) => number | 750 | 应用基于该宽度进行开发,转换后的伸缩视图将会以该宽度的视图作为标准进行比例伸缩;可以传递函数动态生成宽度,例如 file => file.includes("vant") ? 375 : 750 表示在名称包含“vant”的文件内使用 375px 的宽度,而其他文件使用 750px 的宽度 |
mobileUnit | string | “vw” | 移动端竖屏视口视图,转换成什么伸缩单位?设置为 rem 后激活 rem-mode |
maxDisplayWidth | number | / | 伸缩视图的最大宽度 |
basicRemWidth | number | / | rem-mode 的基准宽度,若不设定,将通过 viewportWidth 获取 |
enableMediaQuery | boolean | false | 打开媒体查询模式,打开后将自动关闭 maxDisplayWidth ,激活 mq-mode |
desktopWidth | number | 600 | 适配到桌面端宽度时,展示的视图宽度 |
landscapeWidth | number | 425 | 适配到移动端横屏宽度时,展示的视图宽度 |
appSelector | string | / | 页面最外层选择器,例如“#app ”,用于设置在桌面端和移动端横屏时的居中样式,样式文件中至少要包含空的选择器 #app {} |
appContainingBlock | “calc”|“manual”|“auto” | “calc” | 该属性和矫正 fixed 定位元素有关,manual 将不矫正;calc 将通过插件主动计算的方式矫正元素尺寸,是默认行为;auto 将通过 transform: translateZ(0) 强制设置根包含块为 appSelector ,从而自动矫正元素,并且此时需要设置属性 necessarySelectorWhenAuto |
necessarySelectorWhenAuto | string | / | 当 appContainingBlock 设为 auto 时,需要指定该属性,该属性指定了 appSelector 往内一层的元素选择器,查看一个关于指定元素作为包含块的实验以了解如何使用该属性,您也可以查看使用这个属性的示例项目以了解如何使用这个属性 |
border | boolean|string | false | 在页面外层展示边框吗,用于分辨居中的小版心布局和背景,可以设置颜色字符串 |
disableDesktop | boolean | false | 打开则不做桌面端适配,使用该参数前需要打开 enableMediaQuery |
disableLandscape | boolean | false | 打开则不做移动端横屏适配,使用该参数前需要打开 enableMediaQuery |
disableMobile | boolean | false | 打开则不做移动端竖屏适配,把 px 转换为视口单位,如 vw |
exclude | RegExp|RegExp[] | / | 排除文件或文件夹 |
include | RegExp|RegExp[] | / | 包括文件或文件夹 |
unitPrecision | number | 3 | 单位精确到小数点后几位? |
propList | string[] | [‘*’] | 哪些属性要替换,哪些属性忽略?用法参考 postcss-px-to-viewport 文档 |
selectorBlackList | (string|RegExp)[] | [] | 选择器黑名单,名单上的不转换 |
propertyBlackList | propertyBlackList | [] | 属性黑名单,名单上的不转换,如果要指定选择器内的属性,用对象的键表示选择器名称,具体用法见 vant 的范例代码 |
valueBlackList | (string|RegExp)[] | [] | 属性值黑名单,名单上的值不转换 |
rootContainingBlockSelectorList | (string|RegExp)[] | [] | 包含块是根元素的选择器列表,效果和标注注释 /* root-containing-block */ 相同 |
verticalWritingSelectorList | (string|RegExp)[] | [] | 纵向书写模式的选择器列表,效果和在选择器顶部标注注释 /* vertical-writing-mode */ 相同 |
minDesktopDisplayWidth | number | / | 宽度断点,如果不提供这个值,默认使用 desktopWidth 的值,视图大于这个宽度,则页面宽度是桌面端宽度 desktopWidth ,“原理和输入输出范例”一节具体介绍了该值的触发情况 |
maxLandscapeDisplayHeight | number | 640 | 高度断点,视图小于这个高度,并满足一定条件,则页面使用移动端横屏宽度,“原理和输入输出范例”一节具体介绍了该值的触发情况 |
side | any | / | 侧边配置,在桌面端媒体查询中生效,用于利用宽屏的空间,后文将介绍它的若干子属性 |
comment | any | / | 自定义注释,改变注释的名称,后文将介绍它的若干子属性 |
customLengthProperty | any | / | 用于指定需要添加到桌面端或横屏的自定义变量(css 变量,var(...) ),如果不指定,默认所有和长度有关的属性,如果使用了自定义变量,都会被添加入桌面端和横屏,后文将介绍它的若干子属性 |
experimental.extract | boolean | false | 提取桌面端与横屏样式代码,用于生产环境,用于代码分割优化产包,具体查看“注意事项”一节 |
experimental.minDisplayWidth | number | / | 限制最小宽度,和 maxDisplayWidth 搭配使用 |
下面是属性 customLengthProperty
的子属性,用于自定义变量,并且每一个属性都是可选的。customLengthProperty
有两个作用,一个是指定转换方式,例如基于根包含块的 left
和 right
,则需要 customLengthProperty.rootContainingBlockList_LR
进行指定,来得到正确的转换结果,另一个作用是,在媒体查询模式下,避免所有和长度有关的使用 CSS 变量的属性,都被添加到媒体查询中,用于指定真正需要添加到桌面端或横屏的自定义变量:
Name | Type | Default | Desc |
---|---|---|---|
rootContainingBlockList_LR | string[] | [] | 用于根包含块的,left、right 的自定义属性,例如设置 ["--len-a", "--len-b"] 后,--len-a 和 --len-b 的值会转换为用于 left 和 right 属性,并且包含块是根包含块的值,并添加到桌面端和横屏中 |
rootContainingBlockList_NOT_LR | string[] | [] | 用于根包含块的,非 left、right 的自定义属性 |
ancestorContainingBlockList | string[] | [] | 用于非根包含块的自定义属性,这些属性值不会被转换,但是会添加到桌面端和横屏,用于避免优先级问题 |
disableAutoApply | boolean | false | 关闭自定义属性自动添加到桌面端和横屏,设置上面的三个选项后,这个选项自动为 true |
下面是属性 side
的子属性,每一个属性都是可选的,side
用于配制侧边内容,只有当打开媒体查询模式、disableDesktop
为 false 的时候,side
将生效:
Name | Type | Default | Desc |
---|---|---|---|
width | number | / | 侧边宽度,如果指定的选择器下有 width 属性,则无需设置 |
gap | number | 18 | 侧边布局的上下左右间隔 |
selector1 | string | / | 左上侧边元素选择器 |
selector2 | string | / | 右上侧边元素选择器 |
selector3 | string | / | 右下侧边元素选择器 |
selector4 | string | / | 左下侧边元素选择器 |
width1 | number | / | 左上侧边宽度,优先级大于 width |
width2 | number | / | 右上侧边宽度 |
width3 | number | / | 右下侧边宽度 |
width4 | number | / | 左下侧边宽度 |
也可以通过在样式文件中添加注释,来标记局部的尺寸该如何转换,下面是一些标记注释:
/* apply-without-convert */
,标记在一行属性之后,表示属性不经过转换,将直接添加到桌面端和横屏(可用于属性覆盖的情况);/* root-containing-block */
,标记在选择器上面,用于表示当前选择器的包含块是根元素,是浏览器窗口(如果选择器中已有“position: fixed;
”,则无需标注该注释);/* not-root-containing-block */
,标记在选择器上面,用于表示当前选择器所属元素的包含块不是根元素;/* mobile-ignore-next */
,标记在一行属性的上面,表示下一行属性不需要进行转换;/* mobile-ignore */
,标记在一行属性后面,表示当前行属性不需要进行转换;/* vertical-writing-mode */
,标记在选择器上面,表示当前选择器是纵向书写模式,内部的逻辑属性需要被转换。下面是属性 comment
的子属性,每一个属性都是可选的,comment
用于自定义注释:
Name | Type | Default | Desc |
---|---|---|---|
applyWithoutConvert | string | “apply-without-convert” | 直接添加进屏幕媒体查询,不转换 |
rootContainingBlock | string | “root-containing-block” | 包含块注释 |
notRootContainingBlock | string | “not-root-containing-block” | 非包含块注释 |
ignoreNext | string | “mobile-ignore-next” | 忽略选择器内的转换 |
ignoreLine | string | “mobile-ignore” | 忽略本行转换 |
verticalWritingMode | string | “vertical-writing-mode” | 纵向书写模式 |
下面的配置会激活 max-vw-mode,使用 CSS 函数限制视口单位的最大值,当屏幕宽度超过 600px 后,视图不会再变化:
{
"viewportWidth": 750,
"appSelector": "#app",
"maxDisplayWidth": 600
}
下面的配置会激活 mq-mode,生成媒体查询,适配桌面端和横屏,桌面端视图的宽度是 600px,横屏的宽度是 425px:
{
"viewportWidth": 750,
"appSelector": "#app",
"enableMediaQuery": true
}
如果暂时不希望优化视图在大屏的可访问性,不做最大宽度的限制,可以像下面这样配置激活 vw-mode:
{
"viewportWidth": 750
}
{
"viewportWidth": 750,
"maxDisplayWidth": null,
"enableMediaQuery": false,
"desktopWidth": 600,
"landscapeWidth": 425,
"minDesktopDisplayWidth": null,
"maxLandscapeDisplayHeight": 640,
"appSelector": "#app",
"appContainingBlock": "calc",
"necessarySelectorWhenAuto": null,
"border": false,
"disableDesktop": false,
"disableLandscape": false,
"disableMobile": false,
"exclude": null,
"include": null,
"unitPrecision": 3,
"selectorBlackList": [],
"valueBlackList": [],
"rootContainingBlockSelectorList": [],
"verticalWritingSelectorList": [],
"propList": ["*"],
"mobileUnit": "vw",
"side": {
"width": null,
"gap": 18,
"selector1": null,
"selector2": null,
"selector3": null,
"selector4": null,
"width1": null,
"width2": null,
"width3": null,
"width4": null
},
"comment": {
"applyWithoutConvert": "apply-without-convert",
"rootContainingBlock": "root-containing-block",
"notRootContainingBlock": "not-root-containing-block",
"ignoreNext": "mobile-ignore-next",
"ignoreLine": "mobile-ignore",
"verticalWritingMode": "vertical-writing-mode"
},
"customLengthProperty": {
"rootContainingBlockList_LR": [],
"rootContainingBlockList_NOT_LR": [],
"ancestorContainingBlockList": [],
"disableAutoApply": false
},
"experimental": {
"extract": false,
"minDisplayWidth": null
}
}
npm install
npm run test
修改源码后,编写单元测试,验证是否输出了预期的结果。另外,在文件夹 example/
内提供了一些范例,可以用来模拟生产环境使用插件的场景,这些范例项目中依赖的 postcss-mobile-forever
来自源码,因此当修改源码后,可以通过在范例里 npm i
安装依赖,然后本地运行,通过浏览器验证自己的修改是否符合预期。
如果您是移动端模版的开发者,成功引入 postcss-mobile-forever 之后,您可以按照“移动端模版和范例”一节的格式,提交 PR,将您的模版仓库以及在线演示地址添加到后续的列表中,这样可以为您的模版提供更多的曝光,也能够表示您的移动端模版具有良好的桌面端体验。
一起开发,让程序的变量命名更合适、性能和功能更好。
插件配置:
{
"viewportWidth": 750,
"maxDisplayWidth": 560,
"appSelector": "#app"
}
输入范例:
#app {
width: 100%;
}
.nav {
position: fixed;
width : 100%;
height : 72px;
left : 0;
top : 0;
}
输出范例:
#app {
width : 100%;
max-width : 560px !important;
margin-left : auto !important;
margin-right: auto !important;
}
.nav {
position: fixed;
width : min(100%, 560px);
height : min(9.6vw, 53.76px);
left : calc(50% - min(50%, 280px));
top : 0;
}
插件配置:
{
"viewportWidth": 750,
"maxDisplayWidth": 560,
"mobileUnit": "rem",
"appSelector": "#app"
}
输入范例:
html {}
#app {
width: 100%;
}
.nav {
position: fixed;
width : 100%;
height : 72px;
left : 0;
top : 0;
}
输出范例:
html {
font-size: 13.333333333333334vw !important;
}
#app {
max-width : 560px !important;
margin-left : auto !important;
margin-right: auto !important;
width : 100%;
}
.nav {
position: fixed;
width : 7.5rem;
height : 0.72rem;
left : calc(50% - 3.75rem);
top : 0;
}
@media (min-width: 560px) {
html {
font-size: 74.66666666666667px !important;
}
}
插件配置:
{
"appSelector": "#app",
"enableMediaQuery": true
}
输出范例:
#app {
width: 100%;
}
.nav {
position: fixed;
width : 100%;
height : 9.6vw;
left : 0;
top : 0;
}
/* 桌面端媒体查询 */
@media (min-width: 600px) and (min-height: 640px) { /* 这里的 600 是默认值,可以自定义 */
#app {
max-width: 600px !important;
}
.nav {
height: 57.6px;
top : 0;
left : calc(50% - 300px); /* calc(50% - (600 / 2 - 0 * 600 / 750)px) */
width : 600px; /* 100% -> 600px */
}
}
/* 移动端媒体查询 */
@media (min-width: 600px) and (max-height: 640px),
(max-width: 600px) and (min-width: 425px) and (orientation: landscape) { /* 这里的 640 和 425 是默认值,可自定义 */
#app {
max-width: 425px !important;
}
.nav {
height: 40.8px;
top : 0;
left : calc(50% - 212.5px); /* calc(50% - (425 / 2 - 0 * 425 / 750)px) */
width : 425px; /* 100% -> 425px */
}
}
/* 桌面端和移动端公共的媒体查询 */
@media (min-width: 600px),
(orientation: landscape) and (max-width: 600px) and (min-width: 425px) {
#app {
margin-left : auto !important;
margin-right: auto !important;
}
}
相比使用 CSS 函数,使用媒体查询限制宽度,生成的代码量更大。
查看原理。
appSelector 所在元素的居中属性会被占用,包括 margin-left
、margin-right
、max-width
,如果开启了 border,box-shadow
会被占用。
默认情况,插件会把所有 fixed
定位的元素的包含块当成根元素,如果希望跳过处理非根元素的包含块,请在选择器上方添加注释,/* not-root-containing-block */
,这样设置后,插件会知道这个选择器内的计算方式统一使用非根包含块的计算方式:
/* not-root-containing-block */
.class {
position: fixed;
left: 50%;
}
对于包含块,插件默认的处理方式不能处理下面列表中的情况,如果下面某个情况设置在某个祖先元素上,那么当前定位为 fixed
元素的包含块就是那个祖先元素,而插件默认所有的 fixed
元素的包含块是浏览器窗口(visual viewport):
contain: paint;
);backdrop-filter: blur(10px);
)。.position {
position: fixed;
}
/* root-containing-block */
.top-box {
right: 0;
bottom: 0;
width: 66px;
height: 66px;
border-radius: 9px;
}
mobile.xxx.css
、landscape.xxx.css
和 desktop.xxx.css
,这有利于使用代码分割进行产包优化;modules.getLocalIdent
选项,需要从本插件导入 remakeExtractedGetLocalIdent
函数进行传递,这是为了防止选择器名称中的哈希值出现错误(哈希值会通过文件路径计算,而被分割的文件路径是不同的);optimization.splitChunks.cacheGroups.[group].priority
来决定,优先级越高,插入到 html 的顺序越靠前。const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { defaultGetLocalIdent } = require("css-loader");
const { remakeExtractedGetLocalIdent } = require("postcss-mobile-forever");
const isProdMode = process.env.NODE_ENV === "production";
module.exports = {
mode: isProdMode ? "production" : "development",
entry: "./src/index.js",
output: {
filename: "[name].js",
path: path.resolve(__dirname, "build"),
clean: true,
},
module: {
rules: [
{
test: /\.css$/,
use: [isProdMode ? MiniCssExtractPlugin.loader : "style-loader", {
loader: "css-loader",
options: {
modules: {
localIdentName: isProdMode ? "[hash:base64]" : "[path][name]__[local]",
getLocalIdent: isProdMode ? remakeExtractedGetLocalIdent({ defaultGetLocalIdent }) : undefined, // 开发环境不分割,因此设置为 undefined
},
}
}, {
loader: "postcss-loader",
options: {
postcssOptions: [
["postcss-mobile-forever", {
appSelector: ".root-class",
experimental: {
extract: isProdMode, // 生产环境打开文件的提取
},
}]
]
}
}],
}
],
},
optimization: {
splitChunks: {
cacheGroups: {
desktop: {
chunks: "all",
enforce: true,
test: /desktop[^\\/]*?\.css$/, // 分割桌面端样式
name: "desktop",
priority: 101, // 第三位被加载
},
landscape: {
chunks: "all",
enforce: true,
test: /landscape[^\\/]*?\.css$/, // 分割横屏样式
name: "landscape",
priority: 102, // 第二位被加载
},
mobile: {
chunks: "all",
enforce: true,
test: /mobile[^\\/]*?\.css$/, // 分割移动端样式
name: "mobile",
priority: 103, // 第一位被加载
},
}
}
},
};
前往范例查看可运行的配置。
下面的例子,默认的情况,--len-a
的值在桌面端会被转为 60px
,横屏会被转为 42.5px
,但是可以看到实际的应用场景中,定位是 fixed
,因此包含块是根包含块,所以默认的转换是错误的,正确的转换应该是,桌面端会被转为 calc(50% - 240px)
,横屏会被转为 calc(50% - 170px)
。
:root {
--len-a: 75px;
}
.rule {
left: var(--len-a);
position: fixed;
}
上面的例子中,如果要正确的转换,需要在配置中明确指定,--len-a
用于根包含块,并且被用于 left
属性:
{
// ...其它配置
customLengthProperty: {
rootContainingBlockList_LR: ["--len-a"]
}
}
本插件媒体查询模式的目标是在不同尺寸的屏幕上展示合适的视图,在宽一点的屏幕上展示大一点的视图,在扁一点的屏幕上展示小一点的视图,在窄一些的屏幕展示移动端竖屏视图,而非准确地识别具体的设备或平台来应用对应视图。
使用插件转换后的浏览器兼容性情况:媒体查询模式和 rem 模式下会利用 CSS 函数 calc()
,因此兼容性略有降低,Opera Mini 完全不可用,max-vw-mode 利用了 CSS 函数 max()
、min()
以及 calc()
,会有兼容性问题,IE、Opera Mini、QQ 浏览器 13.1 完全不可用,具体可以查看 caniuse min(), max()、caniuse calc()。
在不同设备上,duozhuayu.com(多抓鱼)公用一套 UI,访问无障碍,没有巨大字体和全宽的问题。
多抓鱼官网用百分比单位做适配,最大宽度是 600px,小于这个宽度则向内挤压,大于这个宽度则居中移动端竖屏视图,这种小版心布局在不同尺寸屏幕的设备上,展示效果很好。这样的适配方法舍弃了对设计稿的“完美”还原,相应的,代码没有了预处理,更轻量了,没有了“完美”的限制,开发的过程也变得灵活,对于一种布局,有很多方式实现适配,而且,这样适配也能很好地触发浏览器的缩放功能,满足了针对缩放的可访问性标准。
查看更新日志。
查看语义化版本 2.0.0。
查看 MIT License。
请随意 Issue、PR 和 Star,您也可以支付该项目,支付金额由您从该项目中获得的收益自行决定。
微信支付 | 支付宝支付 |
<style>
body {
margin: 0;
}
#iframe {
max-width: 520px;
width: 100%;
height: 100%;
margin: 0 auto;
display: block;
}
</style>
<body>
<!-- vw-index.html 为 postcss-px-to-viewport 转换后的伸缩界面 -->
<iframe id="iframe" src="./vw-index.html" frameborder="0"></iframe>
<body>
与本项目有关或者可以配合使用的项目:
相关链接: