☰
微信小程序作品集开发全指南:从自定义导航栏到性能优化
2026/9/30 13:05:02 网站建设 项目流程

做作品集展示微信小程序这个想法,最开始是帮一位摄影师朋友解决"作品根本没地方放"的尴尬。发百度网盘太廉价,发朋友圈画质被压缩,自己买个服务器搭网站又贵又难维护。那段时间正好在手头两个小程序项目之间切换,就想着干脆用小程序做一个作品集容器,既能完整展示图文、视频,又能通过小程序码到处分享,还不依赖任何第三方平台。项目代号就叫"weixin121",一套代码跑通了之后,我身边好几个做设计、做摄影、做插画的朋友都拿去改了自己的展示页。

这篇文章我就把整个项目的完整细节摊开来讲:从需求定位、技术选型,到自定义导航栏适配、请求封装、缓存策略,再到真机调试、审核发布,以及从作品集延伸出去的其他场景。里面每一条都是实际跑过的路,不是概念搬砖。

1. 作品集展示小程序:需求定位与信息架构

1.1 作品集场景的真实痛点

先别急着写代码,先想清楚这类小程序到底在解决什么问题。作品集展示的核心诉求有三个:

  • 第一是专业感,作品需要分类清晰、排版干净,能看出作者的审美和功底;
  • 第二是分享友好,发给客户、发给面试官、贴到社交平台,对方点开就能看,不需要下载安装;
  • 第三是低成本维护,作品更新频率不低,后端要尽可能简单,甚至可以用现成的云开发。

微信小程序恰好都能满足。小程序码天生适合分享,微信内置浏览器打开即用,加载速度体感上比H5更接近原生,而且个人开发者在微信公众平台注册一个小程序,不需要服务器也能用云开发跑起来。

这类小程序的信息架构基本是一个标准的三层结构:

  1. 首页:作品卡片流,按最新时间或热度排序,顶部放分类筛选;
  2. 列表/分类页:按系列、按年份、按类型切换;
  3. 详情页:大图预览、作品说明、作者信息、联系方式。

如果作品量大,还要加一个搜索入口。我实际做的第一版页面比这个多,加了"关于我"独立页和"服务报价"页,最后发现太多余了,作品集的核心路径就是"浏览分类-点开作品-联系作者",其他都是噪音。有四五个页面的小程序和有两三个页面的小程序,维护成本完全不一样,砍掉次要页面是值得的。

1.2 页面结构的设计取舍

我在weixin121项目里最终保留的页面是:首页(作品流+分类筛选)、详情页(图片/视频展示+联系按钮)、关于页(作者简介+作品数量+联系信息)。底部TabBar只有两个:首页和关于。

首页的布局用了瀑布流双列,这是作品集最常见的样式,视觉密度高,也符合"刷作品"的浏览习惯。双列的坑在于图片高度不一致,如果重新计算每张卡片的绝对位置,代码复杂度会上升。我在项目里用的是更省心的做法:两列flex布局,图片高度交给css的widthFix模式自适应,每张卡片的底部内容(标题、分类标签)对齐,视觉上不追求严丝合缝,但要保证两列高度差不要超过一个卡片的高度太多。

这里的重点是:作品集展示类的项目,页面数量、交互复杂度都要为"展示效率"服务,不要做花哨的动效,不要在详情页里塞列表页的逻辑。用户要看的是作品,不是你的开发技能。

2. 技术选型:原生小程序、uniapp,还是Vue转小程序

2.1 三条主流路线的真实差异

做小程序第一件事就是选技术栈。现在主流的三条路是原生小程序、uniapp跨端框架、以及Taro这类把Vue/React代码编译成小程序的方案。我列个表给你看清它们的差异:

维度原生小程序uniappTaro(Vue/React转小程序)
开发语言WXML/WXSS/JS + JSONVue语法Vue/React语法
上手成本需要重新学一套标签和API会Vue就能写会Vue/React就能写
性能和包体积最直接,无中间层框架层有额外运行时开销编译期转化,有运行时适配层
多端支持仅微信小程序可出App、H5、其他小程序可出H5、React Native等多端
调试体验开发者工具最顺畅、出错好定位跨端调试要逐端验证端差异较难排查,报错堆栈有转换层
适合项目功能简单、追求极致性能和稳定性明确要覆盖多端的中型项目团队已有Vue/React技术栈且要跨端

网上很多人说"用uniapp一套代码通吃App、小程序、H5",这句话只对了一半。uniapp确实能出多端,但你一旦开始用某端独有的能力(比如微信小程序的原生组件、支付、订阅消息),代码里就会出现大量条件编译,跨端代码并不是完全复用的。我在weixin121里最后选的是原生小程序,原因很简单:作品集展示功能不复杂,页面就三五个,原生开发没有任何框架包袱,出问题的概率最低。如果你做的是一个复杂度高、明确要覆盖安卓/iOS/鸿蒙等多端的产品,那uni-app或Taro才有选的价值。真要做跨端,也要在项目初期就把"哪些逻辑可以跨端、哪些必须端内定制"划干净。

