# 小程序项目搭建

# 一、项目构建

# 1、创建准备

查看文档,注册小程序账号和下载开发者工具。

创建小程序需要先注册账号 小程序注册 (opens new window) ,注册后你就可以在你的账号上发布一个小程序,一个账号只能发布一个小程序。

参考链接:小程序文档 (opens new window)开发者工具 (opens new window)

# 2、创建项目

打开开发者工具,项目-新建项目,到你注册的账号中去获取 微信公众平台 (opens new window) 找到设置-基础设置复制AppID 创建项目。

# 3、项目结构设计

项目创建好后,我们要配置项目结构中常见的模块目录,如assets、utils、componentsd等,和vue项目目录差不多。

├── api                  # Api ajax 等
├── assets               # 本地静态资源
│   │   ├── images           # 项目图片
│   │   ├── css              # reset、common样式
│   │   ├── font             # 字体库
│   │   ├── iconfont         # iconfont库
│   │   └── less/sass        # 公共less/sass文件
├── components               # 业务公共组件
├── utils                    # 工具库
├── pages                    # 页面目录
│   ├── index                # index页面
│   │   ├── index.js         # 页面逻辑处理
│   │   ├── index.json       # 页面配置
│   │   ├── index.wxml       # html
│   │   ├── index.wxss       # css
│   │   └── index.less       # less/sass
├── .eslintrc.js             # eslint配置文件
├── .gitignore               # git忽略
├── package.json             # 包管理
├── app.js                   # App 实例
├── app.wxss                 # 公共样式表
├── app.json                 # 全局配置
├── README.md
├── project.config.json      # 项目配置文件
├── project.private.config.json # 和project.config相同,项目私有配置文件
└── sitemap.json             # 配置小程序及其页面是否允许被微信索引

# 二、项目配置

# 1、基础配置

如果要配置项目页面文件的路径、窗口表现、设置网络超时时间、设置多 tab、公共组件引入 等,可以通过app.json 文件进行配置。全局配置 (opens new window)

{
  "pages": [
    "pages/index/index",
    "pages/logs/index"
  ],
  "entryPagePath": "pages/index/index",
  "window": {
    "navigationBarTitleText": "Demo"
  },
  "tabBar": {
    "list": [{
      "pagePath": "pages/index/index",
      "text": "首页"
    }, {
      "pagePath": "pages/logs/index",
      "text": "日志"
    }]
  },
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 10000
  },
  "sitemapLocation": "sitemap.json",
  // 全局使用公共组件
  "usingComponents": {
    "van-button": "@vant/weapp/button/index"
  }
}

如果要对某个页面单独配置如设置标题,标题颜色,引入的插件等,只需要在对应页面的 .json 文件中设置即可。页面配置 (opens new window)

{
  "navigationBarBackgroundColor": "#ffffff",
  "navigationBarTextStyle": "black",
  "navigationBarTitleText": "微信接口功能演示",
  "backgroundColor": "#eeeeee",
  "backgroundTextStyle": "light",
  "usingComponents": {
    "van-dropdown-menu": "@vant/weapp/dropdown-menu/index",
  }
}

项目配置,针对整个项目或程序的配置

项目根目录中的 project.config.json 和 project.private.config.json 文件可以对项目进行配置,project.private.config.json 中的相同设置优先级高于 project.config.json。project.private.config.json是配置个人的配置。项目配置文件 (opens new window)

这里配置比如一些小程序基础信息,插件构建路径。

{
  "description": "项目配置文件,详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
  "setting": {
    "packNpmManually": true,
    "packNpmRelationList": [
      {
        "packageJsonPath": "./package.json",
        "miniprogramNpmDistDir": "./"
      }
    ],
  },
  "compileType": "miniprogram",
  "libVersion": "2.16.0",
  "appid": "wx618158e394f27d94",
  "projectname": "sanshiopsmanage",
  },
  "editorSetting": {
    "tabIndent": "insertSpaces",
    "tabSize": 2
  }
}

