UniApp跨平台应用开发:从零构建高性能电商应用完整实战指南

2026-03-09 0 800
免费资源下载
作者:跨端开发实践者
发布日期:2023年11月
技术栈:UniApp + Vue.js + uniCloud

一、UniApp框架深度解析与项目架构设计

1.1 UniApp核心架构优势

UniApp基于Vue.js生态,采用条件编译实现真正的”一次开发,多端发布”。其核心架构特点:

  • 跨端渲染引擎:基于Vue运行时,适配各平台渲染差异
  • 原生能力桥接:通过uni对象统一调用各平台原生API
  • 条件编译机制:使用#ifdef #endif实现平台差异化代码
  • 插件生态系统:丰富的原生插件市场支持

1.2 电商项目架构设计

我们设计一个多端电商应用,支持微信小程序、支付宝小程序、H5和App:

ecommerce-uniapp/
├── pages/                    # 页面目录
│   ├── index/              # 首页
│   ├── category/           # 分类页
│   ├── product/            # 商品详情
│   ├── cart/               # 购物车
│   └── user/               # 用户中心
├── components/              # 公共组件
│   ├── product-card/       # 商品卡片
│   ├── search-bar/         # 搜索组件
│   └── tab-bar/            # 自定义标签栏
├── static/                  # 静态资源
├── store/                   # Vuex状态管理
├── utils/                   # 工具函数
├── uni_modules/             # 插件模块
└── manifest.json           # 应用配置

二、环境搭建与项目初始化

2.1 开发环境配置

使用HBuilderX作为开发工具,配置多端开发环境:

# 通过CLI创建项目(备选方案)
vue create -p dcloudio/uni-preset-vue ecommerce-app

# 选择模板
? 请选择 uni-app 模板 
❯ 默认模板(Vue2)
  默认模板(Vue3)
  自定义模板

2.2 项目配置文件详解

manifest.json配置多端适配:

{
  "name": "电商商城",
  "appid": "__UNI__XXXXXX",
  "description": "多端电商应用",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": false,
  "app-plus": {
    "usingComponents": true,
    "nvueStyleCompiler": "uni-app",
    "compilerVersion": 3,
    "splashscreen": {
      "alwaysShowBeforeRender": true,
      "waiting": true,
      "autoclose": true,
      "delay": 0
    }
  },
  "mp-weixin": {
    "appid": "wx-your-appid",
    "setting": {
      "urlCheck": false,
      "es6": true,
      "postcss": true
    },
    "usingComponents": true,
    "permission": {
      "scope.userLocation": {
        "desc": "获取位置用于配送服务"
      }
    }
  },
  "h5": {
    "router": {
      "mode": "hash",
      "base": "./"
    },
    "template": "template.h5.html"
  }
}

三、核心页面开发实战

3.1 首页开发:高性能商品瀑布流

实现支持虚拟列表的高性能首页:

<template>
  <view class="home-page">
    <!-- 自定义导航栏 -->
    <custom-nav-bar title="电商商城" :show-back="false">
      <template #right>
        <view class="nav-right">
          <uni-icons type="search" size="22" @click="goSearch"></uni-icons>
          <uni-badge :text="cartCount" type="error">
            <uni-icons type="cart" size="22" @click="goCart"></uni-icons>
          </uni-badge>
        </view>
      </template>
    </custom-nav-bar>
    
    <!-- 轮播图 -->
    <swiper class="banner-swiper" 
            :autoplay="true" 
            :interval="3000" 
            :circular="true"
            indicator-dots
            indicator-color="rgba(255,255,255,0.6)"
            indicator-active-color="#ff6b35">
      <swiper-item v-for="(item, index) in banners" :key="index">
        <image :src="item.image" 
               mode="aspectFill" 
               @click="handleBannerClick(item)" 
               class="banner-image">
        </image>
      </swiper-item>
    </swiper>
    
    <!-- 分类入口 -->
    <scroll-view class="category-scroll" scroll-x>
      <view v-for="category in categories" 
            :key="category.id" 
            class="category-item"
            @click="goCategory(category.id)">
        <image :src="category.icon" class="category-icon"></image>
        <text class="category-name">{{category.name}}</text>
      </view>
    </scroll-view>
    
    <!-- 商品瀑布流 -->
    <waterfall ref="waterfall" 
               :column="2" 
               :column-gap="10"
               :data="productList"
               :loading="loading"
               :finished="finished"
               @load="loadMoreProducts">
      <template v-slot:item="{ item }">
        <product-card :product="item" @click="goProductDetail(item.id)"></product-card>
      </template>
    </waterfall>
    
    <!-- 返回顶部 -->
    <back-to-top :show="showBackTop" @click="scrollToTop"></back-to-top>
  </view>
