Taro/TS 快捷开发丰客多裂变小程序

  • 文章目录
  • 项目背景
  • 项目展示
  • 技术选型

    • Taro
    • 丰富的 Taro UI 组件库

  • 项目架构

    • Taro 与原生小程序融合
    • TypeScript 的实践
    • MobX 状态管理
    • API Service、HttpClient 封装
    • 图片等比例缩放
    • 海报分享(分享朋友圈)

  • 总结

项目背景

丰客多是企业业务事业部打造的企业会员制商城,2020 年预期在 Q3 做商城的全面推广,用户增长的任务非常艰巨,因此希望借力 C 端用户的强社交属性,以微信小程序为载体,实现个人推荐企业( C 拉 B )的创新裂变模式。

项目展示

图1
图2
图3

技术选型

image

Taro

Taro 是由 JDC·凹凸实验室 倾力打造的一款多端开发解决方案的框架工具,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/ QQ 小程序/H5 等应用。现如今市面上端的形态多种多样,Web、React Native、微信小程序等各种端大行其道,当业务要求同时在不同的端都要求有所表现的时候,针对不同的端去编写多套代码的成本显然非常高,这时候只编写一套代码就能够适配到多端的能力就显得极为需要。

当前 Taro 已进入 3.x 时代,相较于 Taro 1/2 采用了重运行时的架构,让开发者可以获得完整的 React/Vue 等框架的开发体验,具体请参考《小程序跨框架开发的探索与实践》。

  • 基于 React、Vue 语法规范,上手几乎0成本,满足基本开发需求
  • 支持 TS,支持 ES7/ES8 或更新的语法规范
  • 支持 CSS 预编译器,Sass/Less 等
  • 支持 Hooks (日常开发几乎不需要 redux 场景)
  • 支持状态管理,Redux/MobX

丰富的 Taro UI 组件库

Taro UI 是一款基于 Taro 框架开发的多端 UI 组件库,一套组件可以在 微信小程序,支付宝小程序,百度小程序,H5 多端适配运行(ReactNative 端暂不支持)提供友好的 API,可灵活的使用组件。

支持一定程度的样式定制。(请确保微信基础库版本在 v2.2.3 以上)目前支持三种自定义主题的方式,可以进行不同程度的样式自定义:

  • scss 变量覆盖
  • globalClass 全局样式类
  • 配置 customStyle 属性(仅有部分组件支持,请查看组件文档,不建议使用)

项目架构

在前端架构方面,整体架构设计如下:

Taro 与原生小程序融合

项目中需要接入公用的 京东登录 等其它微信小程序插件来实现登录态打通,那么此时我们就遇到一个问题,多端转换的问题 Taro 帮我们做了,但是第三方的这些插件逻辑调用转换需要我们自己来实现。那么面对此场景,我们采用了以下解决方案:

首先 process.env.TARO_ENV 是关键,Taro 在编译运行时候会对应设置该变量 h5、weapp、alipay、tt ...等,所有我们可以根据不同的变量来调用不同的插件。这种场景我们可以简单运用一个工厂模式来处理此逻辑。下面先简单上图概述一下

  1. 创建抽象 Plugin 类,定制具体插件功能调用方法
  2. 创建实现类(微信小程序、京东小程序、H5 等 )
  3. 创建代工厂类(对外暴露具体方法),初始化时,根据当前场景实例化对应类

/** 抽象类 Plugin 提供具体插件功能 API */

abstract class Plugin {

abstract getToken(): void; /** 获取token信息 */

abstract outLogin(): void; /** 退出登录 */

abstract openLogin(): void; /** 打开登录页 */

}

/** 方法实现类-小程序 */

class WeChatPlugin extends Plugin {

getToken(): void {

// ... 调用对应插件API

}

outLogin(): void {

// ... 调用对应插件API

}

openLogin(): void {

// ... 调用对应插件API

}

...

}

/** 方法实现类-京东小程序 */

