41 KiB
41 KiB
UniApp 移动端项目开发规范
项目概述
本项目是一个基于 UniApp 框架开发的跨平台移动应用,支持 H5、小程序、App 多端运行,采用 Vue 3 + Composition API 开发模式。
技术栈
- UniApp: 跨平台应用开发框架
- Vue 3: 渐进式 JavaScript 框架
- JavaScript: 主要开发语言(非 TypeScript)
- Composition API: Vue 3 组合式 API 开发模式
- Pinia: 状态管理库
- easycom: UniApp 组件自动引入机制
- luch-request: 基于 Promise 的 HTTP 请求库
- mp-html: 富文本渲染组件
- uni.scss: 全局样式变量
开发规范
1. 项目结构
resources/mobile/
├── pages/ # 页面目录
│ ├── index/ # 首页模块
│ │ └── index.vue # 首页
│ ├── auth/ # 认证相关页面
│ │ ├── login/ # 登录页
│ │ ├── register/ # 注册页
│ │ └── forgot-password/ # 忘记密码
│ ├── user/ # 用户中心页面
│ │ ├── profile/ # 个人资料
│ │ ├── settings/ # 设置
│ │ └── address/ # 地址管理
│ ├── shop/ # 商城模块
│ │ ├── product/ # 商品相关
│ │ ├── cart/ # 购物车
│ │ └── order/ # 订单相关
│ └── common/ # 公共页面
│ ├── webview/ # Webview 页面
│ └── error/ # 错误页面
├── components/ # 公共组件目录
│ ├── sc-button/ # 自定义按钮组件
│ ├── sc-card/ # 自定义卡片组件
│ ├── sc-list/ # 自定义列表组件
│ └── sc-upload/ # 上传组件
├── static/ # 静态资源
│ ├── images/ # 图片资源
│ ├── icons/ # 图标资源
│ └── fonts/ # 字体资源
├── api/ # API 接口层
│ ├── index.js # API 入口
│ ├── request.js # 请求封装
│ ├── auth.js # 认证相关接口
│ ├── user.js # 用户相关接口
│ └── shop.js # 商城相关接口
├── stores/ # 状态管理
│ ├── index.js # Store 入口
│ ├── user.js # 用户状态
│ ├── cart.js # 购物车状态
│ └── config.js # 配置状态
├── utils/ # 工具函数
│ ├── index.js # 工具函数入口
│ ├── validate.js # 表单验证
│ ├── format.js # 格式化工具
│ └── storage.js # 存储工具
├── config/ # 配置文件
│ ├── index.js # 主配置
│ ├── env.js # 环境配置
│ └── const.js # 常量配置
├── styles/ # 样式文件
│ ├── common.scss # 公共样式
│ ├── variables.scss # 样式变量
│ └── mixins.scss # 样式混入
├── App.vue # 根组件
├── main.js # 入口文件
├── manifest.json # 应用配置
├── pages.json # 页面路由配置
├── uni.scss # 全局样式变量
├── package.json # 依赖配置
└── README.md # 项目说明
2. 页面开发规范
页面命名规范
- 目录命名: 使用 kebab-case,如
user-profile,product-list - 文件命名: 使用 index.vue 作为入口文件
- 路径格式:
pages/{module}/{feature}/index.vue
scPages 页面框架组件
重要提示: 所有页面必须使用 scPages 组件作为页面框架,确保统一的页面布局和交互体验。
scPages 组件结构
<template>
<view class="sc-pages">
<!-- 自定义导航栏(可选) -->
<view v-if="showNavbar" class="sc-pages-navbar" :style="navbarStyle">
<view class="sc-pages-navbar-left">
<view v-if="showBack" class="sc-pages-back" @click="handleBack">
<text class="iconfont">←</text>
</view>
</view>
<view class="sc-pages-navbar-title">{{ title }}</view>
<view class="sc-pages-navbar-right">
<slot name="navbar-right"></slot>
</view>
</view>
<!-- 页面内容区域 -->
<scroll-view
class="sc-pages-content"
scroll-y
:scroll-top="scrollTop"
@scrolltoupper="handleScrollToUpper"
@scrolltolower="handleScrollToLower"
>
<slot></slot>
</scroll-view>
<!-- 底部操作栏(可选) -->
<view v-if="$slots.footer" class="sc-pages-footer">
<slot name="footer"></slot>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
// 页面标题
title: {
type: String,
default: ''
},
// 是否显示导航栏
showNavbar: {
type: Boolean,
default: true
},
// 是否显示返回按钮
showBack: {
type: Boolean,
default: true
},
// 导航栏背景色
navbarBgColor: {
type: String,
default: '#fff'
},
// 导航栏文字颜色
navbarTextColor: {
type: String,
default: '#333'
},
// 页面背景色
bgColor: {
type: String,
default: '#f5f5f5'
}
})
const emit = defineEmits(['back', 'scrollToUpper', 'scrollToLower'])
// 滚动位置
const scrollTop = ref(0)
// 计算导航栏样式
const navbarStyle = computed(() => ({
backgroundColor: props.navbarBgColor,
color: props.navbarTextColor
}))
// 返回上一页
const handleBack = () => {
emit('back')
uni.navigateBack()
}
// 滚动到顶部
const handleScrollToUpper = () => {
emit('scrollToUpper')
}
// 滚动到底部
const handleScrollToLower = () => {
emit('scrollToLower')
}
// 滚动到指定位置
const scrollToTop = (top = 0) => {
scrollTop.value = top
}
// 暴露方法给父组件
defineExpose({
scrollToTop
})
</script>
<style scoped lang="scss">
.sc-pages {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f5f5;
&-navbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 16px;
background: #fff;
border-bottom: 1px solid #e5e5e5;
position: relative;
z-index: 100;
&-left {
display: flex;
align-items: center;
width: 60px;
}
&-back {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
transition: background 0.3s;
&:active {
background: rgba(0, 0, 0, 0.05);
}
.iconfont {
font-size: 20px;
}
}
&-title {
flex: 1;
font-size: 18px;
font-weight: 500;
text-align: center;
}
&-right {
display: flex;
align-items: center;
width: 60px;
justify-content: flex-end;
}
}
&-content {
flex: 1;
overflow: hidden;
}
&-footer {
padding: 12px 16px;
background: #fff;
border-top: 1px solid #e5e5e5;
}
}
</style>
使用 scPages 组件
<template>
<sc-pages
title="用户详情"
:show-navbar="true"
:show-back="true"
@back="handleBack"
@scrollToUpper="handleRefresh"
@scrollToLower="handleLoadMore"
>
<!-- 导航栏右侧按钮 -->
<template #navbar-right>
<view class="navbar-action" @click="handleShare">
<text class="iconfont">share</text>
</view>
</template>
<!-- 页面内容 -->
<view class="page-content">
<view class="user-info">
<image class="avatar" :src="userInfo.avatar" mode="aspectFill" />
<view class="user-name">{{ userInfo.name }}</view>
<view class="user-role">{{ userInfo.role }}</view>
</view>
<!-- 列表内容 -->
<view class="user-list">
<view v-for="item in list" :key="item.id" class="list-item">
{{ item.title }}
</view>
</view>
</view>
<!-- 底部操作栏 -->
<template #footer>
<view class="footer-actions">
<button class="btn-primary" @click="handleEdit">编辑</button>
<button class="btn-danger" @click="handleDelete">删除</button>
</view>
</template>
</sc-pages>
</template>
<script setup>
import { ref } from 'vue'
const userInfo = ref({
avatar: 'https://example.com/avatar.jpg',
name: '张三',
role: '管理员'
})
const list = ref([
{ id: 1, title: '个人信息' },
{ id: 2, title: '修改密码' },
{ id: 3, title: '权限设置' }
])
const handleBack = () => {
console.log('返回上一页')
}
const handleShare = () => {
console.log('分享')
}
const handleEdit = () => {
console.log('编辑用户')
}
const handleDelete = () => {
console.log('删除用户')
}
const handleRefresh = () => {
console.log('刷新数据')
}
const handleLoadMore = () => {
console.log('加载更多')
}
</script>
<style scoped lang="scss">
.page-content {
padding: 16px;
.user-info {
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 0;
background: #fff;
border-radius: 8px;
margin-bottom: 16px;
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
margin-bottom: 16px;
}
.user-name {
font-size: 18px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.user-role {
font-size: 14px;
color: #999;
}
}
.user-list {
background: #fff;
border-radius: 8px;
padding: 16px;
.list-item {
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #333;
&:last-child {
border-bottom: none;
}
}
}
}
.footer-actions {
display: flex;
gap: 12px;
button {
flex: 1;
height: 44px;
border-radius: 8px;
font-size: 16px;
border: none;
}
.btn-primary {
background: #007AFF;
color: #fff;
}
.btn-danger {
background: #dd524d;
color: #fff;
}
}
.navbar-action {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
transition: background 0.3s;
&:active {
background: rgba(0, 0, 0, 0.05);
}
.iconfont {
font-size: 20px;
}
}
</style>
scPages 组件配置选项
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| title | String | '' | 页面标题 |
| showNavbar | Boolean | true | 是否显示导航栏 |
| showBack | Boolean | true | 是否显示返回按钮 |
| navbarBgColor | String | '#fff' | 导航栏背景色 |
| navbarTextColor | String | '#333' | 导航栏文字颜色 |
| bgColor | String | '#f5f5f5' | 页面背景色 |
插槽
| 插槽名 | 说明 |
|---|---|
| default | 页面内容 |
| navbar-right | 导航栏右侧内容 |
| footer | 底部操作栏 |
事件
| 事件名 | 说明 | 返回值 |
|---|---|---|
| back | 返回上一页 | - |
| scrollToUpper | 滚动到顶部 | - |
| scrollToLower | 滚动到底部 | - |
方法
| 方法名 | 说明 | 参数 |
|---|---|---|
| scrollToTop | 滚动到指定位置 | top (默认: 0) |
页面结构模板
<template>
<view class="page {module}-{feature}-page">
<!-- 顶部导航栏 -->
<view class="navbar">
<view class="navbar-left">
<view class="icon-back" @click="handleBack">
<text class="iconfont">back</text>
</view>
</view>
<view class="navbar-title">{{ pageTitle }}</view>
<view class="navbar-right">
<!-- 右侧操作 -->
</view>
</view>
<!-- 页面内容 -->
<scroll-view class="page-content" scroll-y>
<!-- 页面主体内容 -->
</scroll-view>
<!-- 底部操作栏(可选) -->
<view class="page-footer">
<!-- 底部按钮等 -->
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// 页面标题
const pageTitle = ref('页面标题')
// 返回上一页
const handleBack = () => {
uni.navigateBack()
}
// 生命周期
onMounted(() => {
// 页面加载完成
})
onUnmounted(() => {
// 页面卸载
})
</script>
<style scoped lang="scss">
.{module}-{feature}-page {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f5f5;
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 16px;
background: #fff;
border-bottom: 1px solid #e5e5e5;
.navbar-title {
font-size: 18px;
font-weight: 500;
}
}
.page-content {
flex: 1;
padding: 16px;
}
.page-footer {
padding: 12px 16px;
background: #fff;
border-top: 1px solid #e5e5e5;
}
}
</style>
页面生命周期
<script setup>
import { onMounted, onUnmounted, onLoad, onShow, onHide, onUnload } from 'vue'
// 页面加载(只触发一次)
onLoad((options) => {
console.log('页面加载', options)
})
// 页面显示(每次进入页面都会触发)
onShow(() => {
console.log('页面显示')
})
// 页面隐藏
onHide(() => {
console.log('页面隐藏')
})
// 页面卸载
onUnload(() => {
console.log('页面卸载')
})
// Vue 生命周期(推荐使用)
onMounted(() => {
console.log('组件挂载')
})
onUnmounted(() => {
console.log('组件卸载')
})
</script>
3. 组件引入规范(easycom)
重要提示: 项目使用 UniApp 的 easycom 组件自动引入机制,无需手动 import 和注册组件,直接在模板中使用即可。
easycom 配置
在 pages.json 中配置 easycom:
{
"easycom": {
"autoscan": true,
"custom": {
"^sc-(.*)": "@/components/sc-$1/index.vue"
}
}
}
组件使用
<template>
<view class="page">
<!-- 直接使用组件,无需 import -->
<sc-button type="primary" @click="handleClick">按钮</sc-button>
<sc-card title="卡片标题">
卡片内容
</sc-card>
<sc-list :items="list" />
</view>
</template>
<script setup>
// 无需 import 组件
const handleClick = () => {
console.log('点击按钮')
}
</script>
组件命名规范
- 目录命名: 使用 PascalCase,如
ScButton,ScCard - 文件命名: 使用 index.vue 作为入口文件
- 组件使用: 在模板中使用 kebab-case,如
<sc-button></sc-button> - 前缀规范: 自定义组件统一使用
sc-前缀
组件开发规范
组件结构模板
<template>
<view class="sc-{component-name}" :class="{ '{modifier}': {condition} }">
<!-- 组件内容 -->
<slot></slot>
</view>
</template>
<script setup>
// 定义 Props
const props = defineProps({
// 基础类型
text: String,
count: Number,
isActive: Boolean,
// 复杂类型
data: {
type: Object,
default: () => ({})
},
list: {
type: Array,
default: () => []
},
// 默认值
type: {
type: String,
default: 'default',
validator: (value) => ['default', 'primary', 'danger'].includes(value)
}
})
// 定义 Emits
const emit = defineEmits(['click', 'change', 'update:modelValue'])
// 响应式数据
const localValue = ref(props.modelValue)
// 计算属性
const computedClass = computed(() => {
return `sc-{component-name}--${props.type}`
})
// 方法
const handleClick = () => {
emit('click', props.data)
}
</script>
<style scoped lang="scss">
.sc-{component-name} {
// 基础样式
&--primary {
// 主题样式
}
&--danger {
// 危险样式
}
&.{modifier} {
// 条件样式
}
}
</style>
4. 路由开发规范
页面路由配置
在 pages.json 中配置页面路由:
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom"
}
},
{
"path": "pages/auth/login/index",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/user/profile/index",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "UniApp",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F5F5F5"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#007AFF",
"backgroundColor": "#FFFFFF",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/icons/home.png",
"selectedIconPath": "static/icons/home-active.png"
},
{
"pagePath": "pages/shop/index/index",
"text": "商城",
"iconPath": "static/icons/shop.png",
"selectedIconPath": "static/icons/shop-active.png"
},
{
"pagePath": "pages/user/profile/index",
"text": "我的",
"iconPath": "static/icons/user.png",
"selectedIconPath": "static/icons/user-active.png"
}
]
}
}
路由跳转
// 保留当前页面,跳转到应用内的某个页面
uni.navigateTo({
url: '/pages/user/detail/index?id=123'
})
// 关闭当前页面,跳转到应用内的某个页面
uni.redirectTo({
url: '/pages/auth/login/index'
})
// 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
uni.switchTab({
url: '/pages/index/index'
})
// 关闭所有页面,打开到应用内的某个页面
uni.reLaunch({
url: '/pages/index/index'
})
// 返回上一页
uni.navigateBack({
delta: 1
})
// 获取页面参数
onLoad((options) => {
const { id, type } = options
})
页面传参
// 传递参数
uni.navigateTo({
url: `/pages/product/detail/index?id=${productId}&type=1`
})
// 或者使用对象形式
const params = {
id: productId,
type: 1,
name: '商品名称'
}
uni.navigateTo({
url: `/pages/product/detail/index?data=${encodeURIComponent(JSON.stringify(params))}`
})
// 接收参数
onLoad((options) => {
if (options.data) {
const data = JSON.parse(decodeURIComponent(options.data))
console.log(data)
}
})
5. API 接口开发规范(luch-request)
API 文件组织
每个业务模块对应一个 API 文件,统一放在 api/ 目录下。
// api/auth.js
import http from './http'
export default {
// 登录
login: (data) => {
return http.post('/api/auth/login', data)
},
// 注册
register: (data) => {
return http.post('/api/auth/register', data)
},
// 登出
logout: () => {
return http.post('/api/auth/logout')
},
// 获取用户信息
getUserInfo: () => {
return http.get('/api/auth/user')
},
// 刷新令牌
refreshToken: (refreshToken) => {
return http.post('/api/auth/refresh', { refresh_token: refreshToken })
}
}
luch-request 封装
// api/http.js
import Request from 'luch-request'
import config from '@/config'
import { useUserStore } from '@/stores/user'
const http = new Request({
baseURL: config.apiBaseUrl,
timeout: 60000,
header: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
http.interceptors.request.use((config) => {
// 添加 Token
const userStore = useUserStore()
if (userStore.token) {
config.header = {
...config.header,
'Authorization': `Bearer ${userStore.token}`
}
}
// 添加时间戳防止缓存
if (config.method === 'GET') {
config.params = {
...config.params,
_t: Date.now()
}
}
// 显示加载提示
if (config.loading !== false) {
uni.showLoading({
title: config.loadingText || '加载中...',
mask: true
})
}
return config
}, (error) => {
return Promise.reject(error)
})
// 响应拦截器
http.interceptors.response.use((response) => {
// 隐藏加载提示
uni.hideLoading()
const { statusCode, data } = response
// HTTP 状态码判断
if (statusCode !== 200) {
handleError(statusCode)
return Promise.reject(response)
}
// 业务状态码判断
if (data.code !== 200) {
handleBusinessError(data)
return Promise.reject(data)
}
return data
}, (error) => {
// 隐藏加载提示
uni.hideLoading()
// 网络错误处理
if (error.errMsg) {
uni.showToast({
title: error.errMsg || '网络错误',
icon: 'none'
})
}
return Promise.reject(error)
})
// 错误处理
const handleError = (statusCode) => {
let message = '网络错误'
switch (statusCode) {
case 401:
message = '未授权,请登录'
// 跳转到登录页
uni.navigateTo({
url: '/pages/auth/login/index'
})
break
case 403:
message = '拒绝访问'
break
case 404:
message = '请求错误,未找到该资源'
break
case 500:
message = '服务器错误'
break
}
uni.showToast({
title: message,
icon: 'none'
})
}
const handleBusinessError = (data) => {
uni.showToast({
title: data.message || '操作失败',
icon: 'none'
})
}
export default http
使用示例
<script setup>
import { ref, onMounted } from 'vue'
import authApi from '@/api/auth'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 登录
const handleLogin = async () => {
try {
const res = await authApi.login({
username: 'admin',
password: '123456'
})
// 保存 Token
userStore.setToken(res.data.token)
userStore.setUserInfo(res.data.user)
// 跳转首页
uni.switchTab({
url: '/pages/index/index'
})
} catch (error) {
console.error('登录失败', error)
}
}
// 获取用户信息
const getUserInfo = async () => {
try {
const res = await authApi.getUserInfo()
userStore.setUserInfo(res.data)
} catch (error) {
console.error('获取用户信息失败', error)
}
}
</script>
6. 状态管理规范
Pinia Store 定义
使用组合式 API 定义 Store。
// stores/user.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
// State
const token = ref('')
const refreshToken = ref('')
const userInfo = ref(null)
// Getters
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => userInfo.value?.name || '')
// Actions
function setToken(newToken) {
token.value = newToken
uni.setStorageSync('token', newToken)
}
function setUserInfo(info) {
userInfo.value = info
uni.setStorageSync('userInfo', info)
}
function clearUserData() {
token.value = ''
refreshToken.value = ''
userInfo.value = null
uni.removeStorageSync('token')
uni.removeStorageSync('refreshToken')
uni.removeStorageSync('userInfo')
}
// 从本地存储初始化
function initFromStorage() {
const savedToken = uni.getStorageSync('token')
const savedUserInfo = uni.getStorageSync('userInfo')
if (savedToken) {
token.value = savedToken
}
if (savedUserInfo) {
userInfo.value = savedUserInfo
}
}
return {
token,
refreshToken,
userInfo,
isLoggedIn,
userName,
setToken,
setUserInfo,
clearUserData,
initFromStorage
}
})
Store 使用
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 使用 state
console.log(userStore.token)
console.log(userStore.userInfo)
// 使用 getters
console.log(userStore.isLoggedIn)
console.log(userStore.userName)
// 调用 action
userStore.setToken('xxx')
userStore.setUserInfo(userInfo)
// 初始化存储
onMounted(() => {
userStore.initFromStorage()
})
</script>
7. 工具函数开发规范
常用工具函数
// utils/index.js
// 深拷贝
export const deepClone = (obj) => {
return JSON.parse(JSON.stringify(obj))
}
// 防抖
export const debounce = (fn, delay = 300) => {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
// 节流
export const throttle = (fn, delay = 300) => {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= delay) {
fn.apply(this, args)
lastTime = now
}
}
}
// 格式化时间
export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
if (!date) return ''
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
}
// 格式化金额
export const formatMoney = (amount, decimals = 2) => {
return Number(amount).toFixed(decimals)
}
// 手机号脱敏
export const maskPhone = (phone) => {
if (!phone) return ''
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
// 判断是否为空
export const isEmpty = (value) => {
if (value === null || value === undefined) return true
if (typeof value === 'string') return value.trim() === ''
if (Array.isArray(value)) return value.length === 0
if (typeof value === 'object') return Object.keys(value).length === 0
return false
}
表单验证工具
// utils/validate.js
// 手机号验证
export const isPhone = (phone) => {
return /^1[3-9]\d{9}$/.test(phone)
}
// 邮箱验证
export const isEmail = (email) => {
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email)
}
// 身份证验证
export const isIdCard = (idCard) => {
return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(idCard)
}
// 密码强度验证
export const checkPasswordStrength = (password) => {
let strength = 0
if (password.length >= 8) strength++
if (/[A-Z]/.test(password)) strength++
if (/[a-z]/.test(password)) strength++
if (/[0-9]/.test(password)) strength++
if (/[^A-Za-z0-9]/.test(password)) strength++
return strength
}
8. 样式开发规范
使用 uni.scss 变量
// uni.scss 已内置以下变量,可以直接使用
/* 颜色变量 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color: #333;
$uni-text-color-grey: #999;
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
/* 背景颜色 */
$uni-bg-color: #ffffff;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1;
/* 边框颜色 */
$uni-border-color: #c8c7cc;
/* 尺寸变量 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
$uni-spacing-col-sm: 5px;
$uni-spacing-col-base: 10px;
$uni-spacing-col-lg: 15px;
/* 字体大小 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16px;
$uni-font-size-title: 18px;
/* 图片尺寸 */
$uni-img-size-sm: 40px;
$uni-img-size-base: 52px;
$uni-img-size-lg: 80px;
/* Border Radius */
$uni-border-radius-sm: 4px;
$uni-border-radius-base: 6px;
$uni-border-radius-lg: 12px;
$uni-border-radius-circle: 50%;
/* 阴影 */
$uni-shadow-sm: 0 0 5px rgba(0, 0, 0, 0.1);
$uni-shadow-base: 0 1px 8px 1px rgba(0, 0, 0, 0.1);
$uni-shadow-lg: 0 4px 16px 1px rgba(0, 0, 0, 0.15);
样式组织
<style scoped lang="scss">
// 引入公共样式变量
@import "@/styles/variables.scss";
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $uni-bg-color-grey;
// 嵌套样式
.header {
padding: $uni-spacing-col-lg;
background: $uni-bg-color;
.title {
font-size: $uni-font-size-title;
color: $uni-text-color;
}
}
// 修饰符类
&--loading {
opacity: 0.6;
}
// 伪类
&::after {
content: '';
display: block;
clear: both;
}
}
</style>
响应式单位
// 使用 rpx 单位实现屏幕适配
.container {
width: 750rpx; // 等于屏幕宽度
padding: 30rpx; // 根据屏幕宽度自动缩放
font-size: 28rpx;
}
// 混合使用 px 和 rpx
.button {
font-size: 28rpx;
border-radius: 8px; // 边框圆角建议使用 px
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); // 阴影建议使用 px
}
9. 本地存储规范
使用 uni.storage
// 存储数据
uni.setStorageSync('token', 'xxx')
uni.setStorageSync('userInfo', { name: '张三' })
// 异步存储
uni.setStorage({
key: 'token',
data: 'xxx',
success: () => {
console.log('存储成功')
}
})
// 读取数据
const token = uni.getStorageSync('token')
const userInfo = uni.getStorageSync('userInfo')
// 异步读取
uni.getStorage({
key: 'token',
success: (res) => {
console.log(res.data)
}
})
// 删除数据
uni.removeStorageSync('token')
// 异步删除
uni.removeStorage({
key: 'token',
success: () => {
console.log('删除成功')
}
})
// 清空所有数据
uni.clearStorageSync()
// 获取所有存储信息
uni.getStorageInfo({
success: (res) => {
console.log(res.keys)
console.log(res.currentSize)
console.log(res.limitSize)
}
})
封装存储工具
// utils/storage.js
const STORAGE_PREFIX = 'app_'
export default {
// 设置
set(key, value) {
try {
uni.setStorageSync(STORAGE_PREFIX + key, JSON.stringify(value))
return true
} catch (e) {
console.error('存储失败', e)
return false
}
},
// 获取
get(key, defaultValue = null) {
try {
const value = uni.getStorageSync(STORAGE_PREFIX + key)
if (value) {
return JSON.parse(value)
}
return defaultValue
} catch (e) {
console.error('读取失败', e)
return defaultValue
}
},
// 删除
remove(key) {
try {
uni.removeStorageSync(STORAGE_PREFIX + key)
return true
} catch (e) {
console.error('删除失败', e)
return false
}
},
// 清空
clear() {
try {
uni.clearStorageSync()
return true
} catch (e) {
console.error('清空失败', e)
return false
}
}
}
10. 富文本渲染规范(mp-html)
mp-html 简介
mp-html 是一个强大的富文本渲染组件,支持 HTML 标签和样式,适用于在 UniApp 中渲染富文本内容。
安装依赖
npm install mp-html
基本使用
<template>
<view class="rich-text-container">
<mp-html :content="htmlContent" />
</view>
</template>
<script setup>
import MpHtml from 'mp-html'
import { ref } from 'vue'
const htmlContent = ref(`
<div style="color: #333;">
<h2>文章标题</h2>
<p>这是一段<strong>富文本</strong>内容。</p>
<ul>
<li>列表项 1</li>
<li>列表项 2</li>
</ul>
<img src="https://example.com/image.jpg" alt="图片" />
</div>
`)
</script>
高级配置
<template>
<view class="rich-text-container">
<mp-html
:content="htmlContent"
:tag-style="tagStyle"
:container-style="containerStyle"
:selectable="true"
@load="handleLoad"
@ready="handleReady"
@imgtap="handleImgTap"
@linktap="handleLinkTap"
/>
</view>
</template>
<script setup>
import MpHtml from 'mp-html'
import { ref } from 'vue'
const htmlContent = ref('')
// 标签样式配置
const tagStyle = {
p: 'margin: 10px 0; line-height: 1.6;',
img: 'max-width: 100%; height: auto;',
a: 'color: #007AFF; text-decoration: underline;'
}
// 容器样式
const containerStyle = 'padding: 16px; background: #fff;'
// 图片点击事件
const handleImgTap = (e) => {
const { src } = e.detail
// 预览图片
uni.previewImage({
urls: [src],
current: src
})
}
// 链接点击事件
const handleLinkTap = (e) => {
const { href } = e.detail
uni.navigateTo({
url: `/pages/common/webview/index?url=${encodeURIComponent(href)}`
})
}
// 加载完成
const handleLoad = () => {
console.log('富文本加载完成')
}
// 准备就绪
const handleReady = () => {
console.log('富文本准备就绪')
}
</script>
<style scoped lang="scss">
.rich-text-container {
background: #fff;
border-radius: 8px;
overflow: hidden;
}
</style>
使用场景
- 文章详情页: 渲染后端返回的富文本内容
- 商品描述: 展示商品的详细描述
- 公告通知: 显示富文本格式的公告
- 帮助文档: 渲染帮助文档内容
注意事项
- 安全性: 后端返回的 HTML 内容应经过过滤和消毒,防止 XSS 攻击
- 性能: 避免在列表中大量使用 mp-html,会影响性能
- 样式: 建议使用
tag-style配置标签样式,而不是全局样式 - 图片: 处理图片点击事件,提供图片预览功能
- 链接: 处理链接点击事件,跳转到 webview 页面
11. 图片资源规范
图片路径引用
<template>
<!-- 静态图片 -->
<image src="/static/logo.png" mode="aspectFit"></image>
<!-- 动态图片 -->
<image :src="imageUrl" mode="aspectFill"></image>
<!-- 网络图片 -->
<image src="https://example.com/image.jpg" mode="widthFix"></image>
</template>
<script setup>
// 静态图片引入
import logoImage from '@/static/logo.png'
const imageUrl = ref('https://example.com/image.jpg')
</script>
图片模式
<!-- scaleToFill:不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素 -->
<image src="/static/image.jpg" mode="scaleToFill"></image>
<!-- aspectFit:保持纵横比缩放图片,使图片的长边能完全显示出来 -->
<image src="/static/image.jpg" mode="aspectFit"></image>
<!-- aspectFill:保持纵横比缩放图片,只保证图片的短边能完全显示出来 -->
<image src="/static/image.jpg" mode="aspectFill"></image>
<!-- widthFix:宽度不变,高度自动变化,保持原图宽高比不变 -->
<image src="/static/image.jpg" mode="widthFix"></image>
<!-- heightFix:高度不变,宽度自动变化,保持原图宽高比不变 -->
<image src="/static/image.jpg" mode="heightFix"></image>
11. 上传文件规范
使用 uni.chooseImage
// 选择图片
const chooseImage = () => {
uni.chooseImage({
count: 9, // 最多选择的图片数量
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机
success: (res) => {
const tempFilePaths = res.tempFilePaths
console.log(tempFilePaths)
// 上传图片
uploadImages(tempFilePaths)
}
})
}
// 上传图片
const uploadImages = (files) => {
files.forEach((filePath, index) => {
uni.uploadFile({
url: 'https://example.com/api/upload',
filePath,
name: 'file',
formData: {
'type': 'avatar'
},
header: {
'Authorization': `Bearer ${userStore.token}`
},
success: (uploadFileRes) => {
const data = JSON.parse(uploadFileRes.data)
console.log('上传成功', data)
},
fail: (error) => {
console.error('上传失败', error)
}
})
})
}
12. 常用 API 规范
网络请求
// GET 请求
uni.request({
url: 'https://example.com/api/data',
method: 'GET',
data: {
page: 1,
pageSize: 10
},
header: {
'Content-Type': 'application/json'
},
success: (res) => {
console.log(res.data)
}
})
// POST 请求
uni.request({
url: 'https://example.com/api/save',
method: 'POST',
data: {
name: '张三',
age: 18
},
header: {
'Content-Type': 'application/json'
},
success: (res) => {
console.log(res.data)
}
})
提示框
// 显示提示框
uni.showToast({
title: '操作成功',
icon: 'success',
duration: 2000
})
// 显示加载中
uni.showLoading({
title: '加载中...',
mask: true
})
// 隐藏加载中
uni.hideLoading()
// 显示确认对话框
uni.showModal({
title: '提示',
content: '确定要删除吗?',
success: (res) => {
if (res.confirm) {
console.log('用户点击确定')
} else if (res.cancel) {
console.log('用户点击取消')
}
}
})
// 显示操作菜单
uni.showActionSheet({
itemList: ['拍照', '从相册选择'],
success: (res) => {
console.log(res.tapIndex)
}
})
导航栏
// 设置导航栏标题
uni.setNavigationBarTitle({
title: '新的标题'
})
// 设置导航栏颜色
uni.setNavigationBarColor({
frontColor: '#ffffff',
backgroundColor: '#000000'
})
// 显示导航栏加载动画
uni.showNavigationBarLoading()
// 隐藏导航栏加载动画
uni.hideNavigationBarLoading()
下拉刷新
// 开启下拉刷新
uni.startPullDownRefresh()
// 停止下拉刷新
uni.stopPullDownRefresh()
// 在页面中配置下拉刷新
// pages.json
{
"path": "pages/index/index",
"style": {
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5",
"backgroundTextStyle": "dark"
}
}
// 监听下拉刷新
onPullDownRefresh(() => {
console.log('下拉刷新')
// 执行刷新逻辑
setTimeout(() => {
uni.stopPullDownRefresh()
}, 1000)
})
13. 跨平台兼容性处理
条件编译
// #ifdef H5
console.log('只在 H5 平台执行')
// #endif
// #ifdef MP-WEIXIN
console.log('只在微信小程序执行')
// #endif
// #ifdef APP-PLUS
console.log('只在 App 平台执行')
// #endif
// #ifndef H5
console.log('除了 H5 平台都执行')
// #endif
<template>
<view>
<!-- H5 特有内容 -->
<!-- #ifdef H5 -->
<view class="h5-only">H5 专属内容</view>
<!-- #endif -->
<!-- 小程序特有内容 -->
<!-- #ifdef MP-WEIXIN -->
<view class="mp-only">小程序专属内容</view>
<!-- #endif -->
<!-- App 特有内容 -->
<!-- #ifdef APP-PLUS -->
<view class="app-only">App 专属内容</view>
<!-- #endif -->
</view>
</template>
<style>
/* H5 特有样式 */
/* #ifdef H5 */
.h5-only {
background: #ff0000;
}
/* #endif */
/* App 特有样式 */
/* #ifdef APP-PLUS */
.app-only {
background: #00ff00;
}
/* #endif */
</style>
14. 性能优化建议
列表性能优化
<template>
<!-- 使用 key 提高列表渲染性能 -->
<scroll-view scroll-y>
<view
v-for="item in list"
:key="item.id"
class="list-item"
>
{{ item.name }}
</view>
</scroll-view>
</template>
图片懒加载
<template>
<!-- 使用 lazy-load 实现图片懒加载 -->
<image
v-for="img in imageList"
:key="img.id"
:src="img.url"
lazy-load
mode="aspectFill"
></image>
</template>
避免频繁更新
// 使用防抖避免频繁触发
import { debounce } from '@/utils'
const handleSearch = debounce((keyword) => {
console.log('搜索:', keyword)
}, 300)
15. 常用命令
# 安装依赖
npm install
# 安装 luch-request(HTTP 请求库)
npm install luch-request
# 安装 mp-html(富文本渲染组件)
npm install mp-html
# 安装 pinia(状态管理库)
npm install pinia
# 开发环境运行 H5
npm run dev:h5
# 开发环境运行微信小程序
npm run dev:mp-weixin
# 开发环境运行 App
npm run dev:app
# 生产环境构建 H5
npm run build:h5
# 生产环境构建微信小程序
npm run build:mp-weixin
# 生产环境构建 App
npm run build:app
16. 注意事项
- 使用 Composition API: 新代码统一使用
<script setup>语法 - 避免使用浏览器特有 API: UniApp 不支持所有浏览器 API,请使用 UniApp 提供的 API
- 组件复用: 提取公共组件,避免重复代码
- API 统一管理: 所有接口统一在
api/目录下管理 - 环境变量: 通过
config/env.js配置不同环境的 API 地址 - 跨平台兼容: 使用条件编译处理平台差异
- 不要编写 demo: 开发过程中不编写示例代码
- 测试提示: 如需测试,提示用户是否运行测试,不主动运行
17. 代码质量
- 遵循 Vue 3 官方风格指南
- 保持代码简洁、可读
- 适当添加注释说明复杂逻辑
- 使用语义化的变量和函数命名
- 避免过多的嵌套层级
18. 安全规范
- 敏感数据加密: 本地存储的敏感数据必须加密
- Token 安全: Token 不要存储在 URL 参数中
- 输入验证: 所有用户输入必须进行验证
- HTTPS: 生产环境必须使用 HTTPS
- XSS 防护: 避免直接渲染用户输入的 HTML
19. 版本管理
应用版本号
在 manifest.json 中配置版本号:
{
"versionName": "1.0.0",
"versionCode": "100"
}
versionName: 版本名称,如 "1.0.0"versionCode: 版本号,必须是整数,如 100
版本号规范
- 主版本号:不兼容的 API 修改
- 次版本号:向下兼容的功能性新增
- 修订号:向下兼容的问题修正
示例:
- 1.0.0 → 1.0.1:Bug 修复
- 1.0.1 → 1.1.0:新增功能
- 1.1.0 → 2.0.0:重大更新,不兼容旧版本