微信小程序原生开发这件事,网上的教程多得像米一样,但大部分要么只讲到“能跑就行”,要么直接把你往框架上带。作为一个从page函数写起、从 WXML 一路啃过来的开发者,我这次把基础阶段真正绕不开的点整理成一份超详细指南。它不教你背 API,而是告诉你每个知识点背后的逻辑、常见的坑、以及哪些东西你现在必须死磕,哪些可以等以后再说。你如果正准备从零开始,或者刚写完几个页面但总觉得哪里没打通,这篇文章比你自己刷两个月文档管用。核心关键词,就是原生、从零、基础、可落地。
1. 项目概述与开发准备
1.1 为什么选择原生开发
现在提到小程序,很多人第一反应是 uni-app 或者 Taro 这类跨端框架。但我的看法一直没变:如果你想把基础打牢,原生开发是绕不开的第一步。框架解决的是“多端复用”和“工程化”问题,但底层那些机制——比如页面渲染怎么运作、逻辑层和视图层怎么通信、组件属性怎么传递——框架并不会替你想明白。
原生开发最大的优势是路径最短。你不用先理解虚拟 DOM、编译器、热更新这些概念,只需要打开工具、写代码、看效果,反馈链路短,出错也容易排查。而且很多框架的 API 风格,其实都默默向原生靠拢。你如果原生语法熟练,切到任何一套框架都只是“换个写法”的问题,反之如果一上来就被框架怀抱裹住,遇到诡异问题连该查哪一层都分不清。
所以如果你是第一次接触小程序,建议原原本本从原生开始,至少把生命周期、WXML 数据绑定、WXSS 响应式单位、组件通信这几样摸透,后续不管做什么都顺畅。
1.2 开发环境的安装与初始化
第一步,建议直接去微信公众平台注册一个账号。开发者类型选“个人”完全够用,注册过程中需要扫一个二维码验证,这部分照着流程走就行。注册完成之后,进入后台的“开发管理”,你会看到一个叫 AppID 的东西,这就是你项目的身份标识。
然后在电脑上安装“微信开发者工具”,注意选稳定版就行。安装完使用微信扫码登录,登录后选择“小程序项目”,把刚刚拿到的 AppID 粘贴进去,项目的目录随便找个空文件夹。前端框架那里要选“不使用框架”——这一步很关键,因为工具默认可能会推荐模板,但我们是原生学习,直接选“不使用框架”,工具会生成一个最基础的开发模板。
模板生成之后,你会看到左侧是模拟器,中间是代码编辑区,右侧是调试器。首次打开可能会提示你更新基础库版本,直接选最新稳定版即可。
1.3 项目目录结构到底在讲什么
打开生成的模板,你会看到几个明显的文件。根目录下的app.js、app.json、app.wxss这三个是小程序的全局文件,每个小程序必须有这三个。app.json负责全局配置,比如页面路径、窗口外观、底部 tabBar;app.js负责应用逻辑入口;app.wxss是全局样式表。
然后你会看到pages文件夹,里面每一个子文件夹代表一个页面。每个页面文件夹里通常有四个同名的文件:.js、.wxml、.wxss、.json。其中.json是这个页面的局部配置,.wxml是页面结构(相当于 HTML),.wxss是页面样式(相当于 CSS),.js是页面逻辑(相当于 JavaScript)。
还有一个容易忽略的文件叫project.config.json,它是项目级别的配置文件,记着它存在的意义就好,日常开发几乎不需要手动改它。sitemap.json是关于索引的配置,刚开始也可以不管。
你发现了吗?这个地方的设计思路其实跟 Web 开发很像,但又有几个明显的差异:WXML 不是 HTML,WXSS 不是 CSS,而且每个页面文件之间天然独立。带着这种“相似但不完全一样”的意识去学,会少走很多弯路。
2. 开发环境的配置与全局逻辑
2.1 app.json 全局配置的细节
app.json是小程序的“总控室”,里面最基本的字段是pages,它是一个数组,第一项就是小程序启动后默认展示的页面。每当你新建一个页面,都要手动把对应路径加到这个数组里。很多新手页面白屏或者跳转失败,八成是忘了这个操作。
下面是一个比较典型的配置示例:
{ "pages": [ "pages/index/index", "pages/logs/logs", "pages/detail/detail" ], "window": { "navigationBarBackgroundColor": "#ffffff", "navigationBarTitleText": "我的小程序", "navigationBarTextStyle": "black" }, "tabBar": { "color": "#999999", "selectedColor": "#1296db", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/logs/logs", "text": "记录" } ] } }注意,tabBar的list至少要有两项,否则会报错。如果你不需要底部导航,这个字段就不用写。navigationBarTitleText显示的是顶部标题栏的文字,默认的话每个页面会展示你在该页面.json里定义的标题,优先级是“页面配置高于全局配置”。
window里的navigationBarBackgroundColor只支持十六进制颜色值。另外还有一个字段叫backgroundColor,它是窗口背景色,不是导航栏背景色,很多人会搞混。
把pages和window这两个字段理解透,app.json就算过关了。
2.2 App 实例与全局生命周期
每个小程序都有且仅有一个App实例,定义在app.js里。你可以在onLaunch里做一些初始化工作,比如获取系统信息、检查登录状态。下面是一段标准代码:
App({ onLaunch: function () { const info = wx.getSystemInfoSync() console.log('小程序启动成功,窗口宽度为:', info.windowWidth) }, globalData: { userInfo: null, token: '' } })globalData是一个存储全局数据的对象,你可以用getApp().globalData在任何页面里访问它。需要提醒的是,这里的数据并不是响应式的,如果你在页面里通过getApp()拿到了globalData,然后修改它的值,页面上的数据不会自动更新。这一点跟页面内data的行为完全不同,新手经常在这里踩坑。
关于生命周期真的要记牢:onLaunch是小程序初始化完成时触发的,整个过程只会在首次打开时执行一次。如果小程序被切到后台再重新打开,并不会重新触发onLaunch,onShow才会再次触发。这是一个很基础但极其关键的机制。
2.3 页面的生命周期函数
每个页面都有一组自己的生命周期函数,它们在页面加载、显示、隐藏、卸载的不同阶段被调用。以下是初学者最常用的几个:
Page({ data: { title: '首页' }, onLoad: function (options) { // 页面加载时触发,适合做数据初始化 }, onShow: function () { // 页面显示时触发,适合刷新状态 }, onReady: function () { // 页面首次渲染完成,适合开始交互 }, onHide: function () { // 页面隐藏时触发,比如跳转到了别的页面 }, onUnload: function () { // 页面卸载时触发,比如关闭当前页 } })需要注意几点。第一,onLoad里的options参数来自页面跳转时携带的 query 参数。比如从列表页跳转到详情页,通常这里会拿到一个 id。第二,onReady触发时并不意味着页面 UI 已经准备好渲染完,它只是表示首屏渲染已完成,可以开始使用节点相关的交互。第三,页面每次显示都会触发onShow,包括从后台切回小程序,所以“每次都要刷新数据”的逻辑记得放这里,而onLoad只在首次进入时触发一次。
接触这些生命周期,最大的意义是你知道什么逻辑该放哪里,而不是一股脑写在页面顶层。
3. 核心知识点与基础写法
3.1 WXML 与 HTML 的本质区别
如果你会 HTML,WXML 不难学,但有三个显著的差异,建议在心里反复默念。第一个差异是标签体系更收敛。小程序里没有那么多乱七八糟的 HTML5 标签,核心就是view、text、image、button、input、scroll-view、swiper这些,其中view相当于div,text相当于span。第二个差异是数据绑定,这个待会儿重点讲。第三个差异是事件绑定,WXML 里清清楚楚地使用bindtap这种写法,没有事件流冒泡、捕获那一堆复杂概念。
下面是一段典型的 WXML 片段:
<view class="container"> <text class="title">{{title}}</text> <text class="subtitle">{{subtitle}}</text> <image src="{{imgUrl}}" mode="aspectFill"></image> <button bindtap="onButtonTap">点击我</button> </view>注意这里所有变量都用双大括号包裹,但双大括号不是模板引擎里的那种表达式语言,它只是数据绑定的标记。你可以在里面做简单的运算,比如{{a + b}},但不要指望它能执行复杂逻辑。
<image>组件的mode属性是很有用的,aspectFill代表等比缩放并裁剪填满整个 image 区域,aspectFit代表等比缩放并完整展示图片。这个属性不设置的话,图片默认会以拉伸的方式填满宽高,变形得很明显。
3.2 数据绑定与 setData 的机制
数据绑定是 WXML 最强大的部分。页面.js文件里的data对象,可以被 WXML 直接引用。但你一旦尝试修改数据,就不能直接给this.data赋值,必须使用this.setData()。为什么?
因为setData是逻辑层向视图层发送数据更新的唯一通道。这个过程背后会把数据通过桥接层传输过去,而且会有一定的开销。所以频繁地调用setData其实是个坏习惯,尤其是一些高频操作,比如拖动滑块时每帧都在更新,会造成明显的卡顿。
另一个常见的错误写法是直接this.data.name = 'xxx',这种写法虽然能改变 JavaScript 对象里的值,但页面根本不会刷新。正确的写法如下:
Page({ data: { count: 0 }, onLoad() { setTimeout(() => { this.setData({ count: this.data.count + 1 }) }, 1000) } })setData的第二个参数是回调函数,它在本次数据更新并渲染完成后触发:
this.setData({ count: 2 }, () => { console.log('数据更新已完成,页面已自动刷新') })这个回调在处理一些需要 UI 完成后才进行的操作时,非常有用。
3.3 条件渲染与列表渲染的用法
小程序里通过wx:if和wx:for实现条件与列表渲染。先看条件渲染:
<view wx:if="{{isLogin}}">欢迎回来</view> <view wx:else>请先登录</view>需要注意,wx:if里一定要写表达式,不要只写一个字符串"true"或者"false",否则会当成字符串常量处理。另外wx:elif也可以连续使用,但必须紧跟前面的wx:if块,中间不能插入其他节点。
再看列表渲染:
<view wx:for="{{list}}" wx:key="id"> <text>{{item.name}}</text> </view>这里的item是默认的循环变量名,你也可以用wx:for-item改成其他名字,index也可以用wx:for-index修改。wx:key的设计初衷是帮助框架识别列表项的身份,避免整个列表重新渲染。它的值有两种写法:一种是字符串,表示列表项对象里某个属性的名称;另一种是保留关键字*this,表示列表项本身是一个唯一的字符串或数字。如果你忽略了wx:key,列表更新时可能会出现顺序错乱或者渲染效率低下的问题。
3.4 常见事件类别与参数传递
小程序里的事件绑定的核心思路是:WXML 上写bind事件名="处理函数",然后在页面逻辑里定义这个处理函数。下面是一个带参传递的典型案例:
<view bindtap="onItemTap">Page({ onItemTap: function (e) { const id = e.currentTarget.dataset.id const name = e.currentTarget.dataset.name console.log('点击的 id 是:', id, '名字是:', name) } })这里要特别强调一个新手几乎必犯的错误:不要在><input value="{{inputValue}}" bindinput="onInput" placeholder="请输入内容" /> 这种“受控”的做法保证了你随时可以对输入内容做格式化,比如限制输入长度、过滤非法字符等。如果你只在 页面跳转有 代码示例: 跳转时注意在目标页面的 最经典的一个坑是: 小程序的网络请求都用 下面是一个完整的请求与渲染示例: 需要注意, 顺便提一个请求封装的小思路:实际项目里你不可能在每一个页面都写一遍 小程序提供了一套本地存储 API,同步版本是 存储的内容会自动序列化。也就是说你可以直接存对象,不需要手动 JSON.stringify。这里要提醒的是,本地存储有大小限制,大概在 10MB 左右,不适合放大量图片或视频数据。另外它存储的 key 是全局共享的,不同页面之间可以通过它通信。 存储操作里最容易出现的坑是:读取一个不存在的 key,同步版本会返回空字符串,异步版本走的是 开发者工具里最有价值的东西不仅仅是模拟器,我平时最依赖的三个功能是 Console、Sources 和 Network。 Console 就不用多说了,所有 Sources 面板可以看到编译后的代码,也可以断点调试。你选中左边文件列表里的某个 Network 面板里能看到小程序的请求耗时、返回数据、域名情况。当页面的数据没有渲染出来时,先看这里到底有没有发出请求、请求返回了什么内容,能帮你快速定位是数据问题还是渲染问题。 模拟器和真机之间有差异,这个在任何端上开发都会遇到。小程序尤其明显。首先是底部安全区。iPhone X 之后那些带刘海的机型有底部横条,普通的 然后是域名校验。真机预览时, 最后是真机调试的 console。用真机预览模式看不了 console,你需要使用“真机调试”模式,它会在开发者工具里弹出一个远程调试控制台,这样真机上的日志才能真正显示出来。 代码写得差不多之后,点击工具右上角的“上传”,会要求填版本号和项目备注。上传成功后,去小程序管理后台的“版本管理”里就能看到这条上传记录。点击“选为体验版”后生成一个体验版二维码,用微信扫码就可以真机体验。 体验版二维码的有效期比较短,适合给团队成员或朋友快速体验。正式发布前还要提交审核,审核时注意类目匹配,个人主体不能发布某些需要特定资质的内容,这一点在开始选主体性质时就要想清楚,不然做到一半再换主体就非常头疼。 预览和体验版的区别在于:预览版通常是开发者自己扫码调试用的,体验版是给参与测试的人使用的。两者在功能和接口权限上基本一样,都会受到正式环境接口域名的限制。 白屏的原因很多,但新手遇到最多的是这三种。第一种, 排查思路很简单:先看 Console 有没有红字报错,有报错按报错改;没有报错就看 Network 里的页面数据返回情况,以及 Sources 里能否正确找到该页面。再用二分法把页面内容逐步注释,定位到具体是哪个组件或段落出的问题。 这个问题的根子大多是:要么你直接改了 还有一个隐藏点:如果你 setData 的对象里包含一个多层嵌套的对象,直接修改深层属性再整体 setData,可能会因为引用问题导致视图更新异常。稳妥的做法是每次更新时重新构造一个新对象: 开发时你可能会在 Console 里看到一条警告:“Now you can provide attr wx:key for a wx:for to improve performance.” 这是提示你没写 如果接口需要的时间较长,用户体验会很差。我的建议是至少加一个 loading 状态,在请求前后切换。如果接口本身确实慢,你可以考虑在页面 onLoad 时先渲染缓存里的旧数据,同时后台请求数据,等新数据返回后再 setData 覆盖。这种策略也叫“先展示、后刷新”,在电商列表里非常常见。另外连接超时用 表单场景下,很多人喜欢一个一个监听 这样代码能省掉一半,而且后面加新字段也不需要新增方法。唯一的注意点是 这一条重置,能让你后续写样式时减少很多“为什么比预想宽了 20px”的困惑。同时,小程序里组件间天然存在层级样式隔离,也就是父组件写的 class,默认不会“侵入”子组件内部。这对新手来说其实是个好事——你不用费心防污染。 原生小程序页面里, 有一个容易踩坑的点:在 小程序里的 第一种:用 第二种:直接用内联样式写在 view 的 当你开始写第二个、第三个类似页面时,一定会觉得重复代码太多。这时就该考虑自定义组件了。小程序里自定义组件只需要在 在页面 WXML 里使用: 自定义组件内部通过 多个页面之间如果需要共用一些主题色、间距变量,可以在 然后在任意样式里这么用: 这个方案比“改一个主题色要到处换值”舒服太多。尤其是后期项目变大,想统一调整视觉风格,直接改变量定义就好。 你如果跳过原生,直接套用框架,很多框架封装好的能力在你眼里就是“黑魔法”。出 bug 时很难判断是框架的问题、自己的问题还是 API 本身的限制。但如果你从原生起步,哪怕只是写过几个页面,你对层级、生命周期、数据绑定的理解就是清晰的。后面不管换什么工具,心里都有一张底图。 打个比方,框架是一条搭建好的高速公路,原生是普通的乡间小路。走高速公路确实快,但如果你连方向、路标都看不懂,一次导航失灵就完全蒙了。走一遍乡间小路,虽然慢一点,但你熟悉地形,知道哪里是弯道、哪里会积水——这就是基础的价值。 如果你不知道该拿什么练习,我建议按这个顺序来做。 第一步,写一个静态的个人名片页。包含头像、姓名、签名、技能标签。这个页面用不到网络请求,但会牵涉 view、text、image、WXSS 布局、rpx 换算。做完你就知道页面是怎么渲染的。 第二步,做一个待办清单。支持新增、删除、勾选完成。这个会牵涉 input、button、事件绑定、setData、本地存储。做完你就知道用户交互怎么处理。 第三步,做一个列表页跳详情页的简单应用。从列表跳到详情,用 navigateTo 携带参数,详情页从 onLoad 接收参数,然后模拟网络请求加载详情。做完你就把页面通信串联起来了。 第四步,尝试封装一个通用顶部导航条或者卡片组件。这会用到 properties、triggerEvent。做完你就迈进了组件化开发的门槛。 这个路径走完,你的原生基础就算真正打牢了。 基础打牢以后,按优先级排,你可以依次了解这些内容:分包加载,解决主包体积超限和启动速度的问题;ES6 语法完全体,比如 async/await、解构、展开运算;自定义组件生命周期,比如 后端接口没准备好的话,可以先自己搭一个本地 mock 服务,直接在开发者工具里通过打开“不校验合法域名”来调试。到正式发布前再把域名换成线上 HTTPS 接口。 我个人在实际操作里的一个体会是:很多问题不是你写得不努力,而是你对基础机制的直觉不对。你能解释清楚Page({ data: { inputValue: '' }, onInput: function (e) { this.setData({ inputValue: e.detail.value }) } })bindinput里打印值,不更新value,输入框会表现为“只读不改”,这也是个小坑。scroll-view是滚动容器,尤其适合实现页面局部滚动。使用它的时候,通常要给它一个固定高度,否则内容会自动撑开,达不到局部滚动的效果。它还有一个常用属性enable-flex,开启后内部可以使用 flex 布局。swiper组件是做轮播图的首选。indicator-dots控制是否显示指示点,autoplay控制自动播放,circular控制是否衔接循环。但轮播图的核心其实是通过bindchange事件拿到当前索引,记录到 data 里备用:<swiper bindchange="onSwiperChange" autoplay circular> <swiper-item wx:for="{{banners}}" wx:key="id"> <image src="{{item.imageUrl}}" mode="aspectFill"></image> </swiper-item> </swiper>4.2 页面跳转的几种方式
wx.navigateTo、wx.redirectTo、wx.switchTab、wx.reLaunch、wx.navigateBack这五种主要方式。很多人直接记口诀,但原理值得稍微想一下。wx.navigateTo是保留当前页面,跳转到应用内页面,相当于把一个新页面压入页面栈。因为栈里的页面对象还保留着,所以最多只能打开十层。wx.redirectTo是关闭当前页面,跳转到新页面,相当于替换栈顶。wx.switchTab专门用于跳转到tabBar页面,切换底部 tab。wx.reLaunch会关闭所有页面,重新打开一个新页面,适合做登录后重置或应用内重置。// 带参跳转 wx.navigateTo({ url: '/pages/detail/detail?id=1001&name=test' }) // 返回上一页 wx.navigateBack({ delta: 1 })onLoad里接收参数:onLoad: function (options) { console.log('接收到 id:', options.id) }navigateTo的 url 里不能带http://或者https://开头的地址,只能写应用内路径。另外跳转路径必须以/开头,指向你app.jsonpages 里的路径,否则会报“页面路径不存在”的错。4.3 网络请求与数据渲染全流程
wx.request。它跟浏览器里的 fetch 有点相似,但有几点明显差异。第一,域名必须在小程序后台配置为合法域名,开发者工具里可以勾选“不校验合法域名”来跳过这个限制,但真机上是不行的。第二,网络请求默认是不带 cookie 的,登录状态要用 token 机制解决。第三,请求是异步的,记得处理好回调逻辑。Page({ data: { list: [], loading: false }, onLoad: function () { this.fetchData() }, fetchData: function () { this.setData({ loading: true }) wx.request({ url: 'https://example.com/api/list', method: 'GET', header: { 'content-type': 'application/json' }, success: (res) => { if (res.statusCode === 200) { this.setData({ list: res.data.list, loading: false }) } }, fail: () => { this.setData({ loading: false }) wx.showToast({ title: '请求失败', icon: 'none' }) } }) } })wx.request里this指向的是wx.request内部,所以不要在 success 里直接调用this.setData,要用箭头函数。还有res.data是服务器返回的数据体,一般域名接口会包一层{ code, data, message },你要先判断业务状态码。wx.request,建议把请求封装成一个公共模块,统一处理 baseUrl、token、错误提示等逻辑。这个过程可以等你页面写得多了之后再回头抽象,不用一开始就过度设计。4.4 本地存储的正确姿势
wx.setStorageSync和wx.getStorageSync,还有异步版本wx.setStorage和wx.getStorage。新手阶段用同步版本就行,写法简单直观。// 存储 wx.setStorageSync('token', 'abc123') wx.setStorageSync('userInfo', { name: '张三', level: 5 }) // 读取 const token = wx.getStorageSync('token') const userInfo = wx.getStorageSync('userInfo') // 移除 wx.removeStorageSync('token') // 清空全部缓存 wx.clearStorageSync()fail回调,不会抛异常。所以做判断时,不要把wx.getStorageSync('xxx') === ''当成数据为空,因为可能这个 key 压根没写过。5. 项目管理、调试与发布
5.1 调试的黄金三件套
console.log信息都在这里。但要注意,小程序里的console.log只能输出到开发者工具的调试器里,真机调试时也能看到,但普通用户是看不到的。所以线上环境记得把这些日志去掉,或者说通过条件编译把它们屏蔽掉。.js文件,在行号上点击一下,然后触发执行,就能一步步看变量变化。这个方法调试复杂逻辑比console.log高效太多,特别适合排异步问题。5.2 真机预览常见差异
fixed定位会有被遮挡的问题,建议用env(safe-area-inset-bottom)来适配。其次是视觉差。模拟器宽度跟真机屏幕有差异,rpx 适配在不同真机上表现可能不太一样,所以重要布局建议多找几台真机看一眼。wx.request请求的域名必须是在小程序后台配置好的合法域名,而且必须是https协议,不支持 IP 直接访问式接口。如果你在模拟器上勾了“不校验合法域名”,真机上就会一下子失败,这个问题几乎每个人都遇到过。5.3 上传代码与体验版
6. 常见问题与排查技巧实录
6.1 页面白屏或者空白
app.json的pages里没有注册页面路径。第二种,页面.json文件缺失或者写错,比如usingComponents注入了不存在的组件。第三种,页面.wxml里存在语法错误,比如标签没有闭合。6.2 setData 数据更新但页面不刷新
this.data里的某个属性,要么你 setData 时用了一个未在data里声明的变量。小程序里虽然允许动态添加新字段,但为了可维护性和性能,建议所有需要渲染到 WXML 的数据都提前在data里初始化。this.setData({ userInfo: { ...this.data.userInfo, age: 20 } })6.3 wx:key 相关的警告
wx:key。性能只是其一,更重要的是列表项如果顺序变化,加不加wx:key直接影响渲染时的复用策略。建议每次写wx:for时都顺手带上一个唯一 key,不要偷懒。如果列表项是纯字符串数组,key 写*this就行。6.4 数据请求返回慢的体验优化
timeout字段设定一个合理的秒数,避免请求长时间挂起。6.5 表单提交与页面数据同步
bindinput,然后逐个 setData。字段多了代码会变得很啰嗦。我建议用一个统一的输入监听方法,结合><input name="username" bindinput="onFieldInput">onFieldInput: function (e) { const field = e.currentTarget.dataset.field this.setData({ [field]: e.detail.value }) }>view, text, image, button, input { box-sizing: border-box; }7.2 flex 布局在小程序里的使用频率
flex布局的使用频率非常高。因为它本身就是一个移动端场景,display: flex加上flex-direction: column,几乎可以应付大部分纵向排列的需求。横向排列时用justify-content: space-between实现两端对齐也是经典模板。scroll-view里使用 flex 布局时,默认的flex-direction是 row,也就是横向排列子元素。但如果你容器设了固定高度,子元素会自动拉伸,这时记得给scroll-view加上enable-flex="{{true}}"。7.3 背景图如何处理
image组件不能直接使用background属性,它本质上是图片标签,不是背景容器。如果你想让一个区域有背景图效果,可以有两种方案:image作为第一个子元素,绝对定位铺满整个容器,容器通过position: relative定位:<view class="banner"> <image class="banner-bg" src="/images/bg.png" mode="aspectFill"></image> <view class="banner-content">内容区域</view> </view>.banner { position: relative; width: 100%; height: 300rpx; } .banner-bg { position: absolute; width: 100%; height: 100%; top: 0; left: 0; }style属性上,用 background-image。这种写法对加载图片的路径要求比较严格,不能使用网络图片以外的相对路径吗?其实本地图片路径需要以/开头,但这种方式在动态更新背景时不如第一种方便。7.4 更优雅的组件封装思路
components目录下建一个子文件夹,里面同样放四件套,然后在页面.json里声明:{ "usingComponents": { "custom-card": "/components/custom-card/index" } }<custom-card title="标题" bind:tap="onCardTap"></custom-card>properties接收外部传入的数据,通过triggerEvent向外部发送事件。这是原生小程序组件化的核心,当你需要封装一些可复用的模块时,这个机制比在每个页面里复制粘贴要好维护得多。刚开始学,不必急着封装,但意识到这条路存在很重要。7.5 页面样式隔离与主题变量
app.wxss里定义全局 class。小程序 WXSS 目前支持 CSS 变量,你可以在page或者根节点定义:page { --primary-color: #1296db; --spacing: 20rpx; }.button { background-color: var(--primary-color); margin: var(--spacing); }8. 进阶前的准备:原生基础与框架视角
8.1 我想再强调一遍原生的重要性
8.2 一个适合新手的刻意练习路径
8.3 接下来去学什么
lifetimes里attached和detached;事件总线或全局状态管理,解决复杂应用跨页面通信。这些内容很多都是在前面的基础之上逐步延伸出来的,而不是割裂的新领域。setData为什么慢、wx:key为什么有用、生命周期里的 onShow 和 onLoad 有什么区别,很多看似玄学的报错就自己消解了。希望这篇原生基础指南,能帮你把这些直觉建立起来。如果你照着代码敲完一遍,还是卡在哪个环节,欢迎回头对照本文的排查思路再过一遍——基础这个东西,练一次有一次的收获。