class JDPlugin extends Plugin {

getToken(): void {

// ... 调用对应插件API

}

outLogin(): void {

// ... 调用对应插件API

}

openLogin(): void {

// ... 调用对应插件API

}

...

}

/** 方法实现类 - H5 */

class H5Plugin extends Plugin {

getToken(): void {

// ... 调用对应插件API

}

outLogin(): void {

// ... 调用对应插件API

}

openLogin(): void {

// ... 调用对应插件API

}

...

}

export class pluginHelper {

private plugin: Plugin;

constructor() {

switch (process.env.TARO_ENV) {

case 'weapp':

this.plugin = new WeChatPlugin();

break;

case 'jd':

this.plugin = new JDPlugin();

break;

case 'h5':

this.plugin = new H5Plugin();

break;

// ...

default:

break;

}

}

// 检查是否为原生 APP

get plugin(): Plugin{

return this.plugin;

}

}

export default pluginHelper;

TypeScript 的实践

搜索了一番市面上 React + TS 都是采用 interface 配合使用,下面我们举个栗子看一下,看一下缺点

interface ITsExampleState {

/** 名称 */

name: string

name2: string,

name3: string,

name4: string,

}

export default class TsExample extends Component<ITsExampleState> {

state: Readonly<ITsExampleState> = {

name: "",

name2: "",

name3: "",

name4: "",

//...

}

componentDidShow() {

let tempState: ITsExampleState = {

name: '456',

name2: "",

name3: "",

name4: "",

};

this.setState(tempState)

}

componentDidHide() {

let tempState: ITsExampleState = {

name: '456',

name2: "",

name3: "",

name4: "",

};

this.setState(tempState)

}

}

那么这种方式使用虽然问题,但是我们会发现每次使用时都需要把每一个接口变量初始赋值一下,否则就会报错,如果10多个变量就需要写10次,岂不是很麻烦。

class ITsExampleState {

/** 名称 */

name: string = ""

name2: string = ""

name3: string = ""

name4: string = ""

}

export default class TsExample extends Component<ITsExampleState> {

state: Readonly<ITsExampleState> = new ITsExampleState();

componentDidShow() {

let tempState: ITsExampleState = new ITsExampleState();

tempState.name = '123';

this.setState(tempState)

}

componentDidHide() {

let tempState: ITsExampleState = new ITsExampleState();

tempState.name = '456';

this.setState(tempState)

}

}

34行代码变20行(🤣代码量 KPI 同学慎用),代码量的不同差距会越来越大,同样在另一个小节 API Service 中,再说另一个优点。

MobX 状态管理

[为什么选用 Mobx 不采用 Redux] https://tech.youzan.com/mobx_...

Redux是一个数据管理层,被广泛用于管理复杂应用的数据。但是实际使用中,Redux的表现差强人意,可以说是不好用。而同时,社区也出现了一些数据管理的方案,Mobx就是其中之一。

MobX 是一个经过战火洗礼的库,它通过透明的函数响应式编程(transparently applying functional reactive programming - TFRP)使得状态管理变得简单和可扩展。MobX背后的哲学很简单:

其中包括UI、数据序列化、服务器通讯,等等。

React 和 MobX 是一对强力组合。React 通过提供机制把应用状态转换为可渲染组件树并对其进行渲染。而MobX提供机制来存储和更新应用状态供 React 使用。

对于应用开发中的常见问题,React 和 MobX 都提供了最优和独特的解决方案。React 提供了优化UI渲染的机制, 这种机制就是通过使用虚拟DOM来减少昂贵的DOM变化的数量。MobX 提供了优化应用状态与 React 组件同步的机制,这种机制就是使用响应式虚拟依赖状态图表,它只有在真正需要的时候才更新并且永远保持是最新的。

API Service、HttpClient 封装

面向对象(封装、继承、多态)整个项目开发过程中,服务端是通过判断请求头中携带的 Header 自定义值来校验登录态。每一次数据请求,都需要在请求 Header 上添加自定义字段,随着接口数量越来越多,因此我们将 Http 请求单独封装为一个模块。