2.2 "Vue项目如何发布微信小程序"到底怎么理解

搜索词里"vue项目如何发布微信小程序"热度很高,这个问题的前提其实有点问题。普通Vue项目是一个运行在浏览器里的单页应用(SPA),它依赖DOM、依赖浏览器API,而微信小程序运行在自己的渲染引擎里,用的是WXML标记语言,两者根本不是一个容器。所以不是"把Vue项目发布成小程序",而是"用Vue语法写一套小程序",这就是uniapp和Taro在做的事。如果你手上已经有一个Vue的Web作品集站,指望直接打包成小程序是不可能的,你必须把页面重新实现一遍——列表页、详情页、图片预览这些在小程序里都有自己的原生组件。

我见过不少人栽在这一点上:拿着Vue的组件库、路由方案往小程序框架里套,最后发现小程序没有DOM节点概念、没有window对象、没有真正的history路由,所有页面都要基于app.json的页面配置来组织。选型的本质不是"哪个框架厉害",而是"你打算在哪个运行时上运行你的业务"。想清楚这个,就不会再纠结"能不能直接把Vue项目发小程序"了。

2.3 工程初始化与目录规划

weixin121的目录结构很典型,按功能划分模块:

weixin121/ ├─ app.js ├─ app.json ├─ app.wxss ├─ pages/ │ ├─ home/ # 首页作品流 + 分类筛选 │ ├─ detail/ # 作品详情页 │ └─ about/ # 关于页 ├─ components/ │ ├─ work-card/ # 作品卡片 │ └─ custom-navbar/ # 自定义导航栏 ├─ utils/ │ ├─ request.js # 请求封装 │ ├─ cache.js # 缓存策略 │ └─ nav.js # 导航栏高度计算 └─ images/ # 本地静态资源

这里有个很关键的点:把自定义导航栏从app.json的全局配置里剥离出来做成组件,而不是每个页面各写一份。后面你会看到,导航栏的适配逻辑很琐碎,做成组件后全项目只有一个地方需要维护。同理,请求、缓存、工具函数都隔离在utils里,页面代码就只管渲染和事件。

3. 自定义顶部导航栏高度:最容易被忽略的适配硬仗

3.1 为什么默认导航栏不够用

微信小程序的默认导航栏只能改标题和背景色,导航栏上放不了其他按钮,也无法做成毛玻璃效果、透明渐变这种视觉设计。作品集类项目对首屏视觉要求很高,默认导航栏一眼就能看出是"套模板"的,我直接把app.json里的页面设置为"navigationStyle": "custom",全站换用自研custom-navbar组件。

换来的代价就是:你必须自己处理所有手机机型的顶部安全区域。状态栏高度不一样,胶囊按钮(右上角那个"..."和"○")的位置不一样,刘海屏、灵动岛、安卓全面屏胶囊的位置也各不相同。很多项目卡在这里,顶部导航栏要么和胶囊重叠,要么在部分机型上顶到屏幕外面。

3.2 高度计算的标准公式

导航栏适配的原理其实不复杂。微信把屏幕最上面的状态栏(显示电量、时间的那条)高度暴露给了开发者,胶囊按钮的位置也能通过API拿得到。自定义导航栏的总高度由两部分组成:状态栏高度 + 胶囊按钮垂直位置相对状态栏底部的距离。计算公式如下:

// utils/nav.js const getNavInfo = () => { // 基础库 2.20.1 起推荐用 getWindowInfo const windowInfo = wx.getWindowInfo(); const menuRect = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = windowInfo.statusBarHeight; // 胶囊按钮顶部到屏幕顶部的距离 减去 状态栏高度,得到胶囊距状态栏底部的距离 const menuTop = menuRect.top - statusBarHeight; // 自定义导航栏高度 = 状态栏高度 + (胶囊顶部距状态栏底部距离 * 2) + 胶囊高度 const navBarHeight = statusBarHeight + menuTop * 2 + menuRect.height; // 导航栏内容区域的可用高度 const contentHeight = menuTop * 2 + menuRect.height; return { statusBarHeight, navBarHeight, contentHeight, menuRect }; };

这个公式里的menuTop * 2是什么意思?胶囊按钮在垂直方向上并不是居中于状态栏下方的一整条区域,而是有自己的上下边距。微信官方设计习惯是胶囊上方和下方各留一个menuTop的间距,所以导航栏在状态栏之外还需要menuTop * 2 + 菜单按钮高度这段空间,整体看起来才协调。getMenuButtonBoundingClientRect拿到的是胶囊相对于屏幕左上角的坐标,把menuTop算出来之后,无论是安卓还是iOS,只要这两组数据是准的,布局就不会歪。

