AI摘要

本文详细介绍了如何使用uni-app框架开发一个支持微信小程序和H5的零工平台前端应用,包括技术选型、架构设计、核心功能实现及部署经验。项目采用Vue.js 2.6 + uni-app 3.0 + ColorUI技术栈,实现了模块化架构设计、分包加载策略、双重身份系统、地理位置服务、智能搜索与筛选、实时消息系统等功能。同时,文章还分享了响应式设计与适配、性能优化实践、部署与发布经验以及开发经验总结和未来规划。

本文将详细介绍我如何使用uni-app框架开发一个支持微信小程序和H5的零工平台前端应用,包含技术选型、架构设计、核心功能实现及部署经验。

前言

在当今移动互联网时代,跨平台开发已成为提高开发效率的重要手段。本次我选择uni-app框架开发了一个智慧零工平台的前端应用,该平台致力于为零工与雇主搭建高效便捷的双向服务桥梁。

项目概况

技术选型分析

为什么选择uni-app?

在众多跨平台解决方案中,我最终选择了uni-app,主要基于以下考虑:

  1. 一套代码多端运行: 支持编译到微信小程序、H5、App等10+平台
  2. 学习成本低: 基于Vue.js语法,前端开发者容易上手
  3. 生态完善: 拥有丰富的组件库和插件市场
  4. 性能优异: 接近原生应用的性能表现
  5. 社区活跃: DCloud官方维护,社区支持良好

核心技术栈

{
  "前端框架": "Vue.js 2.6.11",
  "跨平台框架": "uni-app 3.0",
  "UI组件库": "ColorUI 2.1.6",
  "样式预处理": "SCSS/SASS",
  "状态管理": "Vuex",
  "构建工具": "webpack",
  "开发工具": "HBuilderX"
}

项目架构设计

整体架构

项目采用模块化架构设计,清晰分离业务逻辑和技术实现:

smart-gig-platform-front/
├── api/                    # API接口层
├── components/             # 公共组件
├── pages/                  # 页面文件
├── employerPackage/        # 雇主端分包
├── static/                 # 静态资源
├── store/                  # 状态管理
├── colorui/               # UI组件库
└── utils/                 # 工具函数

分包策略

为了优化小程序包体积,我采用了分包加载策略:

{
  "subPackages": [
    {
      "root": "employerPackage",
      "name": "employer",
      "pages": [
        "pages/center/index",
        "pages/postJob/index",
        "pages/resume/index"
      ]
    }
  ]
}

这样可以将雇主端功能独立打包,减少主包体积,提升首屏加载速度。

核心功能实现

1. 双重身份系统

这是项目的一大特色功能,用户可以在零工和雇主身份间无缝切换:

<template>
  <view class="identity-switch">
    <view class="switch-container">
      <view 
        class="switch-item" 
        :class="{ active: currentRole === 'worker' }"
        @click="switchRole('worker')"
      >
        <image src="/static/img/worker-icon.png" />
        <text>我是零工</text>
      </view>
      <view 
        class="switch-item" 
        :class="{ active: currentRole === 'employer' }"
        @click="switchRole('employer')"
      >
        <image src="/static/img/employer-icon.png" />
        <text>我是雇主</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      currentRole: 'worker'
    }
  },
  methods: {
    switchRole(role) {
      this.currentRole = role
      this.$store.commit('setUserRole', role)

      // 切换底部tabBar
      if (role === 'employer') {
        uni.reLaunch({
          url: '/employerPackage/pages/center/index'
        })
      } else {
        uni.reLaunch({
          url: '/pages/index/index'
        })
      }
    }
  }
}
</script>

2. 地理位置服务

实现基于位置的工作推荐功能:

// 获取用户位置
async getUserLocation() {
  try {
    const res = await uni.getLocation({
      type: 'wgs84'
    })

    this.userLocation = {
      latitude: res.latitude,
      longitude: res.longitude
    }

    // 获取附近工作
    await this.getNearbyJobs()
  } catch (error) {
    console.error('获取位置失败:', error)
    uni.showToast({
      title: '位置获取失败',
      icon: 'none'
    })
  }
},

// 计算距离
calculateDistance(lat1, lon1, lat2, lon2) {
  const R = 6371 // 地球半径(km)
  const dLat = (lat2 - lat1) * Math.PI / 180
  const dLon = (lon2 - lon1) * Math.PI / 180
  const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
            Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
            Math.sin(dLon/2) * Math.sin(dLon/2)
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
  return R * c
}