</template>

<script>
import { mapState, mapActions } from 'vuex'
import ProductCard from '@/components/product-card/product-card.vue'
import Waterfall from '@/components/waterfall/waterfall.vue'

export default {
  components: {
    ProductCard,
    Waterfall
  },
  data() {
    return {
      banners: [],
      categories: [],
      productList: [],
      page: 1,
      pageSize: 10,
      loading: false,
      finished: false,
      showBackTop: false
    }
  },
  computed: {
    ...mapState(['cartCount'])
  },
  onLoad() {
    this.initHomeData()
    this.setupScrollListener()
  },
  onPullDownRefresh() {
    this.refreshHomeData()
  },
  methods: {
    ...mapActions(['updateCartCount']),
    
    async initHomeData() {
      try {
        // 并行请求首页数据
        const [bannerRes, categoryRes] = await Promise.all([
          this.$api.home.getBanners(),
          this.$api.home.getCategories()
        ])
        
        this.banners = bannerRes.data
        this.categories = categoryRes.data
        
        // 加载首屏商品
        await this.loadMoreProducts()
      } catch (error) {
        uni.showToast({
          title: '数据加载失败',
          icon: 'none'
        })
      }
    },
    
    async loadMoreProducts() {
      if (this.loading || this.finished) return
      
      this.loading = true
      try {
        const res = await this.$api.product.getList({
          page: this.page,
          pageSize: this.pageSize
        })
        
        if (res.data.length === 0) {
          this.finished = true
          return
        }
        
        // 处理图片懒加载
        const processedList = res.data.map(item => ({
          ...item,
          image: this.processImageUrl(item.image)
        }))
        
        this.productList = [...this.productList, ...processedList]
        this.page++
        
        // 预加载下一页数据
        if (this.page % 3 === 0) {
          this.prefetchNextPage()
        }
      } catch (error) {
        console.error('加载商品失败:', error)
      } finally {
        this.loading = false
      }
    },
    
    processImageUrl(url) {
      // 根据网络环境返回不同质量的图片
      const networkType = uni.getNetworkType()
      if (networkType === 'wifi') {
        return `${url}?quality=high`
      }
      return `${url}?quality=medium&width=375`
    },
    
    setupScrollListener() {
      // 监听滚动显示返回顶部按钮
      uni.pageScrollTo({
        scrollTop: 0,
        duration: 0
      })
      
      const query = uni.createSelectorQuery().in(this)
      query.selectViewport().scrollOffset(res => {
        this.showBackTop = res.scrollTop > 500
      }).exec()
    },
    
    scrollToTop() {
      uni.pageScrollTo({
        scrollTop: 0,
        duration: 300
      })
    },
    
    async prefetchNextPage() {
      // 预加载逻辑
      const nextPage = this.page + 1
      this.$api.product.getList({
        page: nextPage,
        pageSize: this.pageSize
      }).then(res => {
        // 缓存数据
        uni.setStorage({
          key: `prefetch_page_${nextPage}`,
          data: res.data
        })
      })
    }
  }
}
</script>

四、状态管理与数据持久化

4.1 Vuex Store设计

设计支持持久化的全局状态管理:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'

Vue.use(Vuex)

