前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >TypeScript + 微信小程序:构建高效可维护的项目

TypeScript + 微信小程序:构建高效可维护的项目

原创
作者头像
Nian糕
修改2024-12-04 10:04:39
修改2024-12-04 10:04:39
4420
举报
Unsplash
Unsplash

目录结构设计

打开微信开发者工具,新建项目,模板选择【TS-基础模板】,当然你也可以选择带有Sass或是Less的基础版本,根据项目开发习惯和代码风格自行选择,这里仅是作为一个示例,目录结构如下所示,相关基础配置可参考微信官方文档,这里不再复述。

代码语言:txt
复制
/miniprogram                  # 小程序主目录
├── pages/                    # 页面目录,存放小程序的各个页面
│   ├── index/                # 示例页面
│   ├── logs/                 # 日志页面示例
├── utils/                    # 工具函数目录
│   └── util.ts               # 示例工具函数,供各页面调用
├── typings/                  # 类型定义目录
│   └── types/                # 自定义类型和第三方库类型定义
│       ├── wx/               # 微信小程序 API 类型定义
│       │   └── index.d.ts    # 微信 API 类型声明文件
│       └── index.d.ts        # 自定义类型声明文件
├── app.json                  # 全局配置文件,定义小程序的页面路径、窗口样式等
├── app.ts                    # 小程序入口文件,包含生命周期函数
├── app.wxss                  # 全局样式文件
├── .eslintrc.js              # ESLint 配置文件,用于代码风格检查
├── package.json              # npm 配置文件,管理依赖和脚本
├── project.config.json       # 小程序项目配置文件,微信开发工具使用
├── project.private.config.json # 私有配置文件,仅供本地开发使用
└── tsconfig.json             # TypeScript 配置文件,定义编译选项

想在这篇文章跟大家着重分享下,我在根目录添加的static文件夹,里面除了存放一些常规的静态资源和图片,还把项目的功能模块、主包资源存放其中,方便后续维护。而我也是想从这一个文件夹,来跟大家分享,微信小程序在引入TypeScript之后,要如何降低项目的维护成本、提升团队协作效率以及增强项目可扩展性。

代码语言:txt
复制
/miniprogram
│
├── /pages                 # 主包
├── /pagesA                # 分包A
├── /pagesB                # 分包B
├── /pagesC                # 分包C
├── /static                # 存放静态资源
│   ├── /actions           # 调用配置相关方法
│   ├── /api               # 主包接口
│   ├── /behaviors         # behaviors相关
│   ├── /config            # 配置相关
│   │   ├── api.ts         # API请求封装
│   │   ├── app.ts         # 项目配置数据
│   │   └── env.ts         # 环境配置文件
│   ├── /images            # 图片文件
│   ├── /libs              # 第三方SDK
│   ├── /scss              # 样式文件
│   ├── /state             # globalData相关
│   ├── /types             # interface相关
│   └── /utils             # ts文件
│       ├── common.ts      # 公共方法 - 不涉及接口请求
│       ├── report.ts      # 公共方法 - 涉及接口请求
│       └── utils.ts       # 工具函数
├── app.ts                 # 小程序的主入口文件
└── app.json               # 小程序的全局配置文件

actions 文件夹存放了一些调用配置相关的方法,我们通常会在app.globalData当中存放一些全局数据,例如用户信息。我们可以在里面新建一个userActions.ts的文件,用来存放多个页面会使用到的用户相关方法,以下是一个更新用户头像信息的例子。

代码语言:typescript
复制
// static/actions/userActions.ts

const app = getApp<IAppOption>(); 

/**
 * 更新用户头像信息
 * @param imgUrl - 新的头像图片地址
 * @returns boolean - 是否更新成功
 */
export function setUserAvatar(imgUrl: string): boolean {
  if (!imgUrl || typeof imgUrl !== "string") return false;
  if (app.globalData.userInfo) {
    app.globalData.userInfo.avatar = imgUrl;
    return true;
  }
  return false;
}

api 文件夹存放主包的API接口,每个分包都有单独的api文件夹用于存放请求接口,这里也是用用户信息的接口作为例子,这里用到RequestParams类型我会在后面封装API的部分详细讲解。

代码语言:typescript
复制
// static/api/userApi.ts
export const getUserInfoParams: RequestParams = {
  url: '/userInfo/get',
  method: 'GET',
};

