Taro多端开发实现原理与项目实战(二)

news/2024/11/28 19:29:47/

Taro多端开发实现原理与项目实战(二)

 

多端电商平台项目概述及开发准备

学习了前面的基础知识和进阶后是否跃跃欲试?我们准备了一个电商平台的项目来和大家一起实践使用 Taro 开发电商平台。

项目概述

电商平台可以看到的部分大致分为三个方面:信息服务(商详、商品列表)、交易(下单、购物车)及支付(微信、支付宝)。电商品台应用非常广泛,大型的商家入驻平台如淘宝、天猫、京东,中型的自营电商、垂直电商为主如小米商城、华为商城、美的商城,小型如小红书。电商平台背后的商品管理系统、仓储系统、物流系统、统计系统等等,非常庞大。

电商平台希望可以多端触达用户,无论用户在什么终端什么场景,想下单随时可以下单。然而现实并不会如此完美,目前和用户产生关联的终端有:PC、移动端。移动端再细分,则有:APP、H5、小程序、快应用等。受限于开发成本现在大多电商平台依自己的规模和能力在各端上有所取舍,但主流都要有 PC、APP、小程序、H5 四个端。

如果可以写一份代码,实现多端统一,对于大多数小型公司或者个人开发者来说无疑是节约了人力和成本,目前 Taro 支持的转换有 H5、小程序(即将支持 RN、快应用),这相当于解决了主流的 APP(使用 RN ) 、小程序、H5 三端, 不同的端在构建电商平台的时候有不同的优势和劣势。

H5、小程序、RN 优劣势

优势劣势
小程序开发成本低,无需安装,触达快,学习成本低,后期迭代速度快、难度低。
可以直接唤起微信支付,在信息服务和交易方面比 H5 端要更加可靠。
因为嫁接的微信,所以入口较深。
而且微信对小程序的限制比较多,上线需要审核。
没有网络时不能使用。不能使用支付宝。
H5用户不需要下载,扫一扫就能使用,即开即用,非常方便。
H5 端在支付模块占有优势,除了可以使用微信和支付宝之外,还可以使用银联等支付方式。
信息服务方面则较为弱势。
加载速度最慢,没有网络时不能使用,刷新时会丢失用户之前的操作。
RN可实现功能多,而且是自己的平台,自己做主,不用担心第三方的审核等问题。
用户的体验最佳,流畅度高、反应速度快。
RN 端在信息服务、交易及支付方面都表现得最为良好。
开发成本比较高,维护与迭代效率较低,发布需提交多方审核,资料手续繁琐。
没有网络时能使用部分功能。

电商平台实践项目

Taro 可以一次编写,同时满足以上三端的需求,本项目从某平台的代码里抽取出来,作为 Taro 的实践项目。因为电商平台非常庞大,本项目仅实现的功能有: 首页、列表页、商品详情页、购物车、结算页、订单列表页及其后台。

在后面的第 17 至 26 章,将按购物的标准流程所涉及到的页面为大家讲解开发过程中的实践。首页和活动页作为入口,属于分流的展示型页面,逻辑比较简单,后续文章不会介绍,我们直接从商品列表页开始讲解。

小程序云

小程序云是微信团队联合腾讯云团队推出的一套小程序开发解决方案。我们采用小程序云作为我们的后台,本项目开始前我们先介绍小程序云,而后介绍小程序云搭建电商后台服务。

商品列表页

商品列表页主要是罗列商品,提供筛选,方便找到想要的商品。一般情况下,大家都会选择搜索,搜索结果也是列表的形式,相当于提供了某些筛选条件的列表页。本项目的列表页和搜索页是同一个页面,列表页里默认为综合排序,可以选择按最新、具体价格或者价格区间排序,在这个页面你将学到在电商平台里商品列表页将如何做数据筛选以及如何实现一个无限加载的方法。

商品详情页

商品详情页主要展示一个商品消费者想了解的信息,同时引导用户下单。本项目的商品详情页对商品图片在 iPhone X 和非 iPhone X 做了优化,期以给消费者更好的体验,读者可以了解为什么如此处理。同样地也对商品信息做了简单的归类,在该页面读者还可以学习到如何尝试达到自己想要的在轮播里播放视频的效果。

购物车页面

购物车页主要操作是选择将要去结算的商品,购物车页面操作比较多,涉及到多处细节,本项目的购物车将介绍数据拉取与转换、购物车状态的切换、购物车的操作逻辑等。

结算页

结算页主要提交订单的信息,交互比较多,本项目涉及到地址的操作和发票、优惠券、支付信息等,比较复杂,该章节主要讲交互动画和数据流。

授权认证

授权认证的章节主要是介绍在微信获取用户信息的情况下的一种妥协方案。

小结

虽然只是挑了几个页面来阐述本电商示例,有兴趣的读者可以自行查看本项目的代码,看看具体的实现细节。总的来说实现并没有想象地那么复杂,关键还是需要去尝试和练习。

开发准备

开始本项目前还是需要一些开发知识和准备,主要有以下几个方面:

1、需要了解基本的 React 语法,可以看本小册的第 2 章。

2、需要了解微信小程序的开发入门:申请小程序账号,安装开发者工具,看本小册的第 3 章。

3、需要安装 Taro 和了解其使用方法,看本小册的第 4 章。

4、设置好微信开发者工具,看本小册第 5 章。

5、了解 Redux 的使用,看本小册第 7 章。

本小册的电商项目大致需要以上技能,如果你已经具备了,开启我们后面的章节吧。

 

小程序云的介绍与使用

什么是小程序云

先看官方文档的说法:

小程序云是微信团队联合腾讯云团队推出的一套小程序开发解决方案。小程序云为开发者提供完整的云端流程,弱化后端和运维概念,开发者无需购买和管理底层计算资源,包括服务器、数据库、静态存储,只需使用平台提供的简易 API 进行核心业务等开发,实现快速上线和迭代,把握业务发展的黄金时期。

其实翻译过来就是,一个在小程序中使用的,不用购买服务器,不用运维的简易后端体系,主要是为了突出快和简便。所以小程序云,就非常适合那些对数据本身弱依赖的,中小型的功能性小程序使用。

组成部分

小程序云主要有几大部分组成,分别是云控制台数据库云函数云存储。以及分别在小程序端,和云端使用的 js-sdkadmin-sdk,下面逐一介绍一下。

云控制台

控制台是集成于微信开发者工具(IDE)的一个图形化界面管理终端。可以在最新版的微信开发者工具中点击 “云控制台”进入,如下图所示:

 

点击后会是这样的一个界面:

 

可以见到,分别有用户管理、数据库、文件管理、云函数、和统计分析几个板块。这些板块都是字母意思,我就不做过多解释,主要都是为了用户能有一个 IDE,可以更清晰明了地处理小程序云的相关事项。

数据库

先看下官方的话

小程序云数据库是一种 NoSQL 云端数据库,数据以 JSON 格式存储,我们在底层支持弹性可扩展、自动容灾、监控管理。

而关系型数据库和文档型数据库的术语映射关系如下表:

关系型文档型
数据库 database数据库 database
表 table集合 collection
行 row文档 document
列 column字段 field

个人感觉,用法类似于mongodb,主要是在 IDE 中使用 JS-SDKAdmin SDK 进行数据库的操作。

IDE 中数据操作是这样子的

file

在左上角可以先建一个集合,然后添加文档,向里面插入一些数据。对于每个集合,还可以设置权限,指定用户、创建者和管理者的读写权限。除此之外,还可以添加索引,以提高查询效率。

那具体在小程序中如何操作数据呢?在最新的微信者开发工具里面,可以使用 wx.cloud.database() 获取数据库实例:

// 首先先初始化小程序云
wx.cloud.init({env: 'taro-ebook-23bbcb', // 前往云控制台获取环境 IDtraceUser: true // 是否要捕捉每个用户的访问记录。设置为 true,用户可在管理端看到用户访问记录
})// 获取数据库实例
const db = wx.cloud.database()// 获取指定集合
const shopCollection = db.collection('shop')// 获取数据
shopCollection.get({success: res => {console.log(res.data)},fail: err => {console.log(err.errCode, err.errMsg)}
})// 使用 Promise 语法,也可以使用async/await
db.collection('goods').get().then(res => {// 取数据,res.data 即是上述例子数据console.log(res.data)
}).catch(err => {console.log(err.errCode, err.errMsg)
})

除此之外,在服务端,也可以使用类似的 API 进行数据库操作。更多的 API 使用方法可以见文档。

云函数

云函数即在云端(服务器端)运行的函数。在物理设计上,一个云函数可由多个文件组成,占用一定量的 CPU 内存等计算资源;各云函数完全独立;可分别部署在不同的地区。开发者无需购买、搭建服务器,只需编写函数代码并部署到云端即可在小程序端调用,同时云函数之间也可互相调用。

简单来说,就是将一个函数,也可以理解为一个入口放在 Node.js 环境(即服务端环境)下运行,各个小程序之间相互独立,可以在小程序中,或者在另一个云函数中运行。