# 2、CSS设置

通用样式我们在 app.wxss 全局样式文件中设置,相当于reset.css,每个页面都会自动引入。也可以在app.wxss中引入其他公共样式,比如引入插件需要的样式,自定义的样式都可以。

每个页面里的样式在.wxss中写,但是纯css写很麻烦,我们可以借助扩展插件使用less或sass来解决。

微信开发者工具使用插件

# 3、引入依赖包

小程序也可以像vue一样使用npm插件,创建好项目,打开终端,就可以直接安装插件了。

npm 支持 (opens new window)

在根目录创建 package.json 文件,执行命令直接回车。只需初始化时执行一次,后续只要npm i 就可以了。

npm init

然后在小程序 package.json 所在的目录中执行命令安装 npm 包。

npm install xxx

安装好插件依赖后,还需要构建npm。

点击开发者工具中的菜单栏:工具 --> 构建 npm。

低版本的还需要勾选  npm 模块 选项。打开右上详情-本地设置。

为什么小程序要 构建npm,因为node_modules不会参与小程序的构建,需要使用构建npm来生成小程序能识别的包文件,构建会在包同级目录生成miniprogram_npm文件,里面目录结构和node_modules一一对应,我们引入也是引入此文件中的包。

生成的miniprogram_npm文件位置是根据 project.config.json 中 miniprogramRoot 设置的位置生成的,没有 miniprogramRoot 则和 project.config.json同级。

之前的旧版本构建位置默认在 miniprogram 文件中,没有和package.json同级,使用会有问题,需要在project.config.json文件中配置构建的目录,让构建的目录和package.json同级。

"packNpmManually": true,
"packNpmRelationList": [
  {
    "packageJsonPath": "./package.json",
    "miniprogramNpmDistDir": "./"
  }
]

目前最新版本的开发者工具默认构建在和package.json同级的,不需要再配置构建目录,为了保险起见我们都在project.config.json中为所有插件配置好。此配置只需配置一次。

# 4、引入Vant Weapp组件

文档:Vant Weapp (opens new window)

安装配置:

1、npm安装

npm i @vant/weapp -S --production

2、将 app.json 中的 "style": "v2" 去除。 3、修改构建目录,在project.config.json中统一设置,此配置只需配置一次。

"packNpmManually": true,
"packNpmRelationList": [
  {
    "packageJsonPath": "./package.json",
    "miniprogramNpmDistDir": "./"
  }
]

4、安装好后执行构建npm。然后在miniprogram_npm可以看到@vant 文件了 使用

1、在页面中使用

在页面中使用直接在页面的.json文件引入组件,和自定义组件一样,注意的是如果你的页面目录不对,组件引用地址可能会出问题,找不到此组件,可以尝试使用相对路径找到miniprogram_npm下对应的组件就可以了。

"usingComponents": {
  "van-button": "@vant/weapp/button/index"
}

2、全局使用 全局使用直接在app.son中设置usingComponents,这样所有组件就都可以使用了。

"usingComponents": {
  "van-button": "@vant/weapp/button/index"
}
<!--index.wxml-->
<view class="container">
  <van-button type="default">默认按钮</van-button>
  <van-button type="primary">主要按钮</van-button>
  <van-button type="info">信息按钮</van-button>
  <van-button type="warning">警告按钮</van-button>
  <van-button type="danger">危险按钮</van-button>
</view>

使用内置样式

在 app.wxss 中引入内置样式,这样就可以使用vant自带的一些样式类了。

@import '@vant/weapp/common/index.wxss';

# 5、接口封装

小程序调用接口是使用自带的请求方法wx.request(),不需要再使用其他请求插件。但是这样每次调用都很麻烦,也没有做请求拦截,所以要封装一下,统一处理。

1、在utils下创建request.js文件,对接口请求统一处理。

// utils/request.js
/**
 * @description 请求接口封装
 * @author changz
 * */ 

