Skip to content

项目实战基于云开发开发一个在线商城小程序

在学完前 4 个模块之后,我相信你会对微信小程序的开发有一个全新的认识。在前面 3 个模块中,俊鹏分别从微信小程序内在的运行原理,小程序工程化开发以及具体实践层面,深度讲解了微信小程序开发所必要的知识和能力。而第 4 个模块里,我带你认识了微信小程序的云端解决方案------小程序·云开发,解读了云开发的主要能力。

今天这一讲,我将带你基于云开发实现一个在线商城小程序,通过实战加深你对这门课的认知。

项目初始化

我们从项目最简单处做起。在小程序项目开发之前,通常会有完备的产品逻辑设计以及产品视觉设计为开发者提供产品的交互和视觉元素。根据这些条件,你才能编写代码,生成最终可运行的产品。

因为这门课的重点在于如何进行微信小程序开发,以及后端服务开发,并没有包含设计环节,所以我们直接从图中的微信小程序开发开始。

我们先在浏览器中打开基础项目,下载初始化代码(我们会在此代码的基础上开始学习)。

打开项目

使用最新版本的微信开发者工具,导入此项目(选择项目的目录),并在 AppID 处填写小程序的 AppID。

导入成功后,开发者工具中展示的界面如下(左侧的模拟器能够正常运行展示出页面):

紧接着,我们操作一下左侧的模拟器,可以完成整个商城项目的页面交互逻辑:

商城交互逻辑主要有 9 点:

  1. 首页商品列表页(展示商品的列表);

  2. 点击任意商品,进入商品详情页;

  3. 点击商品详情页中加入购物车,选择商品规格后确认加入;

  4. 点击商品详情页中(或主页 Tabbar)进入购物车页,查看已经加入购物车的商品;

  5. 选择购物车中商品,点击结算按钮,进入订单确认页面;

  6. 在订单确认页面中,选择配送方式和输入备注,点击提交订单,转到订单中心页;

  7. 在订单中心页面选择商品执行付款,或者执行取消动作;

  8. 若执行商品付款则变更状态为待收货状态,可点击确认收货;

  9. 确认收货后可在已完成栏中看到,可点击删除订单。

解读项目

为了能让你更容易理解,项目基本上采用了原生语言无框架开发,下面我给你解读一下项目中的一些主要逻辑。

  • 本地化模拟

首先你要清楚,初始项目中你所做的一切产品交互体验都只是在本地流转执行的。也就是说,你的每一个点击和动作,都没有后端服务的工作,全都在小程序里模拟。而你在产品的首页中所看到的商品内容,其实是我预先写在代码里的一系列数据,请你打开项目目录下的 miniprogram/app.js,可以看到如下内容:

这里就是维持项目运转的基本商品数据,通过引入固定的资源来提供交互所必要的数据。而这就引入了我们实战中所要讲的第 1 点:现实中,微信小程序开发和其他前端开发一样,需要预先数据填充,来支撑整个小程序交互逻辑的开发。

我们不能够直接动用后端接口来做小程序页面逻辑开发,这会为后端服务造成不可控的伤害,进而影响项目进度。

  • 虚拟化接口

那么当我们使用预置数据开发页面之后,如何高效地对接后端服务呢? 这就引入了我们讲的第 2 点------虚拟化接口。我们在页面开发时,将需要后端服务的业务操作分离出来,做成一个接口。由于这个接口并没有真正的后端接入,只是提供样子,所以我们称其为虚拟化接口。

请你打开项目目录 miniprogram/pages/index/index.js 文件,定位到 onLoad 函数:

index 页面是项目的首页,用于展示商品列表,所以页面需要商品的列表信息。由于产品最终需要从后端获取商品的列表信息,所以我们将这一动作制作成接口。接着再打开项目目录 miniprogram/app.js,定位到 55 行,如下代码:

我们通过编写一个函数,模拟后端服务将商品列表返回,从操作本身上来看,这个函数就是直接返回了我们之前预置的数据,并没有其他操作,但我们仍需要按照真实情况来模拟,因为后端服务的请求肯定会有网络延迟,所以一般我们在请求时使用回调函数,或者是 promise 来处理,所以在本地模拟时,我们也这样构建。

我们规定:如果有数据需要调用 obj 入参对象的 success 函数将数据传进去,这样处理后,我们便可以在调用方的 success 中编写获取到数据后的有关逻辑了。