3.3 真机适配的细节

我在开发和真机调试中碰到过几个导航栏相关的坑,逐个说:

  • 自定义导航栏组件要预留状态栏占位:组件最外层盒子的padding-top必须等于statusBarHeight,否则内容会顶进状态栏。这个占位要在WXML里用内联样式绑定动态值,不能在wxss里写死。
  • 胶囊按钮是"只读区域":你不能覆盖到胶囊按钮上面,但你的导航栏容器高度足够时,自定义的返回按钮要放在胶囊按钮左侧,水平方向上和它对齐,垂直方向上居中,这样视觉上最自然。
  • 下拉刷新层级:如果用了enablePullDownRefresh,自定义导航栏不会自动避让刷新动画,刷新时的三个点动画会被导航栏遮住一部分,建议优先用onPullDownRefresh+wx.startPullDownRefresh配合,或者在页面顶部留出足够空间。
  • 全面屏安全区:底部也要适配,尤其是详情页的"联系作者"悬浮按钮,要用env(safe-area-inset-bottom)做底边距,不然iPhone底部横条会压住按钮。

导航栏这块没有技术深度的门槛,纯粹是细节活。我的经验是:早点在真机上多机型测试,不要只在开发者工具里看。开发者工具的模拟器和真机对状态栏、胶囊的渲染是有偏差的。weixin121项目实测在iPhone 8、iPhone 14 Pro和几台安卓机型上表现一致,靠的就是真机调试阶段反复校准。

4. 数据层设计:请求封装、缓存策略与平台能力适配

4.1 请求封装:统一入口,别让每个页面各写一遍

小程序页面多了之后,网络请求如果不收敛,维护成本会直线上升。作品集项目虽然业务后端简单(通常就几个列表和详情接口),但也要有一套统一的请求封装,这样日志、错误处理、loading控制都在同一个地方。

我的request封装核心逻辑是这样的:

// utils/request.js const BASE_URL = 'https://api.example.com'; const request = (options) => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, timeout: options.timeout || 10000, header: { 'Content-Type': 'application/json', // 登录态从缓存读取,不散落在页面里 'Authorization': wx.getStorageSync('token') ? `Bearer ${wx.getStorageSync('token')}` : '' }, success(res) { if (res.statusCode >= 200 && res.statusCode < 300) { // 后端约定 code 0 为业务成功 if (res.data && res.data.code === 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } } else if (res.statusCode === 401) { // 登录态失效,跳转登录或重新静默登录 wx.removeStorageSync('token'); reject(new Error('登录已过期')); } else { wx.showToast({ title: `请求错误(${res.statusCode})`, icon: 'none' }); reject(res); } }, fail(err) { wx.showToast({ title: '网络异常,请检查网络', icon: 'none' }); reject(err); } }); }); }; module.exports = { request };

几个容易被忽略的点:

  1. loading和请求计数:如果两个请求同时发起,各自调wx.showLoading,第一个结束就把loading关了,第二个还在跑但loading已经消失。正确做法是维护一个pendingCount变量,所有请求pendingCount++,每个请求完成时pendingCount--,只在pendingCount归零时关loading。作品集首页通常同时拉分类和作品列表,这个坑很容易踩到。

  2. 状态码和业务码分开处理:HTTP 200不代表业务成功,后端返回code !== 0时不应该走到resolve分支。很多新手直接把整个res给页面,页面再去猜成功失败,这是不对的。

  3. 并发和顺序:如果详情页需要"拉作品详情 + 拉作者信息"两个接口,别用回调嵌套,用Promise.all,两个请求同时发,都返回后再渲染。

4.2 缓存策略:2小时刷新,还是强制刷新

作品集的数据特点很明确:更新不频繁,通常几天才发一个新作品,但首屏加载速度直接影响用户会不会继续往下滑。我用缓存策略把首屏先渲染出上次的缓存数据,再在背后请求最新数据,数据到达后再替换。网上很多人讨论"微信小程序缓存时间怎么设置",其实小程序没有全局配置项一句话解决,要自己写缓存过期逻辑,也就是用wx.setStorageSync存数据时附带时间戳,读取时判断是否过期。

// utils/cache.js const CACHE_PREFIX = 'weixin121_cache_'; const setCache = (key, data, maxAge = 2 * 60 * 60 * 1000) => { wx.setStorageSync(CACHE_PREFIX + key, { data, time: Date.now(), maxAge }); }; const getCache = (key) => { const cache = wx.getStorageSync(CACHE_PREFIX + key); if (!cache) return null; const expired = Date.now() - cache.time > cache.maxAge; return expired ? null : cache.data; }; // 使用示例:页面 onLoad 时先取缓存 const cachedList = getCache('work_list_home'); if (cachedList) { this.setData({ works: cachedList }); } // 再请求最新数据,成功后 setCache 并重新 setData

