本文共 13092 字,阅读本文大概须要 10~15 分钟, 技术干货在文章中段,Taro 熟练使用者可跳过前面介绍篇幅
技术选型css
项目架构html
丰客可能是企业业务事业部打造的企业会员制商城,2020 年预期在 Q3 作商城的全面推广,用户增加的任务很是艰巨,所以但愿借力 C 端用户的强社交属性,以微信小程序为载体,实现我的推荐企业( C 拉 B )的创新裂变模式。前端
下方多终端热门框架对比,能够看到 Taro 已经同时支持了 React、Vue 技术栈,相较而言考虑到后期维护成本、框架响应维护速度,所以采用团队自研 Taro 框架
Taro 是由 JDC·凹凸实验室 倾力打造的一款多端开发解决方案的框架工具,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/ QQ 小程序/H5 等应用。现现在市面上端的形态多种多样,Web、React Native、微信小程序等各类端大行其道,当业务要求同时在不一样的端都要求有所表现的时候,针对不一样的端去编写多套代码的成本显然很是高,这时候只编写一套代码就可以适配到多端的能力就显得极为须要。react
当前 Taro 已进入 3.x 时代,相较于 Taro 1/2 采用了重运行时的架构,让开发者能够得到完整的 React/Vue 等框架的开发体验,具体请参考《小程序跨框架开发的探索与实践》。typescript
Taro UI 是一款基于 Taro 框架开发的多端 UI 组件库,一套组件能够在 微信小程序,支付宝小程序,百度小程序,H5 多端适配运行(ReactNative 端暂不支持)提供友好的 API,可灵活的使用组件。编程
支持必定程度的样式定制。(请确保微信基础库版本在 v2.2.3 以上)目前支持三种自定义主题的方式,能够进行不一样程度的样式自定义:redux
在前端架构方面,总体架构设计以下:canvas
项目中须要接入公用的 京东登陆
等其它微信小程序插件来实现登陆态打通,那么此时咱们就遇到一个问题,多端转换的问题 Taro 帮咱们作了,可是第三方的这些插件逻辑调用转换须要咱们本身来实现。那么面对此场景,咱们采用了如下解决方案:小程序
首先 process.env.TARO_ENV
是关键,Taro 在编译运行时候会对应设置该变量 h五、weapp、alipay、tt ...等,全部咱们能够根据不一样的变量来调用不一样的插件。这种场景咱们能够简单运用一个工厂模式来处理此逻辑。下面先简单上图概述一下windows
/** 抽象类 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;
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 不采用 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 组件同步的机制,这种机制就是使用响应式虚拟依赖状态图表,它只有在真正须要的时候才更新而且永远保持是最新的。
面向对象(封装、继承、多态)整个项目开发过程当中,服务端是经过判断请求头中携带的 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 动态绘制生成图片后,保存到用户相册,用户进行分享照片到朋友圈,朋友圈打开图片后识别二维码进入小程序,达到分享目的。
下面带你们实现实现一波:
<Canvas style={`height:${canvasHeight}px;width:${canvasWidth}px`} className='shareCanvas' canvas-id="shareCanvas" ></Canvas>
保证 Canvas 不在用户的视线内
.shareCanvas { width: 100%; height: 100%; background: #fff; position: absolute; opacity: 0; z-index: -1; right: 2000rpx; top: 2000rpx; z-index: 999999; }
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;
/** 用户微信头像 */ 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 语言,做为一个支持多端转化的工具框架值得你们选择。