3. 智能搜索与筛选

实现多维度的工作搜索功能:

<template>
  <view class="search-container">
    <!-- 搜索栏 -->
    <view class="search-bar">
      <input 
        v-model="searchKeyword"
        placeholder="搜索工作职位、公司名称"
        @confirm="handleSearch"
      />
      <button @click="handleSearch">搜索</button>
    </view>

    <!-- 筛选条件 -->
    <view class="filter-section">
      <picker 
        :value="selectedCategory" 
        :range="categories"
        range-key="name"
        @change="onCategoryChange"
      >
        <view class="filter-item">
          {{ selectedCategory ? categories[selectedCategory].name : '选择分类' }}
        </view>
      </picker>

      <picker 
        :value="selectedSalary" 
        :range="salaryRanges"
        @change="onSalaryChange"
      >
        <view class="filter-item">
          {{ selectedSalary ? salaryRanges[selectedSalary] : '薪资范围' }}
        </view>
      </picker>
    </view>

    <!-- 搜索结果 -->
    <scroll-view 
      scroll-y 
      class="job-list"
      @scrolltolower="loadMore"
    >
      <job-card 
        v-for="job in jobList" 
        :key="job.id"
        :job-data="job"
        @click="goToDetail(job.id)"
      />
    </scroll-view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      searchKeyword: '',
      selectedCategory: null,
      selectedSalary: null,
      jobList: [],
      page: 1,
      hasMore: true
    }
  },
  methods: {
    async handleSearch() {
      this.page = 1
      this.jobList = []
      await this.loadJobs()
    },

    async loadJobs() {
      if (!this.hasMore) return

      try {
        const params = {
          page: this.page,
          size: 10,
          keyword: this.searchKeyword,
          categoryId: this.selectedCategory,
          salaryRange: this.selectedSalary
        }

        const response = await this.$api.getJobList(params)
        const newJobs = response.data.list

        if (this.page === 1) {
          this.jobList = newJobs
        } else {
          this.jobList.push(...newJobs)
        }

        this.hasMore = newJobs.length === 10
        this.page++
      } catch (error) {
        console.error('加载工作列表失败:', error)
      }
    }
  }
}
</script>

4. 实时消息系统

实现雇主与零工间的实时通信:

// WebSocket连接管理
class MessageService {
  constructor() {
    this.socket = null
    this.reconnectAttempts = 0
    this.maxReconnectAttempts = 5
  }

  connect(userId) {
    try {
      this.socket = uni.connectSocket({
        url: `wss://lgpt.ybyq.wang/ws/${userId}`,
        header: {
          'Authorization': uni.getStorageSync('token')
        }
      })

      this.socket.onOpen(() => {
        console.log('WebSocket连接成功')
        this.reconnectAttempts = 0
      })

      this.socket.onMessage((res) => {
        const message = JSON.parse(res.data)
        this.handleMessage(message)
      })

      this.socket.onClose(() => {
        console.log('WebSocket连接关闭')
        this.reconnect()
      })

      this.socket.onError((error) => {
        console.error('WebSocket错误:', error)
        this.reconnect()
      })
    } catch (error) {
      console.error('WebSocket连接失败:', error)
    }
  }

  sendMessage(message) {
    if (this.socket) {
      this.socket.send({
        data: JSON.stringify(message)
      })
    }
  }

  handleMessage(message) {
    // 处理接收到的消息
    switch (message.type) {
      case 'chat':
        this.updateChatList(message)
        break
      case 'notification':
        this.showNotification(message)
        break
      case 'status_update':
        this.updateApplicationStatus(message)
        break
    }
  }

  reconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      setTimeout(() => {
        this.reconnectAttempts++
        this.connect()
      }, 2000 * this.reconnectAttempts)
    }
  }
}

响应式设计与适配

多端适配策略

为了确保在不同平台上的一致体验,我采用了以下适配策略:

// 使用rpx单位进行响应式设计
.container {
  padding: 20rpx;

  // H5端特殊处理
  /* #ifdef H5 */
  max-width: 750rpx;
  margin: 0 auto;
  /* #endif */

  // 微信小程序特殊处理
  /* #ifdef MP-WEIXIN */
  padding-top: calc(20rpx + env(safe-area-inset-top));
  /* #endif */
}