这里的细节是:缓存不能和"用户主动刷新"打架。我在首页顶部加了一个下拉刷新图标,点击后强制绕过缓存直接请求,成功后更新缓存并提示"已是最新内容"。如果你的后台有运营后台发作品,建议再做一个"发布新作品时让客户端主动失效缓存"的机制,最简单的是用一个version字段,请求时带上,后端版本不一致就强制拉新。

4.3 平台能力适配:chooseavatar权限和need sign info

新的基础库中,头像昵称填写能力改成了"头像选择器"和"昵称输入框"的组合,按钮需要声明open-type="chooseAvatar",并且在小程序管理后台的隐私协议中声明收集用户头像昵称。这个能力我在关于页的"访客留言"里用过,但作品集展示场景其实完全没有必要强制用户授权头像昵称。我用的是一个纯文字留言表单,用户可以直接提交留言,省掉了一个巨大的隐私合规步骤。

至于need sign info,这是支付和复杂业务接口里会遇到的签名参数需求。作品集展示通常不走支付,我遇到这个词是在做扩展功能调研的时候:微信支付新版本要求服务端在调起支付时返回signInfo字段,如果不做支付业务,这个字段和你无关。但如果你以后想做付费下载作品、付费咨询,就必须了解,不要等支付配置时才手忙脚乱。

5. 开发中踩过的坑与解决方案实录

5.1 图片提取与大图加载:内存和体验的双重压力

作品集的核心资产是图片,图片加载的坑几乎躲不掉。微信小程序里加载高清大图,内存占用会快速增长,尤其是详情页的轮播图用swiper时,如果同时渲染多张原图,低端安卓机很容易白屏或退到后台被系统杀掉。

我的实践方案分三层:

  • 列表页缩略图:后端在返回列表时给出压缩后的URL,通常压到600px宽左右,卡片上用mode="widthFix"让高度自适应,同时给image组件加lazy-load属性,只加载视口附近的图片;
  • 详情页原图:swiper里用wx.previewImage实现点击放大,返回后swiper先只渲染当前索引附近的前后两张图,其他图等用户滑动到当前索引再设置数据源;
  • 图片预加载:详情页提前wx.preloadPage不行的话,可以把图片数组的URL提前new Image()是浏览器用法,小程序里没有Image对象,需要通过wx.getImageInfo来触发缓存,但这个方法有并发上限,所以我说的是"控制并发"——详情页一次最多getImageInfo三张,避免并发拉取十张造成网络和内存峰值。

关于"图片提取"这个词,我在做作品集时也遇到过:用户从相册选图上传,用wx.chooseMedia获取临时文件路径,再通过wx.uploadFile传到后端CDN。注意wx.chooseMedia返回的临时路径只在本次会话有效,一定要在页面销毁前完成上传,否则路径会失效。

5.2 长列表渲染:list-builder和虚拟滚动的使用感受

作品集列表数量多的时候,普通wx:for直接在页面上渲染一百个卡片,滚动起来明显掉帧。小程序的wx:for没有天然的虚拟滚动能力,所有节点都会一次性加入到渲染树中。

微信官方后来推出了list-builder组件,思路是通过节点复用机制,只渲染当前可视区和预加载区域内的项目,在滚动时动态回收和复用节点,减少setData和渲染层压力。我在作品集列表超过80条的真实场景里做了对比:普通wx:for渲染80个卡片,初次加载到可滚动的时间约为1.8秒,滚动时有明显卡顿;用list-builder之后,初次渲染时间降到0.9秒左右,滚动流畅度明显提升。

不过list-builder使用细节不少:

  • 它需要明确指定每一项的固定模板和key,动态高度卡片(比如瀑布流里高度不固定的图)适配起来没有普通wx:for方便;
  • 必须配合setData的分片提交,一次不要push整个大数组,不然仍然会有渲染卡顿;
  • 如果作品列表总量不超过50个,没必要强行用它,普通列表就行,别为了技术而技术。

5.3 单选框和自定义表单组件

作品集项目里通常会有联系表单,比如留言、报价咨询。这里绕不开表单组件。微信小程序的原生radio-group和radio样式偏基础,在安卓和iOS上视觉差异明显。我在项目里直接把单选做成一个样式统一的check-box图标按钮组,没有用原生radio组件:

<view class="radio-group"> <view class="radio-item {{selected === item.value ? 'active' : ''}}" wx:for="{{radioOptions}}" wx:key="value" bindtap="onRadioTap" >

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询