因为网络延迟,所以表现在页面中应该需要体现等待加载的状态,告知用户产品正在通信(避免用户觉得产品卡死,并操作退出),所以我们在接口函数中使用 timeout 来规定 1 秒钟之后再返回数据。

这样,在页面中,我们可以编写和演示用户等待数据加载时的展示效果,来进一步开发页面。

如果你细心,就会发现 app.js 文件中包含了产品中所有需要后端交互的虚拟化接口,这样我们在与后端对接时,就可以直接操作 app.js 文件,非常方便地改写和调试,不需要改动页面逻辑代码:

  • 小程序缓存

当然,我们通过虚拟化接口操作的不仅仅是读取数据,更多的是做数据的存储。比如,产品需要记录用户购物车里加入了哪个商品、订单结算时的收货地址、订单状态等信息。

那么在没有后端服务接入的情况下,我们如何存储这些数据呢? 这就引出了我们要讲的第 3 点------小程序缓存。

我们通过微信小程序提供的缓存接口,将产品交互时需要的数据存储起来,通过这种缓存方式保存的数据将不会随着用户退出小程序而消失,可以一直驻留(除非用户主动操作删除小程序本地数据):

请打开项目目录 miniprogram/app.js 文件,定位到 178 行,为商品付款的虚拟接口 toPayTap

在此接口函数中,我们通过 wx.getStorageSync 同步获取缓存中信息,然后经过一系列处理,将处理后的信息通过 wx.setStorageSync 存回去。

需要注意的是, 我们通过缓存能力实现数据保存的虚拟化接口,需要尽可能地与产品设计时保持一致,也就是尽可能模拟接口文档所应该呈现的信息。比如真实后端服务获取订单时有订单状态(进行中、已完成)的标志,但是在新增订单时我们并没有提供这个标志,也就是说这个标志是后端逻辑添加的,但是为了能够完整复现逻辑,我们应该在虚拟接口时主动添加:

前端虚拟化意义

使用虚拟化接口串联产品的整个逻辑,对微信小程序开发来说可能增加了开发负担,因为你除了要实现页面逻辑交互开发,可能还要进一步思考后端数据的操作逻辑,但你也会获得一些好处,我总结了 3 点。

  • 得到一个完整运行的高保真运行 DEMO

大部分产品开发都会经历需求变更,主要因为产品人员模糊了产品形态,造成一些错误决定。比如微信规定在小程序内不能长按扫描二维码,但是产品同学不清楚这个形态,做了长按二维码识别的设计,导致之后为了绕过这个限制,更改了许多产品交互逻辑,用来替换长按二维码识别的功能。

但如果你通过虚拟化接口完成一个完全可交互的原型 DEMO,就非常容易发现这类设计并不合理,并在后端服务未开工之前及时规避,从而实现高效率的投入产出比。甚至你可以要求产品同学体验 DEMO 确保产品需求无误后,再进行后续的产品开发工作。

  • 理清前后端接口,及时发现不合理的地方

由于虚拟化了所有与后端交互的接口,你会非常容易发现接口设计中不合理的地方,比如在获取订单列表这个接口中,文档并没有给出订单状态信息,但设计稿中却要求显示订单状态。这时你可以及时向产品同学和后端同学纠正,避免后端同学在架构完成之后再变更这个返回结果。(虽然加订单状态比较简单,但如果出现极端情况会影响后端进度)

  • 具备开发后端服务的能力,只缺一个运维

既然你已经在前端通过小程序缓存完成了产品的后端数据交互,那为什么不自己实现一下后端呢?在服务器 server 后端下,搭建服务器环境、配置网络、配置项目运行环境等工作非常复杂,并且开发工作模式和前端本身开发工作模式差别巨大。你很难在短时间掌握和适应独自开发后端,必须借助运维同学的帮助。

而上述 3 点就引出了实战的高潮部分:使用云开发完整复刻前端虚拟化的接口,构建后端服务。 这样一来前端开发者便可以独立开发前后端应用服务了。

后端开发

接下来,我带你通过 3 个关键点来用云开发实现后端服务(如果你未开通过云开发,请按照此指引熟悉并开通)。

多媒体资源上传

首先,我们要将所必要的资源上传到服务中,比如商品的图片、视频等。在前端虚拟化实现时,我们用的是第三方商城的资源链接。而当搭设自己的后端服务时,我们要承载这些资源的分发。明确这一点之后,我们要访问浏览器,下载项目的多媒体资源,下载后解压资源如下图所示:

