简介:一份微信小程序开发入门教程,以 Word 文档形式呈现,适合零基础或刚接触小程序开发的读者,用于快速了解从环境准备到项目发布的完整流程。文档围绕注册微信公众平台、安装开发者工具、新建项目、理解项目结构等环节展开,并结合 app.json、index.wxml、index.wxss、index.js 等关键文件给出示例代码,帮助读者弄清页面结构、样式和逻辑之间的协作方式。资源包内共 1 个 docx 文件,压缩包仅 15KB,篇幅集中、便于对照操作。教程还覆盖了数据绑定和 API 调用,例如利用 setData 更新界面、通过 wx.getLocation 获取位置信息,并给出代码片段供直接参考,最终介绍了上传发布的大致流程,能引导读者完成一个简单小程序的开发闭环。已有 353 人学习下载,适合作为课程配套资料或自学入门手册。
1. 微信小程序开发详细教程,网上能搜出一大堆,但真正能让人从零跑到上线的并不多
很多开发者在第一次打开微信开发者工具的时候,都会有一种"这不就是个网页吗"的错觉,真上手才发现,它的目录结构、样式单位和平台限制跟传统前端完全是两套规则。有人花了大量时间在 App 端,切换到小程序后依旧会被各种真机问题卡住;也有人照着模板代码,一个周末就把内部工具型小程序跑上了真机。这套教程要做的就是帮你把"注册账号 → 工具安装 → 页面编写 → 接口联调 → 发布上线"这条路完整走一遍,同时把那些不写在官方文档里的坑提前标出来。无论你是想给公司做个内部工具、给客户交付演示 Demo,还是认真投入小程序这个方向,照着做都能省下不少绕路的时间。
2. 开发环境与第一个页面:注册AppID、装工具、跑通Hello World
在写任何代码之前,先把账号和工具准备好。这个过程看着简单,实际有非常多的选择会影响后面好几个月,我见过不少开发者一开始图省事随便选,后期发现只能重来。
2.1 账号类型怎么选:个人主体与企业的权限差异
打开微信公众平台的注册页面,第一步选择的"主体类型"往往被忽略。个人主体只需要管理员手机号和身份证信息,审核很快,适合做工具类、内容展示类小程序;但个人主体无法开通微信支付,部分开放接口(比如需要用户授权隐私数据的场景)也受到类目限制。企业主体需要营业执照和对公账户打款验证,流程相对长,但能开通支付能力,可以做电商、预约等服务型产品。
如果你只是先学习或者做内部工具,个人主体足够;如果你判断后续一定会涉及支付或者电商,建议直接用企业主体注册,避免后期迁移。特别提醒:主体类型在注册后想变更非常麻烦,不是简单改个资料就行,很多场景需要重新走审核。
2.2 开发者工具初始化:新建项目时的三个关键配置
从官方网站下载微信开发者工具,一般选稳定版即可。项目类型务必选择"小程序",不是"小游戏",这两者的工程结构和运行逻辑完全不一样。新建项目时,三个配置决定后面是否顺利:
第一步是选择目录,建议用一个空目录,工具会询问是否创建模板项目;第二步是 AppID,这里可以先点"测试号"体验开发流程,但测试号无法做真机预览、云开发、体验版发布,真正投入时请换成自己的正式 AppID;第三步是后端服务,初始模板会让你选择"不使用云服务"还是"微信云开发",如果还没决定,先选"不使用云服务"最干净。
工具界面左侧是模拟器、编辑器、调试器,右上角的"编译"按钮会立刻把项目跑起来。项目创建完成后,默认生成一个包含两个页面、工具库、配置文件的基础模板,这个结构本身就是小程序工程的标准起点。此时建议打开 app.json 看一眼首页配置,把启动页和窗口标题改掉,哪怕只是改个文字,也能确认编译链路是通的。
2.3 拆解最小项目:四个文件各自管什么
用默认模板初始化后,每个页面都是一个独立的文件夹,里面固定放着四个文件:.wxml管页面结构,.wxss管样式,.js管逻辑和数据,.json管这个页面的局部配置。理解四者的关系,是入门小程序的第一步。
先看全局配置文件 app.json,它控制整个小程序的路由、窗口表现和底部导航。下面的配置是模板里最常见的形态:
{ "pages": [ "pages/index/index", "pages/logs/logs" ], "window": { "navigationBarBackgroundColor": "#ffffff", "navigationBarTitleText": "微信小程序", "navigationBarTextStyle": "black", "backgroundColor": "#f5f5f5" }, "style": "v2", "sitemapLocation": "sitemap.json" }pages 数组里第一项就是小程序的启动页,顺序很关键,改启动页就是调整这个数组的顺序;window 里的配置是全局导航栏和背景色的默认值,navigationBarTextStyle 只接受 black 和 white 两个值,填其他会被直接忽略。新增页面的最稳妥做法是手动在 pages 数组里加一行路径,保存后工具会自动创建对应的四个文件,不用自己新建目录。
页面内部则靠 data、事件和 setData 把逻辑和视图串起来。看一个最小的响应式页面:
<view class="container"> <text class="title">{{ message }}</text> <button bindtap="onTap">点我换一句话</button> </view>Page({ data: { message: 'Hello 微信小程序' }, onTap() { this.setData({ message: '数据驱动视图,重新渲染' }) } }){{ message }} 从页面的 data 里取值,页面初始化时渲染;bindtap 把按钮的点击事件绑定到 onTap,用户点击后 this.setData 修改 message,视图自动更新。setData 是全局唯一推荐的数据更新入口,直接修改 this.data 不会触发视图刷新,这个习惯从第一天就要养好。这个 Hello World 虽然简单,但它把数据、视图、事件三者的关系演示完整了,后面的复杂页面全是这个模型的延伸。
3. 页面与逻辑:WXML数据绑定、列表渲染与组件通信
真正开始写业务页面的第一道坎,是从"看得懂模板"到"能随心所欲地组织页面"。小程序的核心思路和现代前端框架一样:数据驱动视图,但它的语法和边界条件有自己的脾气。
3.1 数据驱动界面:setData与{{}}绑定的实际用法
{{ }} 语法可以出现在 WXML 的文本节点里,也可以出现在属性值中,比如 class="{{ active ? 'active' : 'normal' }}"。它支持简单的表达式运算:三元表达式、字符串拼接、数组下标访问都支持,但不支持调用方法,也不支持复杂的函数逻辑,任何需要计算的场景都应该在 JS 里先算好再放进 data。
setData 的更新操作有讲究。更新深层对象时,用路径字符串比传整个对象更高效、更安全:
Page({ data: { userInfo: { name: 'A同学', age: 18 } }, onUpdate() { this.setData({ 'userInfo.name': 'B同学' }) } })这样写的好处有两个:一是避免把整个 userInfo 对象重新传给视图层,减少传输量;二是避免并发更新时互相覆盖。setData 是异步的,视图渲染完成前,如果连续多次 setData,后面的更新可能覆盖前面的,路径更新能降低这种冲突出现的概率。另一个容易忽略的点是:不要把 data 里不参与渲染的临时变量塞进来,页面 data 会参与每次更新的对比,数据体积越干净,渲染越稳。
3.2 列表与条件渲染:wx:for、wx:key与wx:if的取舍
渲染列表是高频需求。常见的写法如下:
<view wx:for="{{ todoList }}" wx:for-item="item" wx:for-index="index" wx:key="id" class="todo-item"> <text>{{ index + 1 }}. {{ item.title }}</text> <text wx:if="{{ item.done }}">已完成</text> <text wx:else>进行中</text> </view>wx:for 默认把当前项命名为 item、下标命名为 index,如果循环嵌套,内层外层的 item 会冲突,此时用 wx:for-item 给内层重新命名,代码可读性会好很多。wx:key 必须传列表中唯一且稳定的字段,比如 id;如果列表没有稳定 id,可以用 *this 代表用整个元素作 key。不写 wx:key 时项目可以编译,但控制台会持续告警,且列表更新时容易出现渲染错位,这个警告值得尊重。
wx:if 和 hidden 的选择取决于切换频率。wx:if 在条件为假时是真正的"不渲染",代价是每次条件切换都要重建节点;hidden 则是始终渲染、只在样式上隐藏,适合频繁切换的 tab 或面板。简单场景记一个原则:初始条件大概率不成立且很少变化,用 wx:if;高频切换、只是显示隐藏,用 hidden。
事件绑定方面,bindtap 会冒泡,catchtap 会阻止冒泡。页面上任何自定义数据都可以通过>// components/custom-card/custom-card.js Component({ properties: { title: { type: String, value: '' }, count: { type: Number, value: 0 } }, data: { innerText: '组件内部状态' }, methods: { onSend() { this.triggerEvent('send', { value: this.data.innerText }) } } })
<!-- components/custom-card/custom-card.wxml --> <view class="card"> <view>{{ title }}</view> <view>{{ innerText }}</view> <button bindtap="onSend">发送事件给页面</button> </view>properties 是组件对外的接口,外部传入的数据通过它进入组件内部;data 是组件私有的;组件要跟页面通信,不直接调用页面方法,而是通过 triggerEvent 抛出一个自定义事件,由页面决定怎么处理。
页面侧的使用方式:
<custom-card title="待办事项" count="{{ 3 }}" bind:send="onCardSend" />在页面的 JS 里写 onCardSend(event) 就能收到组件传来的 detail 对象。这种单向通信和事件回传的模型,能保证组件不依赖外层页面,拿到别的项目里也能复用。样式隔离默认是 isolated,也就是组件的 wxss 不会影响页面,页面也影响不到组件内部;如果确实需要外部控制组件内部节点样式,优先考虑 externalClasses,而不是去修改 styleIsolation 配置,后者容易造成样式失控。
3.4 rpx与尺寸适配:设计稿到不同机型的换算逻辑
写小程序样式最容易犯的错误,是把 Web 端的 px 习惯原样搬过来。小程序引入了一套以屏幕宽度为基准的单位 rpx:无论什么机型,屏幕宽度都是 750rpx。以 iPhone 6/7/8 为例,屏幕宽度是 375px,此时 1rpx = 0.5px;在屏幕更宽的机型上,1rpx 对应的 px 值会自动变大,从而实现等比缩放。
实际开发时,设计稿如果是 750 宽,直接照抄数值就是 rpx;如果是其他宽度,先做一次等比换算。文字大小、间距、圆角、边框都建议用 rpx,但有两类场景我一般保留 px:一是 1px 的物理细边框,rpx 在不同设备上可能出现 0.5px 到 1px 的折损,视觉上会忽细忽粗,用 px 更稳定;二是需要固定不随屏幕缩放的元素,比如用户头像的裁切区域。rpx 不是银弹,它的核心价值是让多数元素跟随屏幕等比缩放,极端大屏或横屏场景下,仍然需要配合 Media Query 做额外适配。
4. 请求、登录与云开发:小程序如何与后端打交道
跑通页面静态展示之后,下一步就是数据交互。小程序的网络请求不是简单的 wx.request 一把梭,它有一整套域名校验、登录态和本地缓存规则。踩过坑的人都知道,大多数"模拟器正常、真机挂掉"的现场都发生在这个环节。
4.1 封装wx.request:域名白名单、超时与统一错误处理
官方提供的 wx.request 是回调式的,直接在页面里调用会有两个层面的问题:一是每个页面都要重复写成功失败处理,代码冗余;二是通用逻辑比如 token 注入、401 跳登录、网络错误提示散落在各处,难以维护。我一般会在项目的 utils 目录下做一次 Promise 封装,统一收口:
// utils/request.js const BASE_URL = 'https://api.example.com' function request({ url, method = 'GET', data = {}, header = {} }) { return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${url}`, method, data, header: { 'content-type': 'application/json', 'Authorization': wx.getStorageSync('token') || '', ...header }, timeout: 10000, success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data) } else if (res.statusCode === 401) { wx.removeStorageSync('token') reject(res) } else { reject(res) } }, fail: (err) => { wx.showToast({ title: '网络异常,请稍后重试', icon: 'none' }) reject(err) } }) }) } module.exports = { request }这里有几个显式设置的参数值得注意。timeout 是请求超时时间,单位毫秒,默认值在不同基础库版本下表现不一致,显式设 10 秒能避免白屏等待太久;Authorization 从 storage 读取并自动注入,页面业务代码里不再感知 token 的存在;401 统一清除登录态,让所有业务请求在登录过期时行为一致。这样封装之后,页面里只需要关注业务成功分支和错误提示,网络细节被隔离在一个文件里。
域名白名单这块提前说明:小程序线上环境请求的 URL 必须事先在后台配置到 request 合法域名里,且域名必须是 HTTPS、已备案、不能是 IP 地址。开发时可以在工具里勾选"不校验合法域名"来跳过限制,但这个选项只对本地调试有效,真机预览、体验版、正式版都不会网开一面。我的习惯是项目一开始就把正式域名确定下来,开发阶段就按正式域名去联调,省得后面上线前临时改 BASE_URL 引发一堆环境问题。
4.2 登录态怎么建:wx.login、code换取openid的标准流程
小程序的登录不是简单的账号密码,它有一套官方定义好的 code 换 session 流程。整体逻辑是:前端调用 wx.login 拿到一个一次性 code,把 code 交给后端,后端用 code 向微信接口换取 openid 和 session_key,再自己生成业务 token 返回给前端。前端把 token 存进 storage,后续请求通过 header 携带。
前端部分的典型实现:
function login() { return new Promise((resolve, reject) => { wx.login({ success: (res) => { if (res.code) { request({ url: '/auth/login', method: 'POST', data: { code: res.code } }).then((data) => { wx.setStorageSync('token', data.token) wx.setStorageSync('userInfo', data.userInfo) resolve(data) }).catch(reject) } else { reject(new Error('获取code失败')) } }, fail: reject }) }) }code 的有效期是 5 分钟,且只能使用一次,换完即失效;openid 是用户在该小程序下的唯一标识,session_key 用于后续解密用户敏感信息。容易踩的坑是把 code 或者 session_key 直接存在前端,这没有意义,session_key 的解密操作应该在后端完成,前端拿到业务 token 就够了。
4.3 云开发模式:云函数与数据库权限的快速落地
如果团队没有现成的后端服务,微信云开发是当前最省事的方案。它把云函数、云数据库、云存储打包在一起,开发者在控制台创建环境后,前端可以直接调用云能力,不用自己操心服务器和鉴权。
前端侧启用云开发的初始化一般在 app.js 的 onLaunch 里完成:
wx.cloud.init({ env: 'your-env-id', traceUser: true })env 是创建环境时生成的环境 ID,一个小程序可以创建多个环境,建议区分开发环境和生产环境;traceUser 开启后可以在控制台看到请求来源的 openid,对排错很有帮助。
云函数是云开发的核心,常见用法是在后端节点里安全地获取 openid。下面是最小的云函数示例:
// cloudfunctions/getOpenid/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { const { OPENID } = cloud.getWXContext() return { openid: OPENID } }关键点是 cloud.DYNAMIC_CURRENT_ENV,它会让云函数始终运行在被调用方所在的环境,不需要硬编码环境 ID。云开发的数据库权限默认是"仅创建者可读写",适合存放个人数据;需要所有人都能读的场景(比如文章列表)要改成"所有用户可读,仅创建者可写",这个权限在控制台和数据权限配置里都能调整,上线前建议每条集合都过一遍权限,避免数据泄露。
云开发的优势是上手快、免运维,适合个人开发者快速交付;劣势是后端逻辑一旦复杂,云函数之间的编排和调试效率不如传统后端。我的建议是:工具型小程序、原型验证、个人项目优先用云开发;有明确复杂业务逻辑、需要对接多个第三方系统的项目,还是走自建后端更可控。
4.4 本地存储与全局状态:storage和globalData的分工
小程序管理全局数据有两个位置:wx.setStorageSync 对应的本地存储,和挂在 app.js 里的 globalData。新手很容易把这两者混着用,其实分工很明确。
本地存储适合需要持久化且下次启动还要用的数据,比如 token、用户设置项、草稿内容。它有几个边界:单个 key 的存储上限约为 1MB,整体存储上限各版本有差异,存大文件或长列表很容易顶到上限;wx.setStorageSync 是同步 API,频繁调用会阻塞主线程,批量写入时用 wx.setStorage 的异步版本更稳妥。
globalData 则是纯内存态,适合存启动时拉取、运行中频繁读取的临时状态,比如当前登录用户资料、全局配置、页面间要传递的引用类型数据。它最大的特点是应用退出即清空,不需要考虑持久化和过期问题。实际项目中数据应该放哪,我按三个问题判断:下次启动还需要吗?需要就放 storage;其他页面会频繁读取吗?需要就放 globalData;只在这个页面内部用吗?那就放 Page 的 data 里。
5. 避坑指南与排查:编译通过只是起点,真机才是真相
小程序开发有个特点:编辑器里一切正常,不代表真机上没事。这一章列出的问题,几乎每一位做小程序的人都遇到过,每条都按"现象、原因、解决"展开,可以直接对照排查。
5.1 页面栈卡死:navigateTo上限与路由跳转的正确选择
现象:页面连续跳转五六次之后,再点跳转按钮没有反应,也没有任何报错。
原因:wx.navigateTo 会不断往页面栈里压入新页面,而页面栈上限是 10 层,超过后 navigateTo 直接静默失败。这个错误既不会抛异常,也不会打印日志,非常容易让人误以为是跳转事件没绑定。
解决:跳转前先判断当前页面栈深度,并区分业务场景选择路由 API。常规详情页用 wx.navigateTo 没问题;列表到详情这类会持续叠加的场景,用 wx.redirectTo 替换当前页,减少栈压力;底部导航页面必须用 wx.switchTab。保险起见,可以把判断封装成一个公共方法,全局统一走这个入口。
function navigateTo(url) { const pages = getCurrentPages() if (pages.length >= 10) { wx.redirectTo({ url }) } else { wx.navigateTo({ url }) } }另一个相关的坑是:wx.navigateBack 返回的层数是以存在的页面栈为基准的,如果上一页已经被 redirectTo 替换掉了,返回时会直接退出小程序或回到首页,这属于预期行为,但新手往往误以为是 bug。
5.2 图片不显示与接口被拒:合法域名配置的前后差异
现象:模拟器里图片正常加载、接口正常返回,一到真机预览或体验版,图片全部裂开,request 请求直接 fail。
原因:开发工具设置了"不校验合法域名",本地模拟器跳过域名校验;真机环境强制校验,图片域名必须加入 downloadFile 合法域名,接口域名必须加入 request 合法域名。如果域名没备案、不是 HTTPS、或者干脆是 IP 地址,都会被拒绝。
解决:在微信公众平台的开发管理里配置合法域名,图片的 CDN 域名和接口域名分开配;配置后不会立即生效,通常需要等几分钟或重新编译。开发阶段的本地联调,我一般用电脑的局域网 IP 加端口的地址,并且在工具详情里勾上"不校验合法域名";但正式项目从第一天起就建议用正式域名,否则上线前临时换 BASE_URL,所有页面环境和缓存都要重新验证一遍。
5.3 setData引发的样式与性能问题
现象:列表数据量一大,页面滚动开始掉帧;或者频繁刷新某个区域,页面出现白块、闪烁;还有一类是视图始终显示旧数据,setData 调用了却没反应。
原因:前两类是 setData 数据量过大或频率过高,小程序每次 setData 都要把数据从逻辑层传到渲染层,传大对象、在滚动回调里高频调用都是性能杀手。第三类通常是因为直接修改了 this.data 的某个值,再 setData 同一字段导致 diff 对比失败,或者更新路径写错了层级。
解决:数据更新务必用路径形式,能更新局部就不要整块替换;列表渲染加上 wx:key 让 diff 更精准;高频触发的回调里加节流或防抖,把多个中间态合并成一次 setData。这里给一个通用检查顺序:先看控制台有没有 setData 相关告警,再看更新路径和实际数据结构是否完全一致,最后把大对象拆成小字段分别更新。
// 高频滚动场景下的节流写法 let timer = null function onScrollToLower() { if (timer) return timer = setTimeout(() => { timer = null this.loadMore() }, 300) }5.4 真机和模拟器的差异:常见三类翻车现场
现象一:iPhone 上页面底部内容被一条横条遮挡,按钮够不到。这是 iOS 全面屏设备的安全区问题,解决方案是给底部元素留出安全区 padding,使用 env(safe-area-inset-bottom)。
现象二:安卓手机上某些圆角、阴影效果直接失效,或者文字被截断。原因通常是 mix-blend-mode、backdrop-filter 这类属性在部分 WebView 内核上不支持。解决:复杂视觉效果用图片代替 CSS 实现,基础布局全部用 flex 完成。
现象三:真机上字体比模拟器明显偏大或偏小。原因是在某些手机上系统字体缩放设置被 WebView 继承,rpx 布局受影响的概率较低,但 px 标注的文字会明显变化。解决:全局样式里对 text 标签做一个字体大小规范化,并避免在文字节点上使用固定 px 拼合布局。
真机调试的第一原则:所有疑似环境问题,先用"真机调试"功能跑一次,再决定是不是代码问题。"真机调试"能看清网络请求、控制台日志和页面渲染状态,比反复在模拟器里猜要高效得多。
6. 进阶:用behavior抽公共逻辑,让登录校验与空状态可以被复用
开发到第 N 个项目时一定会遇到重复劳动:每个需要登录的页面都写一遍"检查 token、不存在就跳登录页",每个列表页都写一遍"空数据时展示空状态"。把这些逻辑抽成 behavior,是让小程序项目从"能用"走向"好维护"的关键一步。
behavior 是官方提供的代码复用机制,作用和前端框架里的 mixin 相近。它可以把 data、methods、生命周期函数合并进 Page 或 Component。下面是一个登录校验 behavior 的典型写法:
// behaviors/auth-check.js module.exports = Behavior({ data: { isLoggedIn: false }, lifetimes: { attached() { const token = wx.getStorageSync('token') this.setData({ isLoggedIn: !!token }) if (!token) { this.checkLogin() } } }, methods: { checkLogin() { wx.navigateTo({ url: '/pages/login/login' }) }, logout() { wx.removeStorageSync('token') this.setData({ isLoggedIn: false }) } } })注意这里用的是 lifetimes.attached,它是组件和页面在实例被创建并挂载后都会触发的生命周期,比 Page.onLoad 更通用,因为它同时兼容 Page 和 Component。页面里引用这段逻辑只需要一行:
const authCheck = require('../../behaviors/auth-check') Page({ behaviors: [authCheck], onLoad() { if (this.data.isLoggedIn) { this.fetchData() } } })加了该 behavior 的页面自动获得 isLoggedIn 状态、跳登录方法、退出登录方法,不需要每个页面重复写。类似可以抽的还有:列表页通用的下拉刷新、触底加载分页逻辑,表单页的校验统一处理,埋点上报等。
我自己的一个教训是,第一版工具小程序里每个页面都手写了一遍 token 检查,第二版改动登录逻辑时要同时改十几个文件,差点改漏。后来统一收敛到 behavior 里,改动就压缩成了一个文件。工具型小程序虽然功能简单,但公共逻辑的收敛越早做越好,不要等项目膨胀后再回头重构。这个方向入门快、落地路径清晰,值得动手做,希望帮到你。
本文还有配套的精品资源,点击获取