const store = new Vuex.Store({
  plugins: [
    createPersistedState({
      key: 'ecommerce-store',
      storage: {
        getItem: key => uni.getStorageSync(key),
        setItem: (key, value) => uni.setStorageSync(key, value),
        removeItem: key => uni.removeStorageSync(key)
      },
      reducer(state) {
        return {
          user: state.user,
          cart: state.cart,
          settings: state.settings
        }
      }
    })
  ],
  
  state: {
    user: null,
    cart: {
      items: [],
      total: 0,
      count: 0
    },
    settings: {
      theme: 'light',
      notification: true
    }
  },
  
  mutations: {
    SET_USER(state, user) {
      state.user = user
    },
    
    ADD_TO_CART(state, product) {
      const existingItem = state.cart.items.find(item => 
        item.id === product.id && item.skuId === product.skuId
      )
      
      if (existingItem) {
        existingItem.quantity += product.quantity
      } else {
        state.cart.items.push({
          ...product,
          selected: true
        })
      }
      
      this.commit('UPDATE_CART_TOTAL')
    },
    
    UPDATE_CART_TOTAL(state) {
      let total = 0
      let count = 0
      
      state.cart.items.forEach(item => {
        if (item.selected) {
          total += item.price * item.quantity
          count += item.quantity
        }
      })
      
      state.cart.total = parseFloat(total.toFixed(2))
      state.cart.count = count
    }
  },
  
  actions: {
    async login({ commit }, credentials) {
      try {
        const res = await uni.request({
          url: '/api/auth/login',
          method: 'POST',
          data: credentials
        })
        
        if (res.data.code === 200) {
          commit('SET_USER', res.data.data)
          uni.setStorageSync('token', res.data.token)
          return Promise.resolve(res.data)
        }
      } catch (error) {
        return Promise.reject(error)
      }
    },
    
    async syncCartToServer({ state }) {
      if (!state.user) return
      
      try {
        await uni.request({
          url: '/api/cart/sync',
          method: 'POST',
          header: {
            'Authorization': `Bearer ${uni.getStorageSync('token')}`
          },
          data: {
            items: state.cart.items
          }
        })
      } catch (error) {
        console.error('同步购物车失败:', error)
      }
    }
  },
  
  getters: {
    isLoggedIn: state => !!state.user,
    cartItemCount: state => state.cart.count,
    selectedCartItems: state => 
      state.cart.items.filter(item => item.selected)
  }
})

export default store

五、原生能力集成与性能优化

5.1 多端原生API封装

统一封装各平台原生能力:

// utils/native.js
class NativeBridge {
  // 获取设备信息
  static getDeviceInfo() {
    return new Promise((resolve, reject) => {
      // #ifdef APP-PLUS
      plus.device.getInfo({
        success: resolve,
        fail: reject
      })
      // #endif
      
      // #ifdef MP-WEIXIN
      wx.getSystemInfo({
        success: resolve,
        fail: reject
      })
      // #endif
      
      // #ifdef H5
      resolve({
        platform: 'h5',
        model: navigator.userAgent,
        system: navigator.platform
      })
      // #endif
    })
  }
  
  // 图片选择与上传
  static chooseImage(options = {}) {
    return new Promise((resolve, reject) => {
      const config = {
        count: options.count || 9,
        sizeType: options.sizeType || ['original', 'compressed'],
        sourceType: options.sourceType || ['album', 'camera']
      }
      
      // #ifdef APP-PLUS
      plus.gallery.pick({
        count: config.count,
        filter: 'image',
        system: false
      }, resolve, reject)
      // #endif
      
      // #ifdef MP-WEIXIN
      wx.chooseImage({
        ...config,
        success: res => resolve({ files: res.tempFilePaths }),
        fail: reject
      })
      // #endif
      
      // #ifdef H5
      const input = document.createElement('input')
      input.type = 'file'
      input.multiple = config.count > 1
      input.accept = 'image/*'
      input.onchange = e => {
        const files = Array.from(e.target.files)
        resolve({ files })
      }
      input.click()
      // #endif
    })
  }
  
  // 支付统一接口
  static async pay(orderInfo) {
    const platform = uni.getSystemInfoSync().platform
    
    switch (platform) {
      case 'android':
      case 'ios':
        return this.appPay(orderInfo)
      case 'mp-weixin':
        return this.wechatPay(orderInfo)
      case 'mp-alipay':
        return this.alipay(orderInfo)
      default:
        return this.h5Pay(orderInfo)
    }
  }
  
