目录
1.小程序打包时会进行分包处理吗?
分包是一个非常重要的性能优化手段。我不仅使用过,还根据不同的业务场景制定了相应的分包策略。下面我从几个方面来详细说明:
一、为什么要进行分包?
1. 解决主包体积限制
小程序官方限制:
- 整个小程序所有分包大小不超过 20M
- 单个分包/主包大小不能超过 2M
- 主包大小直接影响小程序的启动速度
2. 优化启动性能
-
主包体积越小,下载和解包速度越快
-
用户首次打开时只加载主包,按需加载分包
-
提升首屏加载速度和用户体验
3. 业务模块化
-
不同团队可以独立开发不同分包
-
便于代码维护和功能迭代
-
支持按功能模块独立更新
二、实际项目中的分包策略
1. 按业务功能划分
javascript
// 电商小程序的分包策略
{
"subpackages": [
// 商品模块分包
{
"root": "packageGoods",
"pages": [
"pages/goods/list",
"pages/goods/detail",
"pages/goods/search",
"pages/goods/category"
]
},
// 订单模块分包
{
"root": "packageOrder",
"pages": [
"pages/order/list",
"pages/order/detail",
"pages/order/confirm",
"pages/order/refund"
]
},
// 用户中心分包
{
"root": "packageUser",
"pages": [
"pages/user/profile",
"pages/user/address",
"pages/user/coupon",
"pages/user/favorite"
]
},
// 营销活动分包(可独立更新)
{
"root": "packagePromotion",
"pages": [
"pages/promotion/seckill",
"pages/promotion/groupon",
"pages/promotion/coupon"
]
}
]
}
2. 预加载优化策略
javascript
// app.json 中的预加载配置
{
"preloadRule": {
// 首页预加载商品和订单分包
"pages/index/index": {
"network": "wifi", // 仅在wifi下预加载
"packages": ["packageGoods", "packageOrder"]
},
// 商品列表页预加载商品详情
"pages/goods/list": {
"network": "all",
"packages": ["packageGoods"]
},
// 购物车预加载订单确认页
"pages/cart/index": {
"network": "all",
"packages": ["packageOrder"]
}
}
}
三、独立分包的使用场景
1. 独立分包配置
javascript
{
"subpackages": [
{
"root": "independentPackage",
"name": "independent",
"pages": [
"pages/activity/special",
"pages/activity/limited"
],
"independent": true // 声明为独立分包
}
]
}
2. 独立分包的优势
-
可以不依赖主包独立运行
-
独立分包的页面启动速度更快
-
适合营销活动、分享页面等场景
四、分包开发的最佳实践
1. 公共代码处理
javascript
// 问题:分包中无法直接引用主包的组件和工具函数
// 解决方案:复制必要文件或使用分包专用版本
// packageA/utils/request.js (分包专用)
const request = (url, options) => {
// 分包独立的网络请求封装
return new Promise((resolve, reject) => {
wx.request({
url: `https://api.example.com${url}`,
...options,
success: resolve,
fail: reject
})
})
}
export default request
2. 组件复用策略
javascript
// 方案一:公共组件放在主包(适合高频使用)
// 方案二:业务组件随分包(适合特定功能)
// 方案三:组件库拆分为独立分包(大型项目)
// components/ 主包公共组件
// packageA/components/ 分包专用组件
3. 路由跳转优化
javascript
// 分包页面跳转
wx.navigateTo({
url: '/packageA/pages/shop/shop?id=123'
})
// 使用封装的路由工具
import Router from '../utils/router'
Router.navigateTo('shop', { id: 123 }, 'packageA')
五、性能监控与优化
1. 分包大小监控
javascript
// 在开发者工具中查看包大小分布
// 或使用构建脚本分析
const fs = require('fs')
const path = require('path')
function analyzePackageSize() {
const mainPackageSize = calculateDirSize('./miniprogram')
const packageASize = calculateDirSize('./miniprogram/packageA')
const packageBSize = calculateDirSize('./miniprogram/packageB')
console.log(`主包大小: ${(mainPackageSize / 1024 / 1024).toFixed(2)}M`)
console.log(`分包A大小: ${(packageASize / 1024 / 1024).toFixed(2)}M`)
console.log(`分包B大小: ${(packageBSize / 1024 / 1024).toFixed(2)}M`)
}
2. 加载性能优化
javascript
// 监控分包加载时间
const loadStartTime = Date.now()
require('../packageA/pages/shop/shop', () => {
const loadEndTime = Date.now()
console.log(`分包A加载耗时: ${loadEndTime - loadStartTime}ms`)
// 上报性能数据
wx.reportAnalytics('subpackage_load', {
package: 'packageA',
duration: loadEndTime - loadStartTime
})
})
六、实际项目案例
"在我负责的一个电商小程序项目中,我们遇到了主包体积超过限制的问题。通过实施分包策略:
优化前:
-
主包大小:2.3M(超出限制)
-
首屏加载时间:2.8s
-
所有功能都在主包中
优化后:
javascript
// 分包策略
{
"pages": [
"pages/index/index", // 首页
"pages/cart/cart", // 购物车
"pages/category/category" // 分类
],
"subpackages": [
{
"root": "packageGoods", // 商品相关:1.2M
"pages": ["pages/goods/..."]
},
{
"root": "packageOrder", // 订单相关:0.8M
"pages": ["pages/order/..."]
},
{
"root": "packageUser", // 用户中心:0.6M
"pages": ["pages/user/..."]
}
],
"preloadRule": {
"pages/index/index": {
"packages": ["packageGoods"]
}
}
}
优化效果:
-
主包大小:1.2M(符合要求)
-
首屏加载时间:1.5s(提升46%)
-
用户感知性能明显改善"
七、遇到的挑战与解决方案
1. 公共依赖问题
javascript
// 问题:多个分包都需要相同的工具函数
// 解决方案:提取到主包或各自维护
// 主包 utils/common.js
export const formatTime = (date) => {
// 公共时间格式化
}
// 或者使用 npm 包管理公共依赖
2. 组件通信问题
javascript
// 问题:分包间组件无法直接通信
// 解决方案:使用全局状态管理
// stores/userStore.js
class UserStore {
constructor() {
this.userInfo = null
}
setUserInfo(info) {
this.userInfo = info
// 通知所有页面更新
this.notifyListeners()
}
}
// 在主包中初始化,分包通过getApp()访问
2.同套代码怎么区分小程序和App的实现方案
在同套代码中区分小程序和App,我主要采用条件编译 + 平台适配层的架构方案。核心思路是通过构建时和运行时的双重判断,实现代码的智能分发和平台特定逻辑的隔离。
一、构建时环境判断
1. 使用构建工具的条件编译
javascript
// 在构建工具中配置环境变量
// vue.config.js 或 webpack.config.js
module.exports = {
chainWebpack: config => {
config.plugin('define').tap(args => {
// 根据构建目标设置全局变量
args[0]['process.env.PLATFORM'] =
JSON.stringify(process.env.VUE_APP_PLATFORM || 'h5')
return args
})
}
}
// package.json 脚本
{
"scripts": {
"build:mp": "VUE_APP_PLATFORM=mp npm run build",
"build:app": "VUE_APP_PLATFORM=app npm run build",
"build:h5": "VUE_APP_PLATFORM=h5 npm run build"
}
}
2. 条件编译的多种实现方式
javascript
// 方式一:文件后缀条件编译
// 文件结构
src/
├── utils/
│ ├── request.js # 通用逻辑
│ ├── request.mp.js # 小程序特定
│ └── request.app.js # App特定
└── components/
├── Image.vue
├── Image.mp.vue
└── Image.app.vue
// 方式二:目录条件编译
src/
├── platforms/
│ ├── mp/ # 小程序平台代码
│ │ ├── utils/
│ │ └── components/
│ ├── app/ # App平台代码
│ └── h5/ # H5平台代码
└── common/ # 通用代码
二、运行时平台检测
1. 统一的平台检测工具
javascript
// utils/platform.js
class Platform {
constructor() {
this._detectPlatform()
}
_detectPlatform() {
// 微信小程序环境
if (typeof wx !== 'undefined' && wx.getSystemInfo) {
this.platform = 'mp-weixin'
this.env = 'miniprogram'
}
// 支付宝小程序
else if (typeof my !== 'undefined' && my.getSystemInfo) {
this.platform = 'mp-alipay'
this.env = 'miniprogram'
}
// UniApp 环境
else if (typeof uni !== 'undefined') {
// #ifdef MP-WEIXIN
this.platform = 'mp-weixin'
// #endif
// #ifdef APP-PLUS
this.platform = 'app'
// #endif
// #ifdef H5
this.platform = 'h5'
// #endif
this.env = 'uniapp'
}
// App 环境 (如 Cordova、Capacitor)
else if (window.cordova || window.Capacitor) {
this.platform = 'app'
this.env = 'hybrid'
}
// H5 环境
else {
this.platform = 'h5'
this.env = 'web'
}
}
// 平台判断方法
isMP() {
return this.env === 'miniprogram'
}
isApp() {
return this.platform === 'app'
}
isH5() {
return this.platform === 'h5'
}
// 具体平台判断
isWeixinMP() {
return this.platform === 'mp-weixin'
}
isAlipayMP() {
return this.platform === 'mp-alipay'
}
// 获取平台信息
getPlatform() {
return this.platform
}
}
export default new Platform()
2. 在代码中的使用
javascript
// 在任何需要区分平台的地方
import platform from '@/utils/platform'
// 条件执行
if (platform.isMP()) {
// 小程序特定逻辑
wx.request({ ... })
} else if (platform.isApp()) {
// App特定逻辑
nativeBridge.call('api', { ... })
} else {
// H5逻辑
fetch('/api', { ... })
}
// 或者使用策略模式
const strategies = {
'mp': () => { /* 小程序实现 */ },
'app': () => { /* App实现 */ },
'h5': () => { /* H5实现 */ }
}
const executePlatformSpecific = () => {
const strategy = strategies[platform.getPlatform()]
return strategy ? strategy() : strategies.h5()
}
三、核心模块的平台适配
1. 网络请求模块适配
javascript
// utils/request.js
import platform from './platform'
class Request {
constructor() {
this.adapter = this._getAdapter()
}
_getAdapter() {
if (platform.isMP()) {
return this._mpAdapter()
} else if (platform.isApp()) {
return this._appAdapter()
} else {
return this._h5Adapter()
}
}
// 小程序适配器
_mpAdapter() {
return (config) => {
return new Promise((resolve, reject) => {
wx.request({
url: config.url,
method: config.method,
data: config.data,
header: config.headers,
success: (res) => resolve(this._transformResponse(res)),
fail: reject
})
})
}
}
// App适配器 (UniApp示例)
_appAdapter() {
return (config) => {
return new Promise((resolve, reject) => {
uni.request({
url: config.url,
method: config.method,
data: config.data,
header: config.headers,
success: (res) => resolve(this._transformResponse(res)),
fail: reject
})
})
}
}
// H5适配器
_h5Adapter() {
return (config) => {
return fetch(config.url, {
method: config.method,
body: JSON.stringify(config.data),
headers: config.headers
}).then(response => response.json())
}
}
// 统一请求方法
async request(config) {
try {
const response = await this.adapter(config)
return response
} catch (error) {
throw this._transformError(error)
}
}
_transformResponse(response) {
// 统一响应格式
if (platform.isMP() || platform.isApp()) {
return {
data: response.data,
status: response.statusCode,
headers: response.header
}
} else {
return response
}
}
}
export default new Request()
2. 存储模块适配
javascript
// utils/storage.js
import platform from './platform'
class Storage {
// 统一存储接口
setItem(key, value) {
const serializedValue = JSON.stringify(value)
if (platform.isMP()) {
wx.setStorageSync(key, serializedValue)
} else if (platform.isApp()) {
// UniApp
uni.setStorageSync(key, serializedValue)
// 或原生App
// localStorage.setItem(key, serializedValue)
} else {
localStorage.setItem(key, serializedValue)
}
}
getItem(key) {
let value
if (platform.isMP()) {
value = wx.getStorageSync(key)
} else if (platform.isApp()) {
value = uni.getStorageSync(key)
} else {
value = localStorage.getItem(key)
}
try {
return value ? JSON.parse(value) : null
} catch {
return value
}
}
removeItem(key) {
if (platform.isMP()) {
wx.removeStorageSync(key)
} else if (platform.isApp()) {
uni.removeStorageSync(key)
} else {
localStorage.removeItem(key)
}
}
}
export default new Storage()
3. 导航路由适配
javascript
// utils/router.js
import platform from './platform'
class Router {
// 统一跳转方法
navigateTo(url, params = {}) {
const queryString = this._buildQueryString(params)
const targetUrl = queryString ? `${url}?${queryString}` : url
if (platform.isMP()) {
wx.navigateTo({ url: targetUrl })
} else if (platform.isApp()) {
uni.navigateTo({ url: targetUrl })
} else {
// H5使用Vue Router或window.location
window.location.href = targetUrl
}
}
// 返回上一页
navigateBack() {
if (platform.isMP()) {
wx.navigateBack()
} else if (platform.isApp()) {
uni.navigateBack()
} else {
window.history.back()
}
}
// 重定向
redirectTo(url, params = {}) {
const queryString = this._buildQueryString(params)
const targetUrl = queryString ? `${url}?${queryString}` : url
if (platform.isMP()) {
wx.redirectTo({ url: targetUrl })
} else if (platform.isApp()) {
uni.redirectTo({ url: targetUrl })
} else {
window.location.replace(targetUrl)
}
}
_buildQueryString(params) {
return Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&')
}
}
export default new Router()
四、组件层面的平台适配
1. 平台特定组件
javascript
<!-- components/PlatformImage.vue -->
<template>
<!-- 小程序使用 image 组件 -->
<image
v-if="platform.isMP()"
:src="src"
:mode="mode"
@load="handleLoad"
@error="handleError"
/>
<!-- App使用 uni-app 的 image -->
<uni-image
v-else-if="platform.isApp()"
:src="src"
:mode="mode"
@load="handleLoad"
@error="handleError"
/>
<!-- H5使用 img 标签 -->
<img
v-else
:src="src"
:class="imgClass"
@load="handleLoad"
@error="handleError"
/>
</template>
<script setup>
import { computed } from 'vue'
import platform from '@/utils/platform'
const props = defineProps({
src: String,
mode: {
type: String,
default: 'scaleToFill'
}
})
const emit = defineEmits(['load', 'error'])
// 转换模式为CSS object-fit
const imgClass = computed(() => {
const modeMap = {
'scaleToFill': 'object-fill',
'aspectFit': 'object-contain',
'aspectFill': 'object-cover'
}
return modeMap[props.mode] || 'object-fill'
})
const handleLoad = (event) => {
emit('load', event)
}
const handleError = (error) => {
emit('error', error)
}
</script>
2. 条件编译在Vue组件中的使用
javascript
<template>
<div class="container">
<!-- 通用内容 -->
<h1>{{ title }}</h1>
<!-- 平台特定内容 -->
<div v-if="platform.isMP()" class="mp-specific">
<button open-type="share">分享</button>
</div>
<div v-else-if="platform.isApp()" class="app-specific">
<button @click="nativeShare">原生分享</button>
</div>
<div v-else class="h5-specific">
<button @click="webShare">Web分享</button>
</div>
<!-- 平台特定的插槽内容 -->
<slot name="mp-content" v-if="platform.isMP()"></slot>
<slot name="app-content" v-else-if="platform.isApp()"></slot>
<slot name="h5-content" v-else></slot>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import platform from '@/utils/platform'
const title = ref('多平台应用')
// 平台特定的生命周期
onMounted(() => {
if (platform.isMP()) {
// 小程序特定的初始化
initMP()
} else if (platform.isApp()) {
// App特定的初始化
initApp()
} else {
// H5特定的初始化
initH5()
}
})
// 平台特定的方法
const initMP = () => {
// 小程序API调用
// #ifdef MP-WEIXIN
wx.showShareMenu({ withShareTicket: true })
// #endif
}
const initApp = () => {
// App原生功能初始化
if (window.cordova) {
document.addEventListener('deviceready', onDeviceReady, false)
}
}
const initH5 = () => {
// H5特定的初始化
if (navigator.share) {
// 支持Web Share API
}
}
// 分享功能 - 平台特定实现
const handleShare = () => {
if (platform.isMP()) {
mpShare()
} else if (platform.isApp()) {
nativeShare()
} else {
webShare()
}
}
const mpShare = () => {
// 小程序分享逻辑
}
const nativeShare = () => {
// App原生分享
// #ifdef APP-PLUS
plus.share.sendWithSystem({ content: '分享内容' })
// #endif
}
const webShare = () => {
// H5分享
if (navigator.share) {
navigator.share({
title: '分享标题',
text: '分享内容',
url: window.location.href
})
}
}
</script>
<style scoped>
/* 通用样式 */
.container {
padding: 20rpx;
}
/* 平台特定样式 */
/* #ifdef MP-WEIXIN */
.mp-specific {
color: #07C160;
}
/* #endif */
/* #ifdef APP-PLUS */
.app-specific {
color: #007AFF;
}
/* #endif */
/* #ifdef H5 */
.h5-specific {
color: #1890FF;
}
/* #endif */
</style>
五、项目架构组织
1. 多平台项目结构
javascript
src/
├── platforms/ # 平台特定代码
│ ├── mp/ # 小程序平台
│ │ ├── components/ # 小程序特有组件
│ │ ├── utils/ # 小程序工具函数
│ │ └── apis/ # 小程序API封装
│ ├── app/ # App平台
│ └── h5/ # H5平台
├── common/ # 通用代码
│ ├── components/ # 跨平台组件
│ ├── utils/ # 通用工具函数
│ ├── styles/ # 通用样式
│ └── constants/ # 常量定义
├── services/ # 业务服务层
└── stores/ # 状态管理
2. 平台适配器模式
javascript
// common/services/BaseService.js
export class BaseService {
constructor(platformAdapter) {
this.adapter = platformAdapter
}
// 通用业务方法
async fetchData(params) {
// 预处理
const processedParams = this.preProcess(params)
// 调用平台特定的网络请求
const response = await this.adapter.request({
url: this.getApiUrl(),
data: processedParams
})
// 后处理
return this.postProcess(response)
}
// 由子类实现平台特定方法
getApiUrl() {
throw new Error('子类必须实现 getApiUrl 方法')
}
preProcess(params) { return params }
postProcess(response) { return response }
}
// platforms/mp/services/UserService.js
import { BaseService } from '@/common/services/BaseService'
import { mpAdapter } from '@/platforms/mp/adapters/requestAdapter'
export class UserService extends BaseService {
constructor() {
super(mpAdapter)
}
getApiUrl() {
return '/api/user'
}
// 小程序特定的预处理
preProcess(params) {
return {
...params,
appid: wx.getAccountInfoSync().miniProgram.appId
}
}
}
// 工厂函数创建服务实例
export function createUserService() {
if (platform.isMP()) {
return new (require('@/platforms/mp/services/UserService').UserService)()
} else if (platform.isApp()) {
return new (require('@/platforms/app/services/UserService').UserService)()
} else {
return new (require('@/platforms/h5/services/UserService').UserService)()
}
}
六、构建配置和打包策略
1. 多平台构建配置
javascript
// vue.config.js
const PlatformConfig = {
mp: {
css: {
extract: false
},
configureWebpack: {
output: {
filename: 'static/js/[name].js',
chunkFilename: 'static/js/[name].js'
}
}
},
app: {
configureWebpack: {
output: {
filename: 'static/js/[name].[hash].js',
chunkFilename: 'static/js/[name].[hash].js'
}
}
},
h5: {
// H5特定配置
}
}
module.exports = {
chainWebpack: config => {
const platform = process.env.VUE_APP_PLATFORM
// 平台特定的入口文件
if (platform === 'mp') {
config.entry('app').clear().add('./src/platforms/mp/main.js')
} else if (platform === 'app') {
config.entry('app').clear().add('./src/platforms/app/main.js')
}
// 平台特定的别名配置
config.resolve.alias
.set('@platform', `@/platforms/${platform}`)
},
// 合并平台特定配置
...PlatformConfig[process.env.VUE_APP_PLATFORM] || {}
}
七、实际项目案例
"在我负责的一个电商项目中,我们使用这套方案成功实现了小程序、App、H5三端代码的统一管理:
项目结构:
javascript
src/
├── common/ # 85% 通用代码
│ ├── components/ # 业务组件
│ ├── services/ # API服务
│ └── utils/ # 工具函数
├── platforms/
│ ├── mp/ # 8% 小程序特定代码
│ ├── app/ # 5% App特定代码
│ └── h5/ # 2% H5特定代码
└── entries/ # 多入口文件
关键实现:
-
统一的平台检测工具,准确识别运行环境
-
适配器模式封装平台差异,如网络请求、存储、导航
-
条件编译处理平台特定的UI和交互
-
构建配置实现按平台打包,减少包体积