简介:这是一份面向计算机相关专业在校学生与教师的课程大作业级微信小程序源码,聚焦打牌场景下的多人记账需求,采用TypeScript开发,兼顾工程规范性与教学实用性。资源共50个文件,包含17个TypeScript核心逻辑文件(如http.ts、util.ts、app.ts)、11个JSON配置与数据文件(project.config.json、sitemap.json等)、11张UI图标PNG资源,以及WXML页面结构、WXSS样式和README说明文档,整体仅308KB,轻量易读。已有113人学习下载,适合作为前端入门实践、微信小程序课程设计或期末大作业参考,亦可快速拓展为毕设原型——项目目录结构清晰,模块划分明确(pages/、components/、utils/、types/),附带完整类型定义(index.d.ts)与ESLint配置,便于理解TS工程化落地细节与小程序生命周期管理逻辑。
1. 项目缘起与核心价值
最近在整理过往的课程作业和练手项目时,翻到了一个基于TypeScript开发的微信小程序源码,主题是“打牌记账”。这虽然是一个课程作业,但麻雀虽小,五脏俱全,从技术选型、架构设计到具体的业务逻辑实现,都踩过不少坑,也积累了一些在微信小程序开发中结合TypeScript的实战心得。很多同学在入门小程序开发时,往往直接从JavaScript开始,对类型安全和工程化缺乏概念,导致项目稍大一点就难以维护。这个项目恰好可以作为一个不错的参考案例,展示如何在一个具体的业务场景(棋牌记账)下,运用TypeScript来构建一个结构清晰、可维护性高的小程序。
这个“打牌记账”小程序的核心功能并不复杂,主要就是记录牌局参与人、输赢分数,并最终进行结算。但它涉及了小程序开发中几个非常典型的模块:页面导航、表单输入、列表渲染、数据存储以及简单的状态管理。用TypeScript来重构或从头实现这类项目,能让你深刻体会到静态类型检查在减少低级错误、提升开发体验和代码可读性方面的巨大优势。无论你是正在学习微信小程序开发,还是想尝试将TypeScript引入现有项目,这个源码剖析和经验分享或许都能给你带来一些启发。接下来,我会从项目结构、TypeScript配置、核心功能实现以及开发中遇到的典型问题这几个方面,带你一起拆解这个项目。
2. 项目结构与技术栈选型解析
拿到一个项目源码,首先看它的目录结构和技术栈,这能快速了解项目的工程化水平和开发者的思考路径。这个“打牌记账”小程序采用了比较标准的微信小程序项目结构,并集成了TypeScript。
2.1 目录结构剖析
典型的项目根目录下会有以下关键部分:
miniprogram/ ├── app.ts // 小程序入口文件,使用.ts后缀 ├── app.json ├── app.wxss ├── project.config.json // 项目配置文件,需开启TypeScript编译 ├── tsconfig.json // TypeScript配置文件,核心! ├── typings/ // 类型声明文件夹(可选,用于补充微信API类型) │ └── wx.d.ts ├── pages/ // 页面目录 │ ├── index/ // 首页(创建/加入牌局) │ │ ├── index.ts │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.json │ ├── game/ // 游戏进行页(记录分数) │ │ └── ... │ └── settlement/ // 结算页 │ └── ... ├── components/ // 自定义组件目录(如果项目有抽取组件) │ └── player-item/ │ ├── player-item.ts │ ├── player-item.wxml │ └── ... ├── models/ // 数据模型/类型定义目录(TypeScript优势体现区) │ ├── player.ts │ ├── game.ts │ └── record.ts ├── services/ // 服务层,封装网络请求或本地存储操作 │ └── storage.ts └── utils/ // 工具函数 └── calculator.ts这个结构已经超出了基础小程序的范畴,引入了models、services这样的分层概念,这是TypeScript项目促进良好架构的体现。typings/wx.d.ts文件尤其重要,它可能用于补充微信小程序API的类型定义,确保在.ts文件中调用wx.request、wx.setStorage等方法时能有完整的类型提示和检查。
2.2 TypeScript环境配置要点
在微信小程序中使用TypeScript,核心在于project.config.json和tsconfig.json这两个配置文件。
首先,在project.config.json中,需要确保开启了开发者工具的TypeScript编译支持:
{ "setting": { "compileType": "typescript", "typescriptEnable": true, // ... 其他设置 }, // ... 其他配置 }这一步是告诉微信开发者工具:“这个项目我要用TypeScript写,请你用TS编译器来处理我的.ts文件。”
其次,tsconfig.json是TypeScript项目的灵魂。对于微信小程序,一个基础的配置可能如下:
{ "compilerOptions": { "target": "es2015", "module": "commonjs", "lib": ["es2015"], "allowJs": true, "outDir": "./dist", "rootDir": "./", "strict": true, "noImplicitAny": false, // 小程序中一些场景(如`data`初始值)可能暂时设为false "strictNullChecks": true, "moduleResolution": "node", "baseUrl": "./", "paths": { "@/*": ["miniprogram/*"] // 配置路径别名,方便导入 }, "typeRoots": ["./typings", "./node_modules/@types"], "types": ["miniprogram-api-typings"] // 引入微信小程序官方类型定义包 }, "include": ["./miniprogram/**/*.ts"], "exclude": ["node_modules"] }这里有几个关键点:
target: 设置为es2015或es6,因为微信小程序JavaScript运行环境支持ES6语法。module: 必须为commonjs,因为小程序目前使用的是CommonJS模块系统。lib: 只需es2015,避免引入DOM等浏览器特有库,减少干扰。typeRoots和types: 这是获取微信API类型提示的关键。你需要通过npm安装微信小程序的官方类型定义包:npm install miniprogram-api-typings --save-dev。然后在types中引入,这样在代码中写wx.时,编辑器就能自动补全所有API了。paths: 配置路径别名(如@/)是个好习惯,能避免冗长的相对路径(如../../../models/player)。
注意:关于网络热词中提到的“选项‘baseUrl’已弃用,并将停止在 typescript 7.0 中运行。指定 compileroption”。这是一个重要的版本变更提示。在较新的TypeScript版本中,
baseUrl和paths的配置关系发生了变化。为了向前兼容,建议同时明确配置baseUrl(如上例设置为./),并确保paths中的路径是基于baseUrl的。未来如果升级到TS 7.0,应关注官方迁移指南,可能的做法是更明确地使用rootDirs或其他配置。
2.3 为何选择TypeScript而非纯JavaScript?
对于课程作业或小型项目,这个问题值得思考。选择TS主要基于以下几点:
- 类型安全与错误前置:在编写“打牌记账”逻辑时,玩家分数是
number类型,名字是string类型。用JS写,不小心把字符串和数字相加可能得到诡异结果(如“玩家1” + 10 = “玩家110”),运行时才报错。TS在写代码时就会标红提示,强迫你处理类型问题,将很多运行时错误消灭在编码阶段。 - 代码可读性与维护性:
models/目录下的.ts文件定义了清晰的数据接口。例如Player接口明确说明了一个玩家对象应该有哪些字段。半年后回头看代码,或者别人接手项目,一眼就能看懂数据结构,无需猜测。 - 更好的开发体验(IDE支持):配合
miniprogram-api-typings,在VSCode等编辑器中,输入wx.setStorage会自动弹出参数提示,告诉你第一个参数是key: string,第二个参数是data: any,大大减少查文档的频率。 - 便于重构:如果后期想修改“结算规则”,比如从“积分制”改为“金币制”,在TS中,你修改了
Game接口的类型定义,所有用到的地方都会报错,引导你一一修正,避免遗漏。
当然,代价是初期需要学习类型语法和配置环境。但对于旨在学习和体现工程能力的课程作业来说,这个投入是非常值得的。
3. 核心业务逻辑与TypeScript实现细节
接下来,我们深入到“打牌记账”这个具体业务中,看看TypeScript如何帮助实现清晰、健壮的代码。
3.1 数据模型定义(Models)
这是TypeScript大显身手的地方。在models/目录下,我们定义核心数据的形状。
models/player.ts:
// 定义玩家接口 export interface IPlayer { id: string; // 唯一标识,可以用生成函数创建 name: string; avatarUrl?: string; // 头像URL,可选字段 initialScore: number; // 初始分数,例如每人100分 currentScore: number; // 当前分数,随牌局变化 } // 创建一个玩家的工厂函数,确保初始化数据完整 export function createPlayer(name: string, initialScore: number = 100): IPlayer { return { id: `player_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, name, initialScore, currentScore: initialScore, // 开始时当前分数等于初始分数 }; }这里定义了IPlayer接口,并使用?定义了可选字段avatarUrl。同时提供了一个工厂函数createPlayer,用于规范地创建玩家对象,避免散落在各处的{id: ..., name: ...}写法不一致。
models/game.ts:
import { IPlayer } from './player'; // 定义单条分数记录 export interface IScoreRecord { playerId: string; // 关联玩家ID round: number; // 第几轮 scoreChange: number; // 分数变化,正为赢,负为输 timestamp: number; // 记录时间戳 } // 定义牌局游戏 export interface IGame { id: string; name: string; // 牌局名称,如“周末麻将局” players: IPlayer[]; scoreRecords: IScoreRecord[]; createdAt: number; status: 'active' | 'settled'; // 使用字面量类型,限定状态值 currentRound: number; } // 初始化一个游戏 export function initGame(name: string, players: IPlayer[]): IGame { if (players.length < 2) { throw new Error('至少需要2名玩家才能开始游戏'); } return { id: `game_${Date.now()}`, name, players, scoreRecords: [], createdAt: Date.now(), status: 'active', currentRound: 1, }; }这里的关键是使用了字面量类型('active' | 'settled') 来定义status字段。这意味着game.status只能赋值为这两个字符串之一,如果你不小心写成了'finished',TS编译器会立刻报错。这比用字符串常量更安全、更直观。
3.2 页面组件中的类型化实践
有了清晰的数据模型,页面和组件的逻辑就变得简单明了。以记录分数的页面 (pages/game/index.ts) 为例:
// pages/game/index.ts import { IGame, IScoreRecord } from '../../models/game'; import { IPlayer } from '../../models/player'; Page<DataOption, CustomOption>({ data: { game: null as IGame | null, // 初始为null,加载后赋值 currentPlayerId: '', scoreInput: '', // 绑定输入框的值,是字符串 }, onLoad(options: { gameId: string }) { // options 被类型化为 { gameId: string } const gameId = options.gameId; this.loadGameData(gameId); }, loadGameData(gameId: string) { // 从本地存储或全局状态获取游戏数据 const game = getApp().globalData.games.find((g: IGame) => g.id === gameId); if (game) { this.setData({ game }); if (game.players.length > 0) { this.setData({ currentPlayerId: game.players[0].id }); } } else { wx.showToast({ title: '牌局不存在', icon: 'error' }); setTimeout(() => wx.navigateBack(), 1500); } }, onPlayerChange(e: WechatMiniprogram.PickerChange) { // e.detail.value 类型来自微信API类型定义 const index = parseInt(e.detail.value as string, 10); const player = this.data.game!.players[index]; // 使用非空断言,因为此时game已加载 this.setData({ currentPlayerId: player.id }); }, onScoreInput(e: WechatMiniprogram.Input) { // e.detail.value 类型为 string const value = e.detail.value; // 简单的输入校验:只允许输入数字(包括负数) if (/^-?\d*$/.test(value)) { this.setData({ scoreInput: value }); } }, addRecord() { const { game, currentPlayerId, scoreInput } = this.data; if (!game || !currentPlayerId || scoreInput === '') { wx.showToast({ title: '请填写完整', icon: 'none' }); return; } const scoreChange = parseInt(scoreInput, 10); if (isNaN(scoreChange)) { return; } const newRecord: IScoreRecord = { playerId: currentPlayerId, round: game.currentRound, scoreChange, timestamp: Date.now(), }; // 更新游戏数据 const updatedGame: IGame = { ...game, scoreRecords: [...game.scoreRecords, newRecord], // 更新对应玩家的当前分数 players: game.players.map(p => p.id === currentPlayerId ? { ...p, currentScore: p.currentScore + scoreChange } : p ), }; // 保存到全局状态或存储 getApp().globalData.games = getApp().globalData.games.map((g: IGame) => g.id === updatedGame.id ? updatedGame : g ); this.setData({ game: updatedGame, scoreInput: '', // 清空输入框 }); wx.showToast({ title: '记录成功' }); }, });在这个页面中:
data中的game被明确类型化为IGame | null,清晰地表达了其可能的状态。- 事件处理函数
e的参数使用了微信小程序API的类型,如WechatMiniprogram.PickerChange和WechatMiniprogram.Input,这来自于我们安装的miniprogram-api-typings包,提供了完美的代码提示。 - 创建
newRecord时,必须符合IScoreRecord接口,字段名或类型错误都会立即被提示。 - 更新数据时,我们使用了展开运算符 (
...) 来创建新的对象,避免直接修改原数据,这是配合React/小程序这类数据驱动视图框架的良好实践,TS能确保我们新对象的类型正确。
3.3 工具函数与计算逻辑
结算功能涉及计算,这部分逻辑适合放在utils/目录下,并用TypeScript保证计算过程的类型安全。
utils/calculator.ts:
import { IGame, IPlayer } from '../models'; // 计算每个玩家的净输赢 export interface ISettlementResult { playerId: string; playerName: string; netChange: number; // 净输赢,正数为赢,负数为输 } export function calculateSettlement(game: IGame): ISettlementResult[] { // 初始化一个Map来累加每个玩家的总分变化 const scoreMap = new Map<string, number>(); // 遍历所有记录,累加变化 game.scoreRecords.forEach(record => { const current = scoreMap.get(record.playerId) || 0; scoreMap.set(record.playerId, current + record.scoreChange); }); // 转换为结果数组,并关联玩家信息 const results: ISettlementResult[] = []; scoreMap.forEach((netChange, playerId) => { const player = game.players.find(p => p.id === playerId); if (player) { results.push({ playerId, playerName: player.name, netChange, }); } }); // 按净输赢从高到低排序 return results.sort((a, b) => b.netChange - a.netChange); } // 校验分数记录是否平衡(例如,麻将中所有玩家分数变化之和应为0) export function isScoreBalanced(game: IGame): boolean { const totalChange = game.scoreRecords.reduce((sum, record) => sum + record.scoreChange, 0); // 考虑到浮点数计算,允许一个极小的误差 return Math.abs(totalChange) < 0.01; }这个工具模块输出了明确的接口ISettlementResult和纯函数calculateSettlement。函数接收一个IGame类型的参数,并返回ISettlementResult[]。这种强类型约束使得函数的使用者和维护者都非常清楚数据的流入流出格式,极大降低了沟通和理解成本。同时,像isScoreBalanced这样的校验函数,其意图通过函数名和类型就一目了然。
4. 本地数据持久化方案与类型安全存取
微信小程序提供了wx.setStorage和wx.getStorage等API进行本地数据存储。在JavaScript中,我们存取的数据是“黑盒”,取出来后再进行类型断言或转换。而在TypeScript中,我们可以封装一个类型安全的存储服务。
services/storage.ts:
// 定义存储的键名枚举,避免硬编码 export enum StorageKey { GAME_LIST = 'game_list_v1', // 加版本后缀,便于未来数据结构升级时迁移 USER_SETTINGS = 'user_settings', } // 一个通用的、类型安全的存储函数 export function setTypedStorage<T>(key: StorageKey, data: T): Promise<void> { return new Promise((resolve, reject) => { wx.setStorage({ key, data, success: () => resolve(), fail: (err) => reject(err), }); }); } // 一个通用的、类型安全的读取函数 export function getTypedStorage<T>(key: StorageKey): Promise<T | null> { return new Promise((resolve, reject) => { wx.getStorage({ key, success: (res) => resolve(res.data as T), fail: (err) => { // 如果没找到对应的key,不算错误,返回null if (err.errMsg.includes('data not found')) { resolve(null); } else { reject(err); } }, }); }); } // 针对游戏列表的专用方法 export async function saveGameList(games: IGame[]): Promise<void> { await setTypedStorage<IGame[]>(StorageKey.GAME_LIST, games); } export async function loadGameList(): Promise<IGame[]> { const games = await getTypedStorage<IGame[]>(StorageKey.GAME_LIST); return games || []; // 如果没数据,返回空数组 }这个服务层做了几件重要的事:
- 使用枚举定义Key:避免了在代码中散落字符串
'game_list',集中管理,修改方便。 - 泛型函数:
setTypedStorage<T>和getTypedStorage<T>是泛型函数。调用saveGameList时,它内部调用setTypedStorage<IGame[]>,这意味着存入的数据被期望是IGame[]类型。读取时,getTypedStorage<IGame[]>会返回Promise<IGame[] | null>。这就在编译阶段建立了类型契约。 - 异步Promise封装:将微信回调风格的API封装成更现代的
Promise形式,方便使用async/await语法。 - 错误处理:在
getTypedStorage中,对“数据不存在”这种常见情况做了特殊处理,返回null而不是抛出错误,使业务逻辑更简洁。
在页面中使用时:
// 在app.ts或某个页面中 import { loadGameList, saveGameList } from './services/storage'; import { IGame } from './models/game'; async function onLoad() { try { const games: IGame[] = await loadGameList(); // 这里games的类型明确为IGame[] getApp().globalData.games = games; } catch (error) { console.error('加载游戏列表失败:', error); wx.showToast({ title: '加载数据失败', icon: 'error' }); } }这样一来,从存储中取出的games变量,TypeScript就知道它是一个IGame[]数组,你可以安全地调用.map,.find等方法,并且访问game.players属性时会有完整的代码提示。
5. 开发与构建过程中的常见问题与解决方案
在实际开发这个TypeScript小程序作业的过程中,我遇到了几个颇具代表性的问题,这里分享出来,希望能帮你绕过这些坑。
5.1 类型定义缺失或冲突问题
问题描述:在安装miniprogram-api-typings后,大部分API都有类型提示了,但有时一些较新的小程序API或者特定组件的方法可能找不到定义,或者自定义组件的properties和methods在父页面中调用时没有类型提示。
解决方案:
补充类型声明:在项目根目录的
typings文件夹下(或直接在miniprogram目录下)创建自定义的.d.ts文件。例如,为自定义组件补充类型:// typings/custom-components.d.ts declare namespace WechatMiniprogram.Component { interface InstanceProperties { // 假设你有一个自定义的玩家选择器组件,它有一个`selectedId`属性 selectedId?: string; } interface InstanceMethods { // 以及一个`resetSelection`方法 resetSelection: () => void; } }注意,这种全局扩充需要谨慎,最好有文档记录。
使用类型断言:在明确知道数据形状的情况下,可以使用
as进行类型断言。例如,从wx.getStorage取出的数据,如果你确信它的结构:const rawData = wx.getStorageSync('my_data'); const myData = rawData as IMyDataType; // 强制断言这是一种“告诉TypeScript编译器请相信我”的方式,要确保你的断言是正确的,否则会导致运行时错误。
更新类型包:定期检查并更新
miniprogram-api-typings到最新版本,以获取最新的API类型定义。
5.2 小程序Page/Component构造器的类型标注
问题描述:在编写Page或Component构造函数时,如何为data、methods以及生命周期函数提供准确的类型?
标准做法: 微信小程序官方类型定义提供了泛型参数。对于Page,通常是Page<Data, Custom>。
Data:指定data对象的类型。Custom:指定自定义方法(在methods中定义)的类型。
// 定义页面的数据类型 interface PageData { count: number; list: string[]; game: IGame | null; } // 定义页面的自定义方法类型 interface PageCustom { onMyCustomTap: () => void; fetchData: (id: string) => Promise<void>; } Page<PageData, PageCustom>({ data: { count: 0, list: [], game: null, // 这里必须符合PageData类型 }, onMyCustomTap() { // 这个方法可以在wxml中绑定 this.setData({ count: this.data.count + 1 }); }, async fetchData(id: string) { // 异步方法 const data = await someAsyncFunction(id); this.setData({ list: data }); }, onLoad() { // 生命周期函数 this.fetchData('123'); // 调用自定义方法,有类型提示 }, });对于Component也是类似的,使用Component<Data, Props, Custom>泛型。明确地为每个页面或组件定义接口,虽然初期有些繁琐,但带来的类型安全和代码提示收益是巨大的。
5.3 真机调试与类型检查的边界
问题描述:在微信开发者工具中,TypeScript编译检查通过,但在真机上运行时,可能出现undefined或null的错误,尤其是在网络请求回调或异步操作中。
经验与策略:
- 启用严格空值检查:在
tsconfig.json中设置"strictNullChecks": true。这会强制你处理所有可能为null或undefined的情况。上面代码中data里的game: IGame | null就是例子。在访问this.data.game.players之前,你必须先判断this.data.game是否存在。// 错误:Object is possibly 'null'. const players = this.data.game.players; // 正确:使用可选链(?.)和空值合并(??) const players = this.data.game?.players ?? []; // 或者使用条件判断 if (this.data.game) { const players = this.data.game.players; // ... 安全操作 } - 异步数据的状态管理:对于从网络或存储加载的数据,在
data中初始化为null或空状态是一种好习惯。在WXML模板中,也要使用条件渲染来避免访问不存在的属性。<!-- wxml模板 --> <view wx:if="{{game}}"> <text>牌局名称:{{game.name}}</text> <view wx:for="{{game.players}}" wx:key="id"> {{item.name}} - 分数:{{item.currentScore}} </view> </view> <view wx:else>加载中...</view> - 善用非空断言(谨慎!):在极少数你百分百确定某个时刻值一定不为空的情况下,可以使用非空断言操作符
!。
但要慎用,滥用会丧失类型检查的意义。通常更好的做法是优化代码逻辑,确保在访问前值已就位。// 在onLoad中已经成功加载了game,后续某个同步函数中 const gameName = this.data.game!.name; // 使用!断言非空
5.4 分包与异步化下的类型挑战
问题描述:当项目体积增大,使用分包或分包异步化时,类型引用可能会变得复杂。主包中的类型定义如何被分包中的组件使用?
解决方案:
- 将通用类型定义放在主包:将
models/、utils/和通用的services/放在主包目录下(例如miniprogram根目录)。这样所有分包都能引用到。 - 使用路径别名:在
tsconfig.json中配置paths,如"@/*": ["miniprogram/*"]。这样在分包中,你可以通过import { IGame } from '@/models/game';来引入类型,而不需要计算复杂的相对路径(如../../../models/game)。 - 注意编译作用域:确保
tsconfig.json中的include字段包含了所有需要编译的.ts文件,包括分包中的文件。例如:"include": ["./miniprogram/**/*.ts", "./packageA/**/*.ts"]。
关于网络热词中提到的“微信小程序 分包异步化 在其它分包中的插”,这指的是分包异步化特性,它允许独立分包在运行时异步加载。对于TypeScript项目,关键是要确保异步加载的组件或页面所依赖的类型定义(通常来自主包或npm包)在编译时是可用的。只要类型定义文件的物理路径在编译范围内(通过include配置),并且引用路径正确(使用配置好的别名),TypeScript就能正常进行类型检查,运行时则是另一套模块加载机制。
6. 从课程作业到可发布项目的优化思路
这个“打牌记账”作为课程作业,实现了核心功能。但如果想把它打磨成一个更完善、可发布的小程序,还可以从以下几个方面进行优化,这些优化点同样适用于其他TypeScript小程序项目。
6.1 状态管理的升级
目前示例中使用了getApp().globalData来共享游戏数据。这在简单场景下可行,但随着功能复杂(比如多个页面同时修改数据、需要响应式更新视图),会变得难以维护。可以考虑引入更专业的状态管理库,例如:
- MobX:对于小程序有很好的适配(如
mobx-miniprogram和mobx-miniprogram-bindings),其响应式理念与小程序setData结合,能实现自动更新视图,代码更简洁。 - 或者采用更轻量的发布-订阅模式自己封装一个状态管理中心。
使用MobX后,你的Game模型可以变成一个可观察对象,任何对它的修改都会自动触发依赖其数据的组件更新,无需手动调用setData。
6.2 用户体验与性能优化
- 数据缓存与同步:当前数据只存在内存和本地存储。可以考虑加入云开发数据库,实现多端数据同步(前提是项目接入了云开发)。TypeScript同样可以为云数据库的集合定义接口,保证读写操作的类型安全。
- 列表性能:结算页或历史记录页如果列表很长,需要使用
wx:for的优化技巧,如指定唯一的wx:key,或对于超长列表考虑使用虚拟列表组件。 - 输入体验:在记录分数时,除了文本输入,可以考虑增加快捷按钮(如“+10”、“-5”),提升操作效率。这涉及到WXML中绑定多个事件和TS中处理更复杂的逻辑。
6.3 代码组织与重构建议
- 自定义组件抽取:如果“玩家信息展示”、“分数输入面板”、“结算结果行”等在多个页面重复出现,应将其抽离为自定义组件。用TypeScript编写组件时,需要明确定义
properties和methods的类型。Component<PlayerItemData, PlayerItemProps, PlayerItemCustom>({ properties: { player: { type: Object as PropType<IPlayer>, // 使用PropType工具类型 value: null, }, highlight: { type: Boolean, value: false, }, }, methods: { onTap() { this.triggerEvent('playertap', { playerId: this.properties.player.id }); }, }, }); - hooks风格的逻辑复用:对于“加载数据”、“表单验证”等通用逻辑,可以封装成自定义的Composition Functions(类似于Vue 3的Composition API或React Hooks的思想),在多个页面或组件中复用。虽然小程序没有原生hooks,但可以封装成纯函数来复用逻辑。
6.4 构建与部署流程
对于课程作业,可能直接在微信开发者工具中点击“上传”即可。但对于团队协作或持续集成,可以考虑:
- 代码风格与质量:集成ESLint和Prettier,并配置适用于TypeScript和小程序的规则,在提交代码前自动格式化。
- 环境变量:区分开发、测试、生产环境,例如云环境ID、API地址等。可以通过编写不同的配置文件,在构建时注入。
- CI/CD:使用Jenkins、GitHub Actions等工具,在代码推送到仓库后自动执行类型检查、代码风格检查、打包并上传到小程序后台作为体验版。
回过头看,这个基于TypeScript的“打牌记账”微信小程序课程作业,其价值远不止于实现记账功能本身。它更像一个微型样板工程,展示了如何在微信小程序这个特定生态中,系统地应用现代前端开发中类型安全、清晰架构和工程化的思想。从定义严谨的数据模型,到封装类型安全的存储服务,再到处理页面组件中的各种边界情况,每一步都因为TypeScript的介入而变得更加可控和可预测。
本文还有配套的精品资源,点击获取