然后打开小程序开发者工具项目左上方的"云开发"按钮,进入云开发控制台,在"存储栏目"中点击上传文件,选择解压后的资源图片(除 data.json),将其上传至云存储:

上传完成后如下图所示,每个资源都有唯一 File ID

然后将 File ID 中除了文件名之外,前半部分根目录保存下来。以上图为例,保存的根目录为

cloud://cloud-tcb.636c-cloud-tcb-1301077292(保留好这个根目录,我们会在下一步骤用到)。

数据库构建

第二步要构建数据库,在微信小程序开发前,我们已经根据产品同学的设计逻辑设计好数据库的结构了,结构整体如下:

接下来,我们打开下载的多媒体资源包中的 data.json 文件,会看到预置的一些数据,然后用文本工具将 FileID 替换成存储时保存的根目录,最终效果如下:

注意一下,上面的信息里我们把存储中上传的资源和数据源文件做了结合。接下来,我们打开云开发控制台中的"数据库栏目",创建集合,名称为 goods,如下图所示:

创建成功后,点击左侧 goods 集合,点击导入,选择修改后的 data.json 文件,确认导入:

导入后效果如下图所示:

另外,再创建一个集合名为 order,用于记录订单数据,此集合不用导入数据。这样一来,我们就构建完了服务所用的数据库以及基础商品图片。

接口搭建

接下来,我们开始开发后端接口(每个接口一般都会经历 4 个步骤,我们从最简单的获取商品列表的接口开始入手)。

  • 创建云函数

第一次创建云函数时,你需要在项目目录中新建一个名为 cloudfunctions 的文件夹。

小程序开发者工具会自动为我们引入云开发环境(如果你有多个环境可以右键变更,选择之前上传存储和数据库的同一云开发环境)。

目录创建好后,就可以创建云函数了,在 cloudfunctions 右键,点击新建 Node.js 云函数:

在出现的输入框中输入 getGoodlist ,回车后便创建了一个名为 getGoodlist 的云函数,效果如下:

  • 编写代码

接着打开 cloudfunctions/getGoodlist/index.js 文件,将文件中的所有内容删除,替换成下列代码:

java
//引入云开发SDK
const cloud = require('wx-server-sdk')
//初始化云开发SDK
cloud.init({
    //指定当前运行环境
    env: cloud.DYNAMIC_CURRENT_ENV
})
//云数据库操作对象
const db = cloud.database();
exports.main = async (event, context) => {
    //定位数据库集合goods
    const list = (await db.collection('goods')
    .field({//指定所列字段
        title:true,
        price:true,
        origin:true,
        img:true
    })
    .get()).data;//执行取出所有记录的指定字段,返回值中取data

    //返回取出的数据
    return list;
}
  • 上传并部署

然后保存更新后的代码文件,在云函数文件夹处右键,重新部署上传,如下图所示:

如果你并没有变更 package.json 中的依赖引入,可以直接选择 index.js 右键,增量更新文件:

增量更新只变更单一文件,并不会做云函数的重新部署,在频繁改动代码时推荐使用增量更新,速度更快。

  • 小程序端更改调用

当我们在云开发中编写了 getGoodlist,获取商品列表信息的接口云函数之后,就需要变更小程序端的代码,替换之前的虚拟化接口了。

所以咱们来打开项目 miniprogram/app.js,定位 getGoodSList 函数,将其代码改成如下:

java
getGoodSList: function (obj) {
    wx.cloud.callFunction({
      name:'getGoodlist',
      success:function(res){
        if (obj != null && obj.success != null) {
          obj.success(res.result);
        }
      }
    });
  }

在新的代码中,我们使用 wx(小程序SDK)中内置的 cloud(云开发SDK)操作请求云函数(callFunction),名称为 getGoodlist。当请求成功之后,调用传入参数的 success 返回得到的数据。

经过这 4 个步骤后,我们就完成了获取商品列表接口的变更,来看一下实际效果:

通过控制台我们可以看到,小程序请求了云函数,并返回了数据,还通过云存储资源返回了需要的商品图片(在这里我没有介绍每个接口的实现方法,如果你想了解的话,可以通过浏览器访问此链接,获取云函数代码以及变更后的 app.js 代码,参照以上步骤对云函数进行逐个更改)。

如果你已经掌握了上述内容,可以直接下载云开发版的完整项目,在开发者工具中打开运行,接下来,我们将围绕此项目展开讲解。