  static async appPay(orderInfo) {
    // 处理App支付
    const provider = orderInfo.provider || 'wxpay'
    
    return new Promise((resolve, reject) => {
      uni.requestPayment({
        provider,
        orderInfo: orderInfo.data,
        success: resolve,
        fail: reject
      })
    })
  }
}

export default NativeBridge

5.2 性能优化策略

实施关键性能优化措施:

// utils/performance.js
class PerformanceOptimizer {
  // 图片懒加载优化
  static initImageLazyLoad() {
    const io = uni.createIntersectionObserver(this)
    
    // 监听图片进入视口
    io.relativeToViewport({ bottom: 100 }).observe('.lazy-image', res => {
      if (res.intersectionRatio > 0) {
        const img = res.target
        const src = img.dataset.src
        
        if (src) {
          img.src = src
          img.classList.add('loaded')
        }
      }
    })
  }
  
  // 请求缓存
  static createCachedRequest() {
    const cache = new Map()
    const MAX_CACHE_SIZE = 50
    
    return async function cachedRequest(url, options = {}) {
      const cacheKey = `${url}_${JSON.stringify(options)}`
      const now = Date.now()
      
      // 检查缓存
      if (cache.has(cacheKey)) {
        const { data, timestamp, ttl = 60000 } = cache.get(cacheKey)
        
        if (now - timestamp = MAX_CACHE_SIZE) {
          const firstKey = cache.keys().next().value
          cache.delete(firstKey)
        }
        
        cache.set(cacheKey, {
          data: response.data,
          timestamp: now
        })
        
        return response.data
      } catch (error) {
        throw error
      }
    }
  }
  
  // 页面预加载
  static preloadPages(pageRoutes) {
    pageRoutes.forEach(route => {
      // #ifdef APP-PLUS
      plus.webview.preload({
        url: route,
        id: route
      })
      // #endif
      
      // #ifdef H5
      // H5使用link预加载
      const link = document.createElement('link')
      link.rel = 'prefetch'
      link.href = route
      document.head.appendChild(link)
      // #endif
    })
  }
  
  // 内存优化:清理未使用的资源
  static cleanupResources() {
    // 清理过期的缓存
    const now = Date.now()
    const imageCache = uni.getStorageInfoSync()
    
    Object.keys(imageCache).forEach(key => {
      if (key.startsWith('image_cache_')) {
        const cacheData = uni.getStorageSync(key)
        if (now - cacheData.timestamp > 24 * 60 * 60 * 1000) {
          uni.removeStorageSync(key)
        }
      }
    })
    
    // 触发垃圾回收(仅App)
    // #ifdef APP-PLUS
    if (plus.os.name === 'iOS') {
      plus.ios.invoke('performSelector:', 'gc', '')
    }
    // #endif
  }
}

export default PerformanceOptimizer

六、多端适配与发布部署

6.1 条件编译实战

处理各平台差异:

// 导航栏适配
const navBarConfig = {
  // #ifdef MP-WEIXIN
  height: 44,
  paddingTop: 0,
  // #endif
  
  // #ifdef MP-ALIPAY
  height: 48,
  paddingTop: 4,
  // #endif
  
  // #ifdef APP-PLUS
  height: 44,
  paddingTop: plus.navigator.getStatusbarHeight(),
  // #endif
  
  // #ifdef H5
  height: 44,
  paddingTop: 0,
  // #endif
}

// 分享功能适配
function setupShare() {
  // #ifdef MP-WEIXIN
  wx.showShareMenu({
    withShareTicket: true,
    menus: ['shareAppMessage', 'shareTimeline']
  })
  // #endif
  
  // #ifdef APP-PLUS
  plus.share.getServices(services => {
    const weixinService = services.find(s => s.id === 'weixin')
    if (weixinService) {
      weixinService.authorize(() => {
        console.log('微信分享授权成功')
      })
    }
  })
  // #endif
}

// 支付功能适配
async function handlePayment(order) {
  // #ifdef MP-WEIXIN
  const paymentResult = await wx.requestPayment({
    timeStamp: order.timeStamp,
    nonceStr: order.nonceStr,
    package: order.package,
    signType: 'MD5',
    paySign: order.paySign
  })
  // #endif
  
  // #ifdef MP-ALIPAY
  const paymentResult = await my.tradePay({
    tradeNO: order.tradeNo
  })
  // #endif
  
  // #ifdef APP-PLUS
  const paymentResult = await uni.requestPayment({
    provider: order.provider,
    orderInfo: order.orderInfo
  })
  // #endif
  
  return paymentResult
}