其实就是将后端代码颗粒化了,以函数为单位,可以一个个地分别部署,而且不受服务器等的限制,显得十分简便快捷。

代码示例:

// 这个云函数将传入的 a 和 b 相加返回exports.main = (event, context) => {console.log(event)console.log(context)return {event,sum: event.a + event.b}
}

云函数的传入参数有两个,一个是 event 对象,一个是 context 对象。

  • event对象是函数调用时传入的参数,通过此 event对象,代码将与触发函数的事件(event)交互,外加后端自动注入的小程序用户的 OpenID 和小程序的 App ID
  • context 对象包含了此处调用的调用信息和运行状态,可以用它来了解服务运行的情况

函数调用:

wx.cloud.callFunction({// 要调用的云函数名称name: 'add',// 传递给云函数的参数data: {x: 1,y: 2,},success: res => {// output: res.result === 3},fail: err => {// handle error},complete: () => {// ...}
})

name指的是函数的名称,data即上述所说的event入参。更具体的使用和部署可以参照文档,在这里就不多作解释了。

云存储

云存储提供高可用、高稳定、强安全的云端存储服务,支持任意数量和形式的非结构化数据存储,如视频和图片,并在控制台进行可视化管理。

资源的地方,可以当做是一个 CDN。

 

同样的,可以在控制台中对各种资源进行操作,包括上传、下载文件,权限设置等。上面的图片一目了然。

实战使用

这里只作一个使用流程的讲解,不会过于强调细节。

  • 首先,你的小程序需要根据官方文档开通小程序云,下载最新的 开发者工具,然后就可以打开云控制台,在云控制台中获取到环境名称和环境 ID,用于小程序中,小程序云的初始化。
  • 在数据库中建立相关的数据集合(即数据表),插入一些数据,同时设置数据权限。
  • 在小程序中,或者云函数中进行数据库操作,拿到所需的数据。

如此一来整个小程序的前后端流程便打通了,不用再通过拉接口什么的来获取数据,而是直接在小程序中获取数据,可以说是很大的简化了开发流程。

小结

小程序云用起来还是比较便捷的,省去了后台搭建或是后台接口调试的流程,对于一些中小型小程序或是一些独立开发者是非常友好的。下一篇,我们将讲述如何使用小程序云开发电商小程序。

通过小程序云搭建电商后台服务

上一章已经讲述了小程序云及其使用方法,在这里,本章主要讲解如何通过小程云搭建一个电商的后台服务。本章不会拘泥于每个数据集合的具体字段或是具体代码,而是主要介绍一个电商后台搭建思路,以及前后端的交互方式。正所谓授人以鱼不如授人以渔。

后台服务搭建思路

首先,我们知道一个最简单的后端程序就是,开启一个HTTP服务,连接上数据库,然后根据收到的请求进行相关操作,例如数据库的增删查改,返回HTML,返回接口数据之类,如果要外网访问还要部署上线等等。

而用上了小程序云之后,因为云函数这个概念,我们免去了开启服务器和部署的步骤。同时,小程序是天然前后端分离的,也不需要返回HTML。所以在这种情况下,我们所搭建的后台服务最主要为了实现两个部分的内容,分别是数据库的建立前后端的数据交互

数据库建立

数据库建立,指的是数据集合及一些初始数据的创建。在我们搭建的这个 Demo 里,主要是有以下数据集合。

 

  • Information - 首页的资讯数据集,主要是以一个资讯为单位的数据集合,一个资讯可能含有商店图片,商店介绍,商品介绍等,主要作导购作用,点击后引导至相关页面。
  • Shop - 商店页的数据集,以商店为单位,一个商店页面里主要是各种楼层数据。
  • Commodity - 商品的数据集,显然,一个商品数据自然就是该商品所需要的各种信息。
  • Cart - 购物车的数据集,以用户为单位存放购物车数据。
  • Order - 订单的数据集,以用户为单位存放订单数据。
  • User - 用户信息的数据集,存放用户信息数据。

上面所讲述的 6个数据集,基本就涵盖了一个最简单的电商所需要的各种数据,可以构成一个完整的购物流程。

同时如下图,还可以设置数据集权限。例如将InformationShopCommodity设置为所有用户可读、仅管理员可写;将CartOrderUser改成仅创建者及管理员可读写。通过权限限制,增加了数据集的可靠性。

 

数据交互

数据集建立起来后,再往里面填充一些假数据,基本的数据就有了,那么在小程序中如何进行数据交互?

如果不是用小程序云,自然是通过request拉取接口数据,进行展示。而在使用了小程序云的情况下,通过官方提供的sdk,主要有两种办法进行数据拉取:

  1. 直接在小程序端操作数据库,获取所需数据,并进行增删查改等操作
  2. 使用云函数,把数据库的操作放到云端;然后在小程序端调用云函数,达到类似调用接口的效果

第一种方法其实比较适合一些简单的,对数据要求不高,量也不大的小程序。不然在小程序的代码中混合着数据库操作,实践起来不太优雅,也不利于维护。

这里重点说下第二种方法。上篇文章有提到了云函数的概念,这里再回顾一下,所谓云函数,就是将一个函数放在 Node.js(即服务端)环境下运行。因此,我们可以将数据库的操作放到云函数中执行,然后在小程序中调用云函数,达到一种类似调用接口的效果。先看云函数的目录:

├── demo                          代码目录
|   ├── client                    小程序代码目录
|       ├── ...            
|   ├── cloud                     小程序云相关代码目录
|       ├── functions             云函数相关目录 
|           ├── shop              shop 云函数目录
|               ├── index.js      入口函数
|               ├── getShop.js    getShop.js
|               ├── package.json
|               ├── ...
|   ├── project.config.json       小程序配置文件
|   ├── tcb.json                  小程序云配置文件
└── README.md                     readme 文件

这是这个整个 Demo 的目录结构,名字叫 shop 的云函数的具体目录在cloud/functions/shop下,可以见到有一个入口文件index.js,还有其它的子函数。下面看具体代码:

// index.js
const app = require('tcb-admin-node')   // 官方提供的node-sdkconst { getInformation } = require('./getInformation.js')
const { getShop } = require('./getShop.js')app.init({envName: 'taro-ebook-23bbcb',mpAppId: 'wx9504f077bdc24ea2',
})exports.main = async (event) => {const db = app.database()const { func, data } = eventlet resif (func === 'getInformation') {res = await getInformation(db)} else if (func === 'getShop') {res = await getShop(db, data)}return {data: res}
}
// 入口函数引用到的getShop函数
// getShop.js
async function getShop (db, venderId) {const shopColl = db.collection('shop')const commColl = db.collection('commodity')const _ = db.commandconst res = await shopColl.where({venderId: _.eq(Number(venderId))}).get()const shopData = res.data[0]const floors = await Promise.all(shopData.floors.map(async (floor) => {let res = await commColl.where({skuId: _.in(floor.commodities)}).get()floor.commodities = res.datareturn floor}))shopData.floors = floorsreturn shopData
}exports.getShop = getShop

在这个例子中,笔者将一个云函数当成一个模块相关函数的入口,根据函数传入的参数来决定调用哪个函数。而被具体调用的函数,执行的是一些数据的操作,返回数据。也就是说,在这个 Demo 里一个云函数是一个数据模块的入口,里面引用了许多待被调用的具体函数,视入参而定

以数据模块为单位分割云函数,是笔者觉得比较好的做法。一来云函数不必分割得太细,毕竟每个云函数都是独立部署的,省去了一些繁琐的操作;二来以数据模块为单位,就有点类似我们传统后端的 MVC 模式,易于开发者无缝接入。当然,这只是其中的一种云函数代码组织方式,并不代表就一定要遵循这样的方式,具体情况具体分析,还是要结合业务的实际情况。

而具体到小程序的调用,就更简单了,只是想请求接口的操作改为调用云函数的操作。比如:

// 在小程序端,假如请求接口,我们是这样调用
const res = await wx.request({url: 'https://api.test.com/shop/xxx',data: {a: 'foo',b: 'bar'}
})// 这是调用云函数的情况,调用刚才说到的shop云函数
const res = await wx.cloud.callFunction({name: 'shop',data: {func: 'getShop',data: {a: 'foo',b: 'bar'}}
})

可以见到,仅仅是将调用wx.request改为调用wx.cloud.callFunction,其它的地方并不需要多改变什么。返回的数据也是可以自己定义的,所以说是达到了调用接口的效果。

不过云函数有一个缺点,就是每次都要上传部署后才能被小程序端调用,调试测试起来略显麻烦。一个比较好的调试方法是添加一个测试函数,在本地环境中使用 Node.js 进行测试。

 

而官方也在 IDE 中提供了一个测试的方法,同时可以自行定制模板,左边还能看到每次云函数的调用状态。

 

综上,使用小程序云进行数据交互并不复杂,甚至可以说是简单快捷,极大地加速了开发一整套全栈小程序的效率。

小结

使用小程序云开发电商后台服务与传统的开发方式相比,优势是显而易见的。免去了服务器部署,运维等步骤,为一些不太熟悉后台开发方面的前端开发者提供了一个快速上手,搭建后台服务的方式。有需求的读者可以使用起来。

 

