关于前端:TaroTS-快捷开发丰客多裂变小程序

42次阅读

共计 11712 个字符,预计需要花费 30 分钟才能阅读完成。

本文共 13092 字,浏览本文大略须要 10~15 分钟, 技术干货在文章中段,Taro 纯熟使用者可跳过后面介绍篇幅

  • 文章目录
  • 我的项目背景
  • 我的项目展现
  • 技术选型

    • Taro
    • 丰盛的 Taro UI 组件库
  • 我的项目架构

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

我的项目背景

丰客多是企业业务事业部打造的企业会员制商城,2020 年预期在 Q3 做商城的全面推广,用户增长的工作十分艰巨,因而心愿借力 C 端用户的强社交属性,以微信小程序为载体,实现集体举荐企业(C 拉 B)的翻新裂变模式。

我的项目展现



技术选型

下方多终端热门框架比照,能够看到 Taro 曾经同时反对了 React、Vue 技术栈,相较而言思考到前期保护老本、框架响应保护速度,因而采纳团队自研 Taro 框架

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 的实际

State Class 束缚,非 interface 束缚

搜寻了一番市面上 React + TS 都是采纳 interface 配合应用,上面咱们举个栗子看一下,看一下毛病

state + 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 次,岂不是很麻烦。

看一下,我如何来优雅解决这种场景

state + class

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,也节俭了查找办法的工夫,进步开发效率!

注:在 VS code 的编辑器中,当鼠标挪动到某些文本之后,稍作片刻就会呈现一个悬停提醒窗口,这个窗口里会显示跟鼠标下文本相干的信息。如果想要查看对象就具体信息,须要按下 Cmd 键(Windows 上是 Ctrl)。

图片等比例缩放

在咱们的我的项目中首页采纳瀑布流图片,并采纳不规则高度图片,然而在咱们的小程序中 Image 标签又必须设置高度,这可如何是好 …
咱们通过 onLoad 函数来进行等比例缩放


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 语言,作为一个反对多端转化的工具框架值得大家抉择。

正文完
 0