const BASE_URL = 'https://api.itops.yunsee.cn';
let hasInvalid = false; // 判断是否有token失效,不同地方多次调用此变量也可以被访问

/**
 * @description 请求方法封装
 * @desc 错误统一拦截
 * @desc token过期统一处理
 * @desc then里会返回所有的业务状态码,所以不是200的要返回提示
 * @desc catch里只拦截接口请求错误,如404、401、500和fail里的错误
 * 把这些错误统一成一种格式返回提示
 * @param {String} [url] - 请求地址
 * @param {String} [method] - 请求方法
 * @param {Object} [params] - 请求参数 {}
 * @param {String} [loadText] - 加载动画提示,默认无
 * @example request({})
 * @author changz
 * */
function request({url, method, params, loadText}) {
  return new Promise((resolve, reject) => {
    if (loadText) {
      wx.showLoading({
        title: loadText,
      })
    }
    
    let header = {
      'content-type': 'application/json'
    }
    if (wx.getStorageSync('token')) {
      header['Authorization'] = wx.getStorageSync('token');
    }
    
    wx.request({
      url: `${BASE_URL}${url}`,
      method,
      data: params,
      header,
      success(res) {
        wx.hideLoading();
        const { statusCode, data } = res;
        if (statusCode == 200) {
          resolve(data)
        } else if (statusCode == 401) {
          if (hasInvalid) return // 已经有失效跳转到登录
          wx.showToast({
            title: '登录过期',
            icon: 'none',
            duration: 1500
          })
          hasInvalid = true
          // 跳转到登录页
          setTimeout(() => {
            wx.reLaunch({
              url: '/pages/empower/index',
              complete: function() {
                hasInvalid = false
              }
            })
          }, 1500)
        } else {
          reject({
            errMsg: `请求失败:${statusCode}`,
            data: res
          })
        }
      },
      fail(err) {
        wx.hideLoading();
        reject({
          errMsg: err.errMsg,
          data: err
        })
      }
    })
  })
}

export default request

2、在api下创建接口地址统一处理文件

// api/login.js

import request from '../utils/request'

const api = {
  miniLogin: '/api/common/miniLogin'
}

// 登录
export function miniLoginApi(parameter, loadText) {
  return request({
    url: api.miniLogin,
    method: 'POST',
    params: parameter,
    loadText
  })
}

3、在组件调用

import { miniLoginApi } from '../../api/login'

// 登录
userLogin() {
  const that = this
  const { code } = this.data
  const params = {
    code
  }
  miniLoginApi(params, '加装中...')
    .then((res) => {
      if (res.code !== 200) {
        wx.showToast({
          title: res.msg,
          icon: 'none',
          duration: 2000
        })
        return
      }
      console.log(res)
      const { data, token } = res
      wx.setStorageSync('token', token)
      wx.setStorageSync('userInfo', data)
      // 登录成功跳转到首页
      wx.navigateTo({
        url: '/pages/index/index'
      })
    }).catch((err) => {
      console.log(err)
      wx.showToast({
        title: err.errMsg,
        icon: 'none',
        duration: 2000
      })
    })
}

# 6、登录权限判断

小程序登录情形大概分为两种,一种是小程序进来就需要登录,我们可以让小程序每次进入时就默认打开登录页,执行一遍登录操作后再进入;另一种不论登不登录都可以进来,当需要登录时再跳转到登录页或者直接调登录接口登录,然后再根据用户信息判断。

两种情形的权限判断都类似,只是第二种多了一步判断是否登录。

  1. 判断缓存是否有token,或者其他登录凭证。
  2. 没有token跳转到登录页,或者直接调用登录接口,登录后缓存token。
  3. 如果有token则直接进入页面调用接口。
  4. 如果调用接口后token过期失效,则重新跳到登录页登录。 这其中涉及到在哪进行判断是否登录,在哪里判断token是否过期,多次请求同时过期如何解决。

1、在哪里进行判断是否登录