// 统一的颜色变量
$primary-color: #4CAF50;
$success-color: #4CAF50;
$warning-color: #FF9800;
$danger-color: #F44336;

// 字体大小规范
$font-size-xs: 22rpx;
$font-size-sm: 24rpx;
$font-size-base: 28rpx;
$font-size-lg: 32rpx;
$font-size-xl: 36rpx;

组件化开发

开发了一系列可复用组件,提高开发效率:

<!-- job-card.vue - 工作卡片组件 -->
<template>
  <view class="job-card" @click="handleClick">
    <view class="job-header">
      <view class="job-title">{{ jobData.title }}</view>
      <view class="job-salary text-primary">{{ jobData.salary }}</view>
    </view>

    <view class="job-info">
      <view class="company-name">{{ jobData.companyName }}</view>
      <view class="job-location">
        <text class="icon-location"></text>
        {{ jobData.location }}
      </view>
    </view>

    <view class="job-tags">
      <view 
        v-for="tag in jobData.tags" 
        :key="tag"
        class="tag"
      >
        {{ tag }}
      </view>
    </view>

    <view class="job-footer">
      <view class="publish-time">{{ formatTime(jobData.publishTime) }}</view>
      <view class="apply-count">{{ jobData.applyCount }}人申请</view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'JobCard',
  props: {
    jobData: {
      type: Object,
      required: true
    }
  },
  methods: {
    handleClick() {
      this.$emit('click', this.jobData)
    },

    formatTime(timestamp) {
      const now = Date.now()
      const diff = now - timestamp
      const hours = Math.floor(diff / (1000 * 60 * 60))

      if (hours < 1) return '刚刚发布'
      if (hours < 24) return `${hours}小时前`

      const days = Math.floor(hours / 24)
      return `${days}天前`
    }
  }
}
</script>

性能优化实践

1. 图片优化

// 图片懒加载
Vue.directive('lazy-load', {
  bind(el, binding) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value
          observer.unobserve(el)
        }
      })
    })
    observer.observe(el)
  }
})

// 图片压缩处理
function compressImage(file, quality = 0.8) {
  return new Promise((resolve) => {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    const img = new Image()

    img.onload = () => {
      canvas.width = img.width
      canvas.height = img.height
      ctx.drawImage(img, 0, 0)

      canvas.toBlob(resolve, 'image/jpeg', quality)
    }

    img.src = URL.createObjectURL(file)
  })
}

2. 数据缓存策略

// API数据缓存
class CacheManager {
  constructor() {
    this.cache = new Map()
    this.expireTime = 5 * 60 * 1000 // 5分钟
  }

  set(key, data) {
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    })
  }

  get(key) {
    const item = this.cache.get(key)
    if (!item) return null

    if (Date.now() - item.timestamp > this.expireTime) {
      this.cache.delete(key)
      return null
    }

    return item.data
  }

  clear() {
    this.cache.clear()
  }
}

// 使用示例
async function fetchJobList(params) {
  const cacheKey = `job_list_${JSON.stringify(params)}`
  const cachedData = cacheManager.get(cacheKey)

  if (cachedData) {
    return cachedData
  }

  const response = await api.getJobList(params)
  cacheManager.set(cacheKey, response.data)

  return response.data
}

3. 代码分割与按需加载

// 路由懒加载
const routes = [
  {
    path: '/pages/jobDetail/jobDetail',
    component: () => import('@/pages/jobDetail/jobDetail.vue')
  },
  {
    path: '/pages/employer/index',
    component: () => import('@/pages/employer/index.vue')
  }
]

// 组件按需引入
export default {
  components: {
    JobCard: () => import('@/components/job-card.vue'),
    FilterPanel: () => import('@/components/filter-panel.vue')
  }
}

部署与发布

H5部署

  1. 构建生产版本

    npm run build:h5
  2. 服务器配置

    server {
     listen 80;
     server_name lgpt.ybyq.wang;
     root /www/wwwroot/lgpt.ybyq.wang/h5;
     index index.html;
    
     # SPA路由支持
     location / {
         try_files $uri $uri/ /index.html;
     }
    
     # API代理
     location /api/ {
         proxy_pass https://lgpt.ybyq.wang:8081/api/;
         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     }
    
     # 静态资源缓存
     location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff|woff2)$ {
         expires 1y;
         add_header Cache-Control "public, immutable";
     }
    
     # Gzip压缩
     gzip on;
     gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    }

