本文章使用 Vant UI 框架,其他 UI 框架可自行切换
本文章最终推荐方案为:postcss-px-to-viewport-8-plugin + 自定义行内样式转换插件
像素单位:px、em、rem、vw/vh等的区别?
- px:是像素单位。它是代表显示器上每一个显示的像素点,根据用户屏幕显示器的分辨率决定
- em:为相对单位,相对于当前元素内文本的字体尺寸。如果当前元素没有指定字体尺寸,那么以浏览器默认的字体尺寸为准。例如,当前元素设置了字体尺寸为24px,那么2em就代表48px
-
rem:为相对单位,相对于<HTML>元素文本的字体尺寸。如果<HTML>元素没有指定字体尺寸,那么以浏览器默认的字体尺寸为准
例如,<HTML>元素设置了字体尺寸为24px,那么2rem就代表48px
-
vw和vh:相对单位,相对于当前视口(),又叫 Viewport
例如,10vw代表当前视口宽度的10%,20vh代表当前视口高度的20%
-
%:相对单位,相对于父元素的相关尺寸
例如,父元素设置了height: 100px,那么它的子元素height: 50%就代表50px
总结:一般移动端适配,使用 rem布局 或 Viewport (vw/vh)
rem 布局
如果需要使用 rem
单位进行适配,常常使用postcss-pxtorem 或 lib-flexible 两个方案
postcss-pxtorem 是一款 PostCSS 插件,用于将 px 单位转化为 rem 单位
postcss-pxtorem 已有4年没有更新,因此它是不支持 PostCSS 8.0+ 版本的(PostCss 8.0+ 以上的版本是主流,也是未来的方向),因此不推荐使用该方案
lib-flexible 用于设置 rem 基准值
访问 lib-flexible 的github仓库,文档中有这么一段话
由于
viewport
单位得到众多浏览器的兼容,lib-flexible
这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用viewport
来替代此方。因此不推荐使用该方案
Viewport布局
Vant 默认使用 px
作为样式单位(很多UI框架都是默认px),如果需要使用 viewport 单位 (vw, vh, vmin, vmax),推荐使用 postcss-px-to-viewport 进行转换。
postcss-px-to-viewport 是一款 PostCSS 插件,用于将 px 单位转化为 vw/vh 单位。
postcss-px-to-viewport
不适配postcss 8.0+ 最新版本的, 已弃用,目前有基于postcss 8 8.0+的插件:postcss-px-to-viewport-8-plugin
安装插件
npm install postcss postcss-loader postcss-px-to-viewport-8-plugin -D
配置 vite.config.ts
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
// 自动导入vue中hook reactive ref等
import AutoImport from "unplugin-auto-import/vite";
// 自动导入ui 组件
import Components from "unplugin-vue-components/vite";
// Vant 官方基于 unplugin-vue-components 提供的自动导入样式的解析器
import { VantResolver } from "@vant/auto-import-resolver";
// 这个path 需要安装的 @types/node
import path from "path";
// postcss8 插件
import postcsspxtoviewport8plugin from "postcss-px-to-viewport-8-plugin";
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
// 根据当前工作目录中的 `模式` 加载 .env 文件
// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
// 设置第三个参数为 'VITE_' 表示加载'VITE_'开头的环境变量
const env = loadEnv(mode, process.cwd(), "VITE_");
console.log(env);
const port: number = (env.VITE_APP_PORT as any) || 80;
return {
// 应用访问路径 例如使用前缀 /admin/
base: env.VITE_APP_CONTEXT_PATH,
plugins: [
vue(),
AutoImport({
// 安装两行后你会发现在组件中不用再导入ref,reactive等
imports: ["vue", "vue-router"],
// 存放的位置
dts: "src/auto-import.d.ts",
}),
Components({
// 存放的位置: 引入组件的,包括自定义组件
dts: "src/components.d.ts",
resolvers: [VantResolver()],
}),
],
// 配置别名
resolve: {
// https://cn.vitejs.dev/config/shared-options.html#resolve-alias
alias: {
// "~": path.resolve(__dirname, "./"), // ~代替./
"@": path.resolve("./src"), // @代替src
},
},
// https://cn.vitejs.dev/config/server-options
server: {
// 将此设置为 0.0.0.0 或者 true 将监听所有地址,包括局域网和公网地址
host: true,
// 配置启动并端口号
// 注意:如果端口已经被使用,Vite 会自动尝试下一个可用的端口,所以这可能不是开发服务器最终监听的实际端口
port: Number(port),
// 服务启动时是否自动打开浏览器
open: true,
// 代理配置
proxy: {
[env.VITE_APP_BASE_API]: {
// 要代理的服务器地址
target: env.VITE_PROXY_TARGET_URL,
// 允许跨域,可以代理反向的地址
changeOrigin: true,
// 将代理前缀替换为空
// 如:http://xxx.com/dev-api/login 替换成 -> http://xxx.com/login
rewrite: (path) =>
path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""),
},
},
},
css: {
// 适配移动端
postcss: {
plugins: [
postcsspxtoviewport8plugin({
unitToConvert: "px", // 需要转换的单位,默认为 px
viewportWidth: 375, // UI设计稿的视口宽度
unitPrecision: 5, // 单位转换后保留的精度
propList: ["*"], // 能转化为vw的属性列表
viewportUnit: "vw", // 希望使用的视口单位
fontViewportUnit: "vw", // 字体使用的视口单位
selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
mediaQuery: true, // 媒体查询里的单位是否需要转换单位
replace: true, // 是否直接更换属性值,而不添加备用属性
exclude: [/node_modules/], // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
include: [], // 如果设置了include,那将只有匹配到的文件才会被转换
landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
landscapeUnit: "vw", // 横屏时使用的单位
landscapeWidth: 1024, // 横屏时使用的视口宽度
}),
],
},
},
};
案例:
<template>
<!-- 适配生效 -->
<div class="div1">div1</div>
<!-- 适配不生效:写在style里面的则不能转换,插件不支持这种方式 -->
<div style="width: 185px; height: 50px; background-color: blue">div2</div>
</template>
<script setup lang="ts"></script>
<style>
.div1 {
width: 185px;
height: 50px;
background-color: red;
}
</style>
虽然
postcss-px-to-viewport-8-plugin
做适配,但是行内样式
不能转换为vw,所以我们自定义个插件,将内样式
px转成vw
创建文件 src/plugin/vite-plugin-style-vw-loader.ts, 内容为:
// 虽然postcss-px-to-viewport-8-plugin做适配,但是行内样式不能转换为vw,所以我们自定义个插件,将内样式px转成vw
interface IdefaultsProp {
unitToConvert: string;
viewportWidth: number;
unitPrecision: number;
viewportUnit: string;
fontViewportUnit: string;
minPixelValue: number;
}
// 默认参数
const defaultsProp: IdefaultsProp = {
unitToConvert: "px", // 需要转换的单位,默认为"px"
viewportWidth: 375, // 设计稿的视口宽度,如传入函数,函数的参数为当前处理的文件路径
unitPrecision: 5, // 单位转换后保留的精度
viewportUnit: "vw", // 希望使用的视口单位
fontViewportUnit: "vw", // 字体使用的视口单位
minPixelValue: 1, // 设置最小的转换数值,如果为 1 的话,只有大于 1 的值会被转换
};
function toFixed(number: number, precision: number) {
const multiplier = Math.pow(10, precision + 1),
wholeNumber = Math.floor(number * multiplier);
return (Math.round(wholeNumber / 10) * 10) / multiplier;
}
function createPxReplace(
viewportSize: number,
minPixelValue: number,
unitPrecision: number,
viewportUnit: any
) {
return function ($0: any, $1: any) {
if (!$1) return;
const pixels = parseFloat($1);
if (pixels <= minPixelValue) return;
return toFixed((pixels / viewportSize) * 100, unitPrecision) + viewportUnit;
};
}
const templateReg: RegExp = /([\s\S]+)/gi;
const pxGlobalReg: RegExp = /(\d+)px/gi;
function vitePluginStyleVWLoader(customOptions: IdefaultsProp = defaultsProp) {
return {
// 插件名称
name: "vite-plugin-style-vw-loader",
// 构建阶段的通用钩子:在每个传入模块请求时被调用:在每个传入模块请求时被调用,主要是用来转换单个模块
transform(code: any, id: any) {
customOptions = Object.assign(defaultsProp, customOptions);
if (/.vue$/.test(id)) {
let _source = "";
if (templateReg.test(code)) {
_source = code.match(templateReg)[0];
}
if (pxGlobalReg.test(_source)) {
const $_source = _source.replace(
pxGlobalReg,
createPxReplace(
customOptions.viewportWidth,
customOptions.minPixelValue,
customOptions.unitPrecision,
customOptions.viewportUnit
)
);
code = code.replace(_source, $_source);
}
}
return { code };
},
};
}
export default vitePluginStyleVWLoader;
修改tsconfig.node.json 配置为:
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
// 这里加载自定义的插件ts
"include": ["vite.config.ts", "src/plugin/vite-plugin-style-vw-loader.ts"]
}
修改 vite.config.ts 配置为如下:
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
// 自动导入vue中hook reactive ref等
import AutoImport from "unplugin-auto-import/vite";
// 自动导入ui 组件
import Components from "unplugin-vue-components/vite";
// Vant 官方基于 unplugin-vue-components 提供的自动导入样式的解析器
import { VantResolver } from "@vant/auto-import-resolver";
// 这个path 需要安装的 @types/node
import path from "path";
// postcss8 插件
import postcsspxtoviewport8plugin from "postcss-px-to-viewport-8-plugin";
// 虽然postcss-px-to-viewport-8-plugin做适配,但是行内样式不能转换为vw,所以我们自定义个插件,将内样式px转成vw
import vitePluginStyleVwLoader from "./src/plugin/vite-plugin-style-vw-loader";
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
// 根据当前工作目录中的 `模式` 加载 .env 文件
// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
// 设置第三个参数为 'VITE_' 表示加载'VITE_'开头的环境变量
const env = loadEnv(mode, process.cwd(), "VITE_");
console.log(env);
const port: number = (env.VITE_APP_PORT as any) || 80;
return {
// 应用访问路径 例如使用前缀 /admin/
base: env.VITE_APP_CONTEXT_PATH,
plugins: [
// 该插件需要放在vue()之前
vitePluginStyleVwLoader({
unitToConvert: "px",
viewportWidth: 375,
unitPrecision: 5,
viewportUnit: "vw",
fontViewportUnit: "vw",
minPixelValue: 1,
}),
AutoImport({
// 全局引入插件
// 安装两行后你会发现在组件中不用再导入ref,reactive等
imports: ["vue", "vue-router"],
// 存放的位置
dts: "src/auto-import.d.ts",
}),
// 全局注册组件
Components({
// 默认自动导入的目录是: dirs: ['src/components']
dirs: ["src/components"],
// 存放的位置: 引入组件的,包括自定义组件
dts: "src/components.d.ts",
resolvers: [VantResolver()],
}),
vue(),
],
// 配置别名
resolve: {
// https://cn.vitejs.dev/config/shared-options.html#resolve-alias
alias: {
// "~": path.resolve(__dirname, "./"), // ~代替./
"@": path.resolve("./src"), // @代替src
},
},
// https://cn.vitejs.dev/config/server-options
server: {
// 将此设置为 0.0.0.0 或者 true 将监听所有地址,包括局域网和公网地址
host: true,
// 配置启动并端口号
// 注意:如果端口已经被使用,Vite 会自动尝试下一个可用的端口,所以这可能不是开发服务器最终监听的实际端口
port: Number(port),
// 服务启动时是否自动打开浏览器
open: true,
// 代理配置
proxy: {
[env.VITE_APP_BASE_API]: {
// 要代理的服务器地址
target: env.VITE_PROXY_TARGET_URL,
// 允许跨域,可以代理反向的地址
changeOrigin: true,
// 将代理前缀替换为空
// 如:http://xxx.com/dev-api/login 替换成 -> http://xxx.com/login
rewrite: (path) =>
path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""),
},
},
},
css: {
// 适配移动端
postcss: {
plugins: [
postcsspxtoviewport8plugin({
unitToConvert: "px",
viewportWidth: 375,
unitPrecision: 5, // 单位转换后保留的精度
propList: ["*"], // 能转化为vw的属性列表
viewportUnit: "vw", // 希望使用的视口单位
fontViewportUnit: "vw", // 字体使用的视口单位
selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
mediaQuery: true, // 媒体查询里的单位是否需要转换单位
replace: true, // 是否直接更换属性值,而不添加备用属性
exclude: [], // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
include: [], // 如果设置了include,那将只有匹配到的文件才会被转换
landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
landscapeUnit: "vw", // 横屏时使用的单位
landscapeWidth: 1024, // 横屏时使用的视口宽度
}),
],
},
},
};
});
这样自定义样式+行内样式 px都会转vw了