商品列表页开发及性能优化

Taro 是一个高效的多端框架,在列表渲染时内部会对图片进行懒加载处理,对 diff 函数有做优化。本节会给大家分享一下如何使用 Taro 开发商品列表页及性能优化。

案例项目里的搜索列表页包含的组件有:搜索、筛选条件、搜索结果列表等。 这里主要讲搜索列表。

涉及的知识点如下:

  • 列表渲染
  • 数据请求 (Taro.request)
  • 分页的实现

实现的页面

从上图可以看出整个搜索列表页的结构,笔者分成了 6个组件:

  • 吸顶的搜索头部
  • 热搜
  • 历史搜索
  • 搜索条件
  • 商品列表展示
  • 搜索结果状态

从布局结构可以看出,整个搜索列表页的实现难度主要集中在数据拉取与展示搜索条件选项的选择

列表渲染

使用 Taro 渲染列表,类似于 JavaScript 中通过 map 将数组转化为一个数列相似,我们可以使用如下:

  const { list } = this.stateconst renderList = list.map((item, index) => {return (<View className='item' key={index}>{item.text}</View>)})return (  <View className='jingo_detail_content'>{renderList}</View>)

key 作为索引,它必须满足三个条件:稳定、可预测、唯一。 建议尽可能在使用 map 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

数据请求

Taro.request 用法和 wx.request 差不多,支持 promise 和跨域。 由于筛选条件和前面拆分的组件相对较多,并且组件之间的通信较为繁琐,所以用 Redux 作为数据状态管理工具。

import Taro from '@tarojs/taro'Taro.request({url,data: {},header: {'content-type': 'application/json'}
}).then(res => console.log(res.data))

在进行数据搜索的时候,先进行 Dispatch(actionMap'requestSearch'),这样可以更新组件的状态,显示搜索结果,关闭热搜和历史搜索。

分页

本项目采用的是后端进行分页,前端通过滚动到底部触发回调函数去发起请求。然后将请求到的新数据加入 List 中。

滚动到底部触发的事件有两种方式:

  • ScrollView 中可以绑定 onReachBottom
  • Taro 中页面的 onReachBottom 事件,仅在小程序中支持。
 onReachBottom () {this.goSearchWords({page: searchResult.page + 1})}

可以设置 onReachBottomDistance 属性,距离底部稍远开始触发事件:

  config = {navigationBarTitleText: '搜索列表',onReachBottomDistance: 300,}

筛选条件选择

 

根据页面我们可以看出有很多筛选方式,笔者在 Redux 中提供了很多种状态,这样在处理每个流程就会很清晰,互不影响。

比如更新搜索结果如下:

  [RECEIVE_SEARCH_RESULT] (state, action) {return {...state,searchResult: action.payload}}

解决列表页数据长度

刚开始所有商品平铺,拉接口分页展示,但这样展示下来会有一问题,就是随着数据量的增加会造成invokeWebviewMethod 数据传输长度为 1227297 已经超过最大长度 1048576。超出的数据将无法继续渲染的问题。为了解决这个问题,总结以下三个方案:

方案一: 去掉不必要的字段

这种方法其实很简单,就是对每个商品对应的参数进行处理,如本章开头的效果图所示,我们需要的内容就是:图片、商品名称、描述、商品 ID、价格,再看下接口返回的数据如下:我们可以过滤参数,只取我们需要的。

 

商品详情页面开发

 

上一章我们已经学习了列表页的开发,从列表页面里选择感兴趣的商品,进入商品详情页,查看商品的相关信息。本章我们主要讲商品详情页的开发。

商品详情页在电商应用里扮演一个重要的角色,它可以是简单的也可以是复杂的。说重要是因为它是黄金购物流程上的重要一环,首页、搜索页、列表页、营销活动最后的落地页都是商品详情页;简单的可以是由几张商品图和文字说明及加一个「加入购物车」按钮或者一个「去结算」按钮就算详情页了,复杂的详情页,包含页面展示商品图、商品视频、商品属性、商品介绍、评论、商品的关联商品、商品套餐、预售、抢购、历史浏览、推荐购买等,非常复杂。

页面布局

本项目的商品详情页比较简单,只有展示商品图、商品视频、商品属性、商品介绍,没有预售和抢购状态,也没有评论,我们先看一下效果图。

 

按模块分有商品主图、介绍、店铺信息、加车浮层四个模块,店铺信息模块比较简单,我们不讲解,其余三个模块我们逐个讲解。

商品主图模块

商品主图是一个轮播模块,有商品图和视频组成,其中商品图又有长图和方图。小程序有个 Swiper 组件,可以方便地实现该需求。

<Swiper className='detail_swiper'indicator-dots='true'indicator-color='#ddd'indicator-active-color='#232323'current='0'interval='3000'duration='300'circular='true'onChange={this.swpierChange} >{srcs.map((item) => {return <SwiperItem className='detail_swiper_item'><Image src={item} onClick={this.swiperClick} mode='widthFix' /></SwiperItem>})}{mainVideoImgUrl && <SwiperItem className='detail_swiper_item detail_swiper_item_video' >{this.state.isPlayVideo ? <Video className={ isIpx ? 'detail_video detail_video_ipx' : 'detail_video' } src={mainVideoPath} autoplay='true' show-fullscreen-btn='true' /> : <Image className='detail_video_cover' onClick={this.playVideo} src={mainVideoImgUrl} mode='widthFix' />}</SwiperItem>}
</Swiper>

这个模块宽高是 750x960,在 iPhone X 下,是全屏的宽度,按等比例缩放在高度也是恰到好处可以看到商品的标题和价格,但是在非 iPhone X 机器下高度就差不多一屏了,首屏看不到商品的标题和价格,为了更好地体验,两边留白 40px ,宽高为 670x860。

轮播模块放视频,会存在一些问题,从文档里 Video 组件 说明的底部的 Bug&Tip 可以看到:

  1. Tip: Video 组件是由客户端创建的原生组件,它的层级是最高的,不能通过 z-index 控制层级。
  2. Tip: 请勿在 scroll-view、Swiper、picker-view、movable-view 中使用 Video 组件。
  3. Tip: CSS 动画对 Video 组件无效。

文档里没有说为什么不能在 Swiper 里用,所以只能自己尝试了,试了一下才发现,如果在 Swiper 里使用就会导致视频一直存在,也是符合 Tip 1 里所说的 Video 组件是由客户端创建的原生组件,它的层级最高,所以已经超出了 Swiper 组件本身了。

那我们有没有办法实现业务方的需求呢?我们看一下视频在 Swiper 下的 Bug,发现即使视频一直存在也是只存在于 Swiper 区域,那么我们控制它显示和隐藏是不是就可以达到目的。最后的实现是渲染主图轮播的时候,在显示视频的那一帧放入一张视频封面,点击封面的时候,播放视频,切换轮播的时候关闭视频显示封面。

// jsx
<Swiper className='detail_swiper' onChange={this.swpierChange} >{mainVideoImgUrl && <SwiperItem className='detail_swiper_item detail_swiper_item_video' >{this.state.isPlayVideo ? <Video className={ isIpx ? 'detail_video detail_video_ipx' : 'detail_video' } src={mainVideoPath} autoplay='true' show-fullscreen-btn='true' /> : <Image className='detail_video_cover' onClick={this.playVideo} src={mainVideoImgUrl} mode='widthFix' />}</SwiperItem>}
</Swiper>// js 切换的时候把视频关了
swpierChange = (e) => {if (this.state.isPlayVideo) {this.setState({isPlayVideo: false})}
}

商品介绍

商品介绍分四个小模块:商品描述、编辑笔记、尺寸说明、服务说明,需要控制它保证只有一个模块的状态是打开的,这个比较简单。

