主题
uni-app 生命周期与适配(四)
本页对应 uni-app 生命周期与适配,共 6 段视频,约 56 分钟。
学习目标
- 能使用安全区域变量为
custom-stickyy设置顶部偏移,并通过负margin抵消padding造成的滚动位置异常。 - 能说明小程序自定义组件样式隔离的默认表现,并通过选项式
options将styleIsolation设为shared,验证页面样式可影响组件样式。 - 能使用
v-model获取uni-forms中各表单项数据,并通过:model将整体表单数据传给uni-forms。 - 能为
uni-forms-item设置与数据字段对应的name,并定义包含required、pattern、errorMessage的:rules验证规则。 - 能通过
ref获取表单组件实例,在提交按钮事件中调用validate()触发验证,并观察必填与格式错误提示。 - 能区分 uni-app 中应用、页面、组件三类生命周期,并按页面用小程序生命周期、组件用 Vue 生命周期、应用用应用级生命周期的原则正确选用。
知识点
自定义 sticky 组件:先解决“太靠上”,再处理“盒子变大”
为什么需要:自定义 sticky 组件在页面滚动时吸顶,但吸顶位置不能随便写死。写 10px 可能太靠上,写 40px 在小程序里看着正常,到网页里又不协调。
怎么想:吸顶距离应取“顶部安全距离”,而不是固定值;但用 padding-top 撑开距离会让盒子整体变大,滚动前后表现不一致。
怎么做:转写稿中的调试过程是:先给 padding-top: 10px,发现太靠上;改成 40px 后小程序稍正常,但网页不协调;于是改为通过安全区域/API 获取顶部距离,再绑定到样式变量上,并记得加单位 px。随后审查元素发现:滚动到顶部时正常,不滚动时距离偏大,是因为上边多了一层 padding。解决思路是给 sticky 外层再套一个父盒子,用负 margin-top 抵消向下的 padding。
易错点:padding 不能写负值,margin 可以写负值;正负值要抵消一致;转写稿中加父盒子后 sticky 一度失效,明确说“先留一个小 bug,不继续抠”,所以这里不要臆造最终修复方案。
css
/* 示意:用负 margin 抵消 padding 造成的下移 */
.sticky-wrap {
margin-top: -20px;
}样式隔离:为什么网页能改,小程序改不动组件样式
为什么需要:把页面里的一部分表单抽成组件后,页面样式可能突然失效。转写稿中把表单抽成 custom-form 组件,网页里用类名改小眼睛颜色、去掉边框都生效,但小程序里不生效。
怎么想:这不是普通 CSS 优先级问题,而是小程序自定义组件默认有样式隔离。查文档可知 styleIsolation 默认是 isolated,页面样式默认不影响组件内部;改成 shared 后,页面与组件样式才可互相影响。
怎么做:在使用组件的页面/组件里加选项式配置:
js
export default {
options: {
styleIsolation: 'shared'
}
}转写稿中强调:组件里再套组件时,要改样式也要注意把对应组件的样式隔离设为 shared。
易错点:这是小程序侧问题,网页端不一定复现;不要把它当成选择器写错;改的是“使用组件处”的配置,不只是组件内部随便加一行。
uni-forms 第一步:先用 v-model 拿到表单数据
为什么需要:验证前必须拿到用户输入。原生小程序双向绑定很麻烦,而 uni-app 中 v-model 可以直接用。
怎么想:每个输入框绑定 formData 里的字段;整个表单再把 formData 通过 :model 传给 uni-forms。
怎么做:
vue
<uni-forms :model="formData">
<uni-forms-item name="mobile">
<uni-easyinput v-model="formData.mobile" />
</uni-forms-item>
<uni-forms-item name="password">
<uni-easyinput v-model="formData.password" />
</uni-forms-item>
</uni-forms>js
const formData = ref({
mobile: '',
password: ''
})易错点:v-model 负责字段数据,:model 负责把整份数据交给表单组件,两者别混;转写稿中调试时页面更新有时“反应迟钝”,但刷新后能看到数据已变化,不代表绑定失败。
uni-forms 第二步:定义 rules,并让 name 与字段对应
为什么需要:不同字段规则不同,手机号要求 11 位,密码可能要求字母数字下划线,必须把规则传给表单。
怎么想:rules 是数组,每个对象代表一个字段的规则;对象里的 name 要和 uni-forms-item 的 name 对应。规则里常用 required、message、pattern。
怎么做:
js
const rules = {
mobile: {
rules: [
{ required: true, message: '用户名不能为空' },
{ pattern: /^1\d{10}$/, message: '不符合手机号规则' }
]
},
password: {
rules: [
{ required: true, message: '密码不能为空' },
{ pattern: /^\w+$/, message: '只能字母数字下划线' }
]
}
}模板上补 :rules="rules"。
易错点:转写稿特别提醒正则里的 \d 要注意转义,按实际字符串写法可能需要多写一个斜杠;name 不匹配时规则找不到对应字段。
uni-forms 第三步:点击按钮触发 validate
为什么需要:规则写完不会自动生效,通常在提交按钮点击时统一校验。
怎么想:先通过 ref 拿到表单组件实例,再调用实例的 validate()。
怎么做:
vue
<uni-forms ref="formRef" :model="formData" :rules="rules">
<!-- 表单项 -->
</uni-forms>
<button @click="onSubmit">提交</button>js
const formRef = ref(null)
const onSubmit = () => {
formRef.value.validate()
}易错点:ref="formRef" 这里没有冒号;忘记把 :rules 绑到表单上,规则不会参与校验;转写稿调试时清空字段、输入不足 11 位,分别出现了“不能为空”和“不符合手机号规则”的提示。
生命周期:三类并存,按“页面用小程序、组件用 Vue”来选
为什么需要:uni-app 同时有 Vue 和小程序能力,生命周期容易用混。
怎么想:应用级用 onLaunch/onShow/onHide;页面级建议用小程序生命周期,如 onLoad/onShow/onReady/onHide;自定义组件里用 Vue 生命周期,如 mounted/beforeMounted。转写稿明确说:Vue 生命周期在页面里“可以用”,但为了避免混乱,建议页面不要用;onLoad 不能写进组件。
怎么做:页面里写 onLoad 等;组件里写 mounted 等;App 里写应用级生命周期。
易错点:不要因为“能用”就混着用;组件里写 onLoad 不生效;本节只要求知道三类生命周期的存在和选用原则,不需要死背表格。
一、从 sticky 安全区踩坑看“样式为什么要隔离”
为什么需要:课堂演示 custom-sticky 时,顶部吸附距离一开始写死 10px,网页里勉强正常,小程序里却太靠上;改成 40px 后小程序好一点,网页又不协调。说明固定值不能同时适配不同端和安全区域。
怎么想:既然不能写死,就应该用安全区域距离。转写稿里用类似 env(safe-area-inset-top) 的方式拼接,而不是直接写 10px 或 40px。但一用 padding-top,盒子会被撑大,滚动前后表现又不一致;于是又想到给父级加负 margin-top 抵消,所谓“一正一负就抵消”。
怎么做:思路是“安全区 padding + 父级负 margin 抵消”。示意代码:
css
.sticky {
padding-top: calc(10px + env(safe-area-inset-top));
}
.sticky-wrapper {
margin-top: -10px; /* 具体值要与 padding 抵消,课堂中曾用负值演示 */
}易错点:padding 会改变盒子尺寸,margin 可以为负、padding 不能为负;课堂里加完父级负 margin 后,sticky 一度失效,明确说“先留一个小 bug,后面再查”,所以这里不能把“负 margin”当成万能解。调试时要同时看小程序和网页,不能只看一端。
二、自定义组件样式隔离:页面类名为什么改不动组件
为什么需要:课堂把表单相关结构抽成 custom-form 组件,页面里只是引入组件,按理说“只是抽离,不应该有影响”。但页面里用类名改小眼睛颜色、去边框线时,网页生效,小程序不生效。原因就是小程序自定义组件默认有样式隔离:组件外定义的类名不会影响组件内部。
怎么想:先确认不是选择器写错。转写稿里审查元素,看到小眼睛类名,页面 CSS 也写了颜色,网页确实变红;到小程序却不生效。于是转到 styleIsolation:默认隔离时,页面样式影响不到组件;把值改成 shared 后,页面和组件样式可以互相影响。
怎么做:在使用组件的页面/组件配置里加选项式 options。转写稿说“两个地方都加一下,把组件里也加上”,核心代码是:
js
export default {
options: {
styleIsolation: 'shared'
}
}易错点:shared 不要拼错;要加在“使用组件”的页面或组件配置里,不是随便写在 CSS 里;组合式 API 与选项式 options 可以共存;这是小程序端问题,网页端不一定复现。改完还要等编译完成再判断,课堂里就遇到编译慢、报“组件不存在”的情况,不一定是代码错。
三、顺带收束:表单验证与生命周期的固定套路
为什么需要:表单验证是为了拿到用户输入并按规则提示;生命周期是为了知道代码该写在页面还是组件里。
怎么想:表单验证分三步:先拿数据,再定规则,最后触发验证。拿数据用 v-model,并把整体数据通过 :model 交给 uni-forms;规则用 rules,靠 name 与表单字段对应;触发时用 ref 拿表单实例,调用 validate()。
怎么做:关键代码示意:
vue
<uni-forms ref="formRef" :model="formData" :rules="formRules">
<uni-forms-item name="mobile" label="手机号">
<uni-easyinput v-model="formData.mobile" />
</uni-forms-item>
<uni-forms-item name="password" label="密码">
<uni-easyinput v-model="formData.password" />
</uni-forms-item>
</uni-forms>
<button @click="submit">提交</button>js
const formRef = ref()
const formData = ref({ mobile: '', password: '' })
const formRules = {
mobile: {
rules: [
{ required: true, errorMessage: '用户名不能为空' },
{ pattern: /^1\d{10}$/, errorMessage: '不符合手机号规则' }
]
},
password: {
rules: [
{ required: true, errorMessage: '密码不能为空' },
{ pattern: /^\w+$/, errorMessage: '只能是字母数字下划线' }
]
}
}
const submit = () => {
formRef.value.validate()
}易错点:name 要和 rules 的键、数据字段对应;:model 不要漏;正则写在字符串里要注意转义,写在字面量里按 JS 正则写;ref 绑定不要写成 :ref。生命周期方面,转写稿给的原则是:应用级、页面级以小程序生命周期为准,自定义组件以 Vue 生命周期为准;页面里虽然能用 Vue 生命周期,但为了避免混用,建议页面用 onLoad、onShow 等,组件用 mounted 等。
先明确目标:表单验证要解决什么问题
这一节的核心不是“会写一个输入框”,而是让用户填写的数据能够被拿到、被规则校验、被提交时触发验证。转写中把需求拆得很清楚:对表单里的手机号、密码等数据做验证。
为什么需要:如果不验证,空内容、位数不对、格式不对的数据都会进入后续业务。
怎么想:uni-forms 的验证是固定套路,分三步——先获取表单数据,再定义验证规则,最后触发验证。
怎么做:围绕 uni-forms、uni-forms-item、v-model、:model、:rules、validate() 展开。
易错点:不要一上来就写规则,先把数据和表单组件关联起来;否则规则写了也不生效。
第一步:获取表单数据
为什么需要:验证的前提是能拿到用户在输入框里填写的内容。转写中反复强调:用户在表单里写 123,我们要能拿到 123;写 ABC,要能拿到 ABC。
怎么想:每个输入框用 v-model 绑定到响应式数据上;同时把整个表单数据通过 :model 传给 uni-forms。这和 Element UI 的表单用法很像。
怎么做:先定义表单数据,再在模板中绑定。示例按转写中的思路整理:
vue
<script setup>
import { ref } from 'vue'
const formData = ref({
mobile: '',
password: ''
})
</script>
<template>
<uni-forms :model="formData">
<uni-forms-item name="mobile">
<uni-easyinput v-model="formData.mobile" />
</uni-forms-item>
<uni-forms-item name="password">
<uni-easyinput v-model="formData.password" />
</uni-forms-item>
</uni-forms>
</template>转写中调试时就是改输入框内容,观察 formData 是否变化;虽然有时页面反应慢一点,但双向绑定是生效的。
易错点:只写 v-model 不够,还要在 uni-forms 上写 :model="formData",把整体数据交给表单组件。v-model 前没有冒号,:model 前有冒号,不要混。
第二步:定义验证规则
为什么需要:不同字段规则不同。手机号要求 11 位、以 1 开头;密码可能要求不能为空、只能字母数字下划线。规则必须能区分“验证哪个字段”。
怎么想:rules 是一个对象/数组结构,里面的键要和 uni-forms-item 的 name 对应。转写中强调:form-item 加 name,最好和数据名一致,比如 mobile 对应 mobile,这样规则才能找到字段。
怎么做:定义 formRules,并通过 :rules 传给 uni-forms;每个字段里可写 required、message、pattern。
vue
<script setup>
import { ref } from 'vue'
const formData = ref({
mobile: '',
password: ''
})
const formRules = {
mobile: [
{ required: true, message: '用户名不能为空' },
{ pattern: /^1\d{10}$/, message: '不符合手机号规则' }
],
password: [
{ required: true, message: '密码不能为空' },
{ pattern: /^[A-Za-z0-9_]+$/, message: '只能字母数字下划线' }
]
}
</script>
<template>
<uni-forms :model="formData" :rules="formRules">
<uni-forms-item name="mobile">
<uni-easyinput v-model="formData.mobile" />
</uni-forms-item>
<uni-forms-item name="password">
<uni-easyinput v-model="formData.password" />
</uni-forms-item>
</uni-forms>
</template>易错点:name 和 rules 的键不一致,验证不会按预期找到字段;如果正则写成字符串形式,反斜杠要注意转义,转写中特别提醒 \d 这类写法在字符串里要多加斜杠。
第三步:触发验证
为什么需要:规则定义好不会自动在提交时校验,需要用户点按钮时调用表单实例的验证方法。
怎么想:uni-forms 组件实例有 validate 方法;先通过 ref 拿到组件实例,再在提交事件里调用。转写中明确:ref 属性不要加冒号。
怎么做:
vue
<script setup>
import { ref } from 'vue'
const formRef = ref(null)
const formData = ref({ mobile: '', password: '' })
const formRules = {
mobile: [
{ required: true, message: '用户名不能为空' },
{ pattern: /^1\d{10}$/, message: '不符合手机号规则' }
],
password: [
{ required: true, message: '密码不能为空' }
]
}
function submit() {
formRef.value.validate()
}
</script>
<template>
<uni-forms ref="formRef" :model="formData" :rules="formRules">
<uni-forms-item name="mobile">
<uni-easyinput v-model="formData.mobile" />
</uni-forms-item>
<uni-forms-item name="password">
<uni-easyinput v-model="formData.password" />
</uni-forms-item>
</uni-forms>
<button @click="submit">提交</button>
</template>转写中的调试过程是:清空数据点提交,出现“不能为空”;输入不足 11 位点提交,出现“不符合手机号规则”。说明验证链路已经打通。
易错点:忘记写 :rules;忘记给 uni-forms 加 ref;把 ref="formRef" 写成 :ref="formRef";调用时漏掉 .value 或 validate 拼写错误。
调试与边界提醒
表单验证是固定步骤,建议按“数据绑定 → 规则定义 → 点击触发”的顺序排查。若在自定义组件里改样式不生效,那是小程序的样式隔离问题,不是表单数据问题;转写中通过选项式 options: { styleIsolation: 'shared' } 让页面样式影响组件样式。生命周期部分只记原则:应用和页面生命周期以小程序为准,自定义组件生命周期以 Vue 为准。表单这一节先把三步跑通,再处理样式和生命周期细节。
代码示例
示例 1:自定义组件 custom-sticky(安全区顶部距离 + 负 margin 抵消)
vue
<!-- components/custom-sticky/custom-sticky.vue -->
<template>
<!-- 父盒子:用负 margin 把被 padding 顶下去的盒子拉回来 -->
<view class="sticky-wrap" :style="{ marginTop: -safeTop + 'px' }">
<!-- 吸顶盒子:padding-top 撑出安全区距离 -->
<view class="sticky" :style="{ paddingTop: safeTop + 'px' }">
<slot></slot>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
// 动态获取安全区顶部距离,而不是写死 10px / 40px
const safeTop = ref(0)
const info = uni.getSystemInfoSync()
safeTop.value = info.safeAreaInsets ? info.safeAreaInsets.top : info.statusBarHeight
</script>
<style scoped>
.sticky {
position: sticky;
top: 0;
background-color: #fff;
z-index: 99;
}
</style>关键行与易错点
:style="{ paddingTop: safeTop + 'px' }":必须用变量而不是写死10px/40px,否则小程序里正常、H5 里不协调;单位px不能丢。:style="{ marginTop: -safeTop + 'px' }":padding会让盒子整体变大,滚动到顶部时距离显得过大。给外层盒子一个负 margin 抵消,正负必须用同一个变量,写死-20px就对不上了。margin可以给负值,padding不能给负值——这是选负 margin 的原因。- 加了外层包裹盒子后,
position: sticky的层级/包含块会受影响,可能出现吸顶暂时失效,这是本节遗留的小瑕疵,定位元素与包裹盒子的层级关系要一并检查。 uni.getSystemInfoSync()取不到safeAreaInsets时要有statusBarHeight兜底。
示例 2:自定义组件样式隔离(styleIsolation: 'shared')
vue
<!-- pages/notify/index.vue -->
<template>
<view class="page">
<custom-form />
</view>
</template>
<!-- 选项式:专门为小程序样式隔离而写 -->
<script>
export default {
options: {
styleIsolation: 'shared'
}
}
</script>
<!-- 组合式逻辑照常写,两者可以同时存在 -->
<script setup>
import CustomForm from '@/components/custom-form/custom-form.vue'
</script>
<style>
/* 页面样式要穿透进组件,改组件内部类名 */
.field-icon {
color: red !important;
}
</style>vue
<!-- components/custom-form/custom-form.vue -->
<script>
export default {
options: {
styleIsolation: 'shared'
}
}
</script>
<script setup>
// 组件内部逻辑
</script>关键行与易错点
- 小程序默认样式隔离(
isolated),页面里写的类名影响不到组件内部,所以color: red、边框线在 H5 生效、在小程序不生效。 - 解决方式:在使用组件的页面和组件本身都加
options: { styleIsolation: 'shared' }(转写稿强调“两个地方都加一下”),共享后页面样式才能影响组件。 options是选项式写法,和<script setup>组合式可以共存,写在单独的<script>块里。- 这个 bug 只在小程序端出现,H5 端不需要处理,别在 H5 上白折腾。
- 用
!important提优先级,是为了盖住组件内部的默认样式。
示例 3:uniForms 表单数据 + 验证规则 + 触发验证
vue
<!-- components/custom-form/custom-form.vue -->
<template>
<view>
<uni-forms ref="formRef" :model="formData" :rules="formRules">
<uni-forms-item label="手机号" name="mobile">
<uni-easyinput
v-model="formData.mobile"
:inputBorder="false"
placeholder="请输入手机号"
/>
</uni-forms-item>
<uni-forms-item label="密码" name="password">
<uni-easyinput
v-model="formData.password"
type="password"
:inputBorder="false"
placeholder="请输入密码"
/>
</uni-forms-item>
</uni-forms>
<button @click="onSubmit">提交</button>
</view>
</template>
<script setup>
import { ref } from 'vue'
// 1. 表单数据
const formData = ref({
mobile: '',
password: ''
})
// 2. 验证规则
const formRules = {
mobile: {
rules: [
{ required: true, errorMessage: '用户名不能为空' },
{ pattern: '^1\\d{10}$', errorMessage: '不符合手机号的规则' }
]
},
password: {
rules: [
{ required: true, errorMessage: '密码不能为空' },
{ pattern: '^\\w+$', errorMessage: '只能是字母数字下划线' }
]
}
}
// 组件实例
const formRef = ref(null)
// 3. 触发验证
const onSubmit = () => {
formRef.value.validate()
.then(() => {
console.log('校验通过', formData.value)
})
.catch((err) => {
console.log('校验失败', err)
})
}
</script>关键行与易错点
- 第一步取数据:三个
uni-easyinput各写一个v-model(formData.mobile、formData.password),小程序原生没有真正的双向绑定,uni-easyinput里可以直接用。 - 必须再给
uni-forms加:model="formData",把整体数据交给表单组件,否则校验无从下手。 uni-forms-item的name要和数据字段名一致(都叫mobile/password),它决定这条规则校验的是哪个数据。formRules的写法是“对象 →rules数组 → 规则对象”,一个字段可以有多条规则(required+pattern);它不参与响应式,用普通对象即可。- 正则写成字符串时要多加一层转义:
'^1\\d{10}$',少写一个斜杠\d就失效。 ref="formRef"上不能加冒号,加了就成了传值而不是拿实例;拿到实例后调用validate(),返回 Promise,用then/catch区分通过和失败。
常见错误
现象:custom-sticky 吸顶偏移写死
10px/40px,小程序看着正常,网页不协调。→ 原因:没有按安全区动态取值。→ 修法:用安全区 API 取顶部距离,变量拼接单位后绑定,不写死数值。现象:给 sticky 外层加负
margin-top抵消padding-top后,吸顶失效或层级异常。→ 原因:负 margin 改变了盒模型/层叠关系,sticky 受父级和层级影响。→ 修法:先回归验证 sticky 自身定位、父级overflow/transform和层级;负 margin 只做视觉补偿,不能把失效 bug 留到提交。现象:网页里改组件内“小眼睛”颜色、边框线生效,小程序不生效。→ 原因:小程序默认样式隔离,页面样式影响不到组件内部。→ 修法:在组件使用侧或组件选项中设置
styleIsolation: 'shared',再检查选择器和优先级。现象:uniForms 写了
rules,点提交没有任何校验提示。→ 原因:规则没有通过:rules绑定到 form 组件。→ 修法:在 form 上补:rules="formRules",确认规则对象与 form 已关联。现象:校验提示错字段或不触发。→ 原因:
form-item的name与formData字段名不一致。→ 修法:name与数据字段严格对齐,如mobile、password。现象:提交时报
validate is not a function或无反应。→ 原因:ref写法错误或没拿到 form 组件实例。→ 修法:模板写ref="formRef",不要加冒号;脚本定义const formRef = ref();提交时调用formRef.value.validate()。现象:手机号正则复制后规则异常。→ 原因:字符串里的
\d没有转义。→ 修法:写成\\d,或改用RegExp构造,并实际输入 11 位/非 11 位验证。
练习
- 把 sticky 吸顶偏移从写死
40px改为安全区自适应,并分别在小程序和网页验证滚动前后表现。 - 排查“网页改组件内小眼睛生效、小程序不生效”的完整步骤,给出最小修改方案。
- 补全一个 uniForms 表单:三个输入框加
v-model,form 加:model,两个form-item的name对齐字段,配置必填和手机号/密码正则,提交按钮触发validate。 - 改错:模板里出现
:ref="formRef"、form 未绑定:rules、form-item的name与数据字段不一致。逐条指出问题并修正。 - 生命周期选型:页面
onShow、组件mounted、ApponLaunch分别应写在哪里;并说明组件里写onLoad为什么无效。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 自定义组件custom-stickyy应用 | 09:00 |
| 2 | 自定义组件样式隔离 | 14:50 |
| 3 | uniForms表单数据 | 08:20 |
| 4 | uniForms表单验证规则 | 09:35 |
| 5 | uniForms触发验证 | 07:44 |
| 6 | uni-app生命周期 | 06:47 |
小结
- sticky 偏移不要写死,安全区变量加单位;负 margin 补偿后必须回归吸顶和层级。
- 小程序跨组件改样式,先处理样式隔离,再谈选择器优先级。
- uniForms 验证是一条固定链路:取数、汇总、对齐 name、绑定 rules、调用 validate,缺一环就会“不提示/错字段”。
ref获取组件实例不加冒号;validate是组件实例方法,调用前先确认实例存在。- 生命周期选型原则:应用/页面以小程序为准,自定义组件以 Vue 为准;页面别混用 Vue 生命周期,组件别写
onLoad。 - 表单调试顺序:先看数据是否更新,再看 rules 是否绑定,再看 name 是否匹配,最后看 validate 是否调用。
- sticky 的小 bug 不要“先放着”,要定位到负 margin、层叠上下文或父级限制,否则后面联调成本更高。