为了解决这一问题,我们将 HTTP 请求统一配置,生成 HttpClient Class 类,对外暴露 post 、 get 方法。并对后台返回的数据进行统一处理,重新定义返回状态码,避免后端状态码多样性,即使后端状态码做了修改,也不影响前端代码的正确运行。

import Taro, { request } from "@tarojs/taro";

const baseUrl = "https://xxxxx"

const errorMsg = '系统有点忙,耐心等会呗';

export class HttpClient {

/**

* 检查状态

* @param {ResponseData} response 响应值

*/

private checkStatus(response) {

// 如果http状态码正常,则直接返回数据

if (response && (response.statusCode === 200 || response.statusCode === 304 || response.statusCode === 400)) {

response.data = response.data);

let resData: ResponseData = { state: 0, value: response.data.xxx, message: response.data.xxx };

if (response.data.xxx) {

} else {

resData.state = 1;

resData.value = response.data;

resData.message = response.data.xxx;

}

if (resData.state == 1) {

Taro.showToast({

title: resData.message,

icon: 'none',

duration: 2000

})

}

return resData

} else {

Taro.showToast({

title: errorMsg,

icon: 'none',

duration: 2000

})

return null

}

}

public post(url: string, params: any = {}) {

return this.request('post', url, params)

}

public get(url: string, params: any = {},) {

return this.request('get', url, params)

}

async checkNetWorkDiasble() {

return new Promise((resolve, reject) => {

Taro.getNetworkType({

success(res) {

const networkType = res.networkType

resolve(networkType == 'none')

}

})

})

}

/**

* request请求

* @param {string} method get|post

* @param {url} url 请求路径

* @param {*} [params] 请求参数

*/

private async request(method: string, apiUrl: string, params: any): Promise<ResponseData | null> {

// Taro request ...

}

}

/**

* 内部 响应对象

* @param {number} state 0 成功 1失败

* @param {any} value 接口响应数据

* @param {string} message 服务器响应信息msg

*/

interface ResponseData {

state: number;

value?: any;

message: string;

}

对于 HTTP 请求我们还是不满足,在组件中我们调用 HttpClient Class 类进行数据请求时,我们依然要回到请求接口的 Service 模块文件,查看入参,或者是查看 swagger 文档,如何才能一目了
然呢?采用 Class Params 对象方式约束入参,从编译方式上进行约束。我们以下请求为例:

class UserApiService {

// ...

getFansInfo(params: PageParams) {

return this.httpClient.post('/user/xxx', params);

}

}

export class PageParams {

/** 请求页 */

pageNo: number = 1;

/** 请求数量 */

pageSize: number = 10;

}

export class Test{

testFn(){

// 获取粉丝数据

let pageParams:PageParams=new PageParams();

pageParams.pageNo = 1;

pageParams.pageNo = 10;

this.userApiService.getFansInfo(pageParams).then(res => {});

}

}

在 getFansInfo 方法中,我们通过 TypeScript 的方式,约束了接口的参数是一个对象。同时在调用过程中可以采用 . 对应的属性,友好的查看注释,非 interface 使用

是不是很方便,不但避免了参数类型的不一致,出现 bug ,也节省了查找方法的时间,提高开发效率!

图片等比例缩放

export default class Index extends Component {

// ...

render() {

const { imageUrl,imageHeight } = this.state as IState;

return (

<Image

mode="aspectFill"

style={`height:${imageHeight}px`}

src={imageUrl}

onLoad={this.imageOnload(event)} >

</Image>

);

}

imageOnload = (e)=>{

let res = Utils.imageScale(e)

this.setState({

imageHeight: res.imageHeight;

})

}

}