6.2 云打包与发布

配置自动化构建流程:

// package.json 构建脚本
{
  "scripts": {
    "build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",
    "build:app": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build",
    "build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build",
    "build:all": "npm run build:mp-weixin && npm run build:app && npm run build:h5",
    "dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-serve",
    "dev:app": "cross-env NODE_ENV=development UNI_PLATFORM=app-plus vue-cli-service uni-serve"
  }
}

// GitHub Actions 自动化部署
// .github/workflows/deploy.yml
name: Deploy UniApp

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '14'
    
    - name: Install Dependencies
      run: npm ci
    
    - name: Build for WeChat Mini Program
      run: npm run build:mp-weixin
      env:
        UNI_CLOUD_PROVIDER: aliyun
        UNI_APP_ID: ${{ secrets.UNI_APP_ID }}
    
    - name: Build for H5
      run: npm run build:h5
    
    - name: Deploy H5 to Server
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./dist/build/h5

七、调试与监控

7.1 多端调试技巧

// utils/debug.js
class DebugHelper {
  static init() {
    // 开发环境日志
    if (process.env.NODE_ENV === 'development') {
      // 性能监控
      this.monitorPerformance()
      
      // 网络请求拦截
      this.interceptRequests()
      
      // 错误收集
      this.setupErrorHandler()
    }
  }
  
  static monitorPerformance() {
    // 页面加载时间
    const performance = wx.getPerformance ? wx.getPerformance() : null
    if (performance) {
      const observer = performance.createObserver(entries => {
        entries.forEach(entry => {
          console.log(`[Performance] ${entry.name}: ${entry.duration}ms`)
        })
      })
      observer.observe({ entryTypes: ['navigation', 'render', 'script'] })
    }
    
    // 内存监控
    setInterval(() => {
      // #ifdef APP-PLUS
      const memory = plus.android.invoke('Runtime/getRuntime', 'maxMemory')
      console.log(`[Memory] Used: ${memory}`)
      // #endif
    }, 30000)
  }
  
  static interceptRequests() {
    const originalRequest = uni.request
    uni.request = function(config) {
      const startTime = Date.now()
      
      return originalRequest.call(this, {
        ...config,
        success: res => {
          const duration = Date.now() - startTime
          console.log(`[Request] ${config.url}: ${duration}ms`)
          config.success && config.success(res)
        },
        fail: error => {
          console.error(`[Request Failed] ${config.url}:`, error)
          config.fail && config.fail(error)
        }
      })
    }
  }
  
  static setupErrorHandler() {
    // 全局错误捕获
    uni.onError(error => {
      console.error('[Global Error]:', error)
      
      // 上报错误
      this.reportError(error)
    })
    
    // Promise错误捕获
    window.addEventListener('unhandledrejection', event => {
      console.error('[Unhandled Promise]:', event.reason)
      this.reportError(event.reason)
    })
  }
  
  static reportError(error) {
    // 错误上报到服务器
    const errorInfo = {
      time: new Date().toISOString(),
      platform: uni.getSystemInfoSync().platform,
      version: plus.runtime.version,
      error: error.toString(),
      stack: error.stack
    }
    
    // 使用sendBeacon异步上报
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/error/report', JSON.stringify(errorInfo))
    }
  }
}

export default DebugHelper

八、总结与最佳实践

8.1 项目总结

通过本实战项目,我们实现了:

  1. 基于UniApp的跨平台电商应用完整架构
  2. 高性能的商品瀑布流和图片懒加载方案
  3. 统一的多端原生能力封装
  4. 完善的状态管理和数据持久化
  5. 自动化构建和部署流程

8.2 最佳实践建议

  • 代码组织:按功能模块划分目录结构,保持组件单一职责
  • 性能优化:图片懒加载、请求缓存、组件按需加载
  • 错误处理:全局错误捕获和用户友好提示
  • 测试策略:单元测试组件,端到端测试关键流程
  • 持续集成:自动化构建、测试和部署