this.state = {detailInfoRow: [{name: 'desc',title: '商品描述',open: true},{name: 'editor',title: '编辑笔记',open: false},{name: 'size',title: '尺寸说明',open: false},{name: 'service',title: '服务说明',open: false}]
}// jsx
{detailInfoRowCopy.map((item, idx) => {return <View className='detail_intro'>{item.show && <View className={'detail_intro_row ' + item.name}><View className='detail_intro_hd' data-id={item.name} onClick={this.toggleFold.bind(this, idx)}><Text className='detail_intro_title'>{item.title}</Text><Text className='detail_intro_title_icon'>{item.open ? '-':'+'}</Text></View>{item.open && idx < 3 && <View className='detail_intro_bd'>...内容</View>}</View>
})}// 控制显隐
toggleFold = (idx, e) => {this.state.detailInfoRow[idx].open = !this.state.detailInfoRow[idx].openthis.setState({})
}

加车浮层

加车浮层看着简单,要展示的内容还是挺丰富的,我们先来看效果图:

 

了解一下模块的交互,然后我们挑几个要点来讲:

  • 点击加入购物车唤起浮层
  • 浮层有商品信息区:显示小图、价格、商品编号
  • 浮层有地区及运费,自营运费固定,非自营运费由接口取得
  • 优惠券信息,显示当前商品可用优惠券,有多张优惠券时,最多展示 2行,点击券说明下拉展示全部优惠券,每个券占一行,每行展示满减金额及使用限制及限制说明文案。无可用券则隐藏,可用优惠券这一区域
  • 销售属性,按类目显示参数名称「颜色」「尺码」等;单 SKU 时显示 SKU 的参数并选中;SPU 时当前 SKU 的参数为选中状态,从搜索或列表进入时默认选中主 SKU,其他 SKU 的参数可选和不可选,无货或参数不全时不可选;点击可点参数时相当于切换 SKU,层内商品图、价格、编号 、运费、优惠券数据刷新
  • 尺码有三种状态:有货未选择:“黑字白底”按钮,有货已选择:“白字黑底”按钮,无货未选择:“深灰字浅灰底”按钮
  • 关闭加车浮层,判断层内 SKU 与层外是否一至,不一至则刷新

浮层

唤起加车浮层的时候,我们在页面里使用一个 View 来做背景半透明的蒙层,当点击浮层以外的区域就把浮层关闭,其实浮层以外的区域就是蒙层,我们可以在蒙层上设置点击事件,点击就关闭浮层。

唤起浮层出现蒙层,然后上下滑动页面,页面还是会滚动的,因为页面的内容大于一屏,存在滚动条,要处理这个问题需要在根节点使用 ScrollView 组件把内容包起来,然后当唤起浮层的时候设置 scrollY 为 false。这样当出现浮层的时候,整个页面就不能滚动了。

<ScrollView style={viewStyle} scrollY={!this.state.addCartLayer} enableBackToTop={true}><View>这里是内容......</View>
</ScrollView>

商品属性切换

当我们切换商品的颜色的时候,其实是切换了一个新的商品,而商品的唯一 ID 就是 skuId , 因此商品属性我们把 skuId 打在选项 Radio 的属性上,当点击改选项的时候获取该点击的对象里的值,然后拉取新的数据重新渲染浮层里的内容。

// jsx
<RadioGroup className='radiogroup' onChange={this.spuChange}>{item.props && item.props.map(prop => {return <Label className={prop.className}><Radio className='radio' value={prop.skuId} checked={prop.selected} />{prop.name}</Label>}) }
</RadioGroup>
// js
spuChange (e) => {let skuId = e.detail.value // 拿到skuId
}

属性切换的时候数据发生变化,按钮的样式也会跟着变化,按钮有三个状态:正常的灰边白色背景黑色文字,选中态为黑背景白字,无货的状态为灰背景灰字,稍微复杂,我们使用样式的类名来控制。

小结

商品的信息都是从接口拉取出来然后展示,虽然比较丰富,但都是展示型的内容,相对来说还是比较简单,这里就不一一介绍了,大家可以移步 Demo 看看实现代码。

在下一章节中我们将介绍购物车的开发。

总结: 这种方式如果数据库商品不多,是可以这样进行处理,但是当分页一直进行下去,总会超出数据的传输长度,所以还是未能从根本上解决这个问题。

方案二:二维数组

这种方法是在微信开发者论坛上找到的,解决方案的链接,这种方案测试还是可行的,测试加载了几十页都没问题。

方案三:大分页思想

大分页思想其实很简单,就是在下拉列表记录当前分页,达到 10页的时候,就以 10页为分割点,将当前 setState 的 List 取分割点后面的数据,判断滚动向前滚动就将前面数据 setState 进去,流程图如下:

file

按照上面的流程解决了数据分页问题,但是当页面往回滚动的时候,需要将 List 重新 setState,目前采用的是滚动距离顶部小于 500 时,就判定用户在进行上滚操作,List 取上一个大分页的数据。

结论

通过上述的方案,方案二其实最符合要求,方案三会有个切换数据卡顿的现象。

 

购物车开发

购物车可以说是整个购物流程里十分重要的模块了,作为从选购到结算的过渡流程,整体逻辑上还是有点复杂的。下面来给大家展示一下如何使用 Taro 开发一个购物车模块。

页面布局

从这个图便可以看出整个购物车的页面结构了:

  • 以店铺为单位的商品模块,一个店铺的商品放在一块,左边有单选与店铺的全选。
  • 一个吸底的结算栏,也是有全选功能,同时显示总价格,还有去结算的功能。
  • 右上角也有一个吸顶的编辑栏,同时还有优惠券的入口。点击编辑会令购物车变成编辑状态,即可以选择数量、尺码、颜色等。
  • 商品模块后面,下面还会有一个最近浏览的模块,展示最近浏览的商品。

从布局结构就可以看出,整个购物车模块的实现难度主要集中在数据的拉取与显示选购状态与编辑状态的切换商品的编辑逻辑(数量加减,尺码选择,选择与全选等)商品的不同状态(例如补货中是不用被编辑的)。除此之外,还有一些细节的地方需要注意,例如登录态的判断,无商品时的显示等,总而言之,不是一个简单的页面,承载着各种各样的逻辑。

数据拉取与展示

页面比较复杂,我们使用了 Redux 进行开发,以便更好地管理组件之间的状态,同时为了处理异步请求和开发调试,追踪 actions,还用了 redux-thunk 和 redux-logger 中间件。

如果单纯是拉取数据然后渲染出来,那也没什么好讲的。然而,在很多情况下,接口返回的数据和页面显示的数据并不是完全对应的,往往需要再做一层预处理。如下面的例子:

file

例如上图红色方框框住的部分,接口返回的数据是:

{code: 0,data: {shopMap: {...}, // 存放购物车里商品的店铺信息的 mapgoods: {...}, // 购物车里的商品信息...}...
}

对的,购车里的商品店铺和商品是放在两个对象里面的,但视图要求它们要显示在一起。这时候,如果直接将返回的数据存到 store,然后在组件内部 render 的时候东拼西凑,将两者信息匹配,再做显示的话,会显得组件内部的逻辑十分的混乱,不够纯粹。

所以,我个人比较推荐的做法是,在接口返回数据之后,直接将其处理为与页面显示对应的数据,然后再 dispatch 处理后的数据,相当于做了一层拦截,像下面这样:

const data = result.data // result 为接口返回的数据
const cartData = handleCartData(data) // handleCartData 为处理数据的函数
dispatch({type: 'RECEIVE_CART', payload: cartData}) // dispatch 处理过后的函数...
// handleCartData 处理后的数据
{commoditys: [{shop: {...}, // 商品店铺的信息goods: {...}, // 对应商品信息}, ...]
}

可以见到,处理数据的流程在 render 前被拦截处理了,将对应的商品店铺和商品放在了一个对象里。

这样做有如下几个好处:

  • 一个是组件的渲染更纯粹,在组件内部不用再关心如何将数据修改而满足视图要求,只需关心组件本身的逻辑,例如点击事件,用户交互等

  • 二是数据的流动更可控后台数据 ——> 拦截处理 ——> 期望的数据结构 ——> 组件,假如后台返回的数据有变动,我们要做的只是改变 handleCartData 函数里面的逻辑,不用改动组件内部的逻辑。

实际上,不只是后台数据返回的时候,其它数据结构需要变动的时候都可以做一层数据拦截,拦截的时机也可以根据业务逻辑调整,重点是要让组件内部本身不关心数据与视图是否对应,只专注于内部交互的逻辑,这也很符合 React 本身的初衷,数据驱动视图

购物车状态的切换

布局里有说到,右上角有个吸顶的编辑按钮,主要用于切换到购物车的编辑状态。见下图,商品可以加减数量,选择尺寸,底栏变成了删除按钮,大体如同 PC 的购物车,只是交互方式稍微有一些差异。

这里的难点是视图中同一个位置,在不同的购物车状态下有不同的显示和不同的功能。例如全选,在正常状态下是选择结算,在编辑状态下是选择删除;还有商品图片右边的部分变成了编辑栏。下面用全选的逻辑代码举个例子:

// 点击全选时的函数
checkShopCart = (commodity, e) => {const {shop, skus} = commodityconst {isEditStatus, fetchCheckCart, fetchInvertCheckCart, inverseCheckDelCart, checkDelCart} = this.propsconst skusArr = []// 重点在这里,isEditStatus 是全局的判断购物车状态值if (isEditStatus) {skus.forEach((sku) => { skusArr.push({ id: sku.skuId })})shop.checkDelAll ? inverseCheckDelCart(skusArr) : checkDelCart(skusArr)} else {skus.forEach((sku) => {// 判断商品是否缺货if (!sku.isOutOfStock) {skusArr.push({id: sku.skuId})}})shop.checkAll ? fetchInvertCheckCart(skusArr) : fetchCheckCart(skusArr)}}

在整个页面的 redux 里,用了一个 isEditStatus 变量,用于判断当前的页面状态,然后通过 connect 注入到该页面中。所以在用户点击了这个按钮后,会有一个对 isEditStatus 值的判断,从而执行不同的逻辑。聪明的你应该还会发现,还有一个对 checkDelAll 和 checkAll 的值的判断,这里是用于判断当前店铺是否被全选,然后执行商品的全选 or 反选逻辑。

除了事件点击后会根据页面当前状态而发生不同的逻辑以外,界面也会作出相应的变化,如:

// 伪代码
render() {const {isEditStatus} = this.propsreturn <View>{!isEditStatus ? < 非编辑态 /> : < 编辑态 />}</View>
}

总的来说,就是根据不同的状态做不同的事情,与其说是难,倒不如说是繁琐。因为里面涉及的状态,操作会比较多,所以需要细心对应好才能不出错。

购物车的操作逻辑

所谓的操作逻辑,指的就是加车,改变尺寸,删除,改变数量等。实际上,实现这些逻辑其实没有什么很通用的做法,都是跟业务强耦合的,在前端的角度,就是和后端的接口及返回的数据是强耦合的。因为后端接口是如何设计的,返回怎样的数据就直接会决定你的页面逻辑是怎样的。

具体到我们这个项目里,每进行一次操作,都需要请求一次接口,然后返回全新的购物车数据,重新渲染。结合上面的数据的拉取与展示部分,可以得到,每次操作后的完整流程是:操作购物车——> 拉取接口 ——> 后台数据 ——> 拦截处理 ——> 期望的数据结构 ——> 组件渲染

可以看出,这个流程的每一环都是相互独立的,互不影响,这样就很大程度地减少了整个购物车逻辑的复杂性。例如我们要增加数量,或者是减少数量等,都只是拉取不同的接口就好了,后面的数据处理,渲染等并不用作其它的处理,都是一样的逻辑。

对于组件渲染的部分,要做的就是只是确保什么状态对应什么视图,例如这个按钮是否被选中,是否有缺货的显示等等。

另外还有值得注意的是,改变尺寸这个操作是会有一个弹窗的,用来选择需要哪种尺寸,如下图:

 

这里的数据拉取其实脱离上述的操作流程的,或者说,是属于另一个操作流程,不是购物车渲染这块。但是整体的处理和逻辑是类似的,这里就不细说了(这里有个坑,具体可以看《Taro 开发说明与注意事项》章节),重点是要了解这种思想,把页面交互,数据处理与渲染逻辑尽可能地解耦,这样才可以让我们符合数据驱动视图的思想。

其它细节

开发一个购物车页面,除了主要的购物车的显示与操作,还有很多其它细节是需要注意的:

  • 例如未登录时,顶部会有一个去登录的吸顶栏,点击优惠券和底部去结算时都是会跳到登录页面
  • 没有商品时,编辑按钮是不会出现的
  • 加车的数量最多是 200,超过后会自动变为 200,同时 1 个数量再减意味着是 0,即是删除
  • 选择了商品后,底部的结算(删除)按钮会变色,同时会显示数量,超过 99 会显示 99+
  • ......

这些细节相关的都不是很难,而是有点繁琐,重点还是之前所说的,要耐心,细心,具体还是需要在开发中慢慢积累,正所谓熟能生巧。

小结

本文从页面布局、数据拉取与切换、购物车状态的切换、购物车的操作逻辑和其他细节这 5个方面阐述了如何使用 Taro 开发购物车。主要是从原理,架构的角度去剖析一个购物车页面的开发,并没有过于针对某些代码细节。一个是代码太长太多,全部贴出来解释也不现实;二是代码始终是与业务强耦合的,而思想,方法才是可以不断复用的东西,授人以鱼不如授人以渔。

小结

本文从页面布局、数据拉取、列表筛选等方面阐述了如何使用 Taro 开发搜索列表页。主要是从原理,架构的角度去剖析一个列表页面的开发,并没有过于针对某些代码细节。一个是代码太长太多,全部贴出来解释也不现实;主要难点在于数据的无限加载。

在下一章节中我们将介绍商品详情页开发。

 

结算页面开发

上一章我们讲解了购物车页面,而在购物车页面点击「去结算」便会进入结算页。结算页也是在购物流程里十分重要的页面,一般用户会在这里选择收货地址、付款方式、发票等,并进行最终的付款流程。本章我们将进行结算页的讲解。

页面布局

先来看一眼页面结构:

file

可以见到,整个页面的布局分为三大块内容,分别是:

  • 收货地址,结算商品模块;主要是选择收货地址和结算商品的展示
  • 支付信息模块,包括支付方式,优惠券等;主要是支付方式的选择以及优惠券的使用
  • 结算总金额模块,展示支付总额。

在 JSX 上,页面结构如下所示,十分清晰:

<View className='balance'><View className='balance_good'>...</View><View className='balance_pay'>...</View><View className='balance_amount'>...</View><View className='balance_bottom'>...</View>
</View>

从布局可以看出,页面的整体逻辑并不是特别复杂,逻辑比较集中的地方是结算商品数据的拉取与显示地址,优惠券,支付方式的选择支付逻辑吸底地址栏与弹出层的动画实现。不过在第 18 章 《通过小程序云搭建电商后台服务》一文中已提到,我们实现的只是最简单的电商流程,所以 Demo 中并没有实现地址模块与优惠券模块,在这个页面里显示的相关内容只是虚拟数据。因此,下面重点讲述的是数据的拉取与显示动画实现

结算商品的数据拉取与显示

 

上一节中说过,有一些模块采用的是假数据,而真正从数据中拉取数据的是红色框框住的部分,主要是需要结算的商品数据与结算底栏。仅仅从页面结构中也可以看出,相较于购物车页,这里的逻辑是十分简单的,仅仅就是拉取数据,渲染数据就行了。所以我们要做的主要工作是数据的处理。

const data = result.data // result 为接口返回的数据
const balanceData= handleBalanceData(data) // handleCartData 为处理数据的函数
dispatch({type: 'RECEIVE_BALANCE_INFO', payload: balanceData}) // dispatch 处理过后的函数...
// handleBalanceData 处理后的数据
{payCommodities: [{shop: {...},  // 商品店铺的信息goods: {...}, // 对应商品信息}, ...],isNeedBanlance,   // 是否需要结算payNum,           // 总的商品数量totalPrice        // 总结算金额
}

看过我们上一章购物车的读者应该都知道,这里的处理是类似的,都是数据渲染前的预处理。将接口拉到的结算页数据处理为方便直接渲染的结构,分别是 结算商品数据总的商品数量总结算金额,对应着上图两个红框。isNeedBanlance 这个值比较特殊,主要是为了预防一些意外的情况,例如在没有结算商品的情况下进入了结算页,这时候这个值就是 false,然后会进行一个处理,自动跳转到购物车页。

页面动画实现

动画这个,在前面的章节几乎完全没有提及,但其实每个页面或多或少都会有些动画效果,只是受限于篇幅没有讲解。现在结算页逻辑不算太复杂,所以我们在这里就主要说下该页面的两个动画,分别是吸底地址栏浮层弹出动画

吸底地址栏

在 GIF 中可以看到,滚动到顶部时,地址栏渐隐;滚动到非顶部时,出现吸底地址栏,并且有渐现动画。显然,我们需要小程序的 onPageScroll API,用于监听页面的滚动事件,函数部分代码如下:

onPageScroll (e) {// e.scrollTop 为 0,且 this.state.reachTop 为 falseif (!e.scrollTop && !this.state.reachTop) {this.setState({reachTop: true})}if (this.state.reachTop) {this.setState({reachTop: false})}
}// ...JSX
<View className={reachTop ? 'balance_addr top' : 'balance_addr'}><View className='balance_addr_icon' /><Text className='balance_addr_text'> 广东省深圳市宝安区龙光世纪大厦 </Text>
</View>// ...scss
balance_addr {// ....opacity: 1;transition: opacity 300ms ease;&.top {opacity: 0;}
}

在 e.scrollTop 为 0,且 this.state.reachTop 为 false 时,将 this.state.reachTop 设置为 true,说明已经到达顶部。同时可以在 JSX 里见到,在到达顶部时地址栏容器的类名会多一个 top,将透明度变为 0,再配合 transition 展示出渐隐渐现的动画效果。

选项弹出层 - ActionSheet

 

通过 GIF 可以看到,这个动画比吸底地址栏的渐隐渐现稍显复杂,主要包含 ActionSheet 的进场动画和离场动画。如果只通过一个状态控制浮层的展示与隐藏,并不能实现在进场和离场时都有动画,例如入下面的代码:

showMask && <View className='paybox'>...</View>

这样会发现,在控制隐藏时,浮层是直接从 DOM 结构中移除的,会导致没有隐藏部分的动画。

所以,我们用了两个状态控制,分别用于控制 DOM 的隐藏及离场的动画显示。代码如下:

showPayWay &&
<View className={aniShowPayWay ? 'balance_pay_choose show' : 'balance_pay_choose'}><View className='mask' onAnimationEnd={this.onAnimationEnd}></View><View className='main'>...</View>
</View>// onAnimationEnd
// 监听隐藏动画已结束,再将浮层从 DOM 中移除
onAnimationEnd () {if (!this.state.aniShowPayWay) {this.setState({showPayWay: false})}
}// ..scss
&.show {opacity: 1;z-index: 10;.mask {animation-name: showPanelMask;}.main {animation-name: showDetailWrapper}
}
.mask {animation-name: hidePanelMask;
}
.main {animation-name: hideDetailWrapper;
}

aniShowPayWay 用于控制进场动画和离场动画,可以见到,是否有 show 样式类名会决定执行的是进场还是离场动画。同时对动画进行事件监听,假如是离场动画结束(this.state.aniShowPayWay 为 false),则隐藏 DOM。至此,进场离场的动画便能完成实现。

支付模块

支付模块可以说是结算页面的最主要的部分了,一个普通的电商程序的支付流程一般如下:

  1. 提交订单请求,后台生成订单,同时返回生成的订单号
  2. 调取微信接口取得用户参数,同时请求生成支付相关信息的接口,而后台返回数据包,签名数据等
  3. 根据后台返回的支付数据,调取微信小程序 Taro.requestPayment API,唤起支付

也就是说,这里请求了两次,先是请求生成订单并得到订单号;第二次根据订单号和用户信息,再次请求,获得调取支付的各种信息,最后再调取支付。

这里值得注意的点就是第二次请求需要支付相关的 userinfoiv 和 encryptedData,而这些信息需要通过 Taro.getUserInfo 接口拿到,但由于接口调整的原因,在用户未授权的情况下,调取该接口不再弹出浮层提示用户授权,而是需要用户手动去点击小程序中 open-type 为 getUserInfo 的 button 组件以进行授权。(具体细节可以查看本小册第 23 章《 微信小程序端用户授权处理》)。所以我们在全局维护一个是否授权字段时,若未授权,则点击支付时再弹出自己模拟的模态框引导用户授权。

PS:需要说明的是,由于跑通真实的支付流程是需要商家证书等一系列认证,所以我们的 Demo 并没有跑通真正的支付,而只是会生成一个订单,上面仅作支付流程唤起的讲解。

小结

本文从页面布局数据拉取与展示动画实现支付模块 这四个方面来阐述如何使用 Taro 开发结算页,整体的开发难度相较购物车可能有一定的下降,但同样也是电商黄金流程中十分重要的一环。

下一章我们将介绍小程序用户端的授权处理。

 

微信小程序端用户授权处理

获取用户信息需要授权已经是互联网的共识了,各手机系统和公司的应用都十分重视这一块。但在大数据时代,拥有数据便是拥有某些能力在诱惑着很多公司游走在法律边缘,作为非常重视用户信息的微信,小程序的开发者要拿到用户信息需要用户先授权才行。

file

小程序里使用 wx.authorize(OBJECT) 提前向用户发起授权请求。调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口。如果用户之前已经同意授权,则不会出现弹窗,直接返回成功。需要授权的信息有:用户信息、地理位置、通讯地址、发票抬头、微信运动步数、录音功能、保存到相册、摄像头,其中最常用等莫过于用户信息了。使用 wx.getUserInfo() 可以获取用户信息。

wx.authorize(OBJECT) 和 wx.getUserInfo() 的关系是什么呢?wx.authorize(OBJECT) 是提前向用户发起授权请求,调用后会立刻弹窗询问用户,不会实际调用接口,用户已经同意小程序使用用户信息,后续调用 wx.getUserInfo() 接口不会弹窗询问。

在 2018 年 4 月 30 号之前的小程序里,调用 wx.getUserInfo() 接口会出现授权窗口。在该日期之后,微信优化了用户体验,小程序的体验版、开发版调用wx.getUserInfo 接口,将无法弹出授权询问框,默认调用失败,但正式版暂不受影响。

不出现用户授权弹窗,同时又需要 <button open-type="getUserInfo"></button> 引导用户主动进行授权操作,微信为什么这么改呢?因为大部分小程序并不考虑进入小程序是否申请授权,而不是在需要的时候才申请授权。站在用户的角度来说,还是有些心理压力,毕竟都还没有看到页面,点进来可能仅仅因为是朋友分享到群里或者朋友圈,进入就要被索要个人信息,无形之中产生压力。另外,如果后续只能通过 <open-data></open-data> 来显示用户个人信息,那么开发者既可以显示相应的信息而又不被开发偷偷保存并泄漏出去。

// 需要点击「获取用户信息」才会执行 onGotUserInfo
<button open-type="getUserInfo" lang="zh_CN" bindgetuserinfo="onGotUserInfo">获取用户信息</button>
// 展示用户头像、用户昵称
<open-data type="userAvatarUrl"></open-data>
<open-data type="userNickName" lang="zh_CN"></open-data>

试想,如果页面上一个需要展示用户信息的区域,出现一个按钮“微信授权登陆”,你点击后显示你头像和昵称,十分优雅。

有时候现实总是迫不得已,如果我们需要用户一进入就取得用户的授权,以便于进行某些记录用户信息的操作,而微信又要求用户去点页面上的某个按钮才能获取信息,那怎么办呢?只能把一个按钮放在用户不能不点的地方,那就只有弹窗了。微信 wx.showModal 不能满足我们的需求,只能自己造一个,在用户第一次进来的时候弹窗,再次进来的时候则不显示。为了让这个组件具有拓展性,我们根据传入的值来修改 确认 位置按钮的属性,如果是授权的弹窗就改按钮属性为 openType='getUserInfo'

import Taro, { Component } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'import './modal.scss'class Modal extends Component {constructor() {super(...arguments)this.state = {}}confirmClick = () => {this.props.onConfirmCallback()}cancelClick = () => {this.props.onCancelCallback()}authConfirmClick = (e) => {this.props.onConfirmCallback(e.detail)}preventTouchMove = (e) => {e.stopPropagation()}render() {const { title, contentText, cancelText, confirmText} = this.propsreturn (<View class='toplife_modal' onTouchMove={this.preventTouchMove}><View class='toplife_modal_content'><View class='toplife_modal_title'>{title}</View><View class='toplife_modal_text'>{contentText}</View><View class='toplife_modal_btn'><Button class='toplife_modal_btn_cancel' onClick={this.cancelClick}>{cancelText}</Button>{!isAuth ?<Button class='toplife_modal_btn_confirm' onClick={this.confirmClick}>{confirmText}</Button> :<Button class='toplife_modal_btn_confirm' openType='getUserInfo' onGetuserinfo={this.authConfirmClick} onClick={this.cancelClick}>授权</Button> }</View></View></View>)}
}Modal.defaultProps = {title: '',contentText: '',cancelText: '取消',confirmText: '确定',isAuth: false,cancelCallback: () => {},confirmCallback: () => {},
}export default Modal

Modal 组件还算比较简单,组件的属性:

字段说明
title提示的标题
contentText提示的描述
cancelText取消按钮的文案
cancelCallback取消回调的函数
confirmText确认按钮的文案
confirmCallback确认回调函数
isAuth标记是否为授权按钮

细心的读者可能发现了,我们在内部设置了一个函数 preventTouchMove,其作用是弹窗出现蒙层的时候,阻止在蒙版上的滑动手势 onTouchMove。另外一个函数 authConfirmClick, 当 isAuth 为真时,确认按钮为取得个人信息的授权按钮,此时把个人信息当值传递给调用的函数。

在引用该组件的时候,如下面这个页面:

import Taro, { Component, eventCenter } from '@tarojs/taro'
import Modal from '../../components/gb/modal/modal'
class Index extends Component {constructor() {super(...arguments)this.state = {showAuthModal: false}this.$app = this.$app || {}}hideAuthModal () {this.setState({showAuthModal: false})Taro.setStorageSync('isHomeLongHideAuthModal', true)}prcoessAuthResult (userData) {Taro.setStorageSync('isHomeLongHideAuthModal', true)if (userData.userInfo) {this.$app.userData = userData}this.setState({showAuthModal: false})}render () {const { showAuthModal } = this.statereturn (<View className={indexClassNames}>{showAuthModal && <Modaltitle={'授权提示'}contentText={'Taro电商邀您完成授权,尊享时尚奢华之旅'}onCancelCallback={this.hideAuthModal.bind(this)}onConfirmCallback={this.prcoessAuthResult.bind(this)}isAuth={true} />}</View>)}
}

我们是如何保证这个应用只有一次授权弹窗呢? 关键代码是 Taro.setStorageSync('isHomeLongHideAuthModal', true) ,如果弹出了一次,就在本地存一个标记已经弹过授权框,下一次弹窗之前可以根据此判断。

至此我们完成了授权处理,但如果可以的话还是要优雅一些,在需要的时候才征求用户授权,保证用户体验。

参考资料:

《小程序与小游戏获取用户信息接口调整,请开发者注意升级。》 —— 微信团队

 

微信小程序开发填坑指南

众所周知,小程序开发是有诸多限制的,所以必然少不了会遇到很多坑,下面就说一说我们在开发时候遇到的一些问题,并给与解决方法与建议。

页面栈只有 10 层

估计是每个页面的数据在小程序内部都有缓存,所以做了 10 层的限制。带来的问题就是假如页面存在循环跳转,即 A 页面可以跳到 B 页面,B 页面也可以跳到 A 页面,然后用户从 A 进入了 B,想返回 A 的时候,往往是直接在 B 页面里点击跳转到 A,而不是点返回到 A,如此一来,10 层很快就突破了。

所以我们自己对 navigateTo 函数做了一层封装,防止溢出。

// 处理微信跳转超过10层
function jumpUrl (url, options = {} ) {const pages = Taro.getCurrentPages()let method = options.method || 'navigateTo'if (url && typeof url === 'string') {if (method === 'navigateTo' && pages.length >= PAGE_LEVEL_LIMIT - 3) {method = 'redirectTo'}if (method === 'navigateToByForce') {method = 'navigateTo'}if (method === 'navigateTo' && pages.length == PAGE_LEVEL_LIMIT) {method = 'redirectTo'}Taro[method]({url})}
}

可以看到,代码很简单,先是判断当前页面的栈数,超过了限定值后,就改为使用redirectTo方法,防止页面栈溢出导致错误。

页面内容有缓存

上面说到,页面内容有缓存。所以假如某个页面是根据不同的数据渲染视图,新渲染时会有上一次渲染的缓存,导致页面看起来有个闪烁的变化,用户体验非常不好。其实解决的办法也很简单,每次在onHide 生命周期中清理一下当前页面的数据就好了。

页面不会自动刷新

怎样理解这句话?常见的有两种情况:

  1. A 页面 —> B 页面,B 页面返回 A 页面时,A 页面不会自动刷新
  2. 底部的 TabBar,切换时,页面也不会自动刷新

这会导致什么情况呢?例如购物车页面是在底部的 TabBar 里的,假如笔者在别的页面将某个商品加入购物车,点回来的时候并不会刷新,这就和需求很不符合、体验也不友好。解决的办法很简单,每次都在页面的 onShow 生命周期里触发拉数据的逻辑,而不是在 onLoadonReady 等生命周期去做这个事情。

小程序说到底不是 H5,不会说每次进入页面就会刷新,也不会离开就销毁,刷新、清理数据的动作都需要自己在生命周期函数里主动触发。

不能随时地监听页面滚动事件

页面的滚动事件只能通过 onPageScroll 来监听,所以当笔者想在组件里进监听操作时,要将该部分的逻辑提前到 onPageScroll 函数,提高了抽象成本。例如笔者需要开发一个滚动到某个位置就吸顶的 tab,本来可以在 tab 内部处理的逻辑就被提前了,减少了其可复用性。这个是小程序本身的设计缺陷,似乎没有什么很好的解决办法。

小程序与 WebView 之间不能随意通信

小程序不支持本地存储,抑或是接口之类的让两者可以随意通信,只有 bindmessage 属性允许两者在特定情况下获取到 H5 传过来的信息。或者你也可以选择在 URL 里携带信息。

之前遇到的一个情况就是 H5 里的某个请求需要携带上登录后的 token 信息,而这个登录逻辑是在小程序里的,因而 token 储存在了小程序的 storage 里,无法在 H5 里获取。经过了一番挣扎后,最终还是通过 URL 把 token 信息带到了 H5,实现了在 WebView 里的登陆态传递。

decodeURIComponent

decodeURIComponent 在访问时不仅能生成小程序所携带的参数,也可以分享页面携带的参数。官方文档的例子:

// 这是首页的 js
Page({onLoad: function(options) {// options 中的 scene 需要使用 decodeURIComponent 才能获取到生成二维码时传入的 scenevar scene = decodeURIComponent(options.scene)}
})

之前写某个页面的时候,忘记了 decodeURIComponent,导致投放广告时,没有获得正确的参数。最后排查的结果是没有 decodeURIComponent 这个参数,又重新发了一版后,才完美解决这个问题。

某些组件总是会在最上层

像 mapcanvasvideotextarea 等是由客户端创建的原生组件,原生组件的层级是最高的,并不能通过设置 z-index 来更改元素层级。之前有个需求是使用 canvas 来绘制一些东西,绘制完后需要弹出一个浮层,然后 canvas 直接把浮层挡住了。

解决的办法是使用官方提供的 cover-view 组件来包裹浮层,但体验并不好。

有弹出层时,难以禁止底部页面滚动

有弹出层时,我们希望可以把底部页面的滚动给禁止掉,这个在 H5 可以用 position:fixed 定位加滚动指定高度实现。而在小程序中,这样做会有一个延迟,页面先是会回到顶部,然后再跳到指定的地方,观感非常差。而如果是直接用 catchtouchmove='preventTouchMove' 来阻止滑动事件冒泡,那会导致弹出层里面的滚动效果给禁掉,也是行不通的。

最后的办法是在最外层使用 scroll-view 组件包裹,然后有弹出层时,将 scroll-y 属性设置为 false 从而禁止页面滚动。

Taro 开发需要注意的地方

Taro 在我们部门的几个核心开发者的努力下,经过多个版本的迭代,已经开发出 ESLint 插件,及补充完整了 Taro 文档。大家只要遵循 ESLint 插件规范,查看文档,不会有太大问题,有问题欢迎提 Issue。

 

微信小程序及 H5 端预览适配与发布

简单介绍

Taro 是一个多端适配的框架,可以运行在各个端。

怎么样编写一套代码适配各端呢?在 Taro 中其实很简单,只需要通过不同的命令就可以生成各个端的代码,具体可以查看官方文档。

适配方案

在 Taro 中是通过 rpx 和 rem 分别适配小程序和 H5 端,在开发项目时,尺寸单位建议使用 px 和百分比, Taro 默认会对所有的单位进行转换。在小程序中 Taro 会转成 rpx ,在 H5 中默认转换成 rem。具体转换参数可以参考官方说明。

小程序开发框架中运行与发布

在微信开发者工具中新建项目的时候,选择项目地址应选择项目中编译的 dist 目录。

小程序发布过程

  • 注册账号

在微信公众平台注册微信小程序账号,分为个人和企业注册,个人注册的功能有的有限制,还无法认证。 注册完成后,会在公众平台设置一个 AppID。

file

  • 开发代码及注意事项

注意选择目录是编译 dist。

file

小程序前端代码在微信开发者工具,就可以上传,如下图所示:file

上传后是体验版本,需要提交微信审核,审核完成就发布成功了,接下来后面两个步骤就是配置后端服务。

file

目前小程序的审核在一小时到 N 天不等。官方说的是 7个工作日内,笔者的经历来看,也有在晚上发布,第二天早上就审核通过了。 笔者建议如果有公司则以公司申请,不要以个人身份注册小程序,很多的类目对个人未开放,限制有点多。 这里推荐一款小程序小程序开发助手,界面如下:

 

这款小程序会显示你参与的所有小程序,提交的记录。 需要注意的是涉及到区块链、贷款、彩票、色情低俗、代购类的小程序基本上不会通过。总结如下:

- 常见被拒绝情形诱导关注、诱导分享朋友圈。
凡涉及到关注公众号、分享到朋友圈,均可能封掉相应接口。
- 虚拟支付
凡非实物销售均可列入此范围。如付费购买音视频内容、付费购买教育课程。
- 未取得腾讯授权,不可以借助其他小程序或 APP 实现自身功能。
优惠券、导购类小程序。
- 需补充社交,笔记类目。
涉及可编辑、转发的小程序,涉及用户自定义内容及分享的小程序。
- 需补充社交-社区/论坛类目。
涉及回复互动类小程序
- 需补充文娱-视频类目。
涉及在线视频观看的小程序
- 无法被搜索。
部分包含恶意或风险信息的小程序(如涉嫌混淆官方产品名称、色情低俗、欺诈等),可能不能被搜索出。
- 小程序实际所提供的服务属于尚未开放的服务类目
小程序还未开放的服务。
- 小程序服务涉及可编辑、发布内容,属个人未开放类目。
以个人为主体开发的小程序,目前限制太多,基本上只能做信息查询类小程序。

个人建议如下

  • 上线前多进行测试,减少 Bug,这样就能减少提交审核的次数。
  • 心态放好,不要想着别人都上线了,为啥自己的不能上,先从自己身上找原因。
  • 申请小程序时,证件号码等要仔细。
  • 同一时间提交的小程序,审核时间不一定一致。
  • 小程序中不要出现色情等低俗内容,否则被举报后,会被暂停服务。
  • 不要在提交审核的小程序中使用测试数据。
  • 名称中含有特殊行业名词,若没有选择相应类目,很可能会被拒绝。如名称中有“保险”二字,会要求你提供保险行业的资质。特殊名称还有:社区。
  • 不要在页面提供二维码,供用户下载 App,否则可能会被视为“诱导下载”。
  • 关于企业认证中给腾讯账号小额打款,腾讯公司的小额打款账号为招商银行 25位账号,若不支持,则不要选择这种方式认证。

H5 发布过程

  • 部署腾讯云【也可用其他,无参考价值,主要是部署小程序后端】,如果是初次部署,审核周期会有点长,需要耐心,
    • 注册腾讯云
    • 腾讯云实名认证
    • 购买腾讯云产品,链接远程桌面
  • 在远程桌面搭建环境,部署后台代码到远程主机。

 

React Native 端打包与发布

如何愉快地打包发布,可能你还在头疼安卓的签名、难缠的 Gradle 和各种配置,还在郁闷 iOS 打包发布时在 Xcode 来回折腾,为什么不能脱离这些原生开发才需要的步骤呢?React Native 本身就是为了统一安卓和 iOS,如今到打包这一步却要区别对待,颇为不妥,Expo 就是个很好的解决方案,它提供壳子,我们只需要关心我们自己的代码,然后放进壳里即可。

在打包发布步骤之前,我们先对开发者的源代码进行预处理打包,转成 React Native 代码:

taro build --type rn

打包

在 dist 中得到热腾腾的 React Native 代码,就可以开始进行打包了,打包教程可以查阅 Expo 文档:Building Standalone Apps。

发布

发布到 Expo

Expo 的发布教程可以查阅文档:Publishing(发布到 Expo 不需要先经过打包),通过 Expo 客户端打开发布后的应用 CDN 链接来访问。

 

发布后的应用有个专属的地址,比如应用 Expo APIs,通过 Expo 客户端扫描页面中的二维码进行访问(二维码是个持久化地址 persistent URL)。

正式发布

如果你需要正式发布你的独立版应用,可以把打包所得的 IPA 和 APK 发布到 Apple Store 和应用市场,详细参阅 Distributing Your App,后续的更新可以通过发布到 Expo 更新 CDN 的资源来实现。

参考资料

  • 为什么选择 Expo?
  • Expo Docs: Building Standalone Apps
  • Expo Docs: Publishing
  • Expo Docs: Distributing Your App
  • Expo Docs: How Expo Works

 

总结

本书按入门篇、基础篇、原理篇、实战篇、总结篇进行编排,先从前端多端统一开发的背景开始说起,讲述了前端多端统一开发背景与趋势,以及说明了 Taro 的由来。

基础篇

而在基础篇上,主要是介绍了 Taro 所使用到的 React 的语法知识,Taro 的安装使用以及小程序的开发入门。同时针对目前比较流行的小程序开发框架,我们进行了框架对比及技术选型建议。整体来说,基础篇主要面向的是不太了解小程序开发或是不太了解 Taro 的读者,所以我们叙述的内容都会较为简单,通俗易懂。例如《实现一个简单的 Taro Todo 项目》、《在 Taro 中使用 Redux》等,目的还是为了让读者快速地上手,并使用 Taro 来开发小程序。

进阶篇

进阶篇的内容较为复杂,也较为精髓。主要介绍了 Taro 的技术原理与细节,对 Taro 的设计思路及架构、CLI 脚手架的原理、文件转换处理、组件库及 API 的设计与适配以及 JSX 转换小程序模板的实现、运行时等,都有较为详细,深入的阐述,可以说是把整个 Taro 的实现都进行了剖析。

当然,我们不是源码解读地抠每一个细节,也并没有贴过多的代码,更多的还是从整体性和实现思路上进行阐述。尽管如此,对于一些初学者来说,读起来可能还是会略显吃力,毕竟内容偏理论,比较生涩。

实战篇

实战篇则是以一个电商平台为切入点,向大家讲解如何使用 Taro 开发一个带有电商黄金流程的小程序。同时,我们使用了小程序云进行电商数据的搭建及后台接口支持,从而构成一个完整的电商流程,具体的代码仓库可在 GitHub 上找到。

在这个章节里,更多的是从业务层面来进行讲解。结合实际的业务需求,使用 Taro 来开发一个较为完整的电商小程序,完成从入门到实战的过渡。略为遗憾的是,由于篇幅限制,我们并不能把 Demo 中的每个页面都进行剖析,只是挑选了其中的比较重要的商品列表页、商详页、购物车页和结算页进行了单独讲解,其实还有首页、店铺、订单等相关内容。希望看完该文章的读者能够快速掌握使用 Taro 快速开发具体业务需求的能力,同时习得一些经过我们总结提炼出来的开发技巧。

其它想说的话

截止至编写完本篇文章,Taro 已经逐步稳定了下来,不管是在我们团队还是外部团队都有一些已经成功上线的项目,具体可以在 GitHub 的业界案例里找到。

Taro 的成长离不开大家的努力,无论是我们内部还是外部团队,都有在使用了 Taro 的项目开发中找出问题并完善 Taro,而 Taro 也令项目的开发更加高效便捷,这是个互相进步的过程。同时,我们建立了多个 Taro 交流群,里面会有很多热心的群友给我们提意见,提 PR,而我们的开发人员也会在群内尽可能地解答大家的疑问。不过关于这点,稍微需要注意的是,群里每天都有大量的反馈和问题是重复的或者是文档上或者 Issue 上已经有相关说明,所以还是希望大家先认真阅读完文档后再来提出问题,同时善用搜索功能,来让 Taro 的迭代更加高效。

除此之外,我们不仅仅限于对 Taro 进行线上的交流,还会组织一些线下的交流活动。目前已经成功在深圳举行了一次线下交流会。在交流的过程中,我们了解到了大家对 Taro 的期许,也听取并采纳了很多建议。有些同学希望可以能有一些更集中的关于 Taro 的相关资料,从而更全面地了解 Taro,这与我们推出该小册的想法不谋而合。互相交流学习才会促进进步,这也是我们开发 Taro 的初衷之一。

最后

本书从多个方面围绕多端开发这个概念进行详细阐述。无论是想了解多端统一开发框架 Taro;或是想了解 Taro 的设计思想及架构;或是了解 CLI 原理及不同端的运行机制;或是想实现 JSX 转换微信小程序模版;或是实战电商平台的读者,相信都能够从这本小册中获得想要的东西。

 

 

推荐阅读: Taro多端开发实现原理与项目实战(一)

推荐阅读:Taro多端开发实现原理与项目实战(二)

 


http://www.ppmy.cn/news/442848.html

相关文章

【转载】2021互联网行业术语@20210311

一&#xff0c;互联网行业术语 互联网行业 一般指的是互联网企业,是指以计算机网络技术为基础&#xff0c;利用网络平台提供服务并因此获得收入的企业。其业务范围通常覆盖全国甚至全球,注册用户通常达到千万级以上,用户活跃度也非常高,经常在同一时间点出现请求并行的情况。代…

[转][学习]软件绿色联盟应用体验标准5.0_功耗标准-公示版

文档来源&#xff1a;软件绿色联盟 软件绿色联盟应用体验标准5.0_功耗标准-公示版.pdf 软件绿色联盟官方-下载网址 https://www.china-sga.com/index.html 软件绿色联盟应用体验标准 5.0-功耗标准 编制单位:软件绿色联盟技术与标准工作组 2021 年 1 1 月 前 言 本标准由…

互联网行业术语

一&#xff0c;互联网行业术语 互联网行业 一般指的是互联网企业,是指以计算机网络技术为基础&#xff0c;利用网络平台提供服务并因此获得收入的企业。其业务范围通常覆盖全国甚至全球,注册用户通常达到千万级以上,用户活跃度也非常高,经常在同一时间点出现请求并行的情况。…

怎么做好Java性能优化

作者&#xff1a;苏木 0. 开篇 性能优化是一个很复杂的工作&#xff0c;且充满了不确定性。它不像Java业务代码&#xff0c;可以一次编写到处运行(write once, run anywhere)&#xff0c;往往一些我们可能并不能察觉的变化&#xff0c;就会带来惊喜/惊吓。能够全面的了解并评…

cdn讲解

阿里巴巴淘系技术 ​ 已认证的官方帐号 933 人赞同了该回答 淘宝的图片访问&#xff0c;有98%的流量都走了CDN缓存。只有2%会回源到源站&#xff0c;节省了大量的服务器资源。 但是&#xff0c;如果在用户访问高峰期&#xff0c;图片内容大批量发生变化&#xff0c;大量用户…

一个故事,一段代码告诉你如何使用不同语言(GolangC#)提供相同的能力基于Consul做服务注册与发现

文章目录 引言什么是微服务传统服务微服务 什么是服务注册与服务发现为什么要使用不同的语言提供相同的服务能力 服务协调器服务注册GolangC#&#xff08;.NetCore3.1&#xff09; 服务发现通过HttpClient发现服务&#xff0c;并访问注销一个coffee-service实例再访问 引言 趁…

Java性能优化怎么做好

开篇 性能优化是一个很复杂的工作&#xff0c;且充满了不确定性。它不像Java业务代码&#xff0c;可以一次编写到处运行(write once, run anywhere)&#xff0c;往往一些我们可能并不能察觉的变化&#xff0c;就会带来惊喜/惊吓。能够全面的了解并评估我们所负责应用的性能&am…

华为X系列服务器,云服务器的首选——华为Tecal X系列

【IT168 导购】随着“云计算”时代的来临&#xff0c;传统概念的通用服务器&#xff0c;如塔式、机架和刀片&#xff0c;已难以应对这种应用&#xff0c;一种面向云计算的服务器应用而生。那么&#xff0c;什么类型的服务器&#xff0c;能满足云计算和虚拟化的需求&#xff1f;…