export default class Utils {

static imageScale = (e) => {

let imageSize = {

imageWidth: 0,

imageHeight: 0

};

let originalWidth = e.detail.width;//图片原始宽

let originalHeight = e.detail.height;//图片原始高

let originalScale = originalHeight / originalWidth;//图片高宽比

// console.log('originalWidth: ' + originalWidth)

// console.log('originalHeight: ' + originalHeight)

//获取屏幕宽高

let res = Taro.getSystemInfoSync();

let windowWidth = res.windowWidth;

let windowHeight = res.windowHeight;

let windowscale = windowHeight / windowWidth;//屏幕高宽比

// console.log('windowWidth: ' + windowWidth)

// console.log('windowHeight: ' + windowHeight)

if (originalScale < windowscale) {//图片高宽比小于屏幕高宽比

//图片缩放后的宽为屏幕宽

imageSize.imageWidth = windowWidth;

imageSize.imageHeight = (windowWidth * originalHeight) / originalWidth;

} else {//图片高宽比大于屏幕高宽比

//图片缩放后的高为屏幕高

imageSize.imageHeight = windowHeight;

imageSize.imageWidth = (windowHeight * originalWidth) / originalHeight;

}

// console.log('缩放后的宽: ' + imageSize.imageWidth)

// console.log('缩放后的高: ' + imageSize.imageHeight)

return imageSize;

}

}

海报分享

在微信中小程序无法分享到朋友圈,目前大部分的解决方案都是,Canvas 动态绘制生成图片后,保存到用户相册,用户进行分享照片到朋友圈,朋友圈打开图片后识别二维码进入小程序,达到分享目的。
下面带大家实现实现一波:

  1. 海报分析

  1. 代码 Canvas 初始化创建

<Canvas style={`height:${canvasHeight}px;width:${canvasWidth}px`} className='shareCanvas' canvas-id="shareCanvas" ></Canvas>

  1. 样式设置

保证 Canvas 不在用户的视线内

.shareCanvas {

width: 100%;

height: 100%;

background: #fff;

position: absolute;

opacity: 0;

z-index: -1;

right: 2000rpx;

top: 2000rpx;

z-index: 999999;

}

  1. CanvasUtil 工具类

export class CanvasUtil {

/**

* canvas 文本换行计算

* @param {*} context CanvasContext

* @param {string} text 文本

* @param {number} width 内容宽度

* @param {font} font 字体(字体大小会影响宽)

*/

static breakLinesForCanvas(context, text: string, width: number, font) {

function findBreakPoint(text: string, width: number, context) {

var min = 0;

var max = text.length - 1;

while (min <= max) {

var middle = Math.floor((min + max) / 2);

var middleWidth = context.measureText(text.substr(0, middle)).width;

var oneCharWiderThanMiddleWidth = context.measureText(text.substr(0, middle + 1)).width;

if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) {

return middle;

}

if (middleWidth < width) {

min = middle + 1;

} else {

max = middle - 1;

}

}

return -1;

}

var result = [];

if (font) {

context.font = font;

}

var textArray = text.split('\r\n');

for (let i = 0; i < textArray.length; i++) {

let item = textArray[i];

var breakPoint = 0;

while ((breakPoint = findBreakPoint(item, width, context)) !== -1) {

result.push(item.substr(0, breakPoint));

item = item.substr(breakPoint);

}

if (item) {

result.push(item);

}

}

return result;

}

/**

* 图片裁剪画圆

* @param {*} ctx CanvasContext

* @param {string} img 图片

* @param {number} x x轴 坐标

* @param {number} y y轴 坐标

* @param {number*} r 半径

*/

static circleImg(ctx, img: string, x: number, y: number, r: number) {

ctx.save();

ctx.beginPath()

var d = 2 * r;

var cx = x + r;

var cy = y + r;

ctx.arc(cx, cy, r, 0, 2 * Math.PI);

ctx.clip();

ctx.drawImage(img, x, y, d, d);

ctx.restore();

}

/**

* 绘制圆角矩形

* @param {*} ctx CanvasContext

* @param {number} x x轴 坐标

* @param {number} y y轴 坐标

* @param {number} width 宽

* @param {number} height 高

* @param {number} r r 圆角

* @param {boolean} fill 是否填充颜色

*/

static drawRoundedRect(ctx, x: number, y: number, width: number, height: number, r: number, fill: boolean) {

ctx.beginPath();

ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2);

