一、模块化的基本概念
模块化就是把一整份代码按职责拆成若干独立文件,每个文件只负责一件事,对外通过固定接口暴露能力,其它文件按需把能力取过来用。
它要解决的是三个很具体的问题:
避免重复:同一段逻辑如果写两遍,修改时很容易只改到其中一处,另一处被遗漏。
界定边界:文件一旦过长,就很难判断改动某一行会影响到哪些地方。
便于协作:拆成模块之后,不同的人可以各自负责不同的文件,互不干扰。
1.1 小程序用的是 CommonJS,不是 ES6 模块
这是最容易搞错的一点。小程序里写模块用 module.exports(或 exports),取模块用 require;不是 ES6 的 import / export。
动作 | 小程序写法(CommonJS) | 不能这样写(ES6) |
暴露 | module.exports = {...} | export default {...} |
引入 | const m = require('路径') | import m from '路径' |
1.2 模块化只有两个动作
动作 | 代码 | 说明 |
出口(暴露) | module.exports = {...} | 本文件把哪些变量、函数交给外部使用 |
入口(引入) | const m = require('路径') | 把别的模块的出口拿到手,赋给一个变量 |
拿到手之后,就可以像用普通对象一样使用它:m.comMsg、m.comFunc()。
二、miniprogram-6 的模块版图
这个项目的规模很小,但把小程序里常见的模块形态都覆盖到了,因此很适合用来讲模块化。
2.1 目录结构
miniprogram-6 ├── app.js # 小程序入口,全局模块 ├── app.json # 全局配置(注册页面、窗口样式) ├── app.wxss # 全局样式 ├── common │ └── common.js # 跨目录的公共模块 ├── components │ └── navigation-bar │ ├── navigation-bar.js │ ├── navigation-bar.json │ ├── navigation-bar.wxml │ └── navigation-bar.wxss ├── pages │ └── index │ ├── index.js # 页面逻辑:引用模块 │ ├── index.json # 声明使用 navigation-bar 组件 │ ├── index.wxml # 页面结构 │ ├── index.wxss # 页面样式 │ └── my-index.js # 与 index.js 同目录的模块 ├── project.config.json ├── project.private.config.json └── sitemap.json2.2 四种模块来源
pages/index/index.js 是整条链路的终点,它一口气引用了四种不同来源的模块。这四种来源基本覆盖了小程序里全部的可能性:
# | 模块来源 | 怎么拿到 | 本项目对应 |
1 | 全局 App 实例 | getApp() | app.appMsg / app.appFunc |
2 | 跨目录自定义模块 | require('../../common/common') | comm.comMsg / comm.comFunc |
3 | 同目录自定义模块 | require('my-index.js') | myIndeX.myIndexMsg 等 |
4 | 本模块内部定义 | const / function 直接写 | indexMsg / indexFunc |
三、四种模块来源逐一拆解
3.1 全局模块:app.js + getApp()
app.js 里用 App() 注册的是整个小程序唯一的全局实例,它上面的属性和方法任何页面都能取到:
// app.js App({ appMsg:'来自全局模块的变量', appFunc(){ return '来自全局模块的函数' } })页面里用 getApp() 拿到这个实例:
// pages/index/index.js const app = getApp() console.log(app.appMsg) // 来自全局模块的变量 console.log(app.appFunc()) // 来自全局模块的函数注意:getApp() 是全局函数,不需要 require。
适用场景:登录态、全局配置、设备信息这类「整个小程序只有一份」的数据。不要滥用:全局变量一多,就很难追查是谁改了它。
3.2 跨目录模块:common/common.js
这是最常见的模块形态——把公共逻辑抽到单独目录,再 require 进来:
// common/common.js const comMsg = '来自不同目录下模块的变量' function comFunc(){ return '来自不同目录下模块的函数' }//导出模块的变量和函数 module.exports = { comMsg, comFunc }页面里用相对路径引入:
// pages/index/index.js const comm = require('../../common/common') console.log(comm.comMsg) // 来自不同目录下模块的变量 console.log(comm.comFunc()) // 来自不同目录下模块的函数路径怎么数:index.js 在 pages/index/ 下,要回到项目根目录需要退两级,所以是 ../../,再进入 common/。require 的路径是相对于当前文件的,不是相对于项目根目录——这是最容易写错的地方。
3.3 同目录模块:pages/index/my-index.js
my-index.js 和 index.js 在同一个目录里,直接用文件名引入:
// pages/index/my-index.js const myIndexMsg = '来自相同目录下不同模块的变量' function myIndexFunc(){ return '来自相同目录下不同模块的变量' } module.exports = { myIndexMsg, myIndexFunc }// pages/index/index.js const myIndeX = require('my-index.js')这种直接写文件名的写法与微信官方文档「同一包内调用」的示例一致(官方示例写的是 require('common.js'))。
上面这段 my-index.js 里有一个实际存在的笔误:myIndexFunc() 返回的是「变量」而不是「函数」。为了如实反映项目现状这里保留原样,正确写法见第六节。
3.4 本模块内部:直接定义
不需要跨文件复用的内容,就在文件里直接定义,不必为了「模块化」而模块化:
// pages/index/index.js const indexMsg = '来自本模块的变量' function indexFunc(){ return '来自本模块的函数' }四、页面如何把四种模块拼起来
pages/index/index.js 会把四种来源的模块全部引入,并把结果显示到页面上:
// pages/index/index.js const indexMsg = '来自本模块的变量' function indexFunc(){ return '来自本模块的函数' } const app = getApp() const comm = require('../../common/common') const myIndeX = require('my-index.js') Page({ data:{ msg1:'index.js模块引用'+app.appMsg, msg2:'index,js模块引用'+app.appFunc(), msg3:'index.js模块引用'+comm.comMsg, msg4:'index.js模块引用'+comm.comFunc(), msg5:'index.js模块引用'+myIndeX.myIndexMsg, msg6:'index.js模块引用'+myIndeX.myIndexFunc(), msg7:'index.js模块引用'+indexMsg, msg8:'index.js模块引用'+indexFunc() } })4.1 八个结果分别来自哪个模块
页面用 msg1 到 msg8 八个变量展示结果,并用不同背景色区分来源:
变量 | 运行时结果 | 来源模块 | 背景色 |
msg1 | 来自全局模块的变量 | app.js | brown |
msg2 | 来自全局模块的函数 | app.js | brown |
msg3 | 来自不同目录下模块的变量 | common/common.js | yellow |
msg4 | 来自不同目录下模块的函数 | common/common.js | yellow |
msg5 | 来自相同目录下不同模块的变量 | pages/index/my-index.js | aqua |
msg6 | 来自相同目录下不同模块的变量(原代码有误) | pages/index/my-index.js | aqua |
msg7 | 来自本模块的变量 | pages/index/index.js | aquamarine |
msg8 | 来自本模块的函数 | pages/index/index.js | aquamarine |
4.2 页面结构与组件声明
<!-- pages/index/index.wxml --> <navigation-bar title="Weixin" back="{{false}}" color="black" background="#FFF"></navigation-bar> <scroll-view class="scrollarea" scroll-y type="list"> <view class="box"> <view class="title">引用来自不同模块的变量和函数</view> <view style="background-color: brown;">{{msg1}}</view> <view style="background-color: brown;">{{msg2}}</view> <view style="background-color: yellow;">{{msg3}}</view> <view style="background-color: yellow;">{{msg4}}</view> <view style="background-color: aqua;">{{msg5}}</view> <view style="background-color: aqua;">{{msg6}}</view> <view style="background-color: aquamarine;">{{msg7}}</view> <view style="background-color: aquamarine;">{{msg8}}</view> </view> </scroll-view>index.wxml 用到的 navigation-bar 不是内置组件,而是一个自定义组件,必须在 index.json 里声明后才能使用:
// pages/index/index.json { "usingComponents": { "navigation-bar": "/components/navigation-bar/navigation-bar" } }这里又体现了一次模块化:components/navigation-bar/ 目录下的四个文件(.js / .json / .wxml / .wxss)组成了一个自带结构与样式的独立模块,页面只需要一行声明就能复用它的全部能力。
五、三个必须记住的要点
5.1 路径是相对当前文件的
require 的路径以当前文件所在目录为起点。同样是引入 common.js,在不同位置写法完全不同:
当前文件位置 | 正确的引入写法 |
pages/index/index.js | require('../../common/common') |
app.js(项目根目录) | require('./common/common') |
common/other.js | require('./common') |
5.2 模块只会被执行一次
require 同一个模块多次,拿到的都是同一个对象(单例)。这意味着模块里的 const 只初始化一次,可以放心用它做缓存或共享状态。
5.3 模块导出的是一个对象
不管用哪种写法,最终暴露出来的都是一个对象,所以使用方永远是「模块名.成员名」:
module.exports = { comMsg, comFunc } // 导出
const comm = require('...') // 引入
comm.comMsg // 使用:点出成员
六、通读项目后发现的问题
6.1 my-index.js 的函数返回了错误的文字(建议修)
myIndexFunc() 应该返回「函数」,但实际返回的是「变量」,导致页面上 msg6 显示为「来自相同目录下不同模块的变量」,和它上面那行 msg5 一模一样,读者会以为这两行是同一个东西。
// pages/index/my-index.js —— 当前代码(有误)
const myIndexMsg = '来自相同目录下不同模块的变量'
function myIndexFunc(){
return '来自相同目录下不同模块的变量' // ← 应为「函数」
}
修改为:
const myIndexMsg = '来自相同目录下不同模块的变量'
function myIndexFunc(){
return '来自相同目录下不同模块的函数' // ← 改为「函数」
}
七、什么时候该拆模块
模块化不是越多越好。出现下面任一情况,就该考虑拆:
• 同一段逻辑出现在两个以上文件里:抽成公共模块。
• 一个文件超过两百行,且能明确分成几块:按块拆开。
• 有纯粹的、与页面无关的工具函数(日期格式化、金额计算):放 common/ 或 utils/。
• 有需要复用界面和交互的:用自定义组件(components/),而不是普通 JS 模块。
反过来,只被一个页面用到、又只有几行代码的东西,留在页面里就好——为了拆而拆,只会让人多跳几个文件才能读懂逻辑