uniapp 动态字体大小
一、当前项目方案
项目已经内置动态字体大小能力,主要用于在 标准版 和 关怀版 之间切换字号。
当前实现不是单独提供一个滑块配置页,而是在 我的 -> 切换版本 中弹出 version-switch-popup,由用户选择:
- 标准版:
16px - 关怀版:
20px
整体链路如下:
vite.config.js通过PageRootInsertElement给页面自动插入page-meta。src/main.js挂载$getRootFontSize(),供page-meta读取当前根字号。src/composables/useFont.js维护根字号状态,并持久化到本地缓存。src/components/version-switch-popup/version-switch-popup.vue提供标准版/关怀版切换弹窗。src/pages/admin-profile/index.vue在「我的」页面触发切换。
二、核心原理
微信小程序从基础库 2.9.0 开始支持 page-meta 组件。项目给每个页面注入:
<page-meta :root-font-size="$getRootFontSize()"></page-meta>页面中的 rem 会跟随 root-font-size 变化,因此只要把需要动态缩放的 px / rpx 转成 rem,就可以通过修改根字号完成整体字体缩放。
三、相关文件
1. scripts/pxtorem-plugin.js
项目封装了一个 Vite 插件,用 postcss-pxtorem 把指定属性转成 rem:
import pxtorem from "postcss-pxtorem"
export default function pxtoremPlugin(options = {}) {
const defaultOptions = {
rootValue: 16,
unitPrecision: 5,
propList: ["font", "font-size", "line-height", "letter-spacing", "--wot-n-*"],
selectorBlackList: [],
replace: true,
mediaQuery: false,
minPixelValue: 0,
exclude: null,
unit: "px"
}
const mergedOptions = { ...defaultOptions, ...options }
return {
name: "vite-pxtorem-plugin",
enforce: "post",
config: () => ({
css: {
postcss: {
plugins: [pxtorem(mergedOptions)]
}
}
})
}
}这里的 propList 只处理字体相关属性和 Wot UI 的部分字号变量,避免把布局尺寸也跟着关怀版放大。
2. scripts/page-root-insert-element.js
该插件负责给页面组件自动插入 page-meta:
import { parse } from "@vue/compiler-dom"
export default function pageRootinsertElementPlugin(content = "") {
return {
name: "vite-pageRootInsertElement-plugin",
enforce: "pre",
transform(code, id) {
if (id.includes("node_modules")) return
const pathRegex = /(pages|[\w-]+-modules)\//
if (!pathRegex.test(id)) return
if (id.includes("components/")) return
if (!id.endsWith(".vue")) return
try {
const parsed = parse(code, { comments: true })
const templateNode = parsed.children.find(node => node.tag === "template")
if (!templateNode) return
const templateContent = code.slice(templateNode.loc.start.offset, templateNode.loc.end.offset)
const insertPosition = templateContent.indexOf(">") + 1
const beforeInsert = templateContent.slice(0, insertPosition)
const afterInsert = templateContent.slice(insertPosition)
return code.replace(templateContent, beforeInsert + content + afterInsert)
} catch (e) {
console.warn("page-root-insert-element插件处理失败:", e.message)
return code
}
}
}
}注意:当前匹配范围包含 src/pages 和 src/*-modules 下的页面,排除了组件目录。
3. src/composables/useFont.js
字体大小状态统一由 useFont 管理:
import { ref } from "vue"
const DEFAULT_ROOT_FONT_SIZE = 16
const ROOT_FONT_SIZE_STORAGE_KEY = "rootFontSize"
const ROOT_FONT_SCALE_CLASS = "wot-font-scale-vars"
const rootFontSize = ref(DEFAULT_ROOT_FONT_SIZE)
let initialized = false
function syncRootFontSize() {
if (typeof document === "undefined") {
return
}
const root = document.documentElement
root.style.fontSize = `${rootFontSize.value}px`
root.classList.add(ROOT_FONT_SCALE_CLASS)
document.body?.classList.add(ROOT_FONT_SCALE_CLASS)
}
export function initRootFontSize() {
if (initialized) {
syncRootFontSize()
return
}
if (typeof uni === "undefined") {
syncRootFontSize()
return
}
const cachedSize = Number(uni.getStorageSync(ROOT_FONT_SIZE_STORAGE_KEY))
if (Number.isFinite(cachedSize) && cachedSize > 0) {
rootFontSize.value = cachedSize
}
initialized = true
syncRootFontSize()
}
export function useFont() {
initRootFontSize()
const setRootFontSize = value => {
const nextSize = Number(value)
if (!Number.isFinite(nextSize) || nextSize <= 0) {
return
}
rootFontSize.value = nextSize
uni.setStorageSync(ROOT_FONT_SIZE_STORAGE_KEY, nextSize)
syncRootFontSize()
}
return {
rootFontSize,
setRootFontSize
}
}wot-font-scale-vars 会同步加到根节点和 body,弹窗内也通过 custom-class / wd-config-provider 使用同一个类,保证 Wot UI 组件的字体变量能跟随当前字号。
四、Vite 配置
当前项目在 vite.config.js 中同时处理两类转换:
import postcssRemToResponsivePixel from "postcss-rem-to-responsive-pixel"
import pxtoremPlugin from "./scripts/pxtorem-plugin"
import PageRootInsertElement from "./scripts/page-root-insert-element"
const isH5 = process.env.UNI_PLATFORM === "h5"
const isApp = process.env.UNI_PLATFORM === "app"
const WeappTailwindcssDisabled = isH5 || isApp
export default defineConfig(() => ({
plugins: [
// 处理 text-[22rpx] 这类 rpx 字号
pxtoremPlugin({
rootValue: 32,
unit: "rpx"
}),
// 处理 Wot UI 等依赖里的 px 字号
pxtoremPlugin({
rootValue: 16,
unit: "px"
}),
UnifiedViteWeappTailwindcssPlugin({
rem2rpx: false,
disabled: WeappTailwindcssDisabled,
cssEntries: [path.resolve(__dirname, "src/main.css")]
}),
PageRootInsertElement(`\n <page-meta :root-font-size="$getRootFontSize()"></page-meta>`)
],
css: {
postcss: {
plugins: [
tailwindcss(),
postcssLogical(),
postcssOKLabFunction(),
postcssRemToResponsivePixel({
rootValue: 32,
propList: ["*", "!font", "!font-size", "!line-height", "!letter-spacing"],
transformUnit: "rpx",
disabled: WeappTailwindcssDisabled
})
]
}
}
}))这里需要注意执行目的:
pxtoremPlugin:把字体相关的px/rpx转成rem,让字体能随根字号变化。postcss-rem-to-responsive-pixel:把 Tailwind 生成的布局类从rem转回rpx,但排除字体相关属性。UnifiedViteWeappTailwindcssPlugin.rem2rpx:当前关闭,由外置 PostCSS 插件接管转换规则。
五、应用初始化
src/main.js 中初始化字体,并挂载全局方法:
import { initRootFontSize, useFont } from "./composables/useFont"
export function createApp() {
const app = createSSRApp(App)
initRootFontSize()
const { rootFontSize } = useFont()
app.config.globalProperties.$getRootFontSize = () => `${rootFontSize.value}px`
return {
app
}
}六、页面切换入口
当前切换入口在 src/pages/admin-profile/index.vue:
<wd-cell title="切换版本" center size="large" border is-link @click="versionPopupVisible = true">
<text class="text-text-auxiliary text-[24rpx]">{{ versionLabel }}</text>
</wd-cell>
<version-switch-popup v-model="versionPopupVisible" :value="rootFontSize" @confirm="handleVersionConfirm" />const { rootFontSize, setRootFontSize } = useFont()
const versionMap = {
16: "标准版",
20: "关怀版"
}
const versionLabel = computed(() => versionMap[rootFontSize.value] || `${rootFontSize.value}px`)
const handleVersionConfirm = value => {
setRootFontSize(value)
uni.toast({
title: `已切换为${versionMap[value] || `${value}px`}`,
icon: "none"
})
}弹窗默认选项定义在 src/components/version-switch-popup/version-switch-popup.vue:
options: {
type: Array,
default: () => [
{
label: "标准版",
description: "常规字体大小,适合日常使用",
value: 16,
iconText: "A"
},
{
label: "关怀版",
description: "大字体设计,视觉更清晰",
value: 20,
iconText: "Aa"
}
]
}七、扩展建议
如果后续要增加更多字号档位,只需要同步调整两处:
version-switch-popup.vue的options。- 使用页面里的
versionMap展示文案。
如果某个页面不需要动态字号,可以在 PageRootInsertElement 的匹配规则中排除该页面,或者在具体样式中避免使用会被转成 rem 的字体属性。