工程化开发

到目前为止,通过一系列实践,我已经带你使用云开发构建了在线商城小程序,为了便于你学习和理解,项目本身并不复杂,没有使用一些依赖库。但在真实开发时,我们不可避免地要使用一些依赖库来提升开发效率。在 05 讲,俊鹏就带你了解了如何使用 WebPack 提升研发效率,接下来,我们实际感受一下。

首先,我们先来体验一下微信小程序官方 npm 的构建方法,假设我们需要使用非常有名的lodash 依赖进行开发。

打开项目目录 miniprogram/app.js,添加并使用 lodash 依赖,如下图所示:

你会发现出现这样的情况:在运行时报错,提示找不到这个依赖。所以接下来,我们需要安装这个依赖。

使用官方 npm 构建方法

当我们使用官方 npm 构建方法时,我们需要执行如下步骤。

  • 初始化NPM

在开发者工具项目目录中右键,打开内建终端(注意,定位到 miniprogram 目录)

输入以下代码,初始化 npm:

java
npm init --yes
  • 安装依赖

在终端中继续输入如下代码,安装 lodash 依赖

java
npm i lodash

这时会在 miniprogram 目录中出现 node_module 文件夹:

  • 构建 npm

接下来就来构建微信小程序 npm ,点击开发者工具------菜单栏中的工具,选择"构建npm"。

开发者工具将会为我们构建 npm,最终效果如下:

在与 node_module 目录同级中出现 miniprogram_npm 目录,这就是我们构建好的微信小程序 npm。构建完毕后,我们来重新运行一下小程序,发现并不能很好的运行,报如下错误:

这是因为微信小程序不支持 lodash 包中的一些属性,所以我们需要绕行,我们在示例中使用的是 lodash.add 方法,所以继续进行如下步骤。

再次打开终端,输入如下代码安装 lodash.add:

html
npm i lodash.add

重新使用微信 npm 构建,构建 lodash.add,效果如下:

更改项目 miniprogram/app.js 文件,将引入代码进行更改:

重新运行小程序,发现可以正常使用了:

当然,俊鹏已经详细描述了官方构建的一些问题和弊端,比如依赖代码占用体积过大、引入流程特别复杂等。的确如此,在上图控制台中我们也发现警告提示,告知 lodash 包过大,已经超过500kb了,而且在实际开发中,我们需要用到各种依赖,如此构建会加重代码包的负担。最终上传之后,效果如下:

包体积已经超过 1M,这显得非常臃肿,并且开发体验非常不好。所以我们再来体验一下WebPack 的构建方法,对比一下体会其中的优势。

使用WebPack构建方法

首先我们先删除之前官方 npm 构建的文件夹 miniprogram_npm,以及 package-lock\package.json 文件,暂时保持 app.js 代码不变,效果如下:

接下来,我们开始从这里构建 WebPack 化的小程序。

  • 更改项目结构

首先在项目根目录创建文件夹,名称为 src,并将 miniprogram、cloudfunctions 文件夹移到里面,最终效果如下:

  • 安装WebPack依赖

在项目根目录启动内建终端,初始化 npm:

java
npm init --yes

接着安装 webpack、webpack-cli、copy-webpack-plugin、clean-webpack-plugin 4个依赖:

java
npm i --save-dev webpack webpack-cli copy-webpack-plugin clean-webpack-plugin

然后再次执行,安装 babel 等相关依赖:

``java

npm i --save-dev @babel/core @babel/preset-env babel-loader

最终效果如下:

  • 处理配置文件

在项目目录中,创建 webpack.config.js 文件,并在文件中添加如下代码:

java
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin')
const {
    CleanWebpackPlugin
} = require('clean-webpack-plugin')
const srcdir = path.resolve(__dirname, 'src')
const putdir = path.resolve(__dirname, 'dist')
module.exports = {
    entry: {
        'app':'./app.js',
        'pages/cart/cart':'./pages/cart/cart.js',
        'pages/detail/detail':'./pages/detail/detail.js',
        'pages/index/index':'./pages/index/index.js',
        'pages/order/order':'./pages/order/order.js',
        'pages/submit/submit':'./pages/submit/submit.js',
    },
    output: {
        filename: '[name].js',
        path: path.resolve(putdir, 'miniprogram')
    },
    module: {
        rules: [{
            test: /\.js$/,
            use: 'babel-loader'
        }]
    },
    plugins: [
        new CleanWebpackPlugin({
            cleanStaleWebpackAssets: false,
        }),
        new CopyPlugin({
            patterns: [{
                from: path.resolve(srcdir, 'cloudfunctions'),
                to: path.resolve(putdir, 'cloudfunctions')
            }, {
                from: path.resolve(srcdir, 'miniprogram'),
                to: path.resolve(putdir, 'miniprogram'),
                globOptions: {
                    ignore: ['**/*.js'],
                }
            }]
        })
    ]
};

