先说一个我带新人时反复遇到的场景同学兴冲冲地跑来跟我说学会了微信小程序打开他写的项目一看用的是 uni-app连原生项目的 app.json 都没摸过。不是说框架不好但如果你连原生小程序的页面结构、全局配置、事件绑定都没跑通过一旦遇到问题你根本分不清到底是框架的锅还是平台的锅。所以这篇入门篇我打算用最朴素的方式带你走一遍从注册小程序账号开始在微信开发者工具里从零写一个能交互、能请求接口、能在手机上打开的 Demo途中把所有新手必踩的坑——导航栏高度、单选组件、真机联调、登录换 code——全部拆开讲清楚。文章默认你会一点 HTML、CSS、JavaScript 基础语法但我会尽量做到代码全贴、步骤全给就算照着抄也能跑通。1. 上手的最小闭环从注册账号到开发者工具里跑起第一个页面很多教程默认你已经有了小程序账号其实这一步最容易被卡住。我见过有人折腾了一下午最后发现连开发者工具都装错了版本。这里把整个流程捋一遍你照着做就行。1.1 注册账号时最容易忽略的主体类型问题打开微信公众平台选小程序注册。注册的时候会让你选主体类型个人、企业、组织等。这里有一个非常重要的区别个人主体的小程序无法使用很多开放接口比如获取手机号、微信支付等。如果你未来要做电商、会员系统建议直接注册企业主体或者先用个人主体练手但心里要清楚这个边界。另外企业主体通常需要缴纳微信认证费用每年一次具体金额以微信官方为准。个人主体不需要交认证费但功能受限。我自己的建议是学习阶段用个人主体正式上线前再换成企业主体或者重新注册一个企业主体账号别在一棵树上纠结太久。还有一个容易忽略的点每个身份证和手机号能绑定的小程序管理员数量是有限的注册时不要频繁更换管理员否则会被限制。我踩过这个坑注册到第三个测试号的时候提示操作频繁等了好几天才恢复。1.2 安装开发者工具与创建项目AppID 到底怎么填下载微信开发者工具的时候注意选稳定版不要追新。新版本有时候会有一些还没修复的 bug稳定版对新手的友好程度高很多。安装完成后打开工具扫码登录然后创建项目。这个环节新手最容易困惑的就是 AppID 的填写用测试号工具上有测试号选项会自动生成一个临时 AppID适合纯学习不用注册账号也能跑起来但很多能力受限。用自己注册的 AppID在公众平台的设置-开发设置里可以看到这个才是正路。填自己的 AppID预览、真机调试、上传代码都需要它。我建议你一定用自己的 AppID 创建项目因为后面涉及真机调试和接口请求测试号会给你带来额外的麻烦。创建项目的时候模板选JavaScript或基础模板都可以。不要选云开发模板虽然它很方便但那是另一套体系入门阶段先搞懂本地代码再碰云开发。1.3 第一个页面把Hello World拆成四个文件看当你创建完项目左侧目录栏里会自动生成一个pages/index目录。微信小程序一个页面由四个文件组成这是最基础但也最核心的知识点index.js页面逻辑负责数据、事件处理index.wxml页面结构类似 HTMLindex.wxss页面样式类似 CSSindex.json页面配置比如标题栏文字、导航栏样式四个文件必须保持同名这点和网页开发不一样。你改文件名的时候一定要四个一起改否则会报错module is not defined之类的诡异问题。我第一次改页面名的时候只改了.js和.wxml两个文件结果页面怎么也加载不出来就是这个原因。index.js里你会看到这样的结构Page({ data: { msg: Hello World } })data就是页面数据WXML 里用双花括号引用view{{ msg }}/view。这个模型贯穿小程序整个生命周期后面所有交互都建立在数据驱动的基础上。2. 全局配置与导航栏适配先搞懂 app.json再处理顶部那个胶囊跑起了 Hello World 之后你要做的第一件事不是急着写页面而是把项目的骨架摸透。小程序不像网页页面不是一个 URL 对应一个 HTML 文件而是由app.json这个全局配置文件统一管理的。2.1 app.json 的页面注册顺序不是你想的那个意思app.json里有一个pages数组第一项是启动页面。很多人以为这个数组的顺序无关紧要其实它决定了两个东西启动页是哪个以及页面文件的查找顺序。{ pages: [ pages/index/index, pages/logs/logs ], window: { navigationBarTitleText: 我的小程序, navigationBarBackgroundColor: #ffffff } }这里pages/index/index就是启动页。如果你新增一个页面需要手动把路径加到这个数组里开发者工具不会自动帮你注册。这是新手最高频的报错page is not found十有八九就是这个数组里忘了加路径。你还会发现每个页面自己的.json文件可以覆盖app.json里的window配置这就是页面级配置优先于全局配置的规则。比如某个页面想要红色导航栏单独在页面index.json里写navigationBarBackgroundColor: #ff0000就行。2.2 tabBar、window 与页面级配置的优先级app.json里还有一个经常被问到的配置项tabBar。如果你的小程序底部需要切换栏就在这里配置{ tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/logs/logs, text: 日志 } ] } }这里有个很容易踩的坑tabBar 里引用的页面路径必须在 pages 数组里也已经注册而且 tabBar 页面不能使用自定义导航栏。我记得有一次改了 tabBar 之后整个页面白屏排查了半天发现是路径大小写对不上。关于优先级我的理解是app.json的全局配置是最低优先级页面级json可以覆盖而某些特殊配置比如navigationStyle设为custom可以隐藏导航栏只能通过页面级或全局级同时设置才生效。这个优先级关系建议你现在就记住后面做主题切换、分端适配的时候会频繁用到。2.3 自定义导航栏为什么默认导航栏在不同机型上高度不一致热搜里有人搜微信小程序顶部导航栏高度这个我太有发言权了。默认导航栏在不同手机上高度不一样iPhone 的刘海屏和安卓的挖孔屏差异很大如果你做的是自定义导航栏稍不注意标题就会顶到状态栏或者和胶囊按钮重叠。自定导航栏的标准做法是在页面json里设置navigationStyle: custom然后自己在 WXML 里写一个导航栏 view最上面留出状态栏高度。状态栏高度获取方式const { statusBarHeight } wx.getWindowInfo() const menu wx.getMenuButtonBoundingClientRect()statusBarHeight是手机顶部状态栏高度menu是右上角胶囊按钮的位置。导航栏的高度通常用这个公式计算const navHeight (menu.top - statusBarHeight) * 2 menu.height这个公式的原理是胶囊按钮在导航栏里垂直居中所以menu.top - statusBarHeight等于胶囊上方到导航栏顶部的距离乘以 2 再加上胶囊本身高度就是整个导航栏高度。你不用管它为什么是这个公式直接用就行非常稳定。我第一次做自定义导航栏的时候把menu.top当成了导航栏高度结果 iPhone 上标题偏上、安卓上偏下调了很久才发现问题。记住永远不要硬编码导航栏高度一定要动态计算。3. 用三件套写一个可交互页面数据绑定、事件与表单组件Hello World 只能证明环境没问题真正让你感觉原来小程序是这样写的是从第一个带交互的页面开始。这一章我们用原生语法写一个简单的今日计划页面把数据绑定、事件绑定、单选框组全部串起来。3.1 从写死到动态data、setData 与 {{ }} 渲染链路小程序的数据驱动模型和 Vue 有些像但有本质区别它不是自动响应式的你必须用setData去更新数据页面才会重新渲染。Page({ data: { name: 张三, planCount: 0 }, addPlan() { this.setData({ planCount: this.data.planCount 1 }) } })WXML 里对应的结构view{{ name }}/view view今日计划数{{ planCount }}/view button bindtapaddPlan新增计划/button注意这里bindtap就是绑定点击事件事件处理函数名直接写在属性里不加括号。点击按钮后setData会触发页面重新渲染planCount立即更新。这里我要重点强调一个性能习惯setData是异步渲染的频繁调用会造成性能问题。尽量把多个数据变更合并成一次setData调用不要在一个事件里连续写好几个this.setData。这个习惯在页面数据量大的时候效果非常明显。3.2 单选框组的正确用法radio-group 与 event.detail.value热搜里有人搜微信小程序单选框这个问题出现频率极高因为小程序的信号输入组件和 HTML 的表单标签用法差异很大。WXML 里没有原生的select单选框必须用radio-group包一层再用radio做子项。radio-group bindchangeonRoleChange label wx:for{{roles}} wx:keyvalue radio value{{item.value}} checked{{item.checked}} / text{{item.name}}/text /label /radio-group对应的 JS 逻辑Page({ data: { roles: [ { name: 前端开发, value: frontend, checked: true }, { name: 后端开发, value: backend, checked: false }, { name: 全栈开发, value: fullstack, checked: false } ], currentRole: frontend }, onRoleChange(e) { const role e.detail.value this.setData({ currentRole: role }) } })这里有两个重点radio-group的bindchange事件回调参数是e.detail.value拿到的是被选中 radio 的value值不是索引。label标签包裹radio和文本之后用户点击文本也能选中这个体验很重要。如果不写label用户只能精准点到那个小圆点移动端上体验很差。我还见过不少人在wx:for里给每个radio单独绑定bindtap然后自己判断选中状态这完全是绕远路。radio-group和bindchange就是用来干这个的事件冒泡机制会帮你把选中状态统一汇报上来。3.3 一个完整的今日计划小页面把上述知识串起来下面贴一个完整的入门 Demo把数据绑定、事件、单选框、列表渲染合在一起。你可以直接在开发者工具里新建一个页面然后把代码替换进去。plan.wxmlview classcontainer view classtitle今日计划/view view classrole-select text计划类型/text radio-group bindchangeonTypeChange label wx:for{{types}} wx:keyvalue radio value{{item.value}} checked{{item.checked}} / text{{item.name}}/text /label /radio-group /view view classplan-list view wx:for{{plans}} wx:keyid classplan-item text{{item.content}}/text text classtime{{item.time}}/text /view /view button bindtapaddPlan添加一条计划/button /viewplan.jsPage({ data: { types: [ { name: 工作, value: work, checked: true }, { name: 学习, value: study, checked: false } ], plans: [ { id: 1, content: 完成小程序入门实战, time: 09:00 } ] }, onTypeChange(e) { console.log(当前类型, e.detail.value) }, addPlan() { const id this.data.plans.length 1 const newPlan { id, content: 新增计划 id, time: 10:00 } this.setData({ plans: this.data.plans.concat(newPlan) }) } })plan.wxss和布局样式你可以先随便写重点是把逻辑跑通。这个页面麻雀虽小五脏俱全它有数据初始渲染、事件绑定、动态追加数组、一组单选框够你把基础语法练熟了。这里我想额外提醒一个新手容易犯的错wx:for循环里一定要写wx:key。如果你不写小程序会警告你。它的作用是让框架在列表更新时能高效地复用节点而不是全部重新渲染。列表数据如果固定不变你可能感觉不到差别但一旦涉及到删除、排序不写wx:key会出现各种莫名其妙的渲染错乱。4. 首次遇到接口与真机联调域名白名单、wx.request 和登录链路本地页面写得再漂亮不连接口的小程序也只是个壳。这一章讲的是从开发环境请求成功到真机上也能请求成功这段路也是热搜里那个高频问题微信小程序开发工具接口访问正常 真机接口访问失败。我直接告诉你答案同时把背后的机制讲透。4.1 为什么开发者工具里请求成功、真机上却一片空白这个问题的标准答案开发者工具默认勾选了不校验合法域名选项所以你随便请求一个 http 接口甚至本机接口都能通。但真机上没有这个选项小程序会强制校验请求地址必须是 HTTPS域名必须在微信公众平台后台配置为request合法域名域名必须有备案所以你的开发工具能通真机不通百分之九十九是这三个原因之一。处理顺序建议是打开公众平台后台进入开发管理-开发设置-服务器域名把接口域名加到request合法域名里。确认接口是 HTTPS证书有效域名已备案。如果只是临时调试本机接口可以在开发者工具详情里勾选不校验合法域名但只能用于本地开发体验版和正式版不会生效。我见过有人卡在这个问题上整整两天最后发现是接口域名带了端口号而小程序wx.request的合法域名是不允许带端口的。这一点特别容易忽略你在后台配置域名时千万别带端口。4.2 登录链路最小的理解wx.login 拿 code 只是第一步很多教程会教你在前端直接调wx.login然后把code拿去换 openid。但如果你把整个登录链路理解成前端调一个接口后面一定会出问题。wx.login的作用是生成一个临时登录凭证code这个 code 的有效期只有五分钟而且只能用一次。你把 code 发给自己的后端后端再用 code 到微信的code2Session接口换取openid和session_key。openid 是用户在小程序里的唯一标识session_key 是加密用户数据时用的密钥不能下发到前端。最小化实现的思路是这样wx.login({ success(res) { if (res.code) { // 把 code 发给自己的后端 wx.request({ url: https://your-backend.com/login, method: POST, data: { code: res.code }, success(response) { // 后端返回自定义登录态比如 token const token response.data.token wx.setStorageSync(token, token) } }) } } })后端拿到 code 之后用自己的appid和appsecret去调微信接口换 openid然后生成自己的会话标识返回给前端。注意appsecret 绝对不能出现在小程序代码里它一旦泄露别人就可以冒充你的小程序操作数据。这也是为什么登录必须要有后端参与而不是前端直接调微信接口。新手最容易在这里踩的坑是把appsecret写在前端代码里然后发现code2Session接口永远调不通。这不是微信接口问题而是 secret 泄露后的安全限制。4.3 获取手机号的按钮没反应先搞懂 open-type 与接口能力热搜里有一个微信小程序登录获取手机号这个功能现在和几年前完全不同了。以前直接调wx.getPhoneNumber就能拿到明文手机号现在出于隐私保护微信改成了通过button组件的open-type来触发授权button open-typegetPhoneNumber bindgetphonenumberonGetPhoneNumber获取手机号/buttononGetPhoneNumber(e) { if (e.detail.code) { // 把 e.detail.code 发给后端 // 后端用 code 换取手机号 } }这里有三点新手必知不是所有小程序都能用这个能力。获取手机号需要申请对应接口权限个人主体基本无望企业主体也需要完成认证。返回的不是明文手机号而是 code。你需要把e.detail.code传给后端由后端调微信接口换取手机号。这个 code 和wx.login的 code 不是同一个东西但设计思路类似。如果用户点击没反应先看平台版本。老版本基础库对这个接口支持不完整建议基础库版本调到最新再试。我给你的建议是入门阶段别在这个功能上花太多时间先把wx.login和自定义登录态跑通手机号获取等你的项目真正需要商用资质的时候再研究否则很容易被权限卡住白费热情。5. 从能跑到还算像个工程目录规范、代码自查与性能习惯最后一个章节聊的是进阶意识。很多人把 Demo 跑通了就觉得自己会了但如果你打算把这个项目继续做下去甚至提交审核上线下面这些经验能帮你少走很多弯路。5.1 目录结构与组件化的分界线在哪里小程序的目录结构表面是自由的但工程上必须有规矩。我带团队时一般约定这样一个结构miniprogram/ pages/ 页面目录 components/ 公共组件 utils/ 工具函数 styles/ 公共样式 assets/ 静态资源页面内部按业务拆文件pages/order/list/这种三层结构比pages/orderList/清晰得多。组件单独放components/下组件内部也是四件套。有个新手常犯的错误把页面拆得特别碎每个小组件都做成一个页面结果app.json的pages数组越来越长维护成本直线上升。页面和组件的分界线很简单页面有独立路由、需要被wx.navigateTo跳转的就是页面只在其他页面/组件内复用的就是组件。组件用Component({})构造器页面用Page({})构造器这两个不要混用。5.2 检查代码规范ESLint 之外的几个土办法热搜里有检查代码规范说明大家还是在乎工程质量的。微信开发者工具本身有格式化功能但真正的代码规范建议引入 ESLint。不过对入门读者我有几个更接地气的土办法保持 setData 数据扁平。嵌套太深的数据结构会让 WXML 渲染和调试变得痛苦。每个事件处理函数只做一件事。如果一个bindtap处理函数里有超过十个this.setData大概率需要拆函数了。定期全局搜索console.log。上线前把没用的日志删掉一是避免泄漏数据二是排查问题的时候日志太多会干扰判断。给页面和组件起名用有意义的英文不要用test1、aaa这种临时名字。我见过一个项目里留着十几个abcd页面等业务迭代的时候谁都不知道这是啥。这些习惯不花钱不耗时但能让你的代码在三个月后回看时还能看懂。5.3 检视性能的三个最容易上手的习惯最后聊聊性能。不要一上来就追求极致的渲染性能先把这三个习惯养成大部分性能问题都能规避。第一控制 setData 的频率和数据量。前面提过合并setData的事这里再强调一次每次setData都会触发一次渲染流水线如果你一秒内调了十次性能一定很难看。能合并的合并能不传到 WXML 的数据就别放 data。第二图片资源要处理。小程序包是有体积限制的主包 2M、整个小程序 20M 这些限制老生常谈具体以官方最新文档为准设计师给的图直接扔进去包体积瞬间超标。过大的图片先用工具压缩能放 CDN 的放 CDN能懒加载就用懒加载。第三熟悉使用开发者工具的性能面板。工具面板里的 Network 和 Performance 是你最直接的体检报告不需要背指标只需要看两点页面加载时请求瀑布流是否太长主包是否过大。这两个点解决了你的小程序体验基本就过关了。5.4 我踩了三次才明白的教训把预览二维码发给真实手机最后分享一个我自己的习惯。跑通代码之后不要只在开发者工具的模拟器里自我感动。把预览生成的二维码用微信扫一扫真机点一遍你会发现大量模拟器里发现不了的问题。模拟器里永远岁月静好真机上可能导航栏高度不对、字体大小不对、请求直接失败。我踩过三次每次都是因为偷懒没用真机验证最后被用户反馈打脸。这个习惯建议你从入门阶段就建立每次写完一个功能模块都主动扫一次真机预览把真机能跑当成完成的标准而不是模拟器能跑。我把这个 Demo 的完整代码放到了项目里你可以直接对照着敲一遍。下一步可以进阶的方向是分包加载、自定义组件通信、组件库引入这些我会放在进阶篇里展开。先把今天这篇吃透你的小程序入门第一课就过关了。