微信小程序发布

  1. 构建小程序版本

    npm run build:mp-weixin
  2. 小程序配置

    {
    "appid": "your-app-id",
    "projectname": "智慧零工平台",
    "setting": {
     "urlCheck": false,
     "es6": true,
     "postcss": true,
     "minified": true,
     "newFeature": true
    },
    "compileType": "miniprogram"
    }
  3. 发布流程

    • 使用微信开发者工具导入项目
    • 配置合法域名白名单
    • 进行真机测试
    • 提交审核并发布

开发经验总结

遇到的挑战

  1. 跨平台兼容性问题

    • 不同平台API差异
    • 样式表现不一致
    • 事件处理机制差异
  2. 性能优化挑战

    • 小程序包体积限制
    • 长列表渲染性能
    • 图片加载优化
  3. 用户体验优化

    • 网络异常处理
    • 加载状态管理
    • 错误边界处理

最佳实践

  1. 代码组织

    // 统一的错误处理
    Vue.config.errorHandler = (err, vm, info) => {
      console.error('Vue Error:', err, info)
      // 上报错误日志
      reportError(err, info)
    }
    
    // 全局混入
    Vue.mixin({
      methods: {
        $showLoading(title = '加载中...') {
          uni.showLoading({ title })
        },
        $hideLoading() {
          uni.hideLoading()
        },
        $showToast(title, icon = 'none') {
          uni.showToast({ title, icon })
        }
      }
    })
  2. API请求封装

    // request.js
    const request = (options) => {
      return new Promise((resolve, reject) => {
        uni.request({
          url: BASE_URL + options.url,
          method: options.method || 'GET',
          data: options.data || {},
          header: {
            'Content-Type': 'application/json',
            'Authorization': getToken(),
            ...options.header
          },
          success: (res) => {
            if (res.data.code === 0) {
              resolve(res.data)
            } else {
              uni.showToast({
                title: res.data.message,
                icon: 'none'
              })
              reject(res.data)
            }
          },
          fail: (error) => {
            uni.showToast({
              title: '网络错误',
              icon: 'none'
            })
            reject(error)
          }
        })
      })
    }
  3. 状态管理

    // store/index.js
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
        userInfo: null,
        userRole: 'worker', // worker | employer
        jobList: [],
        messageList: []
      },
      mutations: {
        SET_USER_INFO(state, userInfo) {
          state.userInfo = userInfo
          uni.setStorageSync('userInfo', userInfo)
        },
        SET_USER_ROLE(state, role) {
          state.userRole = role
          uni.setStorageSync('userRole', role)
        }
      },
      actions: {
        async login({ commit }, loginData) {
          try {
            const response = await api.login(loginData)
            const { token, user } = response.data
    
            uni.setStorageSync('token', token)
            commit('SET_USER_INFO', user)
    
            return response
          } catch (error) {
            throw error
          }
        }
      }
    })

未来规划

  1. 功能扩展

    • 增加语音通话功能
    • 实现AR技能展示
    • 加入AI智能推荐
  2. 技术升级

    • 升级到Vue 3 + Composition API
    • 引入TypeScript增强类型安全
    • 使用Vite提升构建性能
  3. 用户体验优化

    • 加入暗黑模式支持
    • 优化无障碍访问
    • 提升加载性能

结语

通过这次uni-app跨平台开发实践,我深刻体会到了现代前端技术的强大能力。uni-app让我们能够用一套代码覆盖多个平台,大大提高了开发效率。

项目中最有价值的经验包括:

  • 合理的架构设计是项目成功的基础
  • 组件化开发能显著提高代码复用率
  • 性能优化需要从多个维度综合考虑
  • 用户体验是产品成功的关键因素

希望这篇文章能为正在学习uni-app或准备进行跨平台开发的朋友们提供一些参考和启发。


项目链接:

如果您对项目有任何问题或建议,欢迎在GitHub上提Issue或通过邮箱 676567473@qq.com 联系我!

如果觉得我的文章对你有用,请随意赞赏
END
本文作者:
文章标题:智慧零工平台前端开发实战:从uni-app到跨平台应用
本文地址:https://blog.ybyq.wang/archives/542.html
版权说明:若无注明,本文皆Xuan's blog原创,转载请保留文章出处。