在项目根目录中创建文件 .babelrc ,在文件中填充如下代码:

html
{
  "presets": ["@babel/env"]
}

上述 WebPack 配置文件主要是用来描述 WebPack 如何处理我们的项目:主要通过 babel 来处理 js 文件,其余文件类型全部使用 copyplugin 插件进行复制转移即可。

Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。我们在终端中执行如下代码:

java
webpack

在日志中出现一个错误,我们在之前 app.js 中仍然保持引入的 lodash 依赖,但是并没有安装这个依赖,导致错误。所以我们要对 app.js 的文件进行修改,效果如下:

同时在终端中输入以下命令,安装 lodash 依赖:

java
	npm i lodash

完成之后重新执行 WebPack 命令,就会发现项目目录中构造出 dist 文件夹:

然后打开项目目录下 project.config.json,配置 root 根目录,如下图所示:

配置完成后,我们便能够重新在模拟器中看到小程序运行了。

  • 新增遍历 js 插件

你要注意,在我们操作演示的过程中,我们通过 babel 处理 js 文件,然后使用 copy 插件直接复制其他的文件,不做任何更改。

那怎么让 WebPack知道小程序代码中哪些地方存在 js 文件呢?我们使用配置的方式来匹配,如下图配置:

但这样意味着我们每增加一个页面,就要特定配置一次,效果并不好,所以我们可以通过写一个遍历插件来实现自动配置。

在项目根目录创建 plugin 文件夹,在文件夹内创建 loadpath.js 文件,文件中填写如下代码:

java
const path = require('path')
const fs = require('fs')
const replaceExt = require('replace-ext')
var minidir = null
function _inflateEntries (entries = [], entry) {
  const configFile = replaceExt(entry, '.json')
  const content = fs.readFileSync(configFile, 'utf8')
  const config = JSON.parse(content)
  const items = config.pages
  if (typeof items === 'object') {
    Object.values(items).forEach(item => {
      inflateEntries(entries, item)
    })
  }
}
function inflateEntries (entries, entry) {
  entry = path.resolve(minidir, entry)
  if (entry != null && !entries.includes(entry)) {
    replaceExt(entry, '.js')
    entries.push(entry)
    _inflateEntries(entries, entry)
  }
}
class loadpath {
  constructor () {
    this.entries = []
  }
  init (options) {
    minidir = path.resolve('./src/miniprogram')
    inflateEntries(this.entries, options.src)
    const output = {}
    this.entries.map(item => {
      output[replaceExt(path.relative(minidir, item), '')] = item
    })
    return output
  }
}
module.exports = loadpath

打开终端,安装 replace-ext 依赖,最终效果如下图:

java
npm i replace-ext

打开项目目录下 webpack.config.js 文件,更改如下配置:

保存后重新终端执行 webpack,效果如下:

此插件是根据 app.json 中配置的 page 内容来自动生成相应 entry 配置,达到自动遍历生成的目的。

关于 WebPack 的实战内容,我就讲这么多,关于WebPack 更多使用方法和插件用法,你可以通过 WebPack 官网学习和使用。最后,我们看一下在同样项目代码的情况下,使用 WebPack打包构建的效果:

相比于官方 npm,这大大缩小了代码包的体积,另外我们可以使用通用前端依赖使用方法来便捷的开发小程序,对我们的研发效率也有非常大的提升(关于项目最终版代码,请点击此链接下载)。

总结

今天这一讲,我详细讲解了原生的小程序本地开发、云开发后端服务的搭建,以及工程化开发转型升级,本次实战的项目并不复杂,即使是初入前端也可以轻松掌握和学习。

当然,如果你细心的话应该会注意到,在项目中有些代码我做了一些不合理的设计,所以我这次留给你的作业是:结合前 4 个模块的内容,对这一讲项目不合理处进行完善修改,并尝试在云开发服务的执行效率上,根据第 4 模块的知识点做一些优化实践。