因为很多页面都可能需要判断,所以我们在utils下创建一个permission.js文件单独用来做判断,然后引入到所需的页面去调用和判断。当然,可以在这个文件里根据项目需求进行相应的逻辑处理。

// utils/permission.js

/**
 * 权限判断
 * 在需要做权限判断的页面引入
 * 如果返回true则已登录,则进行已登录的操作,没有登录跳转都登录页
 * 当然也可以进行页面拦截,没有登录直接返回上一页
 * 判断登录页也可以使用接口判断,因为是异步所以要使用async await 等待获取
 * */ 

// 检查是否登录
function checkLogin() {
  let isLogin = false
  const token = wx.getStorageSync('token');
  if (token) {
    isLogin = true;
  } else {
    wx.clearStorageSync();
  }
  return isLogin
}

module.exports = {
  checkLogin
}

在页面中进行获取,这里根据项目需求情况来判断。

<!--index.wxml-->
<view class="index">
  <van-button type="default" bindtap="showToast">{{initData ? '已登录' : '请登录'}}</van-button>
  <van-button type="primary" wx:if="{{!userIsLogin}}" bindtap="getLoginInfo">登录</van-button>
</view>
// index.js
import { menusApi } from '../../api/index'
import permission from '../../utils/permission'
Page({
  data: {
    userIsLogin: false,
    initData: null
  },
  
  onShow: function () {
    const isLogin = permission.checkLogin()
    this.setData({
      userIsLogin: isLogin
    })
    if (this.data.userIsLogin) {
      this.getInitData()
    }
  },

  getInitData() {
    const that = this
    menusApi({}, '加装中...')
      .then((res) => {
        if (res.code !== 200) {
          wx.showToast({
            title: res.msg,
            icon: 'none',
            duration: 2000
          })
          return
        }
        console.log(res)
        const data = res.data
        that.setData({
          initData: data
        })
      }).catch((err) => {
        console.log(err)
        wx.showToast({
          title: err.errMsg,
          icon: 'none',
          duration: 2000
        })
      })
  },

  // 去登录
  getLoginInfo() {
    if (this.data.userIsLogin) return
    wx.navigateTo({
      url: '/pages/empower/index'
    })
  }
})

2、判断token过期

我们在接口封装request.js中统一对错误请求进行了拦截,只需判断请求状态码为401就可以直接跳转到登录就行了。

3、多次请求同时过期如何解决

当一个页面同时请求几个接口,这时token失效了,判断401时会多次跳转到登录页,为了减少这种不必要的跳转,我们需要进行判断,只跳转一次登录页就行了。

我们可以在request.js中定义一个变量,这个变量相当于全局变量,不论在哪里引入都能访问这个变量,所以只需在request.js中对这个变量判断就行。

# 7、编码规范配置

1、格式化代码

格式化代码和vue项目一样,我们使用Prettier进行配置,在扩展中下载prettier插件,然后在项目根目录创建.prettierrc.js文件,进行配置我们需要的格式。

module.exports = {
  // 一行最多 100 字符
  printWidth: 180,
  // 使用 2 个空格缩进
  tabWidth: 2,
  // 不使用缩进符,而使用空格
  useTabs: false,
  // 行尾需要有分号
  semi: false,
  // 使用单引号
  singleQuote: true,
  // 对象的 key 仅在必要时用引号
  quoteProps: 'as-needed',
  // 末尾不需要逗号
  trailingComma: 'none',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // 箭头函数,只有一个参数的时候,也需要括号
  arrowParens: 'always',
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 不需要写文件开头的 @prettier
  requirePragma: false,
  // 不需要自动在文件开头插入 @prettier
  insertPragma: false,
  // 使用默认的折行标准
  proseWrap: 'preserve',
  // 根据显示样式决定 html 要不要折行
  htmlWhitespaceSensitivity: 'css'
}

然后在文件中右击->格式化文档,方法->配置默认格式化->Prettier。配置好后以后只需右击选择格式化文档就行了。

2、代码检查

暂时以vue项目的格式进行编写。