8.3 扩展学习方向

  • uniCloud云开发深度集成
  • 原生插件开发与封装
  • TypeScript在UniApp中的应用
  • 微前端架构在跨端应用中的实践
  • AR/VR等新技术集成

UniApp作为跨端开发的重要解决方案,在不断演进中提供了更强大的能力。掌握其核心原理和最佳实践,能够帮助开发者高效构建高质量的多端应用。

// 页面交互增强
document.addEventListener(‘DOMContentLoaded’, function() {
// 代码高亮和复制
const codeBlocks = document.querySelectorAll(‘pre code’)

codeBlocks.forEach(block => {
// 添加复制按钮
const copyBtn = document.createElement(‘button’)
copyBtn.textContent = ‘复制’
copyBtn.style.cssText = `
position: absolute;
right: 10px;
top: 10px;
background: #007aff;
color: white;
border: none;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
opacity: 0.8;
transition: opacity 0.3s;
`

copyBtn.onmouseover = () => copyBtn.style.opacity = ‘1’
copyBtn.onmouseout = () => copyBtn.style.opacity = ‘0.8’

copyBtn.onclick = async () => {
try {
await navigator.clipboard.writeText(block.textContent)
copyBtn.textContent = ‘已复制’
setTimeout(() => {
copyBtn.textContent = ‘复制’
}, 2000)
} catch (err) {
console.error(‘复制失败:’, err)
}
}

block.parentNode.style.position = ‘relative’
block.parentNode.appendChild(copyBtn)

// 语法高亮(简化版)
const keywords = [‘import’, ‘export’, ‘default’, ‘function’, ‘class’, ‘const’, ‘let’, ‘var’, ‘if’, ‘else’, ‘return’, ‘async’, ‘await’, ‘try’, ‘catch’, ‘finally’]
const html = block.innerHTML

let highlighted = html
keywords.forEach(keyword => {
const regex = new RegExp(`\b${keyword}\b`, ‘g’)
highlighted = highlighted.replace(regex, `${keyword}`)
})

// 字符串高亮
highlighted = highlighted.replace(/'[^’]*’|”[^”]*”/g, match =>
`${match}`
)

// 注释高亮
highlighted = highlighted.replace(///.*$/gm, match =>
`${match}`
)

block.innerHTML = highlighted
})

// 章节导航
const headings = document.querySelectorAll(‘h2, h3’)
const navContainer = document.createElement(‘div’)
navContainer.style.cssText = `
position: fixed;
right: 20px;
top: 100px;
background: white;
border: 1px solid #eaeaea;
border-radius: 8px;
padding: 15px;
max-width: 250px;
max-height: 70vh;
overflow-y: auto;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 1000;
`

const navTitle = document.createElement(‘h4’)
navTitle.textContent = ‘文章导航’
navTitle.style.marginTop = ‘0’
navContainer.appendChild(navTitle)

headings.forEach(heading => {
const link = document.createElement(‘a’)
link.textContent = heading.textContent
link.href = ‘#’
link.style.cssText = `
display: block;
padding: 5px 0;
color: #333;
text-decoration: none;
font-size: ${heading.tagName === ‘H2′ ? ’14px’ : ’13px’};
margin-left: ${heading.tagName === ‘H3′ ? ’15px’ : ‘0’};
border-left: ${heading.tagName === ‘H3’ ? ‘2px solid #007aff’ : ‘none’};
padding-left: ${heading.tagName === ‘H3′ ? ’10px’ : ‘0’};
`

link.onclick = (e) => {
e.preventDefault()
heading.scrollIntoView({
behavior: ‘smooth’,
block: ‘start’
})
}

navContainer.appendChild(link)
})

document.body.appendChild(navContainer)

// 移动端隐藏导航
if (window.innerWidth < 768) {
navContainer.style.display = 'none'
}
})

UniApp跨平台应用开发:从零构建高性能电商应用完整实战指南
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 uniapp UniApp跨平台应用开发:从零构建高性能电商应用完整实战指南 https://www.taomawang.com/web/uniapp/1664.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务