ctx.lineTo(width - r + x, y);

ctx.arc(width - r + x, r + y, r, Math.PI * 3 / 2, Math.PI * 2);

ctx.lineTo(width + x, height + y - r);

ctx.arc(width - r + x, height - r + y, r, 0, Math.PI * 1 / 2);

ctx.lineTo(r + x, height + y);

ctx.arc(r + x, height - r + y, r, Math.PI * 1 / 2, Math.PI);

ctx.closePath();

if (fill) {

ctx.fill();

}

}

}

export default CanvasUtil;

  1. JS 逻辑处理

/** 用户微信头像 */

let avatarUrl = 'https://xxx.360buyimg.com/xxxxx.png';

// 海报背景图片

let inviteImageUrl = 'https://xxx.360buyimg.com/xxxxx.png';

// 二维码背景白尺寸

let qrBgHeight = 85;

let qrBgWidth = 85;

// 图片居中尺寸

let centerPx = canvasWidth / 2;

// 二维码背景白 x轴 ,y轴 坐标

let qrBgX = centerPx - qrBgWidth / 2;

let qrBgY = 370;

let context = Taro.createCanvasContext('shareCanvas');

//海报背景绘制

context.drawImage(inviteImageUrl, 0, 0, canvasWidth, canvasHeight);

//矩形颜色设置

context.setFillStyle('#ffffff');

//绘制二维码圆角矩形

CanvasUtil.drawRoundedRect(context, qrBgX, qrBgY, qrBgWidth, qrBgHeight, 5, true);

// context.restore();

//绘制二维码

context.drawImage(this.downloadQRcode, qrBgX + 2, qrBgY + 2, qrBgWidth - 4, qrBgHeight - 4);

// 下载微信头像到本地

Taro.downloadFile({

url: avatarUrl,

success: function (res) {

// 微信头像尺寸尺寸

let wxAvatarHeight = 32;

let wxAvatarWidth = 32;

// 微信头像居中 x轴 ,y轴 坐标

let wxAvatarX = centerPx - wxAvatarWidth / 2;

let wxAvatarY = 395.5;

//微信头像绘制

CanvasUtil.circleImg(context, res.tempFilePath, wxAvatarX, wxAvatarY, wxAvatarWidth / 2);

// 文本绘制

context.setTextAlign("center")

context.font = "12px PingFangSC-Regular";

context.fillText("扫一扫", centerPx, qrBgY + qrBgHeight + 20);

context.font = "10px PingFangSC-Regular";

context.fillText("立即注册丰客多", centerPx, qrBgY + qrBgHeight + 34);

context.draw();

Taro.showLoading({

title: '生成中',

})

setTimeout(() => {

Taro.canvasToTempFilePath({

canvasId: 'shareCanvas',

fileType: 'jpg',

success: function (res) {

Taro.hideLoading()

console.log(res.tempFilePath)

Taro.showLoading({

title: '保存中...',

mask: true

});

Taro.saveImageToPhotosAlbum({

filePath: res.tempFilePath,

success: function (res) {

Taro.showToast({

title: '保存成功',

icon: 'success',

duration: 2000

})

},

fail: function (res) {

Taro.hideLoading()

console.log(res)

}

})

}

})

}, 1000);

}

})

总结

在开发此项目之前,都是自己都是采用原生微信小程序进行开发,该项目是我第一次使用 Taro + Taro UI + TypeScript 来开发小程序,在开发过程中通过查阅官方文档,基本属于 0 成本上手。
同时在开发过程中遇到问题一点一滴记录下来,从而得到成长,并沉淀出此文章,达到提高自我帮助他人。目前 Taro 框架也在不断的迭代中,在近期发布的 3.0 候选版本也已经支持使用 Vue 语言,作为一个支持多端转化的工具框架值得大家选择。

以上是 Taro/TS 快捷开发丰客多裂变小程序 的全部内容, 来源链接: utcz.com/p/39287.html

回到顶部