behaviors 文件夹用于存放Behavior文件,类似于一些编程语言中的mixinstraits,具体使用方式可参考微信官方文档 自定义组件-behaviors 部分。这里需要注意的是,你在Behavior里写的方法,需要在声明文件里同步声明该方法,如果有相应的参数和返回值也同样写上,否则会提示你类型Interface上不存在该属性的警告。

警告
警告
代码语言:typescript
复制
// static/behaviors/userBehavior.ts
export const userBehavior = Behavior({
  methods: {
    getUserInfo(){
      console.log('this is user-info.')
    },
  }
代码语言:typescript
复制
// typings/types/wx/lib.wx.component.d.ts

interface InstanceMethods<D extends DataOption> {
  ...
  getUserInfo(): void;
}

config 配置相关文件夹,可根据实际项目进行存放,我这里存放了生产和测试环境的变量配置,以及微信小程序的AppId、订阅消息的推送id,第三方插件的AppID之类的项目配置信息,仅涉及数据读取而不做修改的部分。还有就是项目构建过程中,必不可少的api请求封装。

代码语言:typescript
复制
// static/config/api.ts
import { NODE_ENV } from '@/static/config/env';

declare global {
  // 请求参数类型
  type RequestParams = {
    url: string;
    method: 'GET' | 'POST';
    header?: Record<string, string>;
    timeout?: number;
  };
}
// 定义所有 API 响应的通用结构
interface ApiResponse<T> {
  data: T;
  error?: any;
  message: string;
  prompt: string;
  status: number;
}
type ApiRequestParams = {
  url: string;
  method: 'GET' | 'POST';
  header?: Record<string, string>;
  data?: any;
  params?: any;
  toastMessage?: string; // 加载提示信息
  toastMask?: boolean; // 加载提示蒙层
  timeout?: number; // 请求超时时间
  loadingTime?: number; // 延迟显示 loading 的时间
  showToast?: boolean; // 是否显示错误提示
  showLoading?: boolean; // 是否显示加载提示
};

export const apiRequest = <T>(apiParams: ApiRequestParams): Promise<ApiResponse<T>> => {
  return new Promise((resolve, reject) => {
    // 提取参数并设置默认值
    const {
      url,
      method,
      header,
      data,
      params,
      showLoading = true,
      toastMessage = '加载中',
      toastMask = true,
      timeout = 5000,
      loadingTime = 1000,
      showToast = true,
    } = apiParams;

    const defaultHeaders = {
      'content-type': 'application/x-www-form-urlencoded',
      'version': NODE_ENV.version, // 版本号
    };

    const headers = { ...defaultHeaders, ...(header || {}) };
    let loadingTimer: number | null = null;

    // 延迟显示 loading
    if (showLoading) {
      loadingTimer = setTimeout(() => {
        wx.showLoading({
          title: toastMessage,
          mask: toastMask,
        });
      }, loadingTime) as unknown as number;
    }

    // 处理 GET 请求or特殊请求的查询参数
    let requestUrl = NODE_ENV.mallApi + url;
    if (method === 'GET' && data && Object.keys(data).length > 0) {
      const queryString = Object.entries(data)
        .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
        .join('&');
      requestUrl += (url.includes('?') ? '&' : '?') + queryString;
    }
    if (params && Object.keys(params).length > 0) {
      const queryString = Object.entries(params)
        .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
        .join('&');
      requestUrl += (url.includes('?') ? '&' : '?') + queryString;
    }

    wx.request({
      url: requestUrl,
      method,
      header: headers,
      data: method === 'POST' ? data : undefined,
      timeout,
      success: (res) => {
        if (loadingTimer) clearTimeout(loadingTimer);
        wx.hideLoading();
        const { status } = res.data as ApiResponse<T>;
        if (res.statusCode === 200 && status === 200) {
          const response = res.data as ApiResponse<T>;
          resolve({ ...response });
        } else if (res.statusCode === 200 && (status == 511100)) {
          // 511100为token失效, 这里跳转登录页
        } else {
          if (showToast) {
            wx.showToast({
              title:
                (res?.data as ApiResponse<T>)?.prompt || `太火爆了, 请稍后重试(${res?.statusCode || status || '超时'})`,
              icon: 'none',
            });
          }
          reject(res?.data);
        }
      },
      fail: (err) => {
        if (loadingTimer) clearTimeout(loadingTimer);
        wx.hideLoading();
        if (showToast) {
          wx.showToast({
            title: '请求失败',
            icon: 'none',
          });
        }
        reject(err);
      },
    });
  });
};

几个需要说明的地方,在多个分包多个功能模块都需要引入api文件的话,过于麻烦,所以这里选择把RequestParams进行全局声明;可以将apiRequest挂载到app.globalData上方便引用,也可以直接在页面中获取调用;NODE_ENV里包含了项目域名和版本号,大家可根据实际项目进行调整;setTimeout使用as unknown as number的原因,是TypeScript会将其识别为Timeout对象,所以临时转换为未知类型unknown,再进行类型断言,如果你的项目没有这个报错可直接as number;在请求处理中单独处理params部分,这是在实际接口当中,可能会传递多种查询参数,例如data代表业务参数,而params可能是通用的公共参数(如分页、鉴权等)。

就算是高效的API封装也需要强大的后端支撑,为了保证接口调用的稳定性和响应速度,选择一个可靠、适配小程序业务需求的后端环境至关重要。在这里,腾讯云新一代的云服务器产品轻量应用服务器 TencentCloud Lighthouse为我们提供了一个理想的选择,它能为我们API接口的运行提供以下支持:

  1. 快速部署后端服务:预置了运行环境,可以在 1 分钟内完成应用初始化,让后端服务快速上线;
  2. 高带宽支持:API调用涉及频繁的数据交互,而轻量应用服务器提供高带宽流量包,确保接口响应迅速;
  3. 高性价比:相较于标准云服务器 CVM,它以套餐式的方式售卖,并采用高带宽流量包的网络计费模式。对中小企业、个人开发者和学生用户来说,既节省了成本,又能获得良好的网络性能。

当然无论你是选择轻量应用服务器 TencentCloud Lighthouse还是标准云服务器 CVM,你都可以趁现在参加 腾讯云11.11上云拼团Go 享受全年最优惠的价格。下单满足条件的商品(即卡片展示带有 “可拼团”角标的商品),还可以参加拼团活动,双人即可成团,成团即可享受二重好礼:赠送最高3个月的时长,或者多拿10%的资源包。这里有个小技巧:你可将多个商品合并下单去拼团,这样只需要去发起1次拼团,则每个”可拼团“标签的商品都能享受赠送。


images文件夹存放一些必要的图片,主要应对当网络环境较差的情况下,依旧能显示涉及业务的图片部分,比如关闭按钮;libs文件夹存放第三方SDK;scss文件夹存放全局的样式文件,以及主包一些公共样式文件;state文件夹用于存放globalData相关的数据,types文件夹存放interface相关的文件,这两个分别举个例子。

代码语言:typescript
复制
// static/state/userState.ts

export const userState = {
  user: {
    nickName: 'Nian糕',
    picture: '/static/images/avatar.png',
  },
}
代码语言:typescript
复制
// static/types/userTypes.ts
export interface UserInfo {
  nickName: string;
  picture: string;
}

utils 文件夹我单独区分成三个文件,根据是否涉及接口请求的公共方法common.ts文件和report.ts文件,还有公共函数utils.ts文件,前两个或许还好理解,但utils.ts文件是否跟common.ts文件有功能边界重合的部分,大家或许有不一样的想法,可以根据实际项目进行调整。我这里区分两个文件的理由是:common.ts主要服务于页面调用,偏向具体业务场景;utils.ts文件则是更加通用和底层,除了页面还有其他工具类文件都可以调用它。

代码语言:typescript
复制
// static/utils/common.ts
import { NODE_ENV } from '@/static/config/env';
import { appConfig } from '@/static/config/appConfig';

// 跳转其他小程序
export function toProgram(linkItem?: DrinkLinkItemType, type = 'default') {
  let program = {
    appId: linkItem?.AppId || appConfig.drinkAppId,
    path:  linkItem?.path || '',
    extraData: linkItem?.extraData || {},
    envVersion: linkItem?.envVersion || (NODE_ENV.type === 'Production' ? 'release' : 'trial'),
  }
  if (type === 'default') wx.navigateToMiniProgram(program);
  // 半屏小程序
  if (type === 'half') wx.openEmbeddedMiniProgram(program);
}
代码语言:typescript
复制
// static/utils/utils.ts

/**
 * 转化成时间戳
 * 兼容iOS
 * @param { Sting } date 日期
 */
export const toTimeStamp = (date: string): number => new Date(date.replace(/-/g, '/')).getTime();
End of File

行文过程中出现错误或不妥之处在所难免,希望大家能够给予指正,以免误导更多人,最后,如果你觉得我的文章写的还不错,希望能够点一下👍🏻和⭐️

OTZ!拜托了!这对我真的很重要!!!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录结构设计
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档