关于taro:使用tarocanvas实现微信小程序的图片分享功能-京东云技术团队

业务场景二轮充电业务中,用户充电实现后在订单详情页展现订单相干信息,用户点击分享按钮唤起微信小程序分享菜单,将生成的图片海报分享给微信好友或者下载到本地,好友可通过扫描海报中的二维码加群支付优惠。 应用场景及性能:微信小程序 生成海报图片 分享好友 下载图片 应用技术:Taro vue vant canvas 实现效果图 重点步骤拆分1、封装一个海报分享组件 poster-share.vue 2、用canvas画图,将背景图、费用、二维码等信息绘制在一张图上,其中费用、二维码是动静获取的 3、生成一张本地缓存图片 4、唤起微信分享性能,实现分享和下载性能 重点步骤有了,那么就开干吧! 外围代码实现1、模版局部须要一个画布dom用来绘制图片,一个用来寄存生成图片的dom 问:canvasId为什么须要动静生成呢? 答:防止一个页面中应用多个组件引起的canvasId反复问题 <template> <div class="poster-share__content"> <!-- canvas生成的海报图片 --> <img v-if="posterImg" class="poster-share__content--img" mode="aspectFit" :src="posterImg" > <!-- 分享海报canvas绘制局部 --> <canvas class="poster-share__content--cvs" :canvas-id="canvasId" ></canvas> </div></template>2、款式局部该业务场景下,不能让用户看到画布,然而设置canvas的display为none将不能进行绘制,会报如下谬误,导致绘制失败。 实现形式:采纳定位的形式,将canvas定位到可视区域外,具体代码如下。 .poster-share__content { position: absolute; right: -9999px; top: -9999px; width: 560px; height: 852px; opacity: 0; z-index: -1; &--img { width: 100%; height: 100%; } &--cvs { width: 100%; height: 100%; }}3、外围js局部开始写外围实现啦~ 父组件传参管制子组件是否开始绘制,子组件绘制实现后告诉父组件扭转状态。 name: 'CpPosterShare', model: { prop: 'value', event: 'update:value', }, props: { value: { type: Boolean, default: false, }, config: { type: Object, default: () => ({}), }, }, data () { return { isDraw: false, // 是否开始绘制海报 posterImg: '', // 生成的海报图片地址 canvasId: `canvasId${ Math.random() }`, screenWidth: null, // 屏幕宽度 } }, watch: { value: { handler (val) { this.isDraw = val }, immediate: true, }, isDraw (val) { this.$emit('update:value', val) if (val) { this.init() } }, },首先,咱们做的是一个小程序,将图片放在小程序源码中会加大包的体积,须要从网络上下载图片,因而须要封装一个公共的办法来获取图片的信息。Taro提供getImageInfo办法返回图片的原始宽高、本地门路等信息。 ...

May 18, 2023 · 3 min · jiezi

关于taro:轻松实现Jenkins发布taro小程序

前言手动公布小程序效率低还容易出错,本文就思考如何用Jenkins来公布taro小程序。 手动打包小程序的问题频繁发版 + 发版流程琐碎 + 可能存在的多个小程序,效率低打包时常常固定一个人或者一台机器打包上传,不灵便手动打包可能选错接口环境,导致线上问题Jenkins打包益处固定代码分支以及环境,打包过程由jenkins承当。效率高,缩小环境谬误的危险所有人都能够自在打包以上需要,整顿成一个demo我的项目了,基于Taro 3.x taro小程序打包示例地址 Jenkins如何公布小程序微信小程序ci能力开发者可不关上小程序开发者工具,独立应用 miniprogram-ci 进行小程序代码的上传、预览等操作微信小程序ci官网文档 密钥及 IP 白名单配置应用 miniprogram-ci 前应拜访"微信公众平台-开发-开发设置"后下载代码上传密钥,并配置 IP 白名单 开发者可抉择关上 IP 白名单,关上后只有白名单中的 IP 能力调用相干接口。 通过小程序管理员扫码,就能够取得上传密钥。IP白名单能够本人设置,限度指定IP地址操作。 我公司因为jenkins自身就做了内网拜访限度,这里就没有反复限度了。 密钥的寄存个别下载的密钥,是放到我的项目根目录当中。思考到密钥寄存在我的项目中不太平安,就让运维在jenkins打包阶段,再注入到我的项目中。 应用Taro框架集成的ci公布个别微信原生小程序间接应用miniprogram-ci的能力即可。因为我的项目应用了taro框架,这里就以taro插件来做为例子。taro兼容了各小程序平台ci(继续集成)的能力。 yarn add @tarojs/plugin-mini-ci -D 次要原理:通过小程序密钥进行身份鉴权。ci就具备上传或者预览小程序的权限 小程序继续集成 | Taro 文档 我的项目中进行ci配置参数类型阐明 appidstring小程序我的项目的 appidprivateKeyPathstring私钥文件在我的项目中的相对路径,在获取我的项目属性和上传时用于鉴权应用robotnumber指定应用哪一个 ci 机器人,可选值:1 ~ 30(选填, 3.6.0 版本开始反对)descstring形容versionstring版本下面都是自定义的,个别跟着具体打包脚本来自定义的。如果想要全局自定义version或者desc,能够在package.json下配置taroConfig参数 // package.json{ "taroConfig": { "version": "1.0.0" },}上面是我的项目中配置ci的过程,通过CIPluginFn配置密钥以及具体的参数 // config/index.jsconst { robot = 1, desc } = argvconst CIPluginFn = { weapp: { appid: 'xxxx', privateKeyPath: 'private.xxxx.key', // 配置密钥的门路 robot, }, desc,}const config = { plugins: [['@tarojs/plugin-mini-ci', CIPluginFn]],}自定义打包命令这里自定义了test以及prod两个环境,test测试环境通过--env指定为development,prod默认为线上production ...

May 10, 2023 · 2 min · jiezi

关于taro:Taro源码taro的插件机制

引言基于Taro3.5.5Taro 引入了插件化机制,目标是为了让开发者可能通过编写插件的形式来为 Taro 拓展更多功能或为本身业务定制个性化性能。在《Taro源码-cli我的项目创立的过程》中常常提到插件(Plugins)和插件集(Presets)这一次学习一下Taro的插件机制 Plugins和Presets的区别Presets是插件集,能够了解成Plugins的汇合

May 6, 2023 · 1 min · jiezi

关于taro:Taro源码项目build一个weapp的过程

引言基于Taro3.5.5此前,咱们学习了cli创立一个Taro我的项目,并在packages/taro-cli/bin文件夹下创立了简略的Taro我的项目appname,接着看一下用Taro我的项目去build一个微信小程序weapp的过程 创立的我的项目关上此我的项目,如果应用过taro的开发过小程序的能够很相熟,包含配置config、src等等 关上咱们的首页文件pages/index/index.tsx ...export default class Index extends Component<PropsWithChildren> { ... render () { return ( <View className='index'> <Text>这是一个Taro我的项目</Text> </View> ) }}用taro (dev或者build)命令启动这个我的项目作为微信小程序 build和dev关上创立的taro我的项目下的package.json "scripts": { "build:weapp": "taro build --type weapp", ... "dev:weapp": "npm run build:weapp -- --watch", ... },dev命令相较于build命令就是在build命令后多加了--watch,以此来辨别是开发监听热加载还是打包我的项目,dev命令能够也能够间接这么写 "dev:weapp": "taro build --type weapp --watch",打印承受到的内置命令dev:build: Clipackages/taro-cli/src/cli.ts与cli创立我的项目的init命令一样,build的入口也是packages/taro-cli/bin/taro,在入口文件里执行cli.run(),Cli的作用就是承受内置命令、合成内置命令、设置环境变量、针对不同的内置命令注册对应的命令插件。 在build:weapp之后合成内置命令之后进行环境变量的设置 // 设置环境变量 process.env.NODE_ENV ||= args.env if (process.env.NODE_ENV === 'undefined' && (command === 'build' || command === 'inspect')) { // 依据watch判断是开发环境development还是生产环境production process.env.NODE_ENV = (args.watch ? 'development' : 'production') } args.type ||= args.t if (args.type) { // 我的项目的类型:weapp、tt、qq、h5、rn... process.env.TARO_ENV = args.type } if (typeof args.plugin === 'string') { // plugin小程序插件 process.env.TARO_ENV = 'plugin' } // 咱们build一个weapp那就是process.env.TARO_ENV = 'weapp'实例化Kernel并把presets/commands/build.ts命令插件挂载到kernel上 ...

September 16, 2022 · 7 min · jiezi

关于taro:Taro源码cli项目创建的过程

入口基于Taro3.5.5找到创立taro我的项目的入口文件(packages/taro-cli/bin/taro) // packages/taro-cli/bin/tarorequire('../dist/util').printPkgVersion()const CLI = require('../dist/cli').defaultnew CLI().run()Clipackages/taro-cli/src/cli.ts这个文件的作用就是承受内置命令、合成内置命令、针对不同的内置命令注册对应的命令插件。 首先初始化的时候获取咱们的我的项目路劲 // packages/taro-cli/src/cli.ts constructor (appPath) { this.appPath = appPath || process.cwd() }在bin/taro文件里执行new CLI().run()在cli中看到run办法执行了this.parseArgs()在parseArgs办法里做的第一件是就是承受内置命令以及合成内置命令 // packages/taro-cli/src/cli.ts const args = minimist(process.argv.slice(2), { alias: { version: ['v'], help: ['h'], port: ['p'], resetCache: ['reset-cache'], // specially for rn, Removes cached files. publicPath: ['public-path'], // specially for rn, assets public path. bundleOutput: ['bundle-output'], // specially for rn, File name where to store the resulting bundle. sourcemapOutput: ['sourcemap-output'], // specially for rn, File name where to store the sourcemap file for resulting bundle. sourceMapUrl: ['sourcemap-use-absolute-path'], // specially for rn, Report SourceMapURL using its full path. sourcemapSourcesRoot: ['sourcemap-sources-root'], // specially for rn, Path to make sourcemaps sources entries relative to. assetsDest: ['assets-dest'] // specially for rn, Directory name where to store assets referenced in the bundle. }, boolean: ['version', 'help'] })打印args的后果可得args._里就是咱们的命令这里的aaaaa能够替换成为init、build、inspect这些taro意识的命令 ...

September 14, 2022 · 5 min · jiezi

关于taro:Taro-使用-最新-F2-4x-版本图表

1.装置依赖npm i @antv/f2 --save# 微信小程序npm i @antv/f2-wx --save2.拷贝组件将微信工具包(@antv/f2-wx),拷贝进去,到src/components上面。 而后,在 app.config.js 外面,全局引入小程序组件。 usingComponents: { "f2": "./components/f2-wx"},批改f2-wx/index.js外面的onRender为render,同时改为非函数 properties: { render: { // 改名 type: null, value: '' } }, ... var children = _this.data.render; // 间接取值3.编写图表组件写一个 LineChart 图表,如下: import React, {} from 'react'import {Chart, Interval, Axis} from '@antv/f2'const LineChart: React.FC<any> = (props) => { const data = [ { genre: 'Sports', sold: 275 }, { genre: 'Strategy', sold: 115 }, { genre: 'Action', sold: 120 }, { genre: 'Shooter', sold: 350 }, { genre: 'Other', sold: 150 }, ]; return ( <Chart data={data}> <Axis field="genre" /> <Axis field="sold" /> <Interval x="genre" y="sold" color="genre" /> </Chart> )}export default LineChart4.援用import { ReactNode, FC } from 'react'import LineChart from './LineChart.tsx'const Home: FC<any> = () => { return ( <f2 render={<LineChart />} /> )}export default Home其余当然,你能够再封装一个组件F2Chart,而后<F2Chart><LineChart/></F2Chart>这样去应用。 ...

April 20, 2022 · 1 min · jiezi

关于taro:Taro333最新版本开发企业级出行项目无密分享

Taro@3.3.3最新版本开发企业级出行我的项目|无密分享超清原画 残缺无密 包含所有视频课件以及源码 点击下崽:网盘链接基于Node.js的ORM框架 Prisma的上手使用 Node.js作为咱们前端的一项技术,大多数工夫是作为一个Javascript的运行环境而存在。然而其优良的异步操作以及非阻塞式的程序运行形式,也让Node.js能够同时并发处理数千个连接。前端工程师可能用很低的学习成本来使用它实现罕用的服务端代码。 ORMORM:对象关系映射(Object Relational Mapping)是一种程序设计技术。简略来说ORM可能将咱们的底层数据库的各种操作进行肯定的封装,咱们就可能通过更加熟悉的开发语言来书写对应的数据库命令,ORM则可能将这些数据库操作命令转换为对应的SQL语句。 Prisma下一代 Node.js、TypeScript、Go 的数据库 ORM Prisma是一个开源的数据库工具链我的项目,帮助开发人员更快地构建应用程序并缩小谬误,反对PostgreSQL、MySQL、MongoDB、SQL Server和SQLite。 如果想要了解一门技术的用法,那么咱们则需要通过实际的上手使用它来进行一点点的开发。 首先咱们需要初始化一个我的项目 mkdir prisma-demo # 创建一个目录cd prisma-demo # 通过命令行来进入该我的项目目录npm init -y # 将我的项目进行初始化{ "name": "prisma-demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1"}, "keywords": [], "author": "", "license": "ISC"}复制代码而后咱们将本次所需要使用的prisma进行安装一下 npm install prisma -D # 安装prisma复制代码安装实现后,咱们可能通过npx prisma init -h命令来查看prisma相干的命令帮助信息 Setup a new Prisma projectUsage $ prisma init [options]Options ...

April 7, 2022 · 3 min · jiezi

关于taro:跨端实践-Taro-框架中该如何使用-Vant-组件库适配多端

原文发表于 集体博客掘金发表:跨端实际 | Taro 框架中该如何应用 Vant 组件库-适配多端 在 Taro 中应用有赞前端团队开源的挪动端组件库 Vant,能间接兼容应用的组件大略为 70%,而无奈做到百分百兼容。 那咱们又该如何调整,能力做到所有组件的多端适配呢?通过团队的多番摸索和实际,终于找到了一条光明大道。 跨端 UI 库抉择Taro 是由 京东·凹凸实验室 倾力打造的多端开发解决方案。当应用 Taro 框架做业务开发时,外围目标就是为了可能只写一套代码,达到在不同端的统一体现。从我的项目侧,艰深的讲,就是为了让产品疾速上线。那么在应用 Taro 框架时,咱们还须要抉择一个优质的跨端组件库,晋升开发人员绘制页面的效率。 目前风行的挪动端组件库,次要有Mint UI、WeUI、iView UI、layui、ElementUI、vant UI、Antd Design Mobile等。以往在开发 H5 我的项目时,设计师选中一个适合格调的 UI 组件库,而后开发间接应用就能够了。但当咱们应用 Taro 跨端计划后,就不能随便抉择这些 UI 库了,因为这些 UI 库可能还未实现对应的跨端兼容能力。不过,也不须要太过放心,Taro 团队早就思考到了这个问题,对于一些罕用的 UI 组件库,曾经给予了一些接入计划。当然官网主推的是官网 UI 库:Taro UI、NutUI,不过有时候咱们就 偏偏不必官网举荐的。 从官网博客日志中,咱们发现有提到可应用 WeUI、vant UI、Antd Design Mobile这三个 UI 库,官网做了对应的模块封装。因为思考到团队前期应用 vue 语法栈,同时剖析了目前三个 Demo 案例的整体状况,最终认为 Vant UI 可能更满足前期的业务场景。 兼容 WeUI 的例子兼容 Antd Design Mobile 的例子兼容 VantUI 的例子Taro 中 Vant UI 的兼容性在 Taro 中应用有赞前端团队开源的挪动端组件库 Vant,能间接兼容应用的组件大略为 70%,而无奈做到百分百兼容。原因无他,因为 Vant 是针对于 web 研发的挪动端组件库,而在小程序中因为局部特有 API 的调用限度(例如:获取元素尺寸系列的 API 等)而无奈做到所有性能同步兼容。 ...

March 28, 2022 · 3 min · jiezi

关于taro:Taro333最新版本开发企业级出行项目

download:Taro@3.3.3最新版本开发企业级出行我的项目2021 年 Node.js 的发展趋势世界各地的开发者对 2021 年 Node.js 的发展趋势做了很多预测。在下文中,咱们将解说往年预期的 Node.js 趋势。Node.js 为什么如此有名呢?近些年来,Node.js 凭借其高度灵活和极其轻量的个性,变得非常流行。Node.js 具备大量的 JavaScript 库用于简化开发流程。Node.js 的开源属性也使得它在 web 和移动利用开发方面变得非常出名。根据最近的统计,可能看出: 目前有超过 50% 的开发者在自己的我的项目中使用 Node.js。在美国,基于 Node.js 创建的网站超过 28000 个 。AliExpress、eBay 等出名公司很大程度上依赖于 Node.js。包含 PayPal、Netflix 和 Groupon 在内的大流量的网站都在使用 Node.js。 Node.js 自 2009 年在市场上推出就变得非常受欢迎。Node.js 在 Github 中具备 75.9k stars、3k watchers,还有 19k forks,在 Stack share 中具备 71.8k 的关注者以及 8.3k 的同意。这些数字足以描述 Node.js 流行程度。出名的科技巨头,比如 Netflix 和 Microsoft 都在使用 Node.js。Node.js 胜利在 StackOverflow 2021 年开发者考察中位居榜首,其中超过 50% 的考察参与者声称正在我的项目中使用 Node.js。

March 16, 2022 · 1 min · jiezi

关于taro:Taro-cli流程和插件化机制实现原理

前言自 2.2 开始,Taro 引入了插件化机制,目标是为了让开发者可能通过编写插件的形式来为 Taro 拓展更多功能或为本身业务定制个性化性能。 本文基于Taro3.4.2源码解说CLI流程执行cli命令,如npm run start,实际上在package.json中script脚本列表中能够往下解读始终找到build:weapp这条脚本所执行的对应具体指令信息,dev模式下区别prod模式只是多了一个--watch热加载而已,只是辨别了对应的env环境,在webpack打包的时候别离预设了对应环境不同的打包配置,例如判断生产环境才会默认启用代码压缩等 那么这个taro指令是在哪定义的呢?taro在你全局装置的时候就曾经配置到环境变量了,咱们我的项目目录上来执行`package.json中的script脚本命令,它会在当前目录上来找node脚本,找不到就向下级找,最终执行该脚本。taro的外围指令源码都在taro/cli下,罕用的指令有init(创立我的项目)、build(构建我的项目)。启动命令入口在taro/cli/bin/taro // @taro/cli/bin/taro#! /usr/bin/env noderequire('../dist/util').printPkgVersion()const CLI = require('../dist/cli').defaultnew CLI().run()启动后,CLI实例先实例化了一个继承EventEmitter的Kernel外围类(ctx),解析脚本命令参数后调用customCommand办法,传入kernel实例和所有我的项目参数相干。 // taro-cli/src/cli.ts// runconst kernel = new Kernel({ appPath: this.appPath, presets: [ path.resolve(__dirname, '.', 'presets', 'index.js') ]})let plugin// script 命令中的 --type参数let platform = args.typeconst { publicPath, bundleOutput, sourcemapOutput, sourceMapUrl, sourcemapSourcesRoot, assetsDest } = args// 小程序插件开发, script: taro build --plugin weapp --watchcustomCommand('build', kernel, { _: args._, platform, plugin, isWatch: Boolean(args.watch), port: args.port, env: args.env, deviceType: args.platform, resetCache: !!args.resetCache, publicPath, bundleOutput, sourcemapOutput, sourceMapUrl, sourcemapSourcesRoot, assetsDest, qr: !!args.qr, blended: Boolean(args.blended), h: args.h})customCommand中将所有的参数整顿后调用Kernel.run,传入整顿后的所有参数。 ...

March 8, 2022 · 8 min · jiezi

关于taro:Taro原理分析迁移指南及开发注意事项

如果你应用 Taro 开发感觉 Bug 少,那阐明你的 React 代码写得很标准。 -- Taro团队趁前段时间做基于taro2的微店铺以及taro2升taro3的我的项目教训,简略介绍Taro2跟Taro3的各自的优缺点以及理论应用场景下的语法区别,并分享Taro3降级中和应用中的踩坑点。 举荐浏览小程序跨端框架开发的摸索与实际 Taro1/2Taro3之前的整体架构能够看成两局部:编译时和运行时。这里解释一下两者的用处: 编译时:通过对⽤户的 React 代码进⾏编译来转化代码语法,如jsx转小程序xml等,甚至转换成各个平台(抖音小程序、微信小程序、H5等等)都能够运⾏的代码。编译时工作流程次要是通过babel 将 Taro 代码解析成形象语法树,而后操作语法树生成指标平台的代码,也就是parse -> replace -> generate这样一个工作流程。 以build:weapp编译微信小程序端为例: render() { return ( <View> { dataList.map((data, index) => (<Text key={index}>{data.title}</Text>)) } </View> )}通过babel转换后: <view wx:for="{{dataList}}" wx:for-item="data" wx:for-index="index"> <text>{data.title}</text></view>咱们都晓得 JSX 是一个 JavaScript 的语法扩大,它的写法变幻无穷,非常灵便。这里咱们是采纳 穷举 的形式对 JSX 可能的写法进行了一一适配,这一部分工作量很大,实际上 Taro 有大量的 Commit 都是为了更欠缺的反对 JSX 的各种写法。这是摘自官网对taro2编译时的一句形容,因为应用穷举的适配形式,势必会造成jsx的各种各样的奇怪的bug产生和各种开发时的限度。 运行时能够晓得的是,咱们开发taro我的项目时,援用的是taro库上面的api和组件,调用相似微信原生的api,如: wx.getSettings,在taro外面须要从 @taro/taro援用,而后调用 Taro.getSettings,组件则是通过@taro/components援用。这是因为Taro制订了一套运行时的规范组件库和api,通过对原生api进行拓展和配合编译时曾经抹平了状态、事件绑定、页面配置和生命周期等的差别,实现了框架的适配工作。具体点形容: 编译后的taro代码实现了 BaseComponent 和 createComponent,BaseComponent的作用次要是重写了react外面的render、setState等外围代码,createComponent 次要作用是调用 Component() 构建页面,解决以下等对接工作实现适配: 将组件的 state 对应为小程序组件配置对象的 data将组件的生命周期对应为小程序组件的生命周期将组件的事件处理函数对应为小程序的事件处理函数...简略来说就是先将代码编译成各个平台结构化语言的代码,而后通过适配器模式等等办法适配到各个平台可能让之运行起来,整个Taro2的架构编译时做的工作占次要局部,运行时工作量较小。 ...

January 25, 2022 · 2 min · jiezi

关于taro:Taro333最新版本开发企业级出行项目mkw

download:`Taro@3.3.3最新版本开发企业级出行我的项目` 1.1 没加索引sql语句中where条件的关键字段,或者order by前面的排序字段,忘了加索引,这个问题在我的项目中很常见。我的项目刚开始的时候,因为表中的数据量小,加不加索引sql查问性能差异不大。起初,随着业务的倒退,表中数据量越来越多,就不得不加索引了。能够通过命令:show index from order; 能独自查看某张表的索引状况。 也能够通过命令: show create table order; 查看整张表的建表语句,外面同样会显示索引状况。通过ALTER TABLE命令能够增加索引: ALTER TABLE `order` ADD INDEX idx_name (name);复制代码也能够通过CREATE INDEX命令增加索引: CREATE INDEX idx_name ON `order` (name);不过这里有一个须要留神的中央是:想通过命令批改索引,是不行的。目前在mysql中如果想要批改索引,只能先删除索引,再从新增加新的。删除索引能够用DROP INDEX命令: ALTER TABLE `order` DROP INDEX idx_name;复制代码用DROP INDEX命令也行:DROP INDEX idx_name ON order;

December 23, 2021 · 1 min · jiezi

关于taro:Taro333最新版本开发企业级出行项目网盘分享

download:Taro@3.3.3最新版本开发企业级出行我的项目网盘分享随着Python的沉闷,各大平台都在宣扬,甚至曾经出了对于python黑客入门的书籍。 兴许做一个黑客难如登天,那不如咱们换个思路,去伪装做一个伪黑客如何? 前几天看帖子,发现咱们应用浏览器的时候,当登陆一个须要输出用户名明码的网站时,在你登陆胜利后,零碎会提醒你是否保留明码,如果点击确认,浏览器将会把咱们本次输出的明码,存储在浏览器中,待下次登录时便能够免密登录。 那么,这些明码是怎么保留的,又存储在哪里呢? Chrome浏览器 兴许很多人会说,360浏览器、QQ浏览器,这些国产的加壳浏览器不管好看还是所谓的平安方面都做的很合乎国人需要。但如果你的工作与IT挂钩,无疑Chrome将是很多敌人的首选。当然这篇文章不是介绍Chrome浏览器的使用手册,明天咱们次要来看看Chrome浏览器的明码存储机制。 作者:是程序员吖链接:https://www.jianshu.com/p/e76...起源:简书著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

December 19, 2021 · 1 min · jiezi

关于taro:Taro333最新版本开发企业级出行项目网盘分享

download:Taro@3.3.3最新版本开发企业级出行我的项目网盘分享为什么需要分布式锁用户下单锁住 uid,防止重复下单。 库存扣减锁住库存,防止超卖。 余额扣减锁住账户,防止并发操作。分布式零碎中共享同一个资源时经常需要分布式锁来保障变更资源一致性。 分布式锁需要具备个性排他性锁的基本个性,并且只能被第一个持有者持有。 防死锁高并发场景下临界资源一旦发生死锁非常难以排查,通常可能通过设置超时工夫到期主动开释锁来规避。 可重入锁持有者反对可重入,防止锁持有者再次重入时锁被超时开释。 高性能高可用锁是代码运行的要害前置节点,一旦不可用则业务间接就报故障了。高并发场景下,高性能高可用是基本申请。 实现 Redis 锁应先管制哪些学识点set 命令SET key value [EX seconds] [PX milliseconds] [NX|XX]EX second :设置键的过期工夫为 second 秒。 SET key value EX second 成果同等于 SETEX key second value 。PX millisecond :设置键的过期工夫为 millisecond 毫秒。 SET key value PX millisecond 成果同等于 PSETEX key millisecond value 。NX :只在键不存在时,才对键进行设置操作。 SET key value NX 成果同等于 SETNX key value 。XX :只在键已经存在时,才对键进行设置操作。Redis.lua 脚本使用 redis lua 脚本能将一系列命令操作封装成 pipline 实现整体操作的原子性。 go-zero 分布式锁 RedisLock 源码分析core/stores/redis/redislock.go ...

December 19, 2021 · 1 min · jiezi

关于taro:某课Taro333最新版本开发企业级出行项目mk

download:Taro@3.3.3最新版本开发企业级出行我的项目JSON to Excel for VUE3在浏览器中将JSON格局数据以excel文件的模式下载。该组件是基于this thread 提出的解决方案。反对Vue3.2.25及以上版本应用 重要提醒! Microsoft Excel中的额定提醒此组件中实现的办法应用HTML表绘制。在xls文件中,Microsoft Excel不再将HTML辨认为本机内容,因而在关上文件之前会显示正告音讯。excel的内容曾经完满出现,然而提示信息无奈防止,请不要在意! Getting started装置依赖: npm install vue3-json-excel在vue3的利用入口处有两种注册组件的形式: import Vue from "vue"import {vue3JsonExcel} from "vue3-json-excel"Vue.component("vue3JsonExcel", vue3JsonExcel)或者 import Vue from "vue"import vue3JsonExcel from "vue3-json-excel" Vue.use(vue3JsonExcel)在template文件中间接应用即可 <vue3-json-excel :json-data="json_data"> Download Data</vue3-json-excel>应用 box-shadow 实现如果应用 box-shadow,代码大抵如下: <div class="g-container"> <div class="g-item"></div></div>.g-container { position: relative; width: 400px; height: 300px; overflow: hidden;}.g-item { position: absolute; width: 48px; height: 48px; border-radius: 50%; background: #fff; top: 20px; left: 20px; box-shadow: 0 0 0 0 #fff; transition: box-shadow .3s linear; &:hover { box-shadow: 0 0 0 420px #fff; }}外围就在于: ...

December 18, 2021 · 1 min · jiezi

关于taro:Taro333最新版本开发企业级出行项目

download:Taro@3.3.3最新版本开发企业级出行我的项目1. REST API可能有小伙伴还不懂什么是 REST API,这里就先简略科普下: REST(Representational State Transfer)是一种 Web 软件架构格调,它是一种格调,而不是规范,匹配或兼容这种架构格调的的网络服务称为 REST 服务。 REST 服务简洁并且有档次,它通常基于 HTTP、URI、XML 以及 HTML 这些现有的宽泛风行的协定和规范。在 REST 中,资源是由 URI 来指定,对资源的增删改查操作能够通过 HTTP 协定提供的 GET、POST、PUT、DELETE 等办法实现。 应用 REST 能够更高效的利用缓存来进步响应速度,同时 REST 中的通信会话状态由客户端来保护,这能够让不同的服务器解决一系列申请中的不同申请,进而进步服务器的扩展性。 在前后端拆散我的项目中,一个设计良好的 Web 软件架构必然要满足 REST 格调。 2. 开启 Web 治理页面再来说说如何开启 Web 治理页面,整体上来说,咱们有两种形式开启 Web 治理页面: 装置 RabbitMQ 的时候,间接抉择 rabbitmq:3-management 镜像,装置命令如下:docker run -d --rm --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3-management这样装置好的 RabbitMQ 就能够间接应用 Web 治理页面了。 装置的时候就抉择失常的一般镜像 rabbitmq:3,装置命令如下:docker run -d --hostname my-rabbit --name some-rabbit2 -p 5673:5672 -p 25672:15672 rabbitmq:3这个装置好之后,须要咱们进入到容器中,而后手动开启 Web 治理插件,命令如下: ...

December 18, 2021 · 1 min · jiezi

关于taro:解决taro小程序中引入axios包过大的问题

背景咱们在应用taro 和 @freud/http(公司外部我的项目,基于axios做的二次开发) 的时候,发现构建产物中多了很多没有用的包,导致产物变大了150kb左右。通过一番搜寻,发现是因为taro小程序不能解析package.json中的browser module等字段,而@frued/http 因为要同时反对web和小程序环境,而axios中就有browser属性: "browser": { "./lib/adapters/http.js": "./lib/adapters/xhr.js" },所以在taro中引入axios的时候,会将 lib/adapters/http.js也打包进来,http.js中会有很多依赖包如zlib等等,就会导致上述包过大的问题。 解决思路首先咱们能够定位到axios源码相干的地位: if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter adapter = require('./adapters/xhr'); } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { // For node use HTTP adapter adapter = require('./adapters/http'); }失常状况下,在web环境中,./lib/adapters/http.js会被 ./lib/adapters/xhr.js替换所以以上代码在打包之后就会变成。 if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter adapter = require('./adapters/xhr'); } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { // For node use HTTP adapter adapter = require('./adapters/xhr'); }咱们在晓得了失常状况下,构建产物应该长什么样子之后,再来看咱们目前的构建产物。因为@frued/http 是基于rollup做的构建,所以产物如下图: index.js中保留对axios的援用 var axios = require('axios'),而axios的对应lib/default.js中,仍然在援用 adapter/http图一图二 ...

December 12, 2021 · 1 min · jiezi

关于taro:Taro-Design-移动端页面编辑器

Taro Design一个简略易用,不便扩大和集成的挪动端页面编辑器 特点公布到npm市场,能够很不便的将他集成到你的我的项目中。你能够不便的编写一个组件在这个编辑器中运行,或者将你现有的组件通过简略批改运行在编辑器中。编辑后的数据同时反对小程序、H5、React Native,需在Taro3的我的项目中应用。组件款式遵循以React Native款式为根底的Flex布局,能够同时给设计师和开发人员应用。导出为React组件后,能够持续进行二次开发。模板市场给你提供了存储和应用模板的性能,你能够通过公开的模板疾速创立页面,你也能够依据本人的需要创立模板。运行原理你编辑的后的数据以json的形式运行和存储,上面的示例将一个text组件嵌套在一个view组件的json。 [ { "child": [ { "style": {}, "text": "文本内容", "nodeName": "text", "key": "2e0l1-19tg00", "child": [] } ], "style": {}, "nodeName": "view", "key": "2e0l1VzIiw00" }]对应的JSX代码如下,这些组件并不是原生的Taro组件,二十通过封装的,所以你看到上面的Text组件的文本并不是这样:<Text>文本内容<Text>,而是将文本内容赋值在其text属性上,其余组件的构造也大体如此。 <View> <Text text='文本内容' /></View>在线体验点击返回在线地址 在线模板当初未开放注册账号以及治理性能,请应用上面的账号密码进行体验。 用户名:admin明码:123456GitHub地址:https://github.com/ShaoGongBra/taro-design 快捷键反对ctrl + z 撤销操作 ctrl + shift + z 复原操作 ctrl + c 复制节点 ctrl + v 粘贴节点 delete 删除节点 根本用法yarn add taro-design如果你的依赖库里没有下列组件,请增加yarn add classnames增加配置 h5: { esnextModules: [ 'taro-design' ] }编辑器应用示例import React from 'react'import { Design } from 'taro-design/design'import { TopView } from 'taro-design'export default () => { return <TopView> <Design // 默认数据节点 defaultNodes={[]} // 产生编辑时触发的事件 你能够返回一个Promise对象 将会显示一个正在保留的loading onChange={nodes => {}} // 点击保留按钮时触发的事件,当你配置了这个选项才会呈现保留按钮 你能够返回一个Promise对象 将会显示一个正在保留的loading onSave={nodes => {}} // 开启模板 默认开启 templateOpen // 开启导出 默认开启 exportOpen // 利用在最外层的款式 你也能够通过.taro-design管制款式 style={{ }} /> </TopView>}渲染模式应用示例import React from 'react'import { TopView, Create } from 'taro-design'export default () => { return <TopView> <Create nodes={[]} /> </TopView>}全局款式为了和rn端放弃款式统一,你须要在你的全局款式代码中退出如下的款式。 上面的全局款式可能会导致你曾经存在的我的项目款式错乱,你临时须要自行调试,倡议在新我的项目中应用。 ...

November 18, 2021 · 2 min · jiezi

关于taro:taro生命周期

October 25, 2021 · 0 min · jiezi

关于taro:Taro小程序TaronavigateBack携带参数

在Taro小程序中可能常常会碰到上一个页面须要以后页面数据的场景,那么如何把当前页的数据带到上一个页面并返回上一个页面呢?以后页面: let pages = Taro.getCurrentPages(); // 获取以后的页面栈 let prevPage = pages[pages.length-2]; // 获取上一页面prevPage.setData({ //设置上一个页面的值 list: data});Taro.navigateBack({ delta: 1});上一页面: componentDidShow() { // 对应onShow,只有在onShow中才会监听到以后页面的扭转 let pages = Taro.getCurrentPages(); let currentPage = pages[pages.length - 1]; // 获取以后页面 if (currentPage.__data__.list) { // 获取值 this.setState({ list: currentPage.__data__.list }) } }卡布奇诺今犹在,不见以后倒茶人。欢送大佬指导~

August 3, 2021 · 1 min · jiezi

关于taro:Taro小程序实现文字展示两行显示不生效解决方案

完满解决在Taro中scss超出省略号展现不失效问题在css中常常能碰到超出一行或者两行...几行之后,展现省略号(...)的操作,然而在首次接触Taro时,却碰到了不失效的问题,废话不多说,间接上解决方案: overflow: hidden;text-overflow: ellipsis;display: -webkit-box;-webkit-line-clamp: 2;line-clamp: 2;-webkit-box-orient: vertical;/*! autoprefixer: off */留神:/*! autoprefixer: off */这一句不是正文!!!!具体原理可参考上面链接:https://github.com/postcss/au...

August 2, 2021 · 1 min · jiezi

关于taro:Taro3-开发小程序爬坑汇总

最近又回归小程序了,两头曾经有一年没有搞过了,taro都到V3版本了(都2021了就别再原生开发了),汇总一下应用过成中遇到的问题,共大家借鉴。 # Taro3相干的爬坑。 小小吐槽下,框架是降级的够快,文档及配套更新真是慢。 ## taro-ui 曾经应用了taro3的同学应该发现了,依照官网应用办法引入居然报错了,怎么办呢?上github上看看吧,github又阐明是版本不匹配,地址taro-ui-github,taro-ui 3.x版本正在开发中所以要应用 v3的alpha版本,运行上面的命令即可。 // npm npm install taro-ui@3.0.0-alpha.3 // yarn yarn add taro-ui@3.0.0-alpha.3 redux or @tarojs/redux 的代替计划Taro3版本中应用redux,官网给了一个模板间接是用的react-redux,这个没问题,然而但你想用@tarojs/redux,因为应用上略微不便点儿。然而当你引入我的项目时又时报错,又不能用了么?没错V3中将不再保护此库。因为当初又一个更弱小,配置简略的库redux-model。简略比照下旧版的redux 应用redux-model是不是简洁了很多,省去了繁琐的书写。 原生插件应用举例html2wxml 1.根目录创立plugins 文件夹,放入须要应用的插件。 2.config 文件夹下的index.js 配置拷贝门路,避免webpack对插件编译。 3.用到的页面减少index.config.js 定义应用的插件4.在页面中应用 应用字体图标 fontAwesome 或者 iconfont如果对版本要求不高能够应用 taro-icons 有人做了一版集成了旧版的(fontawesome V4, iconfont) 的图标库,这个也能用,然而打包后你会发现体积起码减少300kb(次要是字体文件体积较大),这对小程序来说是不能承受的,所以针对字体图标库,倡议通过 iconfont 搜寻进去须要的,独自下载下来应用。或者设计稿上间接切图解决吧(如果有更好的计划欢送留言交换)。 对于fontAwesomeV5 版本间接设计切图吧,我用过字体转换网站的办法,转换实现后独自字体文件体积将近10M,间接放弃。 定制主题思考到后续会出皮肤切换的性能,所以设计之初就间接抉择了scss,来编写css. 用到的文间 1.theme.scss // 住要用来定义主题名称。 2.var.scss // 定制主题色彩变量。 3.themeify.scs // 编写函数,提取主题变量,生成css. theme.scss@import './var.scss';$darkTheme: ( page_bg_color: $color-dark, card_color: #191919, card_bg_color: $card_bg_dark, card_title_color: $white, card_desc_color: $white05,);$lightTheme: ( page_bg_color: $color-light, card_bg_color: $white, card_title_color: $black, card_desc_color: $black05,);$themes: ( dark: $darkTheme, light: $lightTheme,);var.scss// 主题次要色彩$color-dark: #000;$color-light: #ededed;// 组件色彩$card_bg_dark: #191919;//字体色彩$black: #000;$black05: rgba(0, 0, 0, 0.5);$white: #fff;$white05: rgba(255, 255, 255, 0.5);// 其它themeify.scs@import './theme.scss';@mixin themeify($themes: $themes) { //$theme-name 主题款式类名, $theme-map款式 @each $theme-name, $theme-map in $themes { // !global 把局部变量强升为全局变量 $theme-map: $theme-map !global; // 小程序局部选择器不反对所以间接应用class类型 如果是网页能够通过body data-theme的属性值 // #{}是sass的插值表达式 & sass嵌套里的父容器标识 .theme-#{$theme-name} & { @content; // @content是混合器插槽,像vue的slot } }}//申明一个依据Key获取色彩的function@function themed($key) { @return map-get($theme-map, $key); //从相应主题中拿到相应key对应的值}//获取背景色彩@mixin background_color($color) { @include themeify { background-color: themed($color); }}具体是用举例 ...

July 7, 2021 · 1 min · jiezi

关于taro:Taro3无埋点的探索与实践

引言对于Taro框架,置信大多数小程序开发者都是有肯定理解的。借助Taro框架,开发者们能够应用React进行小程序的开发,并实现一套代码就可能适配到各端小程序。这种促使开发成本升高的能力使得Taro被各大小程序开发者所应用。应用Taro打包进去的小程序和原生相比是有肯定区别的,GrowingIO小程序的原生SDK还不足以间接在Taro中应用,须要针对其框架的特地进行适配。这点在Taro2期间曾经是实现完满适配的,但在Taro3之后,因为Taro团队对其整体架构的调整,使得之前的形式曾经无奈实现精确的无埋点,促使了本次摸索。 背景GrowingIO小程序SDK无埋点性能的实现有两个外围问题: 如何拦挡到用户事件的触发办法如何为节点生成一个惟一且稳固的标识符只有能解决好这两个问题,那就能实现一个稳固小程序无埋点SDK。在Taro2中,框架在编译期和运行期有不同的工作内容。其中编译时次要是将 Taro 代码通过 Babel 转换成小程序的代码,如:JS、WXML、WXSS、JSON。在运行时Taro2提供了两个外围ApicreateApp,createComponent,别离用来创立小程序App和实现小程序页面的构建。 GrowingIO 小程序SDK通过重写createComponent办法实现了对页面中用户事件的拦挡,拦挡到办法后便能在事件触发的时候获取到触发节点信息和办法名,若节点存在id,则用id+办法名作为标识符,否则就间接应用办法名作为标识符。这里办法名获取上sdk并没有任何解决,因为在Taro2的编译期曾经做好了这一系列的工作,它会将用户办法名残缺的保留下来,并且对于匿名办法,箭头函数也会进行编号赋予适合的办法名。 然而在Taro3之后,Taro的整个外围产生了微小的变动,不论是编译期还是运行期和之前都是不一样的。createApp和createComponent接口也不再提供,编译期也会对用户办法进行压缩,不在保留用户办法名也不会对匿名办法进行编号。这样就导致现有GrowingIO 小程序SDK无奈在Taro3上实现无埋点能力。 问题剖析在面对Taro3的这种变动,GrowingIO之前也做过适配。在剖析Taro3运行期的代码中发现,Taro3会为页面内所有节点调配一个绝对稳固的id,并且节点上的所有事件监听办法都是页面实例中的eh办法。在此条件下之前的GrowingIO便是依照原生小程序SDK的解决形式拦挡该eh办法,在用户事件触发的时候获取到节点上的id以生成惟一标识符。这种解决形式在肯定水平上也是解决了无埋点SDK的两个外围问题。 不难想到,GrowingIO之前的解决形式上,是没方法做到获取一个稳固的节点标识符的。当页面中节点的程序发生变化,或者动静的增删了局部节点,这时Taro3都会给节点调配一个新的id,这样的话那就无奈提供一个稳固的标识符了,导致之前圈选定义的无埋点事件生效。 如果想解决掉已定义无埋点事件生效问题,那就必须能提供一个稳固的标识符。类比与在Taro2上的实现,如果也能在拦挡到事件触发的时候获取到用户办法名,那就能够了。也就是说只有能把以下两个问题解决掉,便能实现这个指标了。 运行时SDK能拦挡用户办法能在生产环境将用户办法名保留下来逐个攻破获取用户办法先看第一个问题,SDK如何获取到用户绑定的办法,并拦挡它。剖析下Taro3的源码,不难就能解决掉。 所有的页面配置都是通过createPageConfig办法返回的,每个page配置都会有一个eh,从这里下手便能获取到绑定的办法。可见taro-runtime源码中的 eventHandler,dispatchEvent办法。 // page配置中的eh即为该办法export function eventHandler (event: MpEvent) { if (event.currentTarget == null) { event.currentTarget = event.target } // 运行时的document是Taro3.0定义的,能够获取虚构dom中的节点 const node = document.getElementById(event.currentTarget.id) if (node != null) { // 触发事件 node.dispatchEvent(createEvent(event, node)) }}// 在看看dispatchEvent办法,简化后class TaroElement extends TaroNode { ... public dispatchEvent (event: TaroEvent) { const cancelable = event.cancelable // 这个__handlers属性是要害,这里保留着该节点上所有监听办法 const listeners = this.__handlers[event.type] // ...省略很多 return listeners != null } ...}__handlers具体构造如下: ...

June 17, 2021 · 2 min · jiezi

关于taro:Taro打包Android-apk

首先,咱们应用应用命令创立模板我的项目,创立的命令如下。 taro init myApp而后,应用 yarn 或者 npm install装置依赖包,并应用上面的命令编译Taro我的项目。 yarn dev:rn启动后会开启一个监听的过程,如下图。 不过,仔细的你可能会发现,应用taro init命令初始化的我的项目是没有原生模块反对的,原来Taro应用了一个壳子工程,首先应用上面的命令下载壳子工程taro-native-shell,如下所示。 git clone git@github.com:NervJS/taro-native-shell.git在taro-native-shell个目录应用 yarn 或者 npm install 装置依赖,并应用上面的命令启动壳子工程。 react-native run-android不过,启动后报了如下的谬误: error: bundling failed: NotFoundError: Cannot find entry file index.js in any of the roots: ["/Users/mac/Taro/work/taro-yanxuan"] at DependencyGraph.getAbsolutePath (/Users/mac/Taro/work/taro-yanxuan/node_modules/metro/src/node-haste/DependencyGraph.js:317:11) at /Users/mac/Taro/work/taro-yanxuan/node_modules/metro/src/DeltaBundler/DeltaCalculator.js:280:416 at Generator.next (<anonymous>) at step (/Users/mac/Taro/work/taro-yanxuan/node_modules/metro/src/DeltaBundler/DeltaCalculator.js:11:445) at /Users/mac/Taro/work/taro-yanxuan/node_modules/metro/src/DeltaBundler/DeltaCalculator.js:11:605 at processTicksAndRejections (internal/process/task_queues.js:97:5)在网上找了下,找到如下一篇帖子:taro-native-shell 壳子,android studio 启动报错。下面报错的意思是找不到RN的入口文件index.js。对于这个问题,只须要将 MainApplication.java 外面的 getJSMainModuleName 批改改为:rn_temp/index即可,因为Taro打的包在rn_temp目录下,最新的 react-native-shell 已修复。 批改后,从新执行react-native run-android命令。不过,因为我的项目是0.60.0版本以下的,所以我在运行的时候又报了上面的谬误。 React Native version mismatchjavascript version 0.55.4Native version 0.64.0这是因为react-native-shell 是0.64.0,而我的RN我的项目是0.55.4,所以只能降级RN我的项目或者降级react-native-shell 。如果没有任何谬误,接下来就能够制作离线的apk包了。首先,你须要生成Android的密钥文件,对于如何生成密钥文件,能够自行查找相干的材料,把生成的密钥文件拷贝到工程中的android/app文件夹中。而后,在在/android/gradle.properties中增加如下常量代码。 ...

May 1, 2021 · 1 min · jiezi

关于taro:Taro-源码解读-TaroMiniPlugin-上篇

本篇文章是 Taro 的源码解读系列的第五篇文章,上面是系列文章链接。 Taro 源码解读 - @tarojs/taro 篇Taro 源码解读 - @tarojs/cli 篇Taro 源码解读 - taro build 篇Taro 源码解读 - miniRunner 篇Taro 源码解读 - TaroMiniPlugin 上篇在上一篇文章 Taro 源码解读 - miniRunner 篇 中,曾经解说了 taro-cli 中 miniRunner 的工作流程,实质上是 webpack 构建流程。 本篇文章将会是对 miniRunner 篇的一个补充,着重介绍 miniRunner 中的 TaroMiniPlugin。 话不多说,咱们开始吧。 TaroMiniPlugin 概览TaroMiniPlugin 是一个 webpack plugin,依据 webpack 插件个性,在装置插件时,将会调用插件实例上的 apply 办法。而 apply 办法个别实现的都是在 webpack compiler 的不同生命周期中注册对应的钩子函数,用于在特定的机会解决额定的逻辑。 咱们先来看看 TaroMiniPlugin 中的 apply 办法吧(如下图) 咱们来剖析 TaroMiniPlugin 的 apply 办法所做的事件,如下: ...

January 30, 2021 · 2 min · jiezi

关于taro:凹凸技术揭秘-Taro-从跨端到开放式跨端跨框架

承载 Web 的主战场转移— 2017 年 1 月 9 日凌晨,微信正式推出小程序,为挪动端家族减少了新的业务状态和玩法,当大家还在探讨这一新兴平台能做什么的时候,京东率先上线了「京东购物」小程序,随后,更多的电商行业执牛耳者纷纷入驻小程序,从此,承载电商的主战场逐步从须要自建流量的挪动端 APP 向小程序歪斜。 小程序的呈现,为电商行业的研发带来了微小的挑战。继微信之后越来越多的头部流量互联网公司纷纷盯上小程序这一蛋糕,相继推出了各自的小程序平台,比方京东、阿里、百度、字节跳动、360 等等,为了让咱们的电商业务能疾速移植到这些小程序平台,帮忙咱们的业务疾速拓展渠道,咱们开始了新的尝试。 咱们开始尝试应用技术的伎俩,摸索一种可能对立所有平台的新技术。 乘风破浪——初代架构诞生— 用 React 写小程序? 后面有提到,为了解决各大小程序平台带来的多端开发的痛点问题,社区先涌现出了 WePy[1] 和 mpvue[2],那咱们为什么不间接采纳,而要抉择“造轮子”呢? 在过后的前端界言及前端框架,必离不开仍然放弃着统治位置的 React[3] 与 Vue[4],这两个都是十分优良的前端 UI 框架,而且在网上也常常能看到两个框架的粉丝之间激情交换,碰撞出一些思维火花,让社区异样沉闷。 而咱们团队也在 2017 年怯懦地摈弃了历史包袱,十分荣幸的引入了 React 技术栈。这让咱们团队丢掉了煤油灯,开始通上了电,远离了刀耕火种的前端开发形式。为了解决过后业务环境对极致性能以及低版本 IE 浏览器兼容性的要求,咱们还研发出了一款优良的类 React 框架 Nerv[5] ,并因而对 React 开发思维以及技术栈了解更加粗浅。 遗憾的是,过后社区并没有一款应用 React 开发小程序的框架。 与小程序的开发方式相比,React 显著显得更加现代化、规范化,而且 React 天生组件化更适宜咱们的业务开发,JSX 也比字符串模板有更强的表现力。那时候咱们开始思考,咱们能不能用 React 来写小程序?[6] 感性地摸索 通过比照体验 小程序和 React ,咱们还是能发现两者之间类似的中央,比方生命周期、数据更新形式以及事件绑定,都具备十分多类似的中央,这为咱们应用 React 来小程序提供了十分良好的根底。 然而,咱们也应该看到小程序和 React 之间的微小的差别,那就是模板。在 React 中,是应用 JSX 来作为组件的模板的,而小程序则与 Vue 一样,是应用字符串模板的。这是两种齐全不一样的货色,也是咱们计划摸索上的微小阻碍。所以,为了实现应用 React 来写小程序这一指标,咱们必须解决两者之间微小差别的问题。 解决差别 既然微信不反对 JSX,那咱们只须要将 JSX 编译成小程序模板不久能够在微信上运行了吗,这一步能够通过 Babel[7] 来实现。 Babel 作为一个 代码编译器 ,可能将 ES6/7/8 的代码编译成 ES5 的代码,其的编译过程次要蕴含三个阶段: 解析过程,在这个过程中进行词法、语法分析,以及语义剖析,生成合乎 ESTree 规范 [8] 虚构语法树(AST) 转换过程,针对 AST 做出已定义好的操作, babel 的配置文件 .babelrc 中定义的 preset 、 plugin 就是在这一步中执行并扭转 AST 的 生成过程,将前一步转换好的 AST 生成指标代码的字符串 ...

January 27, 2021 · 1 min · jiezi

关于taro:Taro-源码解读-miniRunner-篇

因为近期应用到 Taro 编写小程序,出于好奇,筹备研读一下 Taro 的源码。 在上一篇文章 Taro 源码解读 - taro build 篇 中,曾经解说了 taro-cli 的实现原理,而后以 taro build 为案例解释了外围 Kernel + 钩子的运行机制,以及最终达到 webpack 构建阶段。 本篇文章将会是对 taro build 篇的一个补充,着重介绍运行 taro build 后,最终 webpack 实现的打包机制,以及简略介绍一下 Taro Next 从编译时到运行时的转变。 话不多说,咱们开始吧。 miniRunner 概览miniRunner 其实是一个函数,咱们先来整体看看 miniRunner 所做的事件吧(如下图) 咱们来逐行解析一下代码实现: 代码行数解释第 21 行定义构建 mode,也就是 `"production""development""none"`第 24 行欠缺构建配置,这里次要是欠缺一些 sass loader 的配置第 27~37 行依据我的项目配置生成 webpack 的构建配置第 39~80 行应用 webpack 进行代码编译从下面的剖析能够看出,miniRunner 次要做的工作就是依据我的项目配置组装 webpack 配置,而后依据 webpack 配置生成编译后的代码。 接下来,咱们重点关注我的项目自带的 webpackChain 配置(第 27 行),看看默认的配置是什么样的吧~ ...

January 21, 2021 · 2 min · jiezi

关于taro:Taro-源码解读-taro-build-篇

因为近期应用到 Taro 编写小程序,出于好奇,筹备研读一下 Taro 的源码。 在上一篇文章 Taro 源码解读 - @tarojs/cli 篇 中,曾经解说了 taro-cli 的实现原理,而后以 taro init 为案例解释了外围 Kernel + 钩子的运行机制。 本篇文章将会是对 @tarojs/cli 篇的一个补充,着重介绍 taro build 的运行机制,以及不同平台的编译差异。 话不多说,咱们开始吧。 taro build咱们首先来看看在一个 Taro 我的项目中 package.json 的 scripts 局部(如下图) 从上图能够看出,taro build 命令次要的参数是 type,在 dev 模式下,会减少一个额定的 watch 参数。 基于这个印象,咱们来看看运行 taro build 后会产生什么吧~ Kernel首先,Cli 实例将会对命令行参数进行解析,而后进入到 build 操作(如下图) 在上图的第 41 行运行的 build 函数,实际上是运行了 kernel.run() 函数(如下图) 咱们再来解析一下 kernel.run() 函数所做的事件吧~ kernel.run这一段是 Kernel 的核心内容,了解了这段就了解了 taro-cli 的工作模式下图是 kernel.run 办法,咱们来进行逐行剖析(如下图) ...

December 22, 2020 · 1 min · jiezi

关于taro:Taro-周报-7-收获e代驾案例发布-v2216-和-v320canary2

Taro 周报 2020 年 12 月 05 日 - 2020 年 12 月 12 日 ,更多的Taro周报点击Taro 大事件58 技术公布文章《开源 | Taro 3 反对 React Native》Taro 3 公布后暂不反对 React-Native 平台,于是咱们向社区提交了一份实现草案,心愿把 58 在 React-Native 上的技术积攒分享到社区,同时也从社区对 Taro 的共建上获益。 Taro 2 公布 v2.2.16[修复] H5 路由地址替换谬误[修复] 修复插件中援用 taro-ui 组件门路谬误Taro 3 公布 v3.2.0-canary.2疾速修复了试用 Taro 3 React Native 的开发者提出的多个 issue。 播种的案例本周小伙伴们分享了 3 个案例: e代驾 来自 eazdp 提交 逃大拿 来自 王建立 提交 ...

December 17, 2020 · 5 min · jiezi

使用-Taro-Hooks-快速开发一个小程序-GitHub-Pro

在 Taro Hooks 出来 之后就一直想着体验一波 Hooks 小程序开发,不过一直忙着补番 ????。最近补完了,就搞了起来,开发了 20 天左右(其实大部分时间都在改 UI????),基本上是完成了,然后也上架了,遂跟大家分享一点心得 ???? 可以先扫描体验: 网络不稳定的小伙伴看预览: 在 GitHub Pro 的开发中,我写了四个 hooks,来帮助我提高开发效率 useRequestuseRequestWithMoreuseReachBottomEventusePullDownRefreshEvent接下来就分析一下它们的作用 useRequest作用同名字,用来进行网络请求,传入请求参数以及进行请求的函数,存储数据,返回 [currData, refresh] ,其中currData是存储的返回数据,refresh用于刷新请求。 function useRequest<T>( params: any, request: (params: any) => Promise<T | null>): [T | null, () => void] | [] { const [currData, setData] = useState<T | null>(null) const [count, setCount] = useState(0) const pagePullDownRef = useRef('') useEffect(() => { request(params).then(data => { if (data) { setData(data) } }) }, [count]) usePullDownRefresh(() => { refresh() }) useEffect(() => { events.on(PULL_DOWN_REFRESH_EVENT, (page: string) => { if (!pagePullDownRef.current) { pagePullDownRef.current = page } else if (pagePullDownRef.current !== page) { return } refresh() }) return () => { events.off(PULL_DOWN_REFRESH_EVENT) } }, []) const refresh = () => { setCount(count + 1) } return [currData, refresh]}export default useRequestuseRequest ...

September 19, 2019 · 3 min · jiezi

taro采坑-render-jsx与传递jsx

多端开发框架,taro,采坑点: 1.组件内部的渲染jsx,须以render开头,比如: renderTitleclass MinePage extends Component { // good renderLeft () { return ( <Text>left</Text> ) } // bad right () { return ( <Text>right</Text> ) } render () { return ( <View> <View>{ this.renderLeft() }</View> <View>content</View> <View>{ this.right() }</View> </View> ) }}export default MinePage这样子或许体会不是很深刻,都能跑,但是当组件传递jsx的时候会爆炸,譬如以下这个简单的函数式组件例子 LpInput,它在input左右各接收一个参数left、right,父组件可能会传递string、jsx等等: import Taro, {useState} from '@tarojs/taro'import {View, Text, Input} from '@tarojs/components'import './index.css'const LpInput = props => { const {placeholder = '请输入'} = props console.log(props) return ( <View className='lp-input'> <View className='left'> { props.left } // 此处没有写 renderLeft </View> <View className='middle'> <Input placeholder={ placeholder } placeholderClass='placeholder-style'/> </View> <View className='right'> { props.right } // 此处没有写 renderRight </View> </View> )}export default LpInput调用: ...

September 10, 2019 · 1 min · jiezi

微信小程序支付功能全流程实践

前言微信小程序为电商类小程序,提供了非常完善、优秀、安全的支付功能。在小程序内可调用微信的API完成支付功能,方便、快捷。小程序开发者在开发小程序时,支付流程是必然要接触到,今天胡哥就小程序支付的全流程为大家一一细说,让小伙伴能快速得掌握小程序支付能力,避免踩坑! 知己知彼,方能百战不殆 - 小程序支付流程图 举个栗子????:某用户小明在某电商小程序上购买一块肥皂,从浏览、下单到支付经历了什么样的过程呢? 打开电商小程序,搜索浏览到某雕肥皂,点击查看详情后,发现大小、丝滑程度都很合适,点击直接下单wx.login获取用户临时登录凭证code,发送到后端服务器换取openId在下单时,小程序需要将小明购买的商品Id,商品数量,以及小明这个用户的openId传送到服务器服务器在接收到商品Id、商品数量、openId后,生成服务期订单数据,同时经过一定的签名算法,向微信支付发送请求,获取预付单信息(prepay_id),同时将获取的数据再次进行相应规则的签名,向小程序端响应必要的信息(必须字段信息将在下文进行详细说明)小程序端在获取对应的参数后,调用wx.requestPayment()发起微信支付,唤醒支付工作台,进行支付小结进行微信支付,在小程序端我们主要做三件事: 注:服务端调用统一下单API、签名算法不再本次分享讨论范围内,请期待胡哥的另外一次分享。使用wx.login获取临时登录凭证code,发送到后端获取openId wx.login({ success (res) { if (res.code) { // 发起请求,换取openId wx.request({ url: '', data: { code: res.code } }) } }})将openId以及相应需要的商品信息发送到后端,换取服务端进行的签名等信息 wx.request({ url: '', data: { openId: '', num: 1, id: '111' }})接收返回的信息(必须要包含发起微信支付wx.requestPayment的参数),发起微信支付 wx.requestPayment({ // 时间戳 timeStamp: '', // 随机字符串 nonceStr: '', // 统一下单接口返回的 prepay_id 参数值 package: '', // 签名类型 signType: '', // 签名 paySign: '', // 调用成功回调 success () {}, // 失败回调 fail () {}, // 接口调用结束回调 complete () {}})注意:以上信息中timeStamp、nonceStr 、prepay_id、signType、paySign各参数均建议必须都由服务端返回(这样会尽最大可能性保证签名数据一致性),小程序端不做任何处理基于Taro的微信支付实例import Taro, { Component } from '@tarojs/taro'import { View, Text, Button } from '@tarojs/components'import './index.scss'export default class Index extends Component { config = { navigationBarTitleText: '首页' } componentWillMount () { } async componentDidMount () { } componentWillUnmount () { } componentDidShow () { } componentDidHide () { } /** * sendOrderInfo() * @description 提交订单信息,获取支付凭证,唤起支付 */ async sendOrderInfo () { // 获取临时登录凭证code let codeData = await Taro.login() // 换取openId let openId = '' if (codeData.code) { let res = await Taro.request({ // 定义的后端换取openId的接口 url: 'https://www.justbecoder.com/getLogin', data: { code: codeData.code } }) if (res && res.code === 0) { openId = res.openId } } // 发送openId以及对应的商品信息 let data = await Taro.requrest({ url: 'https://www.justbecoder.com/createdOrder', data: { openId, // 实际情况的商品数量 num: 1, // 实际情况的商品Id id: 111, } }) // code === 0 表示提交订单成功,返回需要的签名信息等 if (data && data.code === 0) { let { timeStamp, nonceStr, prepay_id, signType, paySign } = data.payInfo // 唤起支付,按小程序要求格式发送参数 let payData = await Taro.requestPayment({ timeStamp, nonceStr, package: 'prepay_id=' + prepay_id, signType, paySign }) if (payData && payData.errMsg === 'requestPayment:ok') { Taro.showModal({ title: '操作提示', content: '支付成功', showCancel: false, confirmText: '确定' }) } else { Taro.showModal({ title: '操作提示', content: '支付失败,请重新尝试', showCancel: false, confirmText: '确定' }) } } } render () { return ( <View className='index'> <Button onClick={this.sendOrderInfo}>立即下单</Button> </View> ) }}效果图 ...

July 16, 2019 · 2 min · jiezi

基于Taro的微信小程序分享图片功能实践

前言在各种小程序(微信、百度、支付宝)、H5、NativeApp 纷纷扰扰的当下,给大家强烈安利一款基于React的多终端开发利器:京东Taro(泰罗·奥特曼),Taro致力于多终端统一解决方案,一处代码,多处运行。 Taro支持以React语言开发小程序,支持CSS预处理器,支持实时编译更新,支持NPM,等等等等,简直不要太爽!微信小程序分享图片功能是经常在小程序业务中出现的,比如学习打卡分享,推广会员分享,推广商品分享等等。因为小程序是不支持直接分享图片到朋友圈的,一般操作为: 生成包含小程序码(当前也可以是其他特定的信息)的图片;用户点击保存图片,下载到本地,再发布到朋友圈;其他用户长按识别该小程序码,进入当前小程序。今天胡哥给大家分享下,基于Taro框架实现微信小程序分享图片功能的实践。 一、搭建Taro项目框架,创建微信小程序1. 安装taro脚手架工具npm install -g @tarojs/cli2. 初始化taro项目taro init taro-img-share3. 编译项目,开启Dev模式,生成小程序 -- dist目录npm run dev:weapp4. 微信开发者工具,创建小程序,选择项目根目录为taro-img-share下的dist目录二、小程序分享图片功能实践 --- 打卡图片分享功能先上图,再说话 这是重点:使用Canvas绘制图片并展示,保存图片到相册drawImage()方法负责绘制展示,saveCard()方法负责下载图片到相册src/pages/index/index.js import Taro, { Component } from '@tarojs/taro'// 引入对应的组件import { View, Text, Button, Canvas } from '@tarojs/components'import './index.scss'export default class Index extends Component { config = { navigationBarTitleText: '首页' } /** * 初始化信息 */ constructor () { this.state = { // 用户信息 userInfo: {}, // 是否展示canvas isShowCanvas: false } } /** * getUserInfo() 获取用户信息 */ getUserInfo (e) { if (!e.detail.userInfo) { Taro.showToast({ title: '获取用户信息失败,请授权', icon: 'none' }) return } this.setState({ isShowCanvas: true, userInfo: e.detail.userInfo }, () => { // 调用绘制图片方法 this.drawImage() }) } /** * drawImage() 定义绘制图片的方法 */ async drawImage () { // 创建canvas对象 let ctx = Taro.createCanvasContext('cardCanvas') // 填充背景色 let grd = ctx.createLinearGradient(0, 0, 1, 500) grd.addColorStop(0, '#1452d0') grd.addColorStop(0.5, '#FFF') ctx.setFillStyle(grd) ctx.fillRect(0, 0, 400, 500) // // 绘制圆形用户头像 let { userInfo } = this.state let res = await Taro.downloadFile({ url: userInfo.avatarUrl }) ctx.save() ctx.beginPath() // ctx.arc(160, 86, 66, 0, Math.PI * 2, false) ctx.arc(160, 88, 66, 0, Math.PI * 2) ctx.closePath() ctx.clip() ctx.stroke() ctx.translate(160, 88) ctx.drawImage(res.tempFilePath, -66, -66, 132, 132) ctx.restore() // 绘制文字 ctx.save() ctx.setFontSize(20) ctx.setFillStyle('#FFF') ctx.fillText(userInfo.nickName, 100, 200) ctx.setFontSize(16) ctx.setFillStyle('black') ctx.fillText('已在胡哥有话说公众号打卡20天', 50, 240) ctx.restore() // 绘制二维码 let qrcode = await Taro.downloadFile({ url: 'https://upload-images.jianshu.io/upload_images/3091895-f0b4b900390aec73.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/258/format/webp.jpg' }) ctx.drawImage(qrcode.tempFilePath, 70, 260, 180, 180) // 将以上绘画操作进行渲染 ctx.draw() } /** * saveCard() 保存图片到本地 */ async saveCard () { // 将Canvas图片内容导出指定大小的图片 let res = await Taro.canvasToTempFilePath({ x: 0, y: 0, width: 400, height: 500, destWidth: 360, destHeight: 450, canvasId: 'cardCanvas', fileType: 'png' }) let saveRes = await Taro.saveImageToPhotosAlbum({ filePath: res.tempFilePath }) if (saveRes.errMsg === 'saveImageToPhotosAlbum:ok') { Taro.showModal({ title: '图片保存成功', content: '图片成功保存到相册了,快去发朋友圈吧~', showCancel: false, confirmText: '确认' }) } else { Taro.showModal({ title: '图片保存失败', content: '请重新尝试!', showCancel: false, confirmText: '确认' }) } } render () { let { isShowCanvas } = this.state return ( <View className='index'> <Button onGetUserInfo={this.getUserInfo} openType="getUserInfo" type="primary" size="mini">打卡</Button> {/* 使用Canvas绘制分享图片 */} { isShowCanvas && <View className="canvas-wrap"> <Canvas id="card-canvas" className="card-canvas" style="width: 320px; height: 450px" canvasId="cardCanvas" > </Canvas> <Button onClick={this.saveCard} className="btn-save" type="primary" size="mini">保存到相册</Button> </View> } </View> ) }}src/pages/index/index.sass ...

July 15, 2019 · 2 min · jiezi

TypeScript基础类型与联合类型

TypeScript对它的学习其实是在前年就开始了,后面一直没有机会在项目中使用,直到最近我司开发小程序,使用京东的taro才算真正了。使用的过程中,发现对其基本知识点并没有通透,所以才想到要记录学习的过程。 在使用的过程中,它最大的特点其实就是数据类型的定义。 基础类型的定义字符串类型let name: string = 'Mondo'布尔类型let isDone: boolean = true数字类型let dec: number = 1数组let list: number[] = [1, 2]orlet list: Array<number> = [1, 2]元组Tuple定义一个已知元素数量和类型的数组,各元素的类型不必相同 let list: [string, number]list = ['1', 2]枚举enum Animal { dog = 'hei', cat, pig }let dor: Animal = Animal.dogAny定义一个未知数据类型的变量 let nothing: any = 2Void表示没有类型,通常用于一个函数没有任何返回值 function warnUser(): void { console.log("This is void");}Null 和 Undefined默认情况下null和undefined是所有类型的子类型,你可以把他们赋值给任何类型的变量 let u: undefined = undefinedlet n: null = nulllet dec: string = null类型断言类型断言好比语言里的类型转换,但是不进行特殊的数据检查和解构 ...

July 13, 2019 · 1 min · jiezi

Taro-13-震撼发布全面支持-JSX-语法和-Hooks

在 Taro 1.2 发布之后,Taro 在业界收获了巨大的赞誉和关注:GitHub 上 Star 数量超过 19000 粒,NPM 下载量也稳居同类开发框架之首,同时 Taro 团队也和腾讯、百度、华为等数十家业界巨头的研发团队展开了深入和有效的合作。 Taro 1.3 是我们酝酿最久的版本:经历了横跨 6 个月的开发时间,近 2000 次的代码提交,近百位开发者的共同参与。我们终于在今天骄傲地发布了 Taro 1.3。 Taro 1.3 的特性包括但不限于: 支持快应用和 QQ 小程序的开发全面支持 JSX 语法和 React Hooks大幅提高 H5 性能和可用性Taro Doctor支持快应用和 QQ 小程序的开发快应用的开发模式非常特别,它的 API、组件系统、组件库和其他小程序端差异非常大,并且快应用只是一个标准,各家安卓厂商对运行时的实现也各不相同。而这块「硬骨头」终于也被 Taro 啃下了。 QQ 小程序作为新兴的小程序类容器,大家普遍对它知之甚少,但 Taro 也率先实现了对 QQ 小程序的支持。 支持快应用和 QQ 小程序意味着 Taro 真正对业界主流小程序实现了「全覆盖」,不管你的业务要支持哪一个小程序端,只要维护一套代码,Taro 就能生成对应小程序平台的代码。同时 Taro 也成为了业界首个同时支持微信小程序、百度智能小程序、字节跳动小程序、支付宝小程序、快应用、QQ 小程序共 6 端小程序的开发框架。 全面支持 JSX 语法和 React Hooks作为使用 React 和 JSX 语法的开发框架,Taro 早期的版本在编译器和编辑器检查工具都对语法做了高强度的限制。而在 Taro 1.3 中,开发者可以充分发挥自己的创造力和想象力,可以任意地写 if-else,可以任意地写匿名函数,可以把 JSX 放在类函数中,也可以放在普通函数中,等等。只要编译器和和 ESLint 不报错,就可以这么写。 ...

June 14, 2019 · 2 min · jiezi

Taro-优秀学习资源汇总

Awesome Taro多端统一开发框架 Taro 优秀学习资源汇总 官方资源Taro 项目仓库Taro 官方文档Taro UI 项目仓库Taro UI 官方文档微信小程序官方文档百度智能小程序官方文档支付宝小程序官方文档字节跳动小程序官方文档文章教程不敢阅读 npm 包源码?带你揭秘 taro init 背后的哲学从0到1构建适配不同端(微信小程序、H5、React-Native 等)的taro + dva应用【小程序taro最佳实践】http请求封装(方便使用,增加token,统一错误日志记录和上报)【小程序taro 最佳实践】异步action优雅实践(简化流程)使用Taro框架开发小程序Taro下利用Decorator快速实现小程序分享微信小程序授权登陆方案以及在Taro下利用Decorator修饰器实现试用React语法的多端框架Taro问题汇总Taro 在京东购物小程序上的实践Taro实践 - TOPLIFE小程序 开发体验Taro 技术揭秘:taro-cli为何我们要用 React 来写小程序 - Taro 诞生记GitLab-CI微信小程序进行持续集成和持续部署使用Taro和Typescript进行小程序开发微信小程序及h5,基于taro,zoro最佳实践探索手把手教你用Taro框架写一个图像处理类微信小程序Taro 多端开发的正确姿势:打造三端统一的网易严选(小程序、H5、React Native)Taro 与 Redux 结合使用教程微信小程序开发之影分身术开发技巧微信小程序 wx.request 对于 JSON 含 u2028 处理异常Taro 最佳实践封装Taro.request(拦截器,url配置等)示例项目Taro Redux 示例 taro-redux-sampleTodoMVC (小程序/H5/React Native)Taro 组件库示例 taro-components-sampleTaro 端能力示例 taro-apis-sampleTaro 实验性特性项目 taro-todoTaro脚手架(特性: 封装api、redux优雅集成、异常日志上报)仿知乎小程序Taro整合Dva示例V2ex小程序(TypeScript)与微信小程序原生融合的示例taro结合zoro完整方案示例Taro-UI + Rematch 示例Taro+dva+wxParse多端富文本解析示例Taro+Taro-UI+es6 入门demo知识付费小程序(TypeScript)书店小程序: Taro + Redux + 本地 Mock Server 示例项目Taro 示例项目(内置 计数器 与 异步请求): Taro + Dva + Typescript + Immutable记日常小程序 Taro-UI + Rematch + 云开发 + tcb-router开源项目????首个 Taro 多端统一实例 - 网易严选(小程序 + H5 + React Native)????基于Taro + Dva构建的时装衣橱(电商实战项目)????基于Taro + Taro-ui + dva开发的公益AppTaro 掘金小册源码基于taro + dva开发的音乐播放器小程序Taro + Taro-UI GitHub小程序客户端Gitter源码第三方库和组件f2图表封装 兼容H5和微信小程序echarts图表封装taro-plugin-canvas - 基于Taro的小程序海报组件taro-bdmark - 基于Taro的百度小程序md解析器Mounted - 一款基于 Taro 的小程序组件库taro-axios - 在 Taro 中使用 axios

June 10, 2019 · 1 min · jiezi

使用taro开发小程序我遇到的坑-欢迎各位大侠补充

在开发小程序的技术选型上,我选择了taro + taro ui + typescript, 整理了一下我在开发小程序时候遇到的一些问题,后续持续更新,也欢迎大家补充taro 官网taro-ui 官网 一: 怎么加入第三方字体库? 在这里我使用的是阿里的iconfont 1)在iconfont官网新建自己的项目,并且将需要的字体加入到新建的项目中,查看该项目,可以看到如下页面: 2)点击下载到本地,解压后我们选择iconfont.css加入到自己的开发项目中 3)修改.css后缀为.scss(因为项目中用的是scss), 打开iconfont.scss文件,我们可以看到以下内容 @font-face {font-family: "iconfont"; src: url('iconfont.eot?t=1560144283341'); /* IE9 */ src: url('iconfont.eot?t=1560144283341#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAaQAAsAAAAADRwAAAZEAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDQAqMcIpAATYCJAMYCw4ABCAFhG0HfRsTC1FUbfJkPzV5ggizAQXawIIgxu1RDgAAgAQFIC0AxMP/r/1+n3vHmId4U2vQPWoiRBKLRCIkSBC9UTolE4r+/uv8ufb2CkCqKUhiXEg2+ZTFIrtONaCjO3bHek84VECy9jr/Otv/EdpYPZVTm1BtUEnSFnFtwRvwyLlN4A1i07lPvBuxBEbmvn6/Vk81NGzxdqUQ43vfFxVLLkmn3xCSuKSIV0Kjdhada8KcpYAzYqxx7MueT6A1LYPXYVPHAAQq1IEBtS6ZIkDgRpRiwR6a0IrYtxAP4KdJd942gHvz/vEP8iKQpMzqC51dNMpQ85XznfqsP6ybyOuwLpeA8CAyNoGCuCaTrvhMuGmCWmplX/tAq5EkqZYV3zu/04eOlUIRnyikbdjbilYI/fOiUgf1g/qouOQrp/Mn8VXBUsGdEKGAYAUiZAimwH+lsEqwYB6kdYx+hNq0ablaqU+lXlySVtuaTMwkjGFYliFd7miaUg1YHKeNMXTMgddyCZWf4/CiL+nMm0+xIKhIGoSlPUvpn10JWEn3EkbTQiq5Q9LO6bAcVKhdK6WgJHfa19Mj9vb6uruFri4eoj1N6Rbd9yAYCDtFNVQnsYISrD5GhtWQRlFSa+8PF3odOVKRUpSgTJJHI2qDzKm/hmR3ZVR3nTeDcF9YhTCkSQoA6zKzpt+qppT1LksTXrfF7Kl4hYSFHdNprI8VtE8zv+uMR5NTn8VDdelMRY0HM1XWnghWp5A4JdWA44+7o6BQLs0F1bADiKrsAA/jlNJCThLqxLAMKG+SyycoLBkKtEoHLx5P+whSfq5g4Y0omBB+larl5foy6S8f4UV+nk/wbYCkz1T+0T0h51MqE5lPG8xIFYXmfvbTIKxoJ5ML6RMvby0B8uICwTfvGP32OyXNoKTnbFRe7cZyalKZjVDqjbbPa1bVReO5tWlIv20Rlv9xOw0svy98jtt97vOi/JaD814YgWu/ZX/r8DHUf78EdbCJ0G060YRwnPWl1cvZHtu8UxNrsNZWR0i36IT7RF0V8F6OalQ5tszQND88fx4GoF9n+JEAktr8MFA+AIHuhYGB9BLnLeMFHPAHIAe9isBK4P4lnXPsWI4nOcqTPagPPiRlGXWrbsRlU1et3mR4iC5ZMnFgIkaiXydsuJllaomGxWDrVrjka0uG5MVgYOOkj7Lmrd6Xu2L2+ti/GX9vaoROhIiINaKn6r+M6cudfHgyQhw0bdrYvjVYa60jtn7Vww4SETClg5v4wr0/OP6UGOuN+59eNrL3O3a56tnIwvGFtdiwZ2Fk0AEnneiZyOqOyxn4eXzN9I3TDAmnDedJF+nvKf6vz+P7s9cWu4q9Sd5ipyaQsBbfzyu3XybSY/lm++knYchtOUtuzCI499I0a1q/N8FlUaWk5az72MN75DA7TAYxxKW1krbeHuai/4ym3l7wv03fsWuXqgwf9IBNTl7tfz4/f/sWzqcEQ7t1/upHMQNY+Y8QHOfT/7iaADX8PIAjSO5c6eDAu5dcAQVxdfO8D/D+/dupraovXtjAd84jba93XnZkT8B++H5AT871S0+8zjkvokbRbjfk8UtBNV5wjV2S4PkWsHpM/m/XhjR1n4t6VIARWingFpYBOqI/rwHozoc+SQBd+dynhCIa1w3natez/P4zaTqi4j6Q+ifP+kwPPsLMTgF81ukFyzk3yjG7FIVQY1X2qmNZ7pVHEJSScJDt3GbaqC9SsRNkS0JjYgVJbQqyxqK8UG9CqWcbKo0daG1oPrhnjM5HFH1Yt6MgDHsMyaAPyIZ9lBfqz1Ca9A8qw+EDrfOIOWbPcrDgeh0LJpaRuICYxD2kjTWDx9p9WLFcgr4uGJchrBssF+Vm5WyXNmMP1iV2MGaVPNMkiOjcjZrUxbDLxZGqcweWzCxqmmpZdjahdsySuBuCHdBhAlP7lCGiBUdGwnmQmU3BO2/fB1NYXAT6Qqe54xBMZ2BLR3JlyWHIm4M8rE7vpaFhliKPiQkE6lrqODekCRMwV1PEISq9lgMmMWWhFgWqMtnYE+EGZV0/xP0rL4JWfcwqUuQooowqakM3VOCiJfcRDvadKvCQRWoJ8SrEmrOCPHjOmP446yaqmC1SRrFEBQ6KFi2ZwEMNbhmWXSQA') format('woff2'), url('iconfont.woff?t=1560144283341') format('woff'), url('iconfont.ttf?t=1560144283341') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ url('iconfont.svg?t=1560144283341#iconfont') format('svg'); /* iOS 4.1- */}.iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}.iconzhaobudaojieguo:before { content: "\e668";}为了项目结构的干净,我把icon单独抽成了一个文件夹 ...

June 10, 2019 · 2 min · jiezi

taro的坑子组件的默认属性和父组件中修改子组件样式问题以及应用复杂数据

坑一:子组件的默认属性 如果这样获取可选属性的默认值: const { startScore = 0, currentScore = 0, endScore = 0, showStartAndEnd = true } = this.props;而又没有实际传入属性的话页面中就会显示null。我们需要在子组件内这样定义默认属性: static defaultProps = { currentScore: 0, startScore: 0, showStartAndEnd: true, endScore: 0, };坑二:父组件中修改子组件样式 如果子组件在多个地方用,那么就需要在不同地方展示不同的子组件样式,所以就需要在父组件中修改样式。而taro不能像react那样直接修改,而是需要这样麻烦的步骤: 先在子组件中定义有哪些拓展的class: static externalClasses = ['my-class', 'radio-class', 'img-class', 'info-class', 'add-class', 'count-class', 'delete-class'];再将拓展的class应用到子组件样式可变的地方: <View className="Goods-radio radio-class" onClick={this.onSelectGoods}> <PRadio value={selected}/> </View>如图中的radio-class 之后在父组件中应用对应的将对应的class作为属性传输: <Goods stock={item.goods.stock} goodsId={item.goods.id} key={index} my-class={'shopping-good'} radio-class={'Goods-radio'} count-class={'Goods-count'} delete-class={'Goods-remove'} img-class={'Goods-img'} info-class={'Goods-info'} >如图中的radio-class。现在我们终于可以在父组件用Goods-radio作为className来修改子组件的样式了。 ...

May 23, 2019 · 1 min · jiezi

基于Taro开发的小程序多端UI组件库-tarocustomui

taro-custom-uiCustomUI 是一套基于Taro开发的小程序多端UI组件库,目的在于给开发者提供更灵活的布局组件及样式,以满足更多个性化的场景 Featured Components<CustomPage /> <CustomTransition /> //类似vue的transition 支持transition动画Commmon Components<CustomList /><CustomSwitch />Developing , Welcome To Experience // 开发中,欢迎体验

May 10, 2019 · 1 min · jiezi

Taro小程序从0到1架构项目打造自己的完美脚手架

这个可以让你的Taro小程序跑的更优雅一些 https://github.com/wsdo/taro-kit7个月前输出了一套taro-kit 脚手架,有不少人加我微信,咨询一些问题,这段时间把这个脚手架升级后,总结并录制了课程,希望能帮助到大家,提高效率,节约时间。升级后的项目仓库地址: 观看视频的同学加微信,发送你的gitlab账号,添加权限,你就看源代码了。https://gitlab.com/itxishu/ta... 课程地址思否编程 segmentfaulthttps://segmentfault.com/ls/1... 适宜人群taro小程序开发者需要taro基础架构开发人员课程说明本次课程主要针对于,正在使用taro小程序框架的同学,通过课程,你可以学到,框架的request请求优雅封装,异常自动重试,日志异常上报,redux的三剑客优雅的配合使用,reducer 的swich简化繁琐操作,增加state的请求前,请求成功和失败的状态等。从开始架构足以支撑庞大业务小程序项目 课程有问题可以在 https://shudong.wang 我的博客扎到我,添加微信咨询 课程大纲01.taro从0到1项目架构课程介绍02.初始化项目流程介绍、目录设计03.让alias别名解决路径引用的烦恼04.请求api返回redux的状态流程05.封装request get请求,给url添加时间戳防止浏览器缓存06.封装request post Content-Type 分类请求07.把taro-advance脚手架推送到私有仓库08.弱网请求失败时自动发起api重试09.异常日志上报封装设计思路10.异常日志上报封装,五种级别输出。11.上报收集日志平台系统介绍12.实战接入日志平台13.深度序列化错误error控制台上报14.登录流程讲解(前端和后端实现流程)15.登录实现详细讲解(token附加到请求header头)16.用户授权后更新用户信息流程17.设计createApiAction自动dispatch优化开发体验18.改造actionType支持庞大业务19.Action三种ActionType的集合20.简化reducers的swich繁琐操作21.增加request的状态22.课程总结 课程主站https://www.itxishu.comhttps://github.com/itxishu 关于https://shudong.wang 有问题来这里提问http://t.shudong.wang

April 28, 2019 · 1 min · jiezi

Taro 项目 不同平台 打包到不同目录

Taro 项目 不同平台 打包到不同目录目的:同时编译多套代码,一次修改,同时预览不同平台小程序在项目根文件夹下 config/index.js 中 新增const outputRootStrtegy = { h5: ‘dist_h5’, weapp: ‘dist_weapp’, alipay: ‘dist_alipay’, swan: ‘dist_swan’, [‘undefined’]: ‘dist’}const env = JSON.parse(process.env.npm_config_argv)[‘cooked’][1].split(’:’)[1]const outputRoot = outputRootStrtegy[env]修改 outputRoot 选项的值完整代码config/index.jsconst outputRootStrtegy = { h5: ‘dist_h5’, weapp: ‘dist_weapp’, alipay: ‘dist_alipay’, swan: ‘dist_swan’, [‘undefined’]: ‘dist’}const env = JSON.parse(process.env.npm_config_argv)[‘cooked’][1].split(’:’)[1]const outputRoot = outputRootStrtegy[env]const config = { projectName: ’time-paper-mini’, date: ‘2018-12-11’, designWidth: 750, deviceRatio: { ‘640’: 2.34 / 2, ‘750’: 1, ‘828’: 1.81 / 2 }, sourceRoot: ‘src’, outputRoot: outputRoot, plugins: { babel: { sourceMap: true, presets: [ ’env’ ], plugins: [ ’transform-decorators-legacy’, ’transform-class-properties’, ’transform-object-rest-spread’ ] } }, defineConstants: {}, copy: { patterns: [], options: {} }, weapp: { module: { postcss: { autoprefixer: { enable: true, config: { browsers: [ ’last 3 versions’, ‘Android >= 4.1’, ‘ios >= 8’ ] } }, pxtransform: { enable: true, config: {} }, url: { enable: true, config: { limit: 10240 // 设定转换尺寸上限 } } } } }, h5: { publicPath: ‘/’, staticDirectory: ‘static’, module: { postcss: { autoprefixer: { enable: true } } }, h5: { esnextModules: [’taro-ui’] } }}module.exports = function (merge) { if (process.env.NODE_ENV === ‘development’) { return merge({}, config, require(’./dev’)) } return merge({}, config, require(’./prod’))} ...

April 10, 2019 · 1 min · jiezi

跨端开发框架深度横评

上周,Taro 团队发布了一篇《小程序多端框架全面测评》,让开发者对业界主流的跨端框架,有了初步认识。感谢 Taro 团队的付出。不过横评这件事,要想做完善,其实非常花费时间。不是只看文档就行,它需要:真实的动手写多个平台的测试demo,比较各个平台的功能、性能,它们的实际情况到底是不是如文档宣传的那样?真实的学习每个框架,了解它们的学习曲线,在实际开发中遇到问题时,感受它们的文档、教程、社区生态和技服能力到底怎么样?我们 uni-app 团队投入一周完成了这个深度评测,下面我们就分享下,实际开发不同框架的测试例遇到的问题,和最终的测试结果。评测实验介绍开发内容:开发一个仿微博小程序首页的复杂长列表,支持下拉刷新、上拉翻页、点赞。界面如下:开发版本:一共开发了6个版本,包括微信原生版、wepy版、mpvue版、taro版、uni-app版、chameleon版(以这些产品发布时间排序,下同),按照官网指引通过cli方式默认安装(应该是最新稳定版)。测试代码开源(Github仓库地址:https://github.com/dcloudio/test-framework),Tips:若有同学觉得测试代码写法欠妥,欢迎提交 PR 或 Issus测试机型:红米 Redmi 6 Pro、MIUI 10.2.2.0 稳定版(最新版)、微信版本 7.0.3(最新版)测试环境:每个框架开始测试前,杀掉各App进程、清空内存,保证测试机环境基本一致;每次从本地读取静态数据,屏蔽网络差异。测试维度:跨端支持度如何?性能如何?学习门槛工具与周边生态1. 跨端支持度如何开发一次,到处运行,是每个程序员的梦想。但现实往往变成开发一次,到处调错。各个待评测框架,是否真得如宣传的那样,一次开发、多端发布?我们将上述仿微博App依次发布到各平台,验证每个框架在各端的兼容性,结果如下:平台微信原生wepympvuetarouni-appchameleon微信小程序⭕️⭕️⭕️⭕️⭕️⭕️支付宝小程序❌❌⭕️⭕️⭕️❌百度小程序❌❌⭕️⭕️⭕️❌头条小程序❌❌⭕️⭕️⭕️❌H5端❌❌❌上拉加载/下拉刷新失效⭕️上拉加载/下拉刷新失效App端❌❌❌上拉加载失效⭕️列表无法滚动,无法测试上拉加载/下拉刷新测试结果说明:⭕ 表示支持且功能正常,❌ 表示不支持,其它则表示支持但存在部分bug或兼容问题wepy 2.0 宣称版已支持其他家小程序,本测试基于wepy官网指引安装的wepy-cli默认版本为1.7.3,尚不支持多端chameleon尝鲜版宣称支付宝、百度小程序,本测试基于chameleon官网指引安装的chameleon-tool默认版本为0.1.1,尚不支持其它小程序通过这个简单的例子可以看出,跨端支持度测评结论:uni-app > taro > chameleon > mpvue >wepy、原生微信小程序但是仅有上面的测试还不全面,实际业务要比这个测试例复杂很多。但我们没法开发很多复杂业务做评测,所以还需要再对照各家文档补充一些信息。由于每个框架的文档中都描述了各种组件和API的跨端支持程度。我们过了几家的文档,发现各家基本是以微信小程序为基线,然后把各种组件和API在其他端实现了一遍:taro:H5端实现了大部分微信的API,App端和微信的差异比较大。uni-app:组件、API、配置,大部分在各个端均已实现,个别API有说明在某些端不支持。可以看出uni-app是完整在H5端实现了一套微信模拟器,在App端实现了一套微信小程序引擎,才达到比较完善的平台兼容性。chameleon:非常常用的一些组件和API在各端已经实现,这部分的平台差异较少。但大量组件和API需要开发者自己分平台写代码。跨端框架,一方面要考虑框架提供的通用api跨端支持,同时还要考虑不同端的特色差异如何兼容。毕竟每个端都会有自己的特色,不可能完全一致。taro:提供了js环境变量判断和统一接口的多端文件,可以在组件、js、文件方面扩展多端,不支持其他环节的分平台处理。uni-app:提供了条件编译模型,所有代码包括组件、js、css、配置json、文件、目录,均支持条件编译,可不受限的编写各端差异代码。chameleon:提供了多态方案,可以在组件、js、文件方面扩展多端,不支持其他方式的分平台处理。跨端框架,还涉及一个ui框架的跨端问题,评测结果如下:taro:官方提供了taro ui,支持小程序(微信/支付宝/百度)、H5平台,不支持App,详见uni-app:官方提供了uni ui,可全端运行;uni-app还有一个插件市场,里面有很多三方ui组件,详见chameleon:官方提供了cml-ui扩展组件库,可全端运行,但组件数量略少,详见最后补充跨端案例:mpvue:微信端案例丰富,未见其它端案例taro:微信端案例丰富,百度、支付宝、H5端亦有少量案例uni-app:微信、App、H5三端案例丰富,官方示例已发布到6端chameleon:未看到任何端案例综合以上信息,本项的最终评测结论:uni-app > taro > chameleon > mpvue > wepy、原生微信小程序之前曾有友商掀起一番真跨端和伪跨端之争,通过本次Demo实测,这个争论可以盖棺定论了。2. 跨端框架性能如何跨端框架基本都是compiler + runtime模式,引入的runtime是否会降低运行性能?尤其是与原生微信小程序开发相比性能怎么样,这是大家普遍关心的问题。我们依然以上述仿微博小程序为例,测试2个容易出性能问题的点:长列表加载、大量点赞组件的响应。2.1 长列表加载仿微博的列表是一个包含很多组件的列表,这种复杂列表对性能的压力更大,很适合做性能测试。从触发上拉加载到数据更新、页面渲染完成,需要准确计时。人眼视觉计时肯定不行,我们采用程序埋点的方式,制定了如下计时时机:计时开始时机:交互事件触发,框架赋值之前,如:上拉加载(onReachBottom)函数开头计时结束时机:页面渲染完毕(微信setData回调函数开头)Tips:setData回调函数开头可认为是页面渲染完成的时间,是因为微信setData定义如下(微信规范):字段类型必填描述dataObject是这次要改变的数据callbackFunction否setData引起的界面更新渲染完毕后的回调函数测试方式:从页面空列表开始,通过程序自动触发上拉加载,每次新增20条列表,记录单次耗时;固定间隔连续触发 N 次上拉加载,使得页面达到 20*N 条列表,计算这 N 次触发上拉 -> 渲染完成的平均耗时。测试结果如下:列表条数微信原生wepympvuetarouni-appchameleon2007706259697526411261400876781449397474119706001111–125091029178001406–15471113404010001690–187813215196说明:以400条微博列表为例,从页面空列表开始,每隔1秒触发一次上拉加载(新增20条微博),记录单次耗时,触发20次后停止(页面达到400条微博),计算这20次的平均耗时,结果微信原生在这20次 触发上拉 -> 渲染完成 的平均耗时为876毫秒,最快的uni-app是741毫秒,最慢的mpvue是4493毫秒大家初看这个数据,可能比较疑惑,别急,下方有详细说明说明1:为何 mpvue/wepy 测试数据不完整?mpvue、wepy 诞生之初,微信小程序尚不支持自定义组件,无法进行组件化开发;mpvue、wepy 为解决这个问题,将用户编写的Vue组件,编译为WXML中的模板(template),变相实现了组件化开发能力,提高代码复用性,这在当时的技术条件下是很棒的技术方案。但如此方案,在复杂组件较多的页面,会大量增加 dom 节点,甚至超出微信的 dom 节点数限制。我们在 红米手机(Redmi 6 Pro)上实测,页面组件超过500个时,mpvue、wepy 实现的仿微博App就会报出如下异常,并停止渲染,故这两个测试框架在组件较多时,测试数据不完整。这也就意味着,当页面组件太多时,无法使用这2个框架。dom limit exceeded please check if there’s any mistake you’ve madeTips:wepy在400条列表以内,为何性能高于微信原生框架,这个跟自定义组件管理开销及业务场景有关(wepy编译为模板,不涉及组件创建及管理开销),后续对微博点赞,涉及组件数据传递时,微信原生框架的性能优势就提现出来了,详见下方测试数据。说明2:uni-app 比微信原生框架性能更好?逆天了?其实,在页面上有200条记录(200个组件)时,taro 性能数据也比微信原生框架更好。微信原生框架耗时主要在setData调用上,开发者若不单独优化,则每次都会传递大量数据;而 uni-app、taro 都在调用setData之前自动做diff计算,每次仅传递有变化的数据。例如当前页面有20条数据,触发上拉加载时,会新加载20条数据,此时原生框架通过如下代码测试时,setData会传输40条数据data: { listData: []},onReachBottom() { //上拉加载 let listData = this.data.listData; listData.push(…Api.getNews());//新增数据 this.setData({ listData }) //全量数据,发送数据到视图层}开发者使用微信原生框架,完全可以自己优化,精简传递数据,比如修改如下:data: { listData: []},onReachBottom() { //上拉加载 // 通过长度获取下一次渲染的索引 let index = this.data.listData.length; let newData = {}; //新变更数据 Api.getNews().forEach((item) => { newData[’listData[’ + (index++) + ‘]’] = item //赋值,索引递增 }) this.setData(newData) //增量数据,发送数据到视图层}经过如上优化修改后,再次测试,微信原生框架性能数据如下:组件数量微信原生框架(优化前)微信原生框架(优化后)uni-apptaro20077057264175240087668874197460011118559101250800140610551113154710001690126013211878从测试结果可看出,经过开发者手动优化,微信原生框架可达到更好的性能,但 uni-app、taro 相比微信原生,性能差距并不大。这个结果,和web开发类似,web开发也有原生js开发、vue、react框架等情况。如果不做特殊优化,原生js写的网页,性能经常还不如vue、react框架的性能。也恰恰是因为Vue、react框架的优秀,性能好,开发体验好,所以原生js开发已经逐渐减少使用了。复杂长列表加载下一页评测结论:微信原生开发手工优化,uni-app>微信原生开发未手工优化,taro > chameleon > wepy > mpvue2.2 点赞组件响应速度长列表中的某个组件,比如点赞组件,点击时是否能及时的修改未赞和已赞状态?是这项测试的评测点。测试方式:选中某微博,点击“点赞”按钮,实现点赞状态状态切换(已赞高亮、未赞灰色),点赞按钮 onclick函数开头开始计时,setData回调函数开头结束计时;在红米手机(Redmi 6 Pro)上进行多次测试,求其平均值,结果如下:列表数量微信原生wepympvuetarouni-appchameleon2009127966692931014001115011507125107145600144–152148178800176–2141812361000220–229234272说明:也就是在列表数量为400时,微信原生开发的应用,点赞按钮从点击到状态变化需要111毫秒。测试结果数据说明:wepy/mpvue 测试数据不完整的原因同上,在组件较多时,页面已经不再渲染了基于微信自定义组件实现组件开发的框架(uni-app/taro/chameleon),组件数据通讯性能接近于微信原生框架,远高于基于template实现组件开发的框架(wepy/mpvue)性能组件数据更新性能测评:微信原生开发,uni-app,taro > chameleon > wepy > mpvue综上,本性能测试做了2个测试,长列表加载和组件状态更新,综合2个实验,结论如下:微信原生开发手工优化,uni-app>微信原生开发未手工优化,taro > chameleon >> wepy > mpvue3. 学习门槛DSL语法支持度主流跨端框架基本都遵循React、Vue(类Vue)语法,其主要目的:复用工程师的现有技术栈,降低学习成本。此时,跨端框架对于原框架(React/Vue)语法的支持度就是一个重要的衡量标准,如果支持度较低、和原框架语法差异较大,则开发者无异于要学习一门新的框架,成本太高。实际开发中发现,各个多端框架,都没有完全实现vue、react在web上的所有语法:taro 对于 JSX 的语法支持是相对完善的,其文档中描述未来版本计划,更多的 JSX 语法支持,1.3 之后限制生产力的语法只有只能用 map 创造循环组件一条mpvue、uni-app 框架基于 Vue.js 核心,通过修改 Vue.js 的 runtime 和 compiler,实现了在小程序端的运行,支持绝大部分的Vue语法;uni-app 编译到微信端曾经使用过mpvue,但后来重写框架,支持了更多vue语法如filter、复杂 JavaScript 表达式等;wepy、chameleon 都是 类Vue 的实现,仅支持 Vue 的部分语法,开发时需要单独学习它们的规则;DSL语法支持评测:taro,uni-app > mpvue > wepy,chameleon学习资料完善度官方文档、搜索系统的完备度方面:uni-app文档内容丰富,示例demo完备,taro次之,其他几个框架相对要弱一些。mpvue文档虽少,但其概念不复杂,也没有支持H5、App,组件、API文档都可直接看微信的文档,学习难度倒也很低。教程方面:uni-app官方有视频教程,不少三方专业培训机构也录制的uni-app教程,包括腾讯课堂自家NEXT学院也录制了uni-app培训视频课,公开售卖;mpvue在腾讯课堂也有三方视频教程售卖;taro没有视频教程,但官方发布了掘金小册;wepy和chameleon还没有专业教程。学习资料完善度评测:uni-app > mpvue , taro > chameleon > wepy技术支持和社区活跃度开发难免遇到问题,官方技术支持和社区活跃度很重要。目前看,uni-app、taro、chameleon都有专职人员做技术支持,uni-app因交流群过多,还单独引入了智能客服机器人。活跃的社区意味着你遇到问题有人可问、或者前人会沉淀经验到文章里供你搜索。uni-app官方有30多个交流群(其中有很多千人大群),自建论坛中有大量交流帖子;taro和mpvue有9个500人微信群;wepy官网的微信已无法添加,chameleon发布较晚,用户群还较少。除uni-app外,其他框架没有自建论坛社区。本次评测demo开发期间,我们的同学(同时掌握vue和react),在学习研究各个多端框架时,切实感受到由于语法、学习资料、社区的差异带来的学习门槛,吐出了很多槽。综合评估,本项评测结论:uni-app > taro > mpvue > wepy > chameleonTips:本测评忽略React、Vue两框架自身的学习门槛4. 工具和周边生态工具所有多端框架均支持cli模式,可以在主流前端工具中开发。各框架基本都带有d.ts的语法提示库。由于mpvue、uni-app、taro直接支持vue、react语法,配套的ide工具链较丰富,着色、校验、格式化完善,chameleon针对部分编辑器推荐了插件,wepy有一些三方维护的vscode插件。工具属性维度,明显高出一截的框架是uni-app,其出品公司同时也是HBuilder的出品公司,DCloud.io。HBuilder/HBuilderX系列是国产开发工具,有300万开发者用户。HBuilderX为uni-app做了很多优化,故uni-app的开发效率、易用性非其他框架可及。当然对于不习惯HBuilderX的开发者而言,uni-app的这个优势无法体现。周边生态一个底层框架,其周边配套非常重要,比如ui库、js库、项目模板。wepy:出现时间久,开源项目多,占据一定优势。mpvue:发布时间也较早,历史积累较多。taro:官方提供了taro ui,github上有一些开源项目。uni-app:提供了插件市场,ui库、周边模板丰富chameleon:还没有形成周边生态。值得注意的是,uni-app和mpvue的插件生态是互通的,都是vue插件。所以双方还联合举办了插件大赛。这个联合生态的周边丰富度,是目前各个框架中最丰富的。顺便打个广告,欢迎有实力的同学参加 uni-app/mpvue插件开发大赛,领取iPhone Xs Max等丰厚奖品。综上比较,工具和周边生态评测结论:uni-app,mpvue > wepy > taro > chameleon其他常见评测指标github star:wepympvuetarouni-appchameleon17136166501707847284287github star 数对比:wepy > taro > mpvue > uni-app > chameleonTips:star 数采集时间:2019.03.31 21:30star 数量和产品发布时间有关,也和用户使用习惯有关;除uni-app外,其他框架的交流互动主要是github的issus,uni-app的开发者一般在uni-app的问答社区中交流反馈,github页面访问量较低。百度指数百度指数代表了开发者的搜索量和包含关键字的网页数量。如下是各跨端框架近7天(2019-03-24 ~ 2019-03-30)的百度指数:Tips:wepy未被百度指数收录,说明其搜索量和包含该关键字的网页数量都不够多。taro和chameleon 的名称取自于已存在的名称,实际指代开发框架的指数应该更低。案例仅看发布到微信小程序的案例,数量和质量综合对比,wepy > mpvue > taro , uni-app > chameleon如果看多端案例,综合对比,uni-app > taro > mpvue > wepy > chameleon除了uni-app外,其他跨端框架的出品方本身为一线开发商,其内部项目会使用这些框架,经受过实战考验。但同时鲜有其他大开发商使用这类框架。这里面有面子问题,也有兼容问题。很多开发商做的框架,可以满足其自身业务需求,但对外开放后想满足所有开发者,仍然需要投入大量工作完善产品,很多开发商主营业务不在此,并没有这么做。这也是很多开源项目被称为KPI项目的原因。客观讲,凹凸实验室投入如此大精力打磨taro,让uni-app团队也很惊讶和佩服。chameleon团队初期投入也很大,但发布时间还短,如果能长期投入下去,也是令人敬佩的。uni-app团队本身就是专业做开发者服务的,案例很多,但创业者居多。可以说整个多端框架市场仍处于起步期,距离让更多开发者接受,还需要所有框架作者的共同努力。其他补充说明1. 开源和App侧的补充说明有的友商在评测中提到uni-app的开源性不足问题。需要说明下,uni-app和其他多端框架一样,都是前端框架,是纯开源的。除了uni-app,其他框架的App端,或者使用expo(一个基于react native的封装库)、或者使用weex。做过这些开发的人都知道,原生排版引擎和web排版引擎有很多差异。而且不管react native还是weex,都只是渲染器,能力部分还需要开发者写原生代码,这就无法跨端了。expo比react native强的是多封装了一些能力,但也带来新的限制。uni-app的App端,是一个真的小程序引擎,又补充了可选的weex引擎。这也是uni-app在App端能够提供比其他跨端框架更好兼容性的原因。而这个引擎,是另一个开源项目,叫h5p,这个引擎是部分开源状态。整个业内目前还不存在一个完全开源的小程序引擎。不过uni-app的App端使用许可是完全免费,可以放心使用。其实也不用好奇为什么DCloud会有小程序引擎,因为业内第一个做小程序的并不是微信,而是DCloud。关于App端,其实可以再写出一篇很长的专业评测。后续uni-app团队会再做一篇App端与react native、weex、cordova、flutter等框架的对比。2. 转换和混写taro提供了原生小程序转换为taro工程的转换器,也支持在原生小程序里部分页面嵌入taro编写的页面,这是taro的特色,其他跨端框架没有提供。这对于降低入门门槛有不少帮助。结语真实客观的永远是实验和数据,而不是结论。不同需求的开发者,可以根据上述实验数据,自行得出自己的选型结论。但作为一篇完整的评测,我们也必须提供一份总结,虽然它可能加入了我们的主观感受:如果你想多端开发,提升效率,不想踩太多坑,uni-app相对更完善。如果你只开发微信小程序,不做多端,那么使用uni-app、微信原生开发、taro是更优的选择。如果使用微信原生开发,需要注意手动写优化代码来控制setdata如果你是react系,那就用taro如果是vue系,那就用uni-app,uni-app在性能、周边生态和开发效率上更有优势如果你主要为了微信端和H5端,那么uni-app和taro都可以。可以根据自己熟悉的技术栈选择。如果你主要需要跨App端,uni-app兼容性更好,其他框架的App端差异过大。如果你只关心App,不关心小程序和H5,那欢迎关注我们后续的评测:uni-app和cordova、react native、flutter的深度比较。如果你主要为了各家小程序,且不用复杂组件,那除了uni-app和taro,mpvue也是不错的选择。mpvue发布2.0版本后,搜索指数明显爬升,希望能持续更新,迎来二次繁荣。chameleon发布不久,提供的组件和API还很少,但其未来的规划比较令人期待,值得关注。这篇评测写完后,小编有点惴惴不安。一方面本评测不太温和,容易得罪人。但我们相信,这样的评测,会激起所有跨端框架从业者的斗志,让大家投入更多去完善产品,这对整个产业、对前端开发者,是大好事。另一方面,读者可能会以为现阶段的uni-app很完美,其实我们深知uni-app还有很多需要完善的地方。uni-app团队也将持续投入心血,为中国的前端开发者造福!如有读者认为本文中任何评测失真,欢迎在这里报issues。 ...

April 2, 2019 · 2 min · jiezi

小程序多端框架全面测评:chameleon、Taro、uni-app、mpvue、WePY

摘要: 微信小程序开发技巧。作者:coldsnap原文:小程序多端框架全面测评Fundebug经授权转载,版权归原作者所有。最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个?作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。那么,当我们在讨论多端框架时,我们在谈论什么:多端笔者以为,现在流行的多端框架可以大致分为三类:1. 全包型这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。2. Web 技术型这类框架把 Web 技术(JavaScript,CSS)带到移动开发中,自研布局引擎处理 CSS,使用 JavaScript 写业务逻辑,使用流行的前端框架作为 DSL,各端分别使用各自的原生组件渲染。代表框架是 React Native 和 Weex,这样做的优点有:开发迅速复用前端生态易于学习上手,不管前端后端移动端,多多少少都会一点 JS、CSS缺点有:交互复杂时难以写出高性能的代码,这类框架的设计就必然导致 JS 和 Native 之间需要通信,类似于手势操作这样频繁地触发通信就很可能使得 UI 无法在 16ms 内及时绘制。React Native 有一些声明式的组件可以避免这个问题,但声明式的写法很难满足复杂交互的需求。由于没有渲染引擎,使用各端的原生组件渲染,相同代码渲染的一致性没有第一种高。3. JavaScript 编译型这类框架就是我们这篇文章的主角们:Taro、WePY 、uni-app 、 mpvue 、 chameleon,它们的原理也都大同小异:先以 JavaScript 作为基础选定一个 DSL 框架,以这个 DSL 框架为标准在各端分别编译为不同的代码,各端分别有一个运行时框架或兼容组件库保证代码正确运行。这类框架最大优点和创造的最大原因就是小程序,因为第一第二种框架其实除了可以跨系统平台之外,也都能编译运行在浏览器中。(Qt 有 Qt for WebAssembly, Flutter 有 Hummingbird,React Native 有 react-native-web, Weex 原生支持)另外一个优点是在移动端一般会编译到 React Native/Weex,所以它们也都拥有 Web 技术型框架的优点。这看起来很美好,但实际上 React Native/Weex 的缺点编译型框架也无法避免。除此之外,编译型框架的抽象也不是免费的:当 bug 出现时,问题的根源可能出在运行时、编译时、组件库以及三者依赖的库等等各个方面。在 Taro 开源的过程中,我们就遇到过 Babel 的 bug,React Native 的 bug,JavaScript 引擎的 bug,当然也少不了 Taro 本身的 bug。相信其它原理相同的框架也无法避免这一问题。但这并不意味着这类为了小程序而设计的多端框架就都不堪大用。首先现在各巨头超级 App 的小程序百花齐放,框架会为了抹平小程序做了许多工作,这些工作在大部分情况下是不需要开发者关心的。其次是许多业务类型并不需要复杂的逻辑和交互,没那么容易触发到框架底层依赖的 bug。那么当你的业务适合选择编译型框架时,在笔者看来首先要考虑的就是选择 DSL 的起点。因为有多端需求业务通常都希望能快速开发,一个能够快速适应团队开发节奏的 DSL 就至关重要。不管是 React 还是 Vue(或者类 Vue)都有它们的优缺点,大家可以根据团队技术栈和偏好自行选择。如果不管什么 DSL 都能接受,那就可以进入下一个环节:生态以下内容均以各框架现在(2019 年 3 月 11 日)已发布稳定版为标准进行讨论。1. 开发工具就开发工具而言 uni-app 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具,鼠标点点点就能编译测试发布。其它的框架都是使用 CLI 命令行工具,但值得注意的是 chameleon 有独立的语法检查工具,Taro 则单独写了 ESLint 规则和规则集。在语法支持方面,mpvue、uni-app、Taro 、WePY 均支持 TypeScript,四者也都能通过 typing 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。CSS 方面,所有框架均支持 SASS、LESS、Stylus,Taro 则多一个 CSS Modules 的支持。所以这一轮比拼的结果应该是:uni-app > Taro > chameleon > WePY、mpvue2. 多端支持度只从支持端的数量来看,Taro 和 uni-app 以六端略微领先(移动端、H5、微信小程序、百度小程序、支付宝小程序、头条小程序),chameleon 少了头条小程序紧随其后。但值得一提的是 chameleon 有一套自研多态协议,编写多端代码的体验会好许多,可以说是一个能戳到多端开发痛点的功能。uni-app 则有一套独立的条件编译语法,这套语法能同时作用于 js、样式和模板文件。Taro 可以在业务逻辑中根据环境变量使用条件编译,也可以直接使用条件编译文件(类似 React Native 的方式)。在移动端方面,uni-app 基于 weex 定制了一套 nvue 方案 弥补 weex API 的不足;Taro则是暂时基于 expo 达到同样的效果;chameleon 在移动端则有一套 SDK 配合多端协议与原生语言通信。H5 方面,chameleon 同样是由多态协议实现支持,uni-app 和 Taro 则是都在 H5 实现了一套兼容的组件库和 API。mpvue 和 WePY 都提供了转换各端小程序的功能,但都没有 h5 和移动端的支持。所以最后一轮对比的结果是:chameleon > Taro、uni-app > mpvue > WePY3. 组件库/工具库/demo作为开源时间最长的框架,WePY 不管从 Demo,组件库数量 ,工具库来看都占有一定优势。uni-app 则有自己的插件市场和 UI 库,如果算上收费的框架和插件比起 WePy 也是完全不遑多让的。Taro 也有官方维护的跨端 UI 库 taro-ui ,另外在状态管理工具上也有非常丰富的选择(Redux、MobX、dva),但 demo 的数量不如前两个。但 Taro 有一个转换微信小程序代码为 Taro 代码的工具,可以弥补这一问题。而 mpvue 没有官方维护的 UI 库,chameleon 第三方的 demo 和工具库也还基本没有。所以这轮的排序是:WePY > uni-app 、taro > mpvue > chameleon4. 接入成本接入成本有两个方面:第一是框架接入原有微信小程序生态。由于目前微信小程序已呈一家独大之势,开源的组件和库(例如 wxparse、echart、zan-ui 等)多是基于原生微信小程序框架语法写成的。目前看来 uni-app 、Taro、mpvue 均有文档或 demo 在框架中直接使用原生小程序组件/库,WePY 由于运行机制的问题,很多情况需要小改一下目标库的源码,chameleon 则是提供了一个按步骤大改目标库源码的迁移方式。第二是原有微信小程序项目部分接入框架重构。在这个方面 Taro 在京东购物小程序上进行了大胆的实践,具体可以查看文章《Taro 在京东购物小程序上的实践》。其它框架则没有提到相关内容。而对于两种接入方式 Taro 都提供了 taro convert 功能,既可以将原有微信小程序项目转换为 Taro 多端代码,也可以将微信小程序生态的组件转换为 Taro 组件。所以这轮的排序是:Taro > mpvue 、 uni-app > WePY > chameleon流行度从 GitHub 的 star 来看,mpvue 、Taro、WePY 的差距非常小。从 NPM 和 CNPM 的 CLI 工具下载量来看,是 Taro(3k/week)> mpvue (2k/w) > WePY (1k/w)。但发布时间也刚好反过来。笔者估计三家的流行程度和案例都差不太多。uni-app 则号称有上万案例,但不像其它框架一样有一些大厂应用案例。另外从开发者的数量来看也是 uni-app 领先,它拥有 20+ 个 QQ 交流群(最大人数 2000)。所以从流行程度来看应该是:uni-app > Taro、WePY、mpvue > chameleon5. 开源建设一个开源作品能走多远是由框架维护团队和第三方开发者共同决定的。虽然开源建设不能具体地量化,但依然是衡量一个框架/库生命力的非常重要的标准。从第三方贡献者数量来看,Taro 在这一方面领先,并且 Taro 的一些核心包/功能(MobX、CSS Modules、alias)也是由第三方开发者贡献的。除此之外,腾讯开源的 omi 框架小程序部分也是基于 Taro 完成的。WePY 在腾讯开源计划的加持下在这一方面也有不错的表现;mpvue 由于停滞开发了很久就比较落后了;可能是产品策略的原因,uni-app 在开源建设上并不热心,甚至有些部分代码都没有开源;chameleon 刚刚开源不久,但它的代码和测试用例都非常规范,以后或许会有不错的表现。那么这一轮的对比结果是:Taro > WePY > mpvue > chameleon > uni-app最后补一个总的生态对比图表:未来从各框架已经公布的规划来看:WePY 已经发布了 v2.0.alpha 版本,虽然没有公开的文档可以查阅到 2.0 版本有什么新功能/特性,但据其作者介绍,WePY 2.0 会放大招,是一个「对得起开发者」的版本。笔者也非常期待 2.0 正式发布后 WePY 的表现。mpvue 已经发布了 2.0 的版本,主要是更新了其它端小程序的支持。但从代码提交, issue 的回复/解决率来看,mpvue 要想在未来有作为首先要打消社区对于 mpvue不管不顾不更新的质疑。uni-app 已经在生态上建设得很好了,应该会在此基础之上继续稳步发展。如果 uni-app 能加强开源开放,再加强与大厂的合作,相信未来还能更上一层楼。chameleon 的规划比较宏大,虽然是最后发的框架,但已经在规划或正在实现的功能有:快应用和端拓展协议通用组件库和垂直类组件库面向研发的图形化开发工具面向非研发的图形化页面搭建工具如果 chameleon 把这些功能都做出来的话,再继续完善生态,争取更多第三方开发者,那么在未来 chameleon 将大有可为。Taro 的未来也一样值得憧憬。Taro 即将要发布的 1.3 版本就会支持以下功能:快应用支持Taro Doctor,自动化检查项目配置和代码合法性更多的 JSX 语法支持,1.3 之后限制生产力的语法只有 只能用 map 创造循环组件 一条H5 打包体积大幅精简同时 Taro 也正在对移动端进行大规模重构;开发图形化开发工具;开发组件/物料平台以及图形化页面搭建工具。结语那说了那么多,到底用哪个呢?如果不介意尝鲜和学习 DSL 的话,完全可以尝试 WePY 2.0 和 chameleon ,一个是酝酿了很久的 2.0 全新升级,一个有专门针对多端开发的多态协议。uni-app 和 Taro 相比起来就更像是「水桶型」框架,从工具、UI 库,开发体验、多端支持等各方面来看都没有明显的短板。而 mpvue 由于开发一度停滞,现在看来各个方面都不如在小程序端基于它的 uni-app 。当然,Talk is cheap。如果对这个话题有更多兴趣的同学可以去 GitHub 另行研究,有空看代码,没空看提交:chameleon: https://github.com/didi/chameleonmpvue: https://github.com/Meituan-Dianping/mpvueTaro: https://github.com/NervJS/tarouni-app: https://github.com/dcloudio/uni-appWePY: https://github.com/Tencent/wepy(按字母顺序排序)关于FundebugFundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用! ...

March 28, 2019 · 2 min · jiezi

Taro开发多端应用

官方解释: 使用 Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信/百度/支付宝/字节跳动小程序、H5、React-Native 等)运行的代码。使用前第一次看到Taro是在github搜索React插件时看到(个人习惯,有时候会去搜索一个语言的插件在GitHub再按照Star排名,看看各个插件功能,后期开发时用到这功能有个印象),感觉挺好的插件,以后开发小程序和快应用应该用的到,因为它直接使用react可以开发多端,相比于去看各个厂家小程序开发文档,使用Taro几乎没有学习成本。为什么要了解它这次使用它开发一个简单的网页南瓜棋,小时候玩的一个游戏,逻辑还是比较简单的,主要是去了解下Taro优缺点,以后开发公司简单小程序、快应用等做好准备,主要是了解他的局限性。开发感受具体看文档,我简单说下感受,我的前端水平:简单的HTML、CSS了解复杂的网页不会(动画啥的还得看文档),React-Native水平应该还是不错,主流的React-Native框架都会搭建,开发,原生调试,编写没问题,ES6没问题。React看了2周吧,入门。这个Taro,直接写按照文档走,没出现问题。缺点由于之前大段时间开发RN的所以开发时直接使用style={styles.你的}这种开发,开发完H5时,打算运行在小程序上发现尴尬了,样式全乱了,后面给尺寸加px。用Mobx在store里面有个方法我命名onChessGo,H5运行没问题,小程序不行,排查了一段时间发现微信小程序里的Mobx->store方法不能已on开头,这个要注意。好像暂时是不能引用三方UI库的和UI组件库的,这和Taro功能有关,可能一个小程序的库肯定不能用在React-Native,这个缺陷会加大开发复杂页面的时间,可能对于原生(各个小程序新功能)新功能支持可能也不会太及时,由于页面简单,了解时间端更多的缺陷也没有看到。return tsx时在非render里面是不能运行的在微信小程序里,H5没问题。优点快速开发各端的应用,不需要任何学习成本(我这前端小白都直接开写),还提供各个应用的原生功能的接口方便用户调用。确实可以多端打包,亲测有效,但演示和一些细节要注意了。Taro自己开发了一个UI库(Taro-ui)满足了大部分的组件需求,最后最重要的一点是个人认为大多数小程序、H5、快应用都是用于引流或者简单功能开发,这些功能开发Taro应该都可以满足,还有就是时间和人力成本Taro也是有优势。应用南瓜棋H5截图微信小程序截图支付宝小程序截图今日头条小程序截图代码注意事项由于之前以为在H5上运行,其他地方样式就一样,可是后面发现不行,所以样式用的内联样式,建议大家用css、less、scss这样H5和其他端样式应该一样。有些命名不能用例如Mobx里store里的方法@action不能以on开发,微信小程序就调不懂。今日头条小程序打包后有问题确实了project.config.json,反正我这边编译后导入不了,我新建了一个project.config.json复制过去,可能是这个原因导致下面的UI不见了。我这边测试来了正常: H5、微信小程序、支付宝小程序,编译后缺少东西但可以运行:今日头条小程序,其他的没测试太耗时。GitHub应用地址React-Native 篇七分设计感的纯React-Native项目Mung一个完整小巧的Redux全家桶项目react-native拖拽排序多功能React-Native-Toast组件

March 18, 2019 · 1 min · jiezi

使用taro框架将手百小程序转成h5总结

前言历时一周,终于成功兼容了h5和小程序,在此使用的taro框架,遇到的问题在此记录一下。一、环境判断使用 Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信/百度/支付宝/字节跳动小程序、H5、React-Native 等)运行的代码。因为是一套代码兼容不同容器,所以在有些不兼容的情况下,判断所在环境就成了非常重要的一环,taro框架自带环境判断如下图。二、编译配置在小程序中不存在接口访问跨域的问题,但是转成h5后开发时这个问题就会出现了,taro有一个专属于h5的编译配置,使用方法和webpack几乎一模一样,可以非常方便的做proxy转发,使用方法参考webpack-dev-server配置。三、地图功能小程序中自带地图api,如手百小程序中有专门的map组件,涉及位置信息的功能可以直接调用swan.openLocation跳转内置地图页面,十分方便,但是转成h5之后没有子带的地图功能怎么办。。。没错,自己手写一个,使用process.env.TARO_ENV判断环境为h5时跳转到自己写的map页,百度地图有封装好的各种api,可以在页面中调用,详见百度地图开放平台。四、表单手百小程序picker组件是支持region模式,直接可以实现省市区选择,但是taro框架h5不支持,需要用picker的multiSelector功能来实现省市区的选择。所以还需要一个城市列表的接口。五、涉及登录的接口请求上线之后我发现需要登录的接口使用Taro.request都得不到正确返回,抓包发现是没有带cookie,原因是taro有一个属性credentials,只有在h5端有这个参数,它的默认值是omit,是请求时不带cookie的,有效值:include, same-origin, omit。需要在请求时修改它的值才可以正确传递cookie,上线后发现安卓手机请求有问题,但iOS手机都可以正常返回结果,可能因为Safari自带了cookie相关的设置,使同源的请求都可以正确携带cookie吧,安卓就没有这个功能,导致了这个问题,加上credentials: same-origin即可解决问题。六、总结这次大概遇到的问题就是这些,以后遇到新的问题再随时补充,欢迎大家提出意见,一起交流进步。

March 16, 2019 · 1 min · jiezi

Natsuha - 用Taro写个天气微信小程序

去年年底 o2 开源了 Taro,一直手痒痒没去玩。考虑到 wx 的审核制度,所以决定写个工具类小程序。赶在 Taro 喜提第 2000 个 issues 之际,Natsuha 也终于上线了。源码全部释出(除涉及私钥部分,GitHub有说明),文章后面会贴出一些仍需优化的点,欢迎大家一起讨论。前言Natsuha Weather 已开源,欢迎大家一起折腾,给个 star 也是极好的~ GitHub Repo技术栈是 Taro + mobx + TypeScript,接口来自 Yahoo Weather API,当然设计也是参 (chao) 考 (xi) 的 Yahoo Weather. 接口有时会访问失败,尤其是晚上,我也没办法啊。????功能下拉刷新华氏温度、摄氏温度切换分时展示一天的天气预报展示未来10天的天气预报展示当前风向、风速展示日出日落、月相等信息展示一天内的降水预报城市天气检索显示检索记录踩坑小程序篇云开发解决 Bei An 问题由于众所周知的原因,wx 小程序无法调用未 bei an 的接口,哪怕是在开发环境。所以我们用云开发的云函数来 “反代” 接口,下面通过一个例子说一下技术要点。首先在根目录的 project.config.json 文件里添加 “cloudfunctionRoot”: “functions/",然后在根目录创建文件夹 functions. 并点击右键创建一个新的云函数,比如我们叫 getRegion。因为我们的目标是通过云函数请求一个未 bei an 的接口,所以为了更方便的处理异步请求,我们引入 request-promise 这个库。通过硬盘打开进入到这个云函数的文件夹,然后安装依赖:yarn add request request-promise接下来我们在 index.js 中写逻辑,直接上代码。云函数通过 event 对象来获取前端传过来的参数,然后通过 Promise 对象将结果返回。这个例子中我们需要拿到region,// 云函数入口文件const cloud = require(‘wx-server-sdk’)const rp = require(‘request-promise’)cloud.init()exports.main = async (event, context) => { const region = event.region; const res = await rp({ method: ‘get’, uri: https://www.yahoo.com/news/_tdnews/api/resource/WeatherSearch;text=${region}, json: true }).then((body) => { return { regionList: body } }).catch(err => { return err; }) return res;}接下来是前端发请求了,注意这里不能再用 Taro.request(), 而是云函数独有的 wx.cloud.callFunction(), 因为我现在的 Taro 版本尚未实现 Taro.cloud.callFunction(),所以直接用 wx 打头即可。首先封装一下 wx.cloud.callFunction(),其实感觉什么卵用????:export const httpClient = (url: string, data: any) => new Promise((resolve, reject): void => { wx.cloud.callFunction({ name: url, data, }).then(res => { resolve(res.result); }).catch(e => { reject(e) });});然后我们在 store 里面写逻辑,这样基本上就解决了数据请求的坑。 public getRegion = (text: string) => { httpClient(‘getRegion’, { region: encodeURI(text), }) .then((res: any) => { runInAction(() => { if (res.regionList) { this.regionList = res.regionList; } }); }) .catch(() => { setToast(toastTxt.cityFail); }); };????题外话:因为当前版本尚未实现 Taro.cloud.callFunction(),所以 lint 会报错,虽然不影响使用,大家有什么好的方法,可以说一下。地理信息授权问题在这个项目里,我们需要通过小程序拿到的经纬度来反查城市信息,而小程序获取经纬度需要用户授权。这里有个坑,当用户拒绝授权后,小程序默认询问授权的 dialog 在一段时间内不会重复弹出,所以我们必须手动将用户引导到授权页面。以前小程序有个接口叫做 wx.openSetting(),但 tx 把它废掉了,现在只能让用户点击一个特定的按钮。为此我做了一个 modal,这里贴出关键代码。<Button openType=‘openSetting’ onOpenSetting={() => this.onOpenSetting()}> OK</Button>首先我们必须给按钮声明 openType=‘openSetting’,这样当用户点击了之后就会跳转到设置页面。其次,我们需要在用户离开授权页面时,也就是点击了左上角那个返回按钮时,再次去检查一下用户的授权情况。所以我们要添加onOpenSetting={() => this.onOpenSetting(),不得不吐槽这个事件命名,明明应该叫做 onLeaveSetting才合理。在 onOpenSetting() 方法中我们再次执行判断用户是否授权的方法,未授权的话接着弹 modal,否则放行请求相应的数据接口。文字有些累,直接看图。无法用传统方式清空文本框文字当用户关闭搜索 dialog 时,文本框的文字应当被清空,所以一开始写成下面的这样,即点击关闭按钮时将 inputValue = ’’ ,然而发现不行。<Input type=‘text’ value={inputValue} placeholder=‘Enter City or ZIP code’ onInput={e => handleInputTextChange(e)}/><Button onClick={() => hideSearchDialog()}>Close</Button>查了一下官方文档,必须将 Input 和 Button 包裹在一个 Form 下,且要给关闭按钮加上 formType=‘reset’,最后给 Form 添加 onReset 事件指向关闭 dialog 的方法。<Form onReset={() => hideSearchDialog()}> <Input className={styles.input} type=‘text’ placeholder=‘Enter City or ZIP code’ onInput={e => handleInputTextChange(e)} /> <Button formType=‘reset’>Close</Button></Form>;Taro篇大多是编译问题和它 webpack 配的问题,相应的我都提了 issue,有兴趣的话可以跟进。Taro 编译会忽略模版两个之间的空格举个例子,<Text>day - night</Text>,可以正常编译,页面可以正常看到 day - night,但是假如是变量,就会被编译成 day- night,注意,空格被吃掉了。const day = ‘day’const night = ’night’<Text>{day} - {night}</Text>我提了个 issue #2261,然并没人鸟我,有兴趣可以跟进一下。ts 不能识别wx因为用到了云开发,而 Taro 现阶段还没有Taro.cloud(…),所以在使用原生的wx.cloud(…)时,ts 肯定会报错。css module 等静态文件 找不到路径一开始用的import来引入静态文件,但报“找不到路径”,可以看下图(但不影响使用)。提了个 issue #2213,按照大佬的回复修改也没解决问题,实在受不了一片红,索性改成了commonJS.Problem下面是项目中存在的一些问题,有兴趣的话欢迎大家一起讨论。图片加载不友好接口图片的 url 来自aws,因为众所周知的原因,图片经常会挂掉,所以有必要在图片挂掉的时候触发onError事件,然后给用户一个提示。因为小程序不支持new Image(),所以只能用官方提供的Image组件,幸好这个组件支持onLoad和onError事件。加载失败的问题解决了,但因为aws的速度太慢,所以正常加载时也很不友好(可以自行体会)做了一些尝试,比如先加载缩略图,再展示完整图片,但接口提供的最小尺寸的图片也已经达到了70 多 k,并且该死的 Yahoo 恰好将图片 url 控制大小的那段用了加密,所以这个方式 pass 掉了。搜索输入框加个节流现在的做法是在 store 的 构造器加个节流,但不知道这样合不合理。 construtor() { this.getRegion = _.debounce(this.getRegion, 150); }TODO国际化性能优化图片加载优化Jest 搞起来(初始化已搭好)Travis CI 搞起来(初始化已搭好)将搜索模块放到一个新页面(强行加个路由????)最后老子再也不写小程序了! ...

March 12, 2019 · 2 min · jiezi

taro多端实践初探

历史的发展,小程序风行一时,安卓/ios/H5/微信小程序/支付宝小程序/头条小程序,产品经理让你适配这么多,你的心情如何呢?然而总会有人给咱们造出合适的工具,解放生产力,一次编码,多端运行。开始探索之旅吧!taro安装安装 Taro 开发工具 @tarojs/cli 使用 npm 或者 yarn 全局安装,或者直接使用npx$ npm install -g @tarojs/cli$ yarn global add @tarojs/cli使用命令创建模版$ taro init multiportApp按照自己情况选择安装即可启动进入对应目录,执行命令启动。npm run dev:h5会出现启动成功的界面,如下自动就会打开浏览器,出现hello world界面,表示项目启动成功了!todolist功能实现添加数据在pages/index/index.js文件中添加如下constructor(props){ super(props); this.state={ val:’’, todos:[ { title:‘吃饭’, done:false }, { title:‘睡觉’, done:false }, { title:‘coding’, done:false } ] } }渲染数据render () { return ( <View className=‘index’> <Text>Hello world!</Text> { this.state.todos.map((item,index)=>{ return <View key={index}>{item.title}</View> }) } </View> ) }列表渲染搞定。添加输入框和按钮引入组件import { View, Text,Input,Button } from ‘@tarojs/components’render修改render () { return ( <View className=‘index’> <Text>Hello world!</Text> <Input value={this.state.val} onInput={this.handleInput}></Input> <Button onClick={this.handleClick}>添加</Button> { this.state.todos.map((item,index)=>{ return <View key={index}>{item.title}</View> }) } </View> ) }添加方法handleInput=(e)=>{ this.setState({ val:e.detail.value })}handleClick=()=>{ this.setState({ todos:[…this.state.todos,{title:this.state.val,done:false}], val:’’ })}一个简单的todolist就算完成了,界面有点丑,继续优化!优化,引入UI库安装taro-ui官网npm install –save taro-ui简单配置由于引用 node_modules 的模块,默认不会编译,所以需要额外给 H5 配置 esnextModules,在 taro 项目的 config/index.js 中新增如下配置项:h5: { esnextModules: [’taro-ui’]}全局引入在app.scss中引入@import ’taro-ui/dist/style/index.scss’在index.js中引入import { AtList, AtListItem } from “taro-ui"修改renderrender () { return ( <View className=‘index’> <Text>Hello world!</Text> <Input value={this.state.val} onInput={this.handleInput}></Input> <Button onClick={this.handleClick}>添加</Button> <AtList> { this.state.todos.map((item,index)=>{ return <AtListItem key={index} title={item.title}></AtListItem> }) } </AtList> </View> ) }添加滑块开关,改变item状态<AtListItem key={index} title={item.title} className={{‘done’:item.done}} isSwitch switchIsCheck={item.done} onSwitchChange={(e)=>this.handleChange(e,index)}></AtListItem>增加一个isSwitch,switch切换事件,class。 增加事件handleChange=(e,i)=>{ console.log(e,i); const todos=[…this.state.todos]; todos[i].done=e.detail.value; this.setState({ todos }) }在同级目录下index.scss中增加样式.done{ color: red; text-decoration: line-through;}效果h5效果微信小程序中的效果这就是这个框架的威力,感谢taro开发团队。同步发表于个人博客最后在说一句,正在找工作,坐标北京,各位大佬有合适的机会推荐下哈! ...

March 8, 2019 · 1 min · jiezi

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

taro1.安装 tarojsnpm install -g @tarojs/cli2.初始化项目taro init taro-login3.进入目录cd taro-login4.运行编译npm run dev:weapp5.修改文件 /src/app.js 代码…class App extends Component { config = { pages: [ ‘pages/user/user’, // new ‘pages/index/index’, ],6.微信登录需求如果我们需要用户一进入就取得用户的授权,以便于进行某些记录用户信息的操作,而微信又要求用户去点页面上的某个按钮才能获取信息,那怎么办呢?只能把一个按钮放在用户不能不点的地方,那就只有弹窗了。微信 wx.showModal 不能满足我们的需求,只能自己造一个,在用户第一次进来的时候弹窗,再次进来的时候则不显示。为了让这个组件具有拓展性,我们根据传入的值来修改 确认 位置按钮的属性,如果是授权的弹窗就改按钮属性为 openType=‘getUserInfo’。(摘自Taro 多端开发实现原理与项目实战)7.新建文件夹和modal.js文件/src/components/modal/modal.jsimport Taro, { Component } from ‘@tarojs/taro’import { View, Button } from ‘@tarojs/components’import ‘./modal.scss’class Modal extends Component { constructor() { super(…arguments) this.state = {} } onConfirmClick = () => { this.props.onConfirmCallback() } onCancelClick = () => { this.props.onCancelCallback() } onAuthConfirmClick = (e) => { this.props.onConfirmCallback(e.detail) } preventTouchMove = (e) => { e.stopPropagation() } render() { const { title, contentText, cancelText, confirmText, isAuth } = this.props return ( <View className=‘toplife_modal’ onTouchMove={this.preventTouchMove}> <View className=‘toplife_modal_content’> <View className=‘toplife_modal_title’>{title}</View> <View className=‘toplife_modal_text’>{contentText}</View> <View className=‘toplife_modal_btn’> <Button className=‘toplife_modal_btn_cancel’ onClick={this.onCancelClick}>{cancelText}</Button> {!isAuth ? <Button className=‘toplife_modal_btn_confirm’ onClick={this.onConfirmClick}>{confirmText}</Button> : <Button className=‘toplife_modal_btn_confirm’ openType=‘getUserInfo’ onGetUserInfo={this.onAuthConfirmClick}>授权</Button>} </View> </View> </View> ) }}Modal.defaultProps = { title: ‘’, contentText: ‘’, cancelText: ‘取消’, confirmText: ‘确定’, isAuth: false, onCancelCallback: () => { }, onConfirmCallback: () => { }}export default ModalModal 组件还算比较简单,组件的属性:字段说明title提示的标题contentText提示的描述cancelText取消按钮的文案cancelCallback取消回调的函数confirmText确认按钮的文案confirmCallback确认回调函数isAuth标记是否为授权按钮在内部设置了一个函数 preventTouchMove,其作用是弹窗出现蒙层的时候,阻止在蒙版上的滑动手势 onTouchMove。另外一个函数 authConfirmClick, 当 isAuth 为真时,确认按钮为取得个人信息的授权按钮,此时把个人信息当值传递给调用的函数。(摘自Taro 多端开发实现原理与项目实战)8.添加modal.scss文件/postcss-pxtransform rn eject enable/.toplife_modal { position: fixed; width: 100%; height: 100%; left: 0; top: 0; background-color: rgba(0, 0, 0, .8); z-index: 100; &_content { position: absolute; left: 50%; top: 50%; width: 600px; height: 320px; transform: translate(-50%, -50%); background-color: #fff; color: #232321; text-align: center; border-radius: 30px; } &_title { margin-top: 40px; font-size: 32px; } &_text { margin-top: 40px; font-size: 24px; } &_btn { position: absolute; bottom: 0; left: 0; width: 100%; height: 88px; border-top: 2px solid #eee; &_cancel { color: #8c8c8c; border-radius: 0; border: 0; border-right: 2px solid #eee; border-bottom-left-radius: 30px; } &_confirm { color: #666; border-radius: 0; border: 0; border-bottom-right-radius: 30px; } button { display: block; float: left; width: 50%; height: 88px; text-align: center; line-height: 88px; font-size: 32px; box-sizing: border-box; background-color: #fff; &::after { border: 0; } } }}9.新建文件/src/page/user/user.js,在user.js中引用该Modal组件import Taro, { Component } from ‘@tarojs/taro’;import { View, Image, Text } from ‘@tarojs/components’;import classnames from ‘classnames’import Modal from ‘../../components/modal/modal’;import { setGlobalData } from ‘../../utils/globalData’;import { getUserInfo, getIsAuth } from ‘../../utils/getUser’;class Info extends Component { config = { navigationBarTitleText: ‘TARO商城’, enablePullDownRefresh: true, backgroundTextStyle: ‘dark’, disableScroll: true } constructor() { super(…arguments) this.state = { animationClass: ‘’, showAuthModal: false, shouldIndexHidden: false, } this.env = process.env.TARO_ENV } hideAuthModal() { this.setState({ showAuthModal: false }) Taro.setStorage({ key: ‘isHomeLongHideAuthModal’, data: true }) } onProcessAuthResult = (userData) => { Taro.setStorage({ key: ‘isHomeLongHideAuthModal’, data: true }) if (userData.userInfo) { setGlobalData(‘userData’, userData) } this.setState({ showAuthModal: false }) getIsAuth() } async onPullDownRefresh() { if (this.state.shouldIndexHidden) { Taro.stopPullDownRefresh() // 停止下拉刷新 } else { await this.props.onFetchIndexList() Taro.stopPullDownRefresh() // 停止下拉刷新 } } componentDidMount() { if (this.env === ‘weapp’) { // 用类名来控制动画 setTimeout(async () => { const userData = await getUserInfo(); Taro.getStorage({ key: ‘isHomeLongHideAuthModal’, success: (res) => { const isHomeLongHideAuthModal = res.data; let showAuthModal if (!userData && !this.state.showAuthModal && !isHomeLongHideAuthModal) { showAuthModal = true } else { showAuthModal = false } this.setState({ animationClass: ‘animation’, showAuthModal }) }, fail: () => { let showAuthModal if (!userData && !this.state.showAuthModal) { showAuthModal = true } else { showAuthModal = false } this.setState({ animationClass: ‘animation’, showAuthModal }) } }) }, 1000) getIsAuth() } else if (this.env === ‘h5’ || this.env === ‘rn’) { console.log(‘h5登录’) } } render() { const { animationClass, shouldIndexHidden, showAuthModal } = this.state const { loginname, avatar_url } = this.props; const indexClassNames = classnames(‘container’, ‘index’, animationClass, { hidden: shouldIndexHidden }) return ( <View className={indexClassNames}> <View className=‘login-head’> <Image className=‘login-head-back’ src={require(’../../assets/img/loginBack.jpg’)} /> <Image className=‘login-head-head’ src={avatar_url ? avatar_url : require(’../../assets/img/head.png’)} /> {loginname ? <Text classnames=‘login-head-name’>{loginname}</Text> : null} </View> {showAuthModal && <Modal title=‘授权提示’ contentText=‘诚邀您完成授权,尊享畅游体验’ onCancelCallback={this.hideAuthModal.bind(this)} onConfirmCallback={this.onProcessAuthResult.bind(this)} isAuth />} </View> ) }}export default Info我们是如何保证这个应用只有一次授权弹窗呢? 关键代码是 Taro.setStorageSync(‘isHomeLongHideAuthModal’, true) ,如果弹出了一次,就在本地存一个标记已经弹过授权框,下一次弹窗之前可以根据此判断。至此我们完成了授权处理,但如果可以的话还是要优雅一些,在需要的时候才征求用户授权,保证用户体验。(摘自Taro 多端开发实现原理与项目实战)10.新建几个辅助文件/src/utils/globalData.jsconst globalData = {}export function setGlobalData(key, val) { globalData[key] = val}export function getGlobalData(key) { return globalData[key]}/src/utils/request.jsimport Taro from ‘@tarojs/taro’;import ‘@tarojs/async-await’;export function getJSON(url, data) { Taro.showLoading(); return Taro.request({ url: url, data: data, method: ‘GET’ }).then(result => { Taro.hideLoading(); return result; })}export function postJSON(url, data) { Taro.showLoading() return Taro.request({ header: { ‘content-type’: ‘application/json’ }, url: url, data: data, method: ‘POST’ }).then(result => { Taro.hideLoading(); return result; });}/src/constants/apiconst rootPath = ‘http://127.0.0.1:5000/v1’;const apiObject = { registerclient: rootPath + ‘/client/register’, //注册用户 getusertoken: rootPath + ‘/token’, // 登录成功之后获取用户token checkusertoken: rootPath + ‘/token/secret’, //验证用户token getuserinfo: rootPath + ‘/user’, //获取用户信息}export default apiObject;11. 新建一个登录获取token的函数/src/utils/getUser.jsimport Taro from ‘@tarojs/taro’import { getGlobalData } from ‘./globalData’import api from ‘../constants/api’;import { postJSON } from ‘../utils/request’;async function getUserInfo() { const userData = getGlobalData(‘userData’) if (userData) { return userData } try { const userData = await Taro.getUserInfo() return userData } catch (err) { console.log(err) console.log(‘微信登录或用户接口故障’) return null }}async function getIsAuth() { const loginRes = await Taro.login() let { userInfo } = await getUserInfo() let isAuth = false if (userInfo) { // 使用微信注册新用户 let result = await postJSON(api.registerclient, { “avatar”: userInfo.avatarUrl, “sex”: userInfo.gender, “nickname”: userInfo.nickName, “account”: loginRes.code, “type”: 200 }); if (result.data.error_code == 0) { // 登录用户,获取token,缓存到前端 const tokenRes = await Taro.login() let auth_token = await postJSON(api.getusertoken, { “account”: tokenRes.code, “type”: 200 }) if (auth_token.statusCode == 201) { Taro.setStorage({ key: ’token’, data: auth_token.data.token })// 设置到缓存 Taro.showToast({ title: ‘授权成功’ }) userInfo.isAuth = true isAuth = true } } else { Taro.showToast({ title: ‘授权失败,请稍后再试’, icon: ’none’ }) } } else { userInfo = { isAuth: false } } console.log(‘isAuth: ‘, isAuth) return isAuth}export { getUserInfo, getIsAuth}flask1.文件目录├── app│ ├── init.py│ ├── api│ │ ├── init.py│ │ └── v1│ │ ├── init.py│ │ ├── client.py│ │ ├── token.py│ │ └── user.py│ ├── apps.py│ ├── config│ │ ├── secure.py│ │ └── settings.py│ ├── libs│ │ ├── enums.py│ │ ├── error.py│ │ ├── error_code.py│ │ ├── format_time.py│ │ ├── get_openid.py│ │ ├── redprint.py│ │ ├── scope.py│ │ └── token_auth.py│ ├── models│ │ ├── init.py│ │ ├── pycache│ │ │ ├── init.cpython-36.pyc│ │ │ ├── base.cpython-36.pyc│ │ │ └── user.cpython-36.pyc│ │ ├── base.py│ │ └── user.py│ └── validators│ ├── init.py│ ├── base.py│ └── forms.py├── manage.py└── requirements.txt2.新建虚拟环境略过requirements.txtFlaskFlask-SQLAlchemypsycopg2-binarycymysqlFlask-Testingcoverageflake8flask-debugtoolbarflask-corsflask-migrateflask-bcryptpyjwtgunicornrequestsflask-httpauthflask-wtf3. 新建app目录和__init.py文件和apps.py# File: /app/apps.py# -- coding: utf-8 --from flask import Flask as _Flaskfrom flask.json import JSONEncoder as _JSONEncoderfrom app.libs.error_code import ServerErrorfrom datetime import dateclass JSONEncoder(_JSONEncoder): def default(self, o): if hasattr(o, ‘keys’) and hasattr(o, ‘getitem’): return dict(o) if isinstance(o, date): return o.strftime(’%Y-%m-%d’) raise ServerError()class Flask(_Flask): json_encoder = JSONEncoder# File: /app/init.py# -- coding: utf-8 --from .apps import Flaskfrom flask_debugtoolbar import DebugToolbarExtensionfrom flask_cors import CORSfrom flask_migrate import Migratefrom flask_bcrypt import Bcryptfrom app.models.base import db# instantiate 实例化toolbar = DebugToolbarExtension()migrate = Migrate(db=db)bcrypt = Bcrypt()def create_app(): # instantiate the app app = Flask(name) # enable CORS CORS(app) # set config app.config.from_object(‘app.config.settings’) app.config.from_object(‘app.config.secure’) # set up extensions toolbar.init_app(app) migrate.init_app(app, db) bcrypt.init_app(app) # register blueprints register_blueprints(app) register_plugin(app) # shell context for flask cli @app.shell_context_processor def ctx(): return {‘app’: app, ‘db’: db} return appdef register_blueprints(app): from app.api.v1 import create_blueprint_v1 app.register_blueprint(create_blueprint_v1(), url_prefix=’/v1’)def register_plugin(app): db.init_app(app) with app.app_context(): db.create_all()4.新建配置文件/app/config/目录,在这个目录下新建两个文件settings.py和secure.py# File: /app/config/settings.py# -- coding: utf-8 --# TOKEN_EXPIRATION = 30 * 24 * 3600DEBUG = ’true’TOKEN_EXPIRATION_DAYS = 30TOKEN_EXPIRATION_SECONDS = 0# encryption的复杂程度,默认值为12BCRYPT_LOG_ROUNDS = 4# File: /app/config/secure.py# -- coding: utf-8 --SQLALCHEMY_DATABASE_URI = \ ‘mysql+cymysql://root:root1234@localhost/flask-rest’SECRET_KEY = ‘‘SQLALCHEMY_TRACK_MODIFICATIONS = TrueMINA_APP = { ‘AppID’: ‘’, ‘AppSecret’: ‘’}5.在根目录下新建一个 manage.py#File: /manage.py# -- coding: utf-8 --from werkzeug.exceptions import HTTPExceptionfrom app import create_appfrom app.libs.error import APIExceptionfrom app.libs.error_code import ServerErrorapp = create_app()@app.errorhandler(Exception)def framework_error(e): “““全局拦截异常””” if isinstance(e, APIException): return e if isinstance(e, HTTPException): code = e.code msg = e.description error_code = 1007 return APIException(msg, code, error_code) else: if app.config[‘DEBUG’]: return ServerError() else: raise eif name == ‘main’: app.run()6.配置全局错误处理新建文件夹 /app/libs/#File: /app/libs/error.py# -- coding: utf-8 --“““自定义错误文件”““from flask import request, jsonfrom werkzeug.exceptions import HTTPExceptionclass APIException(HTTPException): “““自定义api请求错误,返回的json格式””” code = 500 msg = ‘抱歉,后台发生了错误 ( ̄︶ ̄)!’ error_code = 999 def init(self, msg=None, code=None, error_code=None, headers=None): if code: self.code = code if error_code: self.error_code = error_code if msg: self.msg = msg super(APIException, self).init(msg, None) def get_body(self, environ=None): body = dict( msg=self.msg, error_code=self.error_code, request=request.method + ’ ’ + self.get_url_no_param() ) text = json.dumps(body) return text def get_headers(self, environ=None): return [(‘Content-Type’, ‘application/json’)] @staticmethod def get_url_no_param(): full_path = str(request.full_path) main_path = full_path.split(’?’) return main_path[0]#File: /app/libs/error_code.py# -- coding: utf-8 --from werkzeug.exceptions import HTTPExceptionfrom app.libs.error import APIExceptionclass Success(APIException): code = 201 msg = ‘success’ error_code = 0class DeleteSuccess(Success): code = 202 error_code = 1class ServerError(APIException): code = 500 msg = ‘抱歉,后台发生了错误 ( ̄︶ ̄)!’ error_code = 999class ClientTypeError(APIException): code = 400 msg = ‘未检测到客户端类型’ error_code = 1006class ParameterException(APIException): code = 400 msg = ‘无效参数’ error_code = 1000class NotFound(APIException): code = 404 msg = ‘没有找到对应的资源 O__O…’ error_code = 1001class AuthFailed(APIException): code = 401 error_code = 1005 msg = ‘认证失败’class Forbidden(APIException): code = 403 error_code = 1004 msg = ‘禁止访问,不在对应权限内’class SingleLogin(APIException): code = 400 error_code = 2002 msg = ‘请重新登录’class DuplicateAct(APIException): code = 400 error_code = 2001 msg = ‘请勿重复操作'7.自定义红图#File: /app/libs/redprint.py# -- coding: utf-8 --class Redprint: def init(self, name): self.name = name self.mound = [] def route(self, rule, **options): def decorator(f): self.mound.append((f, rule, options)) return f return decorator def register(self, bp, url_prefix=None): if url_prefix is None: url_prefix = ‘/’ + self.name for f, rule, options in self.mound: endpoint = self.name + ‘+’ + \ options.pop(“endpoint”, f.name) bp.add_url_rule(url_prefix + rule, endpoint, f, **options)8.新建/app/api/v1/ 文件夹#File: /app/api/v1/init.py# -- coding: utf-8 --from flask import Blueprintfrom app.api.v1 import user, client, tokendef create_blueprint_v1(): bp_v1 = Blueprint(‘v1’, name) user.api.register(bp_v1) client.api.register(bp_v1) token.api.register(bp_v1) return bp_v19.新建注册接口client.py#File: /app/api/v1/client.py# -- coding: utf-8 --from app.libs.error_code import Success, ParameterExceptionfrom app.libs.redprint import Redprintfrom app.models.user import Userfrom app.validators.forms import ClientForm, UserEmailForm, MinaFormfrom app.libs.enums import ClientTypeEnumfrom app.libs.get_openid import get_openidapi = Redprint(‘client’)@api.route(’/register’, methods=[‘POST’])def create_client(): form = ClientForm().validate_for_api() promise = { ClientTypeEnum.USER_EMAIL: __register_user_by_email, ClientTypeEnum.USER_MINA: __register_user_by_mina, } promiseform.type.data return Success()def __register_user_by_email(): form = UserEmailForm().validate_for_api() User.register_by_email(form.nickname.data, form.account.data, form.secret.data)def __register_user_by_mina(): form = MinaForm().validate_for_api() account = get_openid(form.account.data) if account is None: raise ParameterException else: User.register_by_mina(form.nickname.data, account, form.sex.data, form.avatar.data)10.登录apitoken.py#File: /app/api/v1/token.py# -- coding: utf-8 --import jwtimport datetimefrom flask import current_app, jsonifyfrom app.libs.enums import ClientTypeEnumfrom app.libs.error_code import AuthFailedfrom app.libs.redprint import Redprintfrom app.models.user import Userfrom app.validators.forms import ClientForm, TokenFormfrom app.libs.format_time import get_format_timestampapi = Redprint(’token’)@api.route(’’, methods=[‘POST’])def get_token(): “““登录功能,认证成功返回token””” form = ClientForm().validate_for_api() promise = { ClientTypeEnum.USER_EMAIL: User.verify, ClientTypeEnum.USER_MINA: User.mina_login, } identity = promise[ClientTypeEnum(form.type.data)]( form.account.data, form.secret.data ) # Token token = generate_auth_token(identity[‘uid’], form.type.data, identity[’login_time’], identity[‘scope’]) t = {’token’: token.decode(‘ascii’)} return jsonify(t), 201@api.route(’/secret’, methods=[‘POST’])def get_token_info(): “““获取令牌信息””” form = TokenForm().validate_for_api() auth_token = form.token.data try: data = jwt.decode(auth_token, current_app.config[‘SECRET_KEY’]) except jwt.ExpiredSignatureError: raise AuthFailed(msg=‘token is expired’, error_code=1003) except jwt.InvalidTokenError: raise AuthFailed(msg=‘token is invalid’, error_code=1002) r = { ‘scope’: data[‘scope’], ‘create_at’: get_format_timestamp(data[‘iat’]), ’expire_in’: get_format_timestamp(data[’exp’]), ‘uid’: data[‘uid’], ’login_time’: get_format_timestamp(data[’login_time’]) } return jsonify(r)def generate_auth_token(uid, ac_type, login_time, scope=None): “““生成令牌””” try: payload = { ’exp’: datetime.datetime.utcnow() + datetime.timedelta( days=current_app.config[‘TOKEN_EXPIRATION_DAYS’], seconds=current_app.config[‘TOKEN_EXPIRATION_SECONDS’], ), ‘iat’: datetime.datetime.utcnow(), ‘uid’: uid, ’type’: ac_type.value, ’login_time’: login_time, ‘scope’: scope, } return jwt.encode( payload, current_app.config[‘SECRET_KEY’], algorithm=‘HS256’ ) except Exception as e: return e11.用户接口user.py#File: /app/api/v1/user.py# -- coding: utf-8 --from flask import jsonify, gfrom app.libs.error_code import DeleteSuccessfrom app.libs.redprint import Redprintfrom app.libs.token_auth import authfrom app.models.base import dbfrom app.models.user import Userapi = Redprint(‘user’)@api.route(’/<int:uid>’, methods=[‘GET’])@auth.login_requireddef super_get_user(uid): user = User.query.filter_by(id=uid).first_or_404() return jsonify(user)@api.route(’’, methods=[‘GET’])@auth.login_requireddef get_user(): uid = g.user.uid user = User.query.filter_by(id=uid).first_or_404() return jsonify(user)@api.route(’/<int:uid>’, methods=[‘DELETE’])def super_delete_user(uid): with db.auto_commit(): user = User.query.filter_by(id=uid).first_or_404() user.delete() return DeleteSuccess()@api.route(’’, methods=[‘DELETE’])@auth.login_requireddef delete_user(): uid = g.user.uid with db.auto_commit(): user = User.query.filter_by(id=uid).first_or_404() user.delete() return DeleteSuccess()@api.route(’’, methods=[‘PUT’])def update_user(): return ‘update'12.新建用户models#File: /app/models/base.py# -- coding: utf-8 --from datetime import datetimefrom flask_sqlalchemy import SQLAlchemy as _SQLAlchemy, BaseQueryfrom sqlalchemy import inspect, Column, Integer, SmallInteger, ormfrom contextlib import contextmanagerfrom app.libs.error_code import NotFoundclass SQLAlchemy(_SQLAlchemy): @contextmanager def auto_commit(self): try: yield self.session.commit() except Exception as e: db.session.rollback() raise eclass Query(BaseQuery): def filter_by(self, **kwargs): if ‘status’ not in kwargs.keys(): kwargs[‘status’] = 1 return super(Query, self).filter_by(**kwargs) def get_or_404(self, ident): rv = self.get(ident) if not rv: raise NotFound() return rv def first_or_404(self): rv = self.first() if not rv: raise NotFound() return rvdb = SQLAlchemy(query_class=Query)class Base(db.Model): abstract = True create_time = Column(Integer) status = Column(SmallInteger, default=1) def init(self): self.create_time = int(datetime.now().timestamp()) def getitem(self, item): return getattr(self, item) @property def create_datetime(self): if self.create_time: return datetime.fromtimestamp(self.create_time) else: return None def set_attrs(self, attrs_dict): for key, value in attrs_dict.items(): if hasattr(self, key) and key != ‘id’: setattr(self, key, value) def delete(self): “““删除用户,注销用户””” self.status = 0 def active(self): “““激活用户””” self.status = 1 def update(self): “““更新数据库的表内容””” try: db.session.commit() except Exception as e: db.session.rollback() return str(e) def keys(self): return self.fields def hide(self, keys): for key in keys: self.fields.remove(key) return self def append(self, keys): for key in keys: self.fields.append(key) return selfclass MixinJSONSerializer: @orm.reconstructor def init_on_load(self): self._fields = [] # self._include = [] self._exclude = [] self._set_fields() self.__prune_fields() def _set_fields(self): pass def __prune_fields(self): columns = inspect(self.class).columns if not self._fields: all_columns = set(columns.keys()) self._fields = list(all_columns - set(self._exclude)) def hide(self, args): for key in args: self._fields.remove(key) return self def keys(self): return self._fields def getitem(self, key): return getattr(self, key)#File: /app/models/user.py# -- coding: utf-8 --from datetime import datetimefrom flask import current_appfrom sqlalchemy import Column, Integer, String, SmallIntegerfrom app import bcryptfrom app.libs.error_code import AuthFailedfrom app.models.base import Base, dbfrom app.libs.format_time import get_current_timestampfrom app.libs.get_openid import get_openidfrom app.libs.error_code import ParameterExceptionclass User(Base): id = Column(Integer, primary_key=True) nickname = Column(String(24), unique=True) email = Column(String(24), unique=True) mobile = Column(String(11), unique=True) sex = Column(Integer, default=0) # 1男2女 avatar = Column(String(200)) # 头像 register_ip = Column(String(100)) # 注册ip auth = Column(SmallInteger, default=1) # 权限 openid = Column(String(80), unique=True) _password = Column(‘password’, String(100)) login_time = Column(Integer, default=int(datetime.now().timestamp())) @property def login_datetime(self): if self.login_time: return datetime.fromtimestamp(self.login_time) else: return None def keys(self): return [‘id’, ’nickname’, ’email’, ‘auth’] @property def password(self): return self._password @password.setter def password(self, raw): self._password = bcrypt.generate_password_hash( raw, current_app.config[‘BCRYPT_LOG_ROUNDS’]).decode(‘utf-8’) @staticmethod def register_by_email(nickname, account, secret): “““通过邮箱注册””” with db.auto_commit(): user = User() user.nickname = nickname user.email = account user.password = secret db.session.add(user) @staticmethod def verify(email, password): “““通过邮箱登录””” user = User.query.filter_by(email=email).first_or_404() if not user.check_password(password): raise AuthFailed() scope = ‘AdminScope’ if user.auth == 2 else ‘UserScope’ login_time = get_current_timestamp() user.login_time = login_time User.update(User) return {‘uid’: user.id, ‘scope’: scope, ’login_time’: login_time} def check_password(self, raw): if not self._password: return False return bcrypt.check_password_hash(self._password, raw) @staticmethod def register_by_mina(nickname, account, sex, avatar): “““通过小程序注册””” with db.auto_commit(): user = User() user.nickname = nickname user.openid = account user.sex = sex user.avatar = avatar db.session.add(user) @staticmethod def mina_login(account, secret): “““通过小程序登录””” openid = get_openid(account) # 通过code来来获取openid if openid is None: raise ParameterException user = User.query.filter_by(openid=openid).first_or_404() scope = ‘AdminScope’ if user.auth == 2 else ‘UserScope’ login_time = get_current_timestamp() user.login_time = login_time User.update(User) return {‘uid’: user.id, ‘scope’: scope, ’login_time’: login_time}13.添加自定义的函数枚举登录类型# File: /app/libs/enums.py# -- coding: utf-8 --from enum import Enumclass ClientTypeEnum(Enum): USER_EMAIL = 100 USER_MOBILE = 101 # 微信小程序 USER_MINA = 200 # 微信公众号 USER_WX = 201 14.时间辅助函数#File: /app/libs/format_time.py# -- coding: utf-8 --import datetimedef get_current_date(): “““获取当前时间””” return datetime.datetime.now()def get_current_timestamp(): “““获取当前时间的时间戳””” return int(datetime.datetime.now().timestamp())def get_format_date(date=None, format_time="%Y-%m-%d %H:%M:%S”): “““获取格式化时间””” if date is None: date = datetime.datetime.now() return date.strftime(format_time)def get_format_timestamp(date=None, format_time="%Y-%m-%d %H:%M:%S”): “““格式化时间戳””” if date is None: date = datetime.datetime.now() return datetime.datetime.fromtimestamp(date).strftime(format_time)15.获取微信openid的函数#File: /app/libs/get_openid.py# -- coding: utf-8 --import requestsimport jsonfrom flask import current_appdef get_openid(code): api = ‘https://api.weixin.qq.com/sns/jscode2session' params = ‘appid={0}&secret={1}&js_code={2}&grant_type=authorization_code’ \ .format(current_app.config[‘MINA_APP’][‘AppID’], current_app.config[‘MINA_APP’][‘AppSecret’], code) url = api + ‘?’ + params response = requests.get(url=url) res = json.loads(response.text) openid = None if ‘openid’ in res: openid = res[‘openid’] return openid16.scope.py权限管理函数#File: /app/libs/scope.py# -- coding: utf-8 --class Scope: allow_api = [] allow_module = [] forbidden = [] def add(self, other): “““重载加号运算符””” self.allow_api = self.allow_api + other.allow_api self.allow_api = list(set(self.allow_api)) self.allow_module = self.allow_module + other.allow_module self.allow_module = list(set(self.allow_module)) self.forbidden = self.forbidden + other.forbidden self.forbidden = list(set(self.forbidden)) return selfclass AdminScope(Scope): allow_module = [‘v1.user’] def init(self): passclass UserScope(Scope): forbidden = [‘v1.user+super_get_user’, ‘v1.user+super_delete_user’] def init(self): self + AdminScope()def is_in_scope(scope, endpoint): # 把类名的字符串实例化 scope = globals()scope splits = endpoint.split(’+’) red_name = splits[0] if endpoint in scope.forbidden: return False if endpoint in scope.allow_api: return True if red_name in scope.allow_module: return True else: return False17.jwt生成token和验证token#File: /app/libs/token_auth.py# -- coding: utf-8 --import jwtfrom collections import namedtuplefrom flask import current_app, g, requestfrom flask_httpauth import HTTPBasicAuthfrom app.models.user import User as Userfrom app.libs.scope import is_in_scopefrom app.libs.error_code import AuthFailed, Forbidden, SingleLoginauth = HTTPBasicAuth()User = namedtuple(‘User’, [‘uid’, ‘ac_type’, ‘scope’, ’login_time’])@auth.verify_passworddef verify_password(token, password): user_info = verify_auth_token(token) if not user_info: return False else: g.user = user_info return Truedef verify_auth_token(token): try: data = jwt.decode(token, current_app.config[‘SECRET_KEY’]) except jwt.ExpiredSignatureError: raise AuthFailed(msg=‘token is expired’, error_code=1003) except jwt.InvalidTokenError: raise AuthFailed(msg=‘token is invalid’, error_code=1002) uid = data[‘uid’] ac_type = data[’type’] scope = data[‘scope’] login_time = data[’login_time’] user = User.query.filter_by(id=uid).first_or_404() if login_time != user.login_time: raise SingleLogin() # request 视图函数 allow = is_in_scope(scope, request.endpoint) if not allow: raise Forbidden() return User(uid, ac_type, scope, login_time)18.验证函数validators#File: /app/validators/base.py# -- coding: utf-8 --from flask import requestfrom wtforms import Formfrom app.libs.error_code import ParameterExceptionclass BaseForm(Form): def init(self): data = request.get_json(silent=True) args = request.args.to_dict() super(BaseForm, self).init(data=data, **args) def validate_for_api(self): valid = super(BaseForm, self).validate() if not valid: raise ParameterException(msg=self.errors) return self#File: /app/validators/forms.py# -- coding: utf-8 --from wtforms import StringField, IntegerFieldfrom wtforms.validators import DataRequired, length, Email, Regexpfrom wtforms import ValidationErrorfrom app.libs.enums import ClientTypeEnumfrom app.models.user import Userfrom app.validators.base import BaseForm as Formclass ClientForm(Form): account = StringField(validators=[ DataRequired(message=‘不允许为空’), length(min=5, max=32)]) secret = StringField() type = IntegerField(validators=[DataRequired()]) def validate_type(self, value): try: client = ClientTypeEnum(value.data) except ValueError as e: raise e self.type.data = clientclass UserEmailForm(ClientForm): account = StringField(validators=[Email(message=‘invalidate email’)]) secret = StringField(validators=[ DataRequired(), # password can only include letters , numbers and “” Regexp(r’^[A-Za-z0-9&$#@]{6,22}$’) ]) nickname = StringField(validators=[DataRequired(), length(min=2, max=22)]) def validate_account(self, value): if User.query.filter_by(email=value.data).first(): raise ValidationError()class TokenForm(Form): token = StringField(validators=[DataRequired()])class MinaForm(Form): account = StringField(validators=[ DataRequired(message=‘不允许为空’), length(min=10, max=80)]) nickname = StringField(validators=[DataRequired()]) sex = IntegerField(validators=[DataRequired()]) avatar = StringField(validators=[DataRequired()]) type = IntegerField(validators=[DataRequired()]) def validate_type(self, value): try: client = ClientTypeEnum(value.data) except ValueError as e: raise e self.type.data = client19. error_code.mderror_codemsg0创建成功1删除成功999未知错误 - 后台发生了错误1000无效参数1001没有找到对应的资源1002token is invalid1003token is expired1004禁止访问,不在对应权限内1005认证失败1006未检测到客户端类型2001请勿重复操作2002请重新登录20. jwt登录图片Flask + PyJWT 实现基于Json Web Token的用户认证授权endpointHTTP MethodAuthenticated?Resultjson Body/v1/client/registerPOSTNO注册用户{“account”:“666@qq.com”,“secret”:“123456”,“type”:100,“nickname”:“666”}/v1/tokenPOSTNO获取token{“account”:“666@qq.com”,“secret”:“123456”,“type”:100,“nickname”:“666”}/v1/userGETNO用户详情空/v1/user/2GETYES管理员获取用户详情空/v1/token/secretPOSTNOtoken详情{“token”:”"}通过 jwt 的 paylod 携带 login_time,和数据库 User 表中的 login_time 进行匹配作为单点登录修改数据库cd usersflask db migrateflask db upgrade运行结果注册成功注册失败登录成功登录失败成功获取用户信息用户重新登录,token变更,原token无法获取用户信息不带token请求,无法获取用户信息token过期,无法获取用户信息有权限获取其他用户信息无权限获取其他用户信息学习资料:慕课网-Python Flask构建可扩展的RESTful API慕课网-掌握Taro多端框架 快速上手小程序/H5开发Taro 多端开发实现原理与项目实战 ...

March 6, 2019 · 13 min · jiezi

Taro 多端开发的正确姿势:打造三端统一的网易严选(小程序、H5、React Native)

前言笔者所在的趣店 FED 早在去年 10 月份就已全面使用 Taro 框架开发小程序(当时版本为 1.1.0-beta.4),至今也上线了 2 个微信小程序、2 个支付宝小程序。之所以选用 Taro,解决微信小程序原生开发的痛点是一方面,另一方面团队也有多端统一开发的诉求,Taro 无疑是当时支持最好的。另外 React 也符合个人及团队的整体技术栈,可显著降低团队学习成本。可以说,Taro 在小程序端、H5 端支持程度已经不错,也有不少上线实例可以查看,但在 React Native 的支持上,Github 中公开的项目在 RN 这块均未适配:这种现况可以理解,毕竟要做到多端统一是有一定难度的,需准确把握各端差异,并做出合理取舍,而 Taro 虽以多端为设计目标,可重心在小程序端,没有对多端做出一定的开发约束,无从下手也便正常。笔者曾在 2018 iWeb 峰会 - 厦门站做过《多端统一开发实践》的分享,提到用 Taro 开发 RN 端的坑与大体思路,并加以实践。结合趣店 FED 在过去小半年的实践经验,我们开发了首个 Taro 三端统一应用:taro-yanxuan(高仿网易严选微信小程序),用以探讨本文的重点:Taro 开发多端应用的正确姿势。相关代码已开源:https://github.com/js-newbee/…。在线预览可在线预览 H5、RN 端(直接调用了网易严选接口,若要体验登录、购物车功能,请使用网易邮箱账号登录):微信小程序H5 - 访问链接React Native请 clone 代码本地运行Expo Snacks如下是 React Native 的运行截图:首页、分类二级分类、详情购物车、个人样式管理样式管理是多端开发的首要挑战,因为 React Native 与一般 Web 样式支持度差异较大,上述几个未适配 RN 的多端项目多数已栽在样式上了,用到了大量 RN 不支持的样式,这种情况再要去兼容 RN 无异于重写页面,想必也是有心无力了。这也是本文所强调的,需把握正确的多端开发姿势。样式上 H5 最为灵活,小程序次之,RN 最弱,统一多端样式即是对齐短板,也就是要以 RN 的约束来管理样式,同时兼顾小程序的限制,核心可以用三点来概括:使用 Flex 布局基于 BEM 写样式采用 style 属性覆盖组件样式使用 Flex 布局在进一步阐述之前,需先了解 RN 端几个影响样式方案的主要差异:display 只有 flex / none,position 只有 relative / absolute;不支持标签选择器、子代选择器、伪元素,不支持 background: url() 等;文本要用 Text 标签包裹,文本的样式不能加在 View 标签上,只能加在 Text 标签上。使用 Flex 布局,不单单是因为 RN 的 View 标签有默认样式 display: flex; flex-direction: column,更重要的是 Flex 可以解决幽灵空白问题:// View 标签高度不会是 100px,图片下方会有几像素空白,称为幽灵空白<View> <Image src={…} style={{ height: ‘100px’ }}</View>常规解决方案是在 View 标签上设置 font-size / line-height: 0, 或 Image 标签 display: inline-block 等,但这些在 RN 中都不支持,给 View 标签设置 display: flex 算是唯一可靠方案了。何况 Flex 布局能力强大,为啥不用呢?只需要注意一点,RN 中 View 标签默认主轴方向是 column,如果不将其他端改成与 RN 一致,就需要在所有用到 display: flex 的地方都显式声明主轴方向。基于 BEM 写样式RN 实际上只支持一种样式声明方式,即声明 style 属性:<View style={{ height: ‘100%’ }}这也导致 Taro 在 RN 端基本只支持 class 选择器这一种写法(最终编译成对象字面量),BEM(Block Element Modifier)在此处就恰如其分的发挥了作用:避免样式冲突(RN、小程序样式独立,但 H5 不是)自解释、语义化例如每行 2 个元素的列表,每行最后 1 个元素有特定样式,用伪元素选择器 :nth-child(even) 很容易实现,在 RN 中就需要自行计算了:{list.map((item, index) => ( <View className={classNames(‘block__element’, index % 2 === 1 && ‘block__element–even’ )} />)}基于 BEM 写 class 样式,不依赖其他选择器,虽然会让代码稍显繁琐,但也能保证多端都是行得通的,不存在支持问题。采用 style 属性覆盖组件样式小程序、RN 在页面、组件间传递样式时均有问题:// 目前 Taro RN 端还未实现往组件传递 className 对应样式<CompA compClass=‘my-style’ />// CompA,样式不生效<View className={this.props.compClass} />上述场景小程序虽可通过组件外部样式 externalClasses 实现,但官网文档有强调 “在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的,因此最好避免这种情况”;用全局样式倒是可以,但这样样式就不好维护了。那么,通过 style 传递、覆盖组件样式也就成了唯一可选方案了。需要注意一点,样式文件是会经过编译处理兼容多端的,但 style 方式需要运行时兼容:<Comp style={postcss({ background: ‘#fff’ })} />// 简单演示,如 RN 不支持 background,需改成 background-colorfunction postcss(style) { const { background, …restStyle } = style const newStyle = {} if (background) { newStyle.backgroundColor = background } return { …newStyle, …restStyle }}从这个角度看,styled-components 或许是多端开发的最佳样式方案,然而 Taro 还不支持,且微信小程序官方文档中也提到 “尽量避免将静态的样式写进 style 中,以免影响渲染速度”,全部样式都用写到 style 中恐怕就不靠谱了,但只用来覆盖少量样式不见得会有太大影响。样式兼容即便是把握了如上样式管理思路,多端样式差异的问题依然存在,例如 white-space: nowrap 这个样式在 RN 端会报错,Taro 有提供解决方案:.text { /postcss-pxtransform rn eject enable/ white-space: nowrap; /postcss-pxtransform rn eject disable/}但项目中不止一处会有这个问题,都这样写实在不太美观,可以用 Sass mixins 稍微封装下:@mixin eject($attr, $value) { /postcss-pxtransform rn eject enable/ #{$attr}: $value; /postcss-pxtransform rn eject disable/}.text { @includes eject(white-soace, nowrap);}Sass mixins 并不能解决差异,但对于部分各端不兼容的样式,通过 Sass mixins 统一处理是比较合理的方式,代码相对美观也方便维护。端能力差异相较于样式,端能力的差异倒是还好,各端差异是客观存在的,更不用说 RN 在 iOS 与 Android 上就已存在大量差异。应对端能力差异,要么改变实现思路,例如 RN 端还不支持 Taro.(get/set)StorageSync,那就改用 async / await + Taro.(get/set)Storage 实现,要么就得使用环境判断方式了。Taro 提供 process.env.TARO_ENV 用于环境判断,多数小的差异都可以用这种方式来解决:function foo() { if (process.env.TARO_ENV === ‘weapp’) { // 微信小程序逻辑 } if (process.env.TARO_ENV === ‘h5’) { // H5 逻辑 } if (process.env.TARO_ENV === ‘rn’) { // RN 逻辑 }}这个时候也比较考验开发者的封装能力了,一般是建议将这些差异逻辑的判断统一起来,例如在 src/utils 中进行封装,对外提供一致的接口,尽量不要在业务页面中杂糅太多的判断。而对于简单的环境判断处理不了的问题,就只能动用原生开发了,例如 Taro 还不支持 RN 端的 WebView 组件,就需要自己用原生 RN 实现:// Taro 页面,根据环境引入 RN 原生页面import { WebView } from ‘@tarojs/components’const WebViewRN = process.env.TARO_ENV === ‘rn’ ? require(’./rn’).default : nullexport default class extends Component { render() { return process.env.TARO_ENV === ‘rn’ ? <WebViewRN src={this.url} /> : <WebView src={this.url} /> }}// 原生 RN 页面,从 react-native 引入 WebViewimport Taro, { Component } from ‘@tarojs/taro’import { WebView } from ‘react-native’export default class WebViewRN extends Component { render() { return <WebView source={{ uri: this.props.src }} /> }}process.env.TARO_ENV 的处理是编译时而不是运行时,也就是说若不是编译 RN,上述用原生写的 RN 页面不会被打包,保证了编译成其他端时不会引入不支持的内容。原生页面能够引入,多端问题也就有了基本的实现保障。Taro RN 端的坑Taro RN 端目前小问题还是不少的,本项目开发过程中也顺带解了几个 bug:除此之外还有好几个问题,时间关系还未提 pr 解决,暂且先绕过,但其中有两个坑还是值得一说的。onClickRN 的 View 标签不支持 onClick ,但这又是很通常的需求,原生解决方式是套一层 Touchable 组件,如:<TouchableOpacity onPress={this.handlePress}> <View>{…}</View></TouchableOpacity>而 Taro 是引入 PanResponder 响应用户操作:<View {…PanResponder.carete({ …})} style={wrapperStyle}> <WrappedComponent style={innerStyle} /></View>问题在于这样多嵌套了一层 View,并把样式拆分成 wrapperStyle、innerStyle 分别应用,但样式拆分有问题,导致绑定 onClick 之后元素的样式错乱了,这点在开发过程中还是相当坑的。宽高自适应onClick 的问题也还好,改改样式能绕过去,宽高自适应的坑就比较尴尬了。小程序、H5 可用 rpx / em 实现自适应,而 RN 的自适应方案麻烦些,一般需通过 Dimensions 获取宽高再进行换算。Taro.pxTransform() 可解决该问题,但编译 RN 端样式文件时并没有考虑这点,即 width: 100px 会被编译成 width: 50,而不是 width: Taro.pxTransform(100),无法适配屏幕不同的屏幕尺寸。因此,目前 Taro RN 端还不好做到自适应,要么非百分比的宽高都用 style + Taro.pxTransform(),要么就得自己写个脚本去处理编译后的样式文件。这两个问题都提了 issue 2204 2205,有需要的可以关注下解决进度其他要做到多端统一,能说的细节点实在太多,上述实现思路虽然简单,但背后也都是隐含着对各端差异的斗争与取舍,本文也仅是列出最基本的几点,用于阐述 Taro 多端开发的核心思路。本项目代码没有做过多封装,方便阅读,也实现了足够多的样式细节进行踩坑,具体涉及的踩坑点、注意事项都在代码中以注释 // TODO(Taro 还未支持的)、// NOTE(开发技巧、注意事项)注明了,更多内容就有待各位去实践、体会了。总结如前言所说,Taro 虽然是以多端为设计目标,但重心是小程序端,RN 端目前的支持情况不算特别理想。但充分理解多端差异、掌握正确的多端开发姿势(特别是样式管理方面,避免项目成型后再去兼容需要大动刀斧)之后,在简单的项目上是完全可以一展拳脚的。若说 2 个礼拜开发一个小程序,是稀疏平常的事,但 2 个礼拜即搞定了小程序端(微信、支付宝、百度等等),还搞定了 H5、React Native 端,后续更新也只要改一处地方,这产出、维护效率就实在太惊人了,这大抵也就是 “Write once, run anywhere” 的魅力所在(虽然在前端领域极容易发展成 “Write once, debug everywhere” ????)相信随着小程序热度不断上升,还会有更多优秀的开源框架、解决方案涌现。而我们不倾向于造轮子,更关注基于现有方案如何更好地去开发多端应用。若有兴趣的前端小伙伴,不妨加入我们,一起搞事 caiminxing#qudian.com ????本文由趣店 FED 出品,首发于趣店技术学院;项目开源地址 https://github.com/js-newbee/…。 ...

February 19, 2019 · 3 min · jiezi

Gitter - 高颜值GitHub小程序客户端诞生记

前言嗯,可能一进来大部分人都会觉得,为什么还会有人重复造轮子,GitHub第三方客户端都已经烂大街啦。确实,一开始我自己也是这么觉得的,也问过自己是否真的有意义再去做这样一个项目。思考再三,以下原因也决定了我愿意去做一个让自己满意的GitHub第三方客户端。对于时常关注GitHub Trending列表的笔者来说,迫切需要一个更简单的方式随时随地去跟随GitHub最新的技术潮流;已有的一些GitHub小程序客户端颜值与功能并不能满足笔者的要求;听说iOS开发没人要了,掌握一门新的开发技能,又何尝不可?其实也没那么多原因,既然想做,那就去做,开心最重要。1. GitterGitHub:https://github.com/huangjiank…,可能是目前颜值最高的GitHub小程序客户端,欢迎star数据来源:GitHub API v3目前实现的功能有:实时查看Trending显示用户列表仓库和用户的搜索仓库:详情展示、README.md展示、Star/Unstar、Fork、Contributors展示、查看仓库文件内容开发者:Follow/Unfollow、显示用户的followers/followingIssue:查看issue列表、新增issue、新增issue评论分享仓库、开发者…Gitter的初衷并不是想把网页端所有功能照搬到小程序上,因为那样的体验并不会很友好,比如说,笔者自己也不想在手机上阅读代码,那将会是一件很痛苦的事。在保证用户体验的前提下,让用户用更简单的方式得到自己想要的,这是一件有趣的事。2. 探索篇技术选型第一次觉得,在茫茫前端的世界里,自己是那么渺小。当决定去做这个项目的时候,就开始了马不停蹄的技术选型,但摆在自己面前的选择是那么的多,也不得不感慨,前端的世界,真的很精彩。原生开发:基本上一开始就放弃了,开发体验很不友好;WePY:之前用这个框架已经开发过一个小程序,诗词墨客,不得不说,坑是真多,用过的都知道;mpvue:用Vue的方式去开发小程序,个人觉得文档并不是很齐全,加上近期维护比较少,可能是趋于稳定了?Taro:用React的方式去开发小程序,Taro团队的小伙伴维护真的很勤快,也很耐心的解答大家疑问,文档也比较齐全,开发体验也很棒,还可以一键生成多端运行的代码(暂没尝试)货比三家,经过一段时间的尝试及采坑,综合自己目前的能力,最终确定了Gitter的技术选型:Taro + Taro UI + Redux + 云开发 Node.js页面设计其实,作为一名Coder,曾经一直想找个UI设计师妹子做老婆的(肯定有和我一样想法的Coder),多搭配啊。现在想想,code不是生活的全部,现在的我一样很幸福。话回正题,没有设计师老婆页面设计怎么办?毕竟笔者想要的是一款高颜值的GitHub小程序。嗯,不慌,默默的拿出了笔者沉寂已久的Photoshop和Sketch。不敢说自己的设计能力如何,Gitter的设计至少是能让笔者自己心情愉悦的,倘若哪位设计爱好者想对Gitter的设计进行改良,欢迎欢迎,十二分的欢迎!3. 开发篇Talk is cheap. Show me the code.作为一篇技术性文章,怎可能少得了代码。在这里主要写写几个采坑点,作为一个前端小白,相信各位读者均是笔者的前辈,还望多多指教!Trending进入开发阶段没多久,就遇到了第一个坑。GitHub居然没有提供Trending列表的API!!!也没有过多的去想GitHub为什么不提供这个API,只想着怎么去尽快填好这个坑。一开始尝试使用Scrapy写一个爬虫对网页端的Trending列表信息进行定时爬取及存储供小程序端使用,但最终还是放弃了这个做法,因为笔者并没有服务器与已经备案好的域名,小程序的云开发也只支持Node.js的部署。开源的力量还是强大,最终找到了github-trending-api,稍作修改,成功部署到小程序云开发后台,在此,感谢原作者的努力。爬取Trending Repositoriesasync function fetchRepositories({ language = ‘’, since = ‘daily’,} = {}) { const url = ${GITHUB_URL}/trending/${language}?since=${since}; const data = await fetch(url); const $ = cheerio.load(await data.text()); return ( $(’.repo-list li’) .get() // eslint-disable-next-line complexity .map(repo => { const $repo = $(repo); const title = $repo .find(‘h3’) .text() .trim(); const relativeUrl = $repo .find(‘h3’) .find(‘a’) .attr(‘href’); const currentPeriodStarsString = $repo .find(’.float-sm-right’) .text() .trim() || /* istanbul ignore next / ‘’; const builtBy = $repo .find(‘span:contains(“Built by”)’) .parent() .find(’[data-hovercard-type=“user”]’) .map((i, user) => { const altString = $(user) .children(‘img’) .attr(‘alt’); const avatarUrl = $(user) .children(‘img’) .attr(‘src’); return { username: altString ? altString.slice(1) : / istanbul ignore next / null, href: ${GITHUB_URL}${user.attribs.href}, avatar: removeDefaultAvatarSize(avatarUrl), }; }) .get(); const colorNode = $repo.find(’.repo-language-color’); const langColor = colorNode.length ? colorNode.css(‘background-color’) : null; const langNode = $repo.find(’[itemprop=programmingLanguage]’); const lang = langNode.length ? langNode.text().trim() : / istanbul ignore next / null; return omitNil({ author: title.split(’ / ‘)[0], name: title.split(’ / ‘)[1], url: ${GITHUB_URL}${relativeUrl}, description: $repo .find(’.py-1 p’) .text() .trim() || / istanbul ignore next / ‘’, language: lang, languageColor: langColor, stars: parseInt( $repo .find([href="${relativeUrl}/stargazers"]) .text() .replace(’,’, ‘’) || / istanbul ignore next / 0, 10 ), forks: parseInt( $repo .find([href="${relativeUrl}/network"]) .text() .replace(’,’, ‘’) || / istanbul ignore next / 0, 10 ), currentPeriodStars: parseInt( currentPeriodStarsString.split(’ ‘)[0].replace(’,’, ‘’) || / istanbul ignore next / 0, 10 ), builtBy, }); }) );}爬取Trending Developersasync function fetchDevelopers({ language = ‘’, since = ‘daily’ } = {}) { const data = await fetch( ${GITHUB_URL}/trending/developers/${language}?since=${since} ); const $ = cheerio.load(await data.text()); return $(’.explore-content li’) .get() .map(dev => { const $dev = $(dev); const relativeUrl = $dev.find(’.f3 a’).attr(‘href’); const name = getMatchString( $dev .find(’.f3 a span’) .text() .trim(), /^((.+))$/i ); $dev.find(’.f3 a span’).remove(); const username = $dev .find(’.f3 a’) .text() .trim(); const $repo = $dev.find(’.repo-snipit’); return omitNil({ username, name, url: ${GITHUB_URL}${relativeUrl}, avatar: removeDefaultAvatarSize($dev.find(‘img’).attr(‘src’)), repo: { name: $repo .find(’.repo-snipit-name span.repo’) .text() .trim(), description: $repo .find(’.repo-snipit-description’) .text() .trim() || / istanbul ignore next */ ‘’, url: ${GITHUB_URL}${$repo.attr('href')}, }, }); });}Trending列表云函数exports.main = async (event, context) => { const { type, language, since } = event let res = null; let date = new Date() if (type === ‘repositories’) { const cacheKey = repositories::${language || 'nolang'}::${since || 'daily'}; const cacheData = await db.collection(‘repositories’).where({ cacheKey: cacheKey }).orderBy(‘cacheDate’, ‘desc’).get() if (cacheData.data.length !== 0 && ((date.getTime() - cacheData.data[0].cacheDate) < 1800 * 1000)) { res = JSON.parse(cacheData.data[0].content) } else { res = await fetchRepositories({ language, since }); await db.collection(‘repositories’).add({ data: { cacheDate: date.getTime(), cacheKey: cacheKey, content: JSON.stringify(res) } }) } } else if (type === ‘developers’) { const cacheKey = developers::${language || 'nolang'}::${since || 'daily'}; const cacheData = await db.collection(‘developers’).where({ cacheKey: cacheKey }).orderBy(‘cacheDate’, ‘desc’).get() if (cacheData.data.length !== 0 && ((date.getTime() - cacheData.data[0].cacheDate) < 1800 * 1000)) { res = JSON.parse(cacheData.data[0].content) } else { res = await fetchDevelopers({ language, since }); await db.collection(‘developers’).add({ data: { cacheDate: date.getTime(), cacheKey: cacheKey, content: JSON.stringify(res) } }) } } return { data: res }}Markdown解析嗯,这是一个大坑。在做技术调研的时候,发现小程序端Markdown解析主要有以下方案:wxParse:作者最后一次提交已是两年前了,经过自己的尝试,也确实发现已经不适合如README.md的解析wemark:一款很优秀的微信小程序Markdown渲染库,但经过笔者尝试之后,发现对README.md的解析并不完美towxml:目前发现是微信小程序最完美的Markdown渲染库,已经能近乎完美的对README.md进行解析并展示在Markdown解析这一块,最终采用的也是towxml,但发现在解析性能这一块,目前并不是很优秀,对一些比较大的数据解析也超出了小程序所能承受的范围,还好贴心的作者(sbfkcel)提供了服务端的支持,在此感谢作者的努力!最近这个项目????了,只能说跟不上年轻人的节奏了。Markdown解析云函数const Towxml = require(’towxml’);const towxml = new Towxml();// 云函数入口函数exports.main = async (event, context) => { const { func, type, content } = event let res if (func === ‘parse’) { if (type === ‘markdown’) { res = await towxml.toJson(content || ‘’, ‘markdown’); } else { res = await towxml.toJson(content || ‘’, ‘html’); } } return { data: res }}markdown.js组件import Taro, { Component } from ‘@tarojs/taro’import PropTypes from ‘prop-types’import { View, Text } from ‘@tarojs/components’import { AtActivityIndicator } from ’taro-ui’import ‘./markdown.less’import Towxml from ‘../towxml/main’const render = new Towxml()export default class Markdown extends Component { static propTypes = { md: PropTypes.string, base: PropTypes.string } static defaultProps = { md: null, base: null } constructor(props) { super(props) this.state = { data: null, fail: false } } componentDidMount() { this.parseReadme() } parseReadme() { const { md, base } = this.props let that = this wx.cloud.callFunction({ // 要调用的云函数名称 name: ‘parse’, // 传递给云函数的event参数 data: { func: ‘parse’, type: ‘markdown’, content: md, } }).then(res => { let data = res.result.data if (base && base.length > 0) { data = render.initData(data, {base: base, app: this.$scope}) } that.setState({ fail: false, data: data }) }).catch(err => { console.log(‘cloud’, err) that.setState({ fail: true }) }) } render() { const { data, fail } = this.state if (fail) { return ( <View className=‘fail’ onClick={this.parseReadme.bind(this)}> <Text className=‘text’>load failed, try it again?</Text> </View> ) } return ( <View> { data ? ( <View> <import src=’../towxml/entry.wxml’ /> <template is=‘entry’ data=’{{…data}}’ /> </View> ) : ( <View className=‘loading’> <AtActivityIndicator size={20} color=’#2d8cf0’ content=‘loading…’ /> </View> ) } </View> ) }}Redux其实,笔者在该项目中,对Redux的使用并不多。一开始,笔者觉得所有的接口请求都应该通过Redux操作,后面才发现,并不是所有的操作都必须使用Redux,最后,在本项目中,只有获取个人信息的时候使用了Redux。// 获取个人信息export const getUserInfo = createApiAction(USERINFO, (params) => api.get(’/user’, params))export function createApiAction(actionType, func = () => {}) { return ( params = {}, callback = { success: () => {}, failed: () => {} }, customActionType = actionType, ) => async (dispatch) => { try { dispatch({ type: ${customActionType }_request, params }); const data = await func(params); dispatch({ type: customActionType, params, payload: data }); callback.success && callback.success({ payload: data }) return data } catch (e) { dispatch({ type: ${customActionType }_failure, params, payload: e }) callback.failed && callback.failed({ payload: e }) } }}getUserInfo() { if (hasLogin()) { userAction.getUserInfo().then(()=>{ Taro.hideLoading() Taro.stopPullDownRefresh() }) } else { Taro.hideLoading() Taro.stopPullDownRefresh() }}const mapStateToProps = (state, ownProps) => { return { userInfo: state.user.userInfo }}export default connect(mapStateToProps)(Index)export default function user (state = INITIAL_STATE, action) { switch (action.type) { case USERINFO: return { …state, userInfo: action.payload.data } default: return state }}目前,笔者对Redux还是处于一知半解的状态,嗯,学习的路还很长。4. 结语篇当Gitter第一个版本通过审核的时候,心情是很激动的,就像自己的孩子一样,看着他一点一点的长大,笔者也很享受这样一个项目从无到有的过程,在此,对那些帮助过笔者的人一并表示感谢。当然,目前功能和体验上可能有些不大完善,也希望大家能提供一些宝贵的意见,Gitter走向完美的路上希望有你!最后,希望Gitter小程序能对你有所帮助!

January 26, 2019 · 5 min · jiezi

【Copy攻城狮日志】借助Taro暴改Nideshop实现电商支付宝小程序雏形

↑开局一张图,故事全靠编↑从一个需求说起作为底层的程序猿,哦不,我连猿都算不上,混的好的叫码神,混得一般的叫码农,混得有点差的叫码畜,混得极差的,就像我这样的,叫码渣。去年,2018年年底,12月份,运营的大佬提出了想做电商类支付宝小程序的想法,需求很简单:做一个自己的商城,上架到支付宝小程序应用市场。一句话,简单明了,需求很明确啊,可这句话对我来说,要实现的难度,比起李白上蜀道还难,比难于上青天还难。细细一想,做商城,得有后台管理系统吧?得有支付系统吧?得有订单管理等一系列业务支撑的后台吧?我一小小的前端,本身业务基础又差,每天上班8小时划水10小时,竟然让我独自完成一个电商支付宝小程序,哈哈哈哈哈。不过,本来没做实质性项目的我,怎么会畏惧,怎么能退缩,生死看淡,不服就干!谁给我的自信?开源社区啊!作为“资深”的Copy码渣,接到任务我就在github开始寻符合需求的demo,皇天不负有心人,我把github翻了个遍,收获寥寥无几,各位大佬有啥支付宝小程序开源的项目请一定推荐给我,Copy选不中对象,就无法愉快地进行Paste。为了快速交付,经过对比选用@tumobi大佬的Nideshop“全家桶”,于是就有了这次借助Taro的taro convert转化微信小程序为支付宝小程序的经历。在我看来,我写不出如此出色的开源项目,倘若我能借助这些项目快速完成自己的工作,享受开源带来的乐趣,对于现阶段的我而言,足矣!(绝逼不敢相信,从业多年的程序员依旧是这么low!)(图片来源于网络)环境准备工欲善其事必先利其器。9102年了,还有谁在用notepad写代码?当然,对于我们前端而言,谁的电脑没装个nodegitvscode之类的软体?如果您还没装的话,赶紧装吧,装完您就会嘿嘿嘿,对于我而言没有ndoe我无法工作,没有前端开发环境,我就不快乐。(图片来源于网络)在您的平台上下载 Node.js 源码或预编译安装包,然后即可马上进行开发。去下载git–distributed-is-the-new-centralized。去下载小程序开发者工具定位于「一站式小程序研发工具」,专门为小程序开发打造,提供了项目管理、编码、调试、真机测试等功能。去下载其他的好像也没啥了,当年好像我的还装了Python|jJava|Android等环境,那是2016年的事了追忆,现在看来很傻很天真,其实没必要。Copy进行时Taro 可以将你的原生微信小程序应用转换为 Taro 代码,进而你可以通过 taro build 的命令将 Taro 代码转换为对应平台的代码,或者对转换后的 Taro 代码进行用 React 的方式进行二次开发。之前一直在期待taro的这个功能,虽然不会React,也要尝试一下,也希望通过这些实践更加了解React并好好学习,从我接触的内容来说,React是前端开发必备的技术栈。Taro安装 /** Quick Start With NPM Or Yarn **/ $ npm install -g @tarojs/cli $ yarn global add @tarojs/clinideshop-mini-program下载 git clone https://github.com/tumobi/nideshop-mini-program.git cd nideshop-mini-program转化为tarotaro convert通过以上步骤可以得到一个taroConvert的文件夹,就算暂时成功的了。安装依赖cd taroConvertnpm i对于大多数前端项目来说,现阶段不可避免的问题是可能一个不算复杂的项目会依赖上百个npm包,也正是因为这些包,大大解放了生产力,一定程度上提高了开发效率。当然,如同硬币有两面,伴随着便捷高效的同时也带来了一定的安全风险。可能大厂都是自己造轮子吧!打包成支付宝小程序npm run build:alipay理想状态是可直接打包成dist的,but……接下来就捋一捋存在的问题,为什么要手动修改一些问题?为什么要暴力修改首先回到taro的官方文档看下 taro convet会遇到哪些坑在小程序 IDE 显示 _createData 错误☞了解这里我们好像暂时没遇到这个问题,也不知道是哪个小程序IDE会有如此问题,先忽略了。转换 wxParse 报错不存在文件☞了解这个问题我们要及时改正,在执行taro conver前先把wxParse.wxml中46行到128行的wxParse1修改为wxParse0不支持 relations 和 Behavior了解这个问题我们代码里好像没有这些组件,暂时忽略转换 wepy 文件不成功了解这个问题我们肯定不存在,因为这个项目没有使用wepy,继续忽略。现在看来,以上问题貌似都不存在,那么我们先回到这个报错凭我多年的copy经验,一定是文件不存在或者文件引用路径有问题。不慌,对比了原文件taro convert之后的taroConvert目录里边的wxParse,的确发现了小问题:wxParse目录下的文件缺失,除了wxParse.js过来了,其他的都没有被转换。那就暴力一回,使出我的Copy大法,手动转换过去,并修改几处引用的相对路径,继续build。接下来,在支付宝小程序开发者工具中打,不出意外能跑起来一个电商支付宝小程序雏形。github地址☞☞nideshop-alipay by taro convert以上是我这个Copy攻城狮对使用taro convert转换原生微信小程序为支付宝小程序的一次微不足道的实践。

January 23, 2019 · 1 min · jiezi

taro原生微信小程序转支付宝小程序趟坑宝典

京东出了特别牛逼的工具taro,号称可以开发一套程序然后转为市面上的多套小程序,比如支付宝小程序,百度小程序,微信小程序,只需要写一套代码,我看到这个的时候是特别兴奋的,因为公司的业务比较广,但是呢开发人员少,对于我们团队简直就是及时雨,于是我立马拿目前我们的原生微信小程序做转支付宝小程序的尝试。说明一下1、微信原生小程序,没有用第三方框架,感觉符合taro的要求2、要做的是将原生的微信小程序先转为taro的写法,然后在转为支付宝的 (taro是支持这么玩的)3、说明一下我是在mac上执行操作的所有过程首先将原生微信小程序转taro写法,执行命令:sudo taro convert这一步很顺利没啥问题很愉快的变会转换完成,转换完之后会在项目目录下多一个taroConvert文件夹,然后进入此文件夹 cd taroConvert后执行命令sudo npm install到这一步你会神奇的看到报一大堆 npm ERR!京东出了特别牛逼的工具taro,号称可以开发一套程序然后转为市面上的多套小程序,比如支付宝小程序,百度小程序,微信小程序,只需要写一套代码,我看到这个的时候是特别兴奋的,因为公司的业务比较广,但是呢开发人员少,对于我们团队简直就是及时雨,于是我立马拿目前我们的原生微信小程序做转支付宝小程序的尝试。说明一下1、微信原生小程序,没有用第三方框架,感觉符合taro的要求2、要做的是将原生的微信小程序先转为taro的写法,然后在转为支付宝的 (taro是支持这么玩的)3、说明一下我是在mac上执行操作的所有过程4、欢迎大家关注公众号:PHP学习网首先将原生微信小程序转taro写法,执行命令:sudo taro convert这一步很顺利没啥问题很愉快的变会转换完成,转换完之后会在项目目录下多一个taroConvert文件夹,然后进入此文件夹 cd taroConvert后执行命令sudo npm install到这一步你会神奇的看到报一大堆 npm ERR!有时还会缺少npm包@tarojsplugin-sass,可能还有别的反正就是少各种各样的包,而且我用了翻墙也没把所有的包下载全,不管错误啦继续往下走。执行命令: sudo npm run dev:alipay到这一步还是很顺利的,偶尔报个错误,基本都是写法问题处理处理还是能解决的最后顺利监听到了文件这会我还是比较高兴的,也许很快就能成功的转为支付宝的小程序啦还是比较激动的,于是按照官方说的打开支付宝的编辑器等待见证奇迹。于是我就看到了很申请的事情,项目跑不起来,开始报各种错误at _class.parseBlockOrModuleBlockBody (/Applications/小程序开发者工具.app/Contents/Resources/app/extensions/volans-ide-tiny-project.asar/node_modules/@babel/parser/lib/index.js:8073:23)pos: 1454,loc: { line: 27, column: 9 },code: ‘BABEL_PARSE_ERROR’ }想想也正常,肯定要报错的然后就各种查找,反正最后也没找到个所以然来,但是我不死心啊,我看到官网说他们用开源项目测试成功,于是我也拿他们说的开源项目进行了测试,效果依旧支付宝小程序编辑器中出现这个之后就这样了,这个开源项目在执行 sudo npm install的时候也是各种缺包,但是下载不下来,我不知道是不是因为这个地方有错误的原因,趟坑太多,也许是自己水平不行吧,反正我得先暂停暂停啦,之后在接着趟。还有一点taro刚出来没多久,别指望能百度出来什么解决办法,还是自己专心研究吧,就先写到这吧,同时欢迎大家关注我的公众号PHP学习网,大家一起学习。

January 18, 2019 · 1 min · jiezi

微信小程序Taro开发(1):Taro安装及使用

全局安装 Taro 开发工具 @tarojs/clinpm install -g @tarojs/cli在要创建项目的目录下,创建项目:taro init test以微信小程序为例,创建项目完毕,要运行项目,则如下:npm run dev:weapp当项目在运行时,在项目目录下dist文件中可以看到编译的小程序代码,打开微信小程序开发工具,就可以边开发边在微信小程序开发工具中看到效果打包项目:npm run build:weapptaro不仅支持微信小程序还支持百度小程序,支付宝小程序,H5,react native等等,具体可进入官网了解更多,官网地址:https://nervjs.github.io/taro…

December 18, 2018 · 1 min · jiezi

微信小程序Taro开发(2):生命周期及开发中注意点

生命周期componentWillMount在微信小程序中这一生命周期方法对应页面的onLoad或入口文件app中的onLaunchcomponentDidMount在微信小程序中这一生命周期方法对应页面的onReady或入口文件app中的onLaunch,在 componentWillMount后执行componentDidShow在微信小程序中这一生命周期方法对应 onShowcomponentDidHide在微信小程序中这一生命周期方法对应 onHidecomponentDidCatchError错误监听函数,在微信小程序中这一生命周期方法对应 onErrorcomponentDidNotFound页面不存在监听函数,在微信小程序中这一生命周期方法对应 onPageNotFoundshouldComponentUpdate页面是否需要更新componentWillUpdate页面即将更新componentDidUpdate页面更新完毕componentWillUnmount页面退出,在微信小程序中这一生命周期方法对应 onUnload在小程序中 ,页面还有一些专属的方法成员,如下:1. onPullDownRefresh: 页面相关事件处理函数–监听用户下拉动作2. onReachBottom: 页面上拉触底事件的处理函数3. onShareAppMessage: 用户点击右上角转发4. onPageScroll: 页面滚动触发事件的处理函数5. onTabItemTap: 当前是 tab 页时,点击 tab 时触发6. componentWillPreload: 预加载,只在微信小程序中可用注意1.通常入口文件会包含一个 config 配置项,这里的配置主要参考微信小程序的全局配置而来,在编译成小程序时,这一部分配置将会被抽离成 app.json,而编译成其他端,亦会有其他作用。2.入口文件继承自 Component 组件基类,它同样拥有组件生命周期,但因为入口文件的特殊性,他的生命周期并不完整,如:componentWillMount、componentDidMount、componentDidShow、componentDidHide、componentDidCatchError、componentDidNotFound。3.入口文件需要包含一个 render 方法,一般返回程序的第一个页面,但值得注意的是不要在入口文件中的 render 方法里写逻辑及引用其他页面、组件,因为编译时 render 方法的内容会被直接替换掉,你的逻辑代码不会起作用。4.Taro 支持组件化开发,组件代码可以放在任意位置,不过建议放在 src 下的 components 目录中。一个组件通常包含组件 JS 文件以及组件样式文件,组织方式与页面类似。taro项目目录如下:├── config 配置目录| ├── dev.js 开发时配置| ├── index.js 默认配置| └── prod.js 打包时配置├── src 源码目录| ├── components 公共组件目录| ├── pages 页面文件目录| | ├── index index 页面目录| | | ├── banner 页面 index 私有组件| | | ├── index.js index 页面逻辑| | | └── index.css index 页面样式| ├── utils 公共方法库| ├── app.css 项目总通用样式| └── app.js 项目入口文件└── package.json ...

December 18, 2018 · 1 min · jiezi

微信小程序Taro开发(3):canvas制作钟表

制作钟表分成两部分,一部分是表盘,一部分是时针、分针、秒针的走动,首先,先绘制表盘:// 绘制表盘 getDialClock = () => { const width = this.state.width; const height = this.state.height; const ctx = Taro.createCanvasContext(‘myCanvas’, this.$scope); const R = width/2 - 30;//圆半径 const r = R - 15; //设置坐标轴原点 ctx.translate(width/2, height/2); ctx.save(); // 圆心 ctx.beginPath(); ctx.arc(0, 0, 5, 0, 2 * Math.PI, true); ctx.fill(); ctx.closePath(); // 表盘外圆 ctx.setLineWidth(2); ctx.beginPath(); ctx.arc(0, 0, R, 0, 2 * Math.PI, true); ctx.closePath(); ctx.stroke(); // 表盘刻度(大格) ctx.beginPath(); ctx.setLineWidth(5); for(var i = 0; i < 12; i++) { ctx.beginPath(); ctx.rotate(Math.PI / 6); ctx.moveTo(R, 0); ctx.lineTo(r, 0); ctx.stroke(); } ctx.closePath(); // 表盘刻度(小格) ctx.beginPath(); ctx.setLineWidth(1); for(var i = 0; i < 60; i++) { ctx.beginPath(); ctx.rotate(Math.PI / 30); ctx.moveTo(R, 0); ctx.lineTo(R-10, 0); ctx.stroke(); } ctx.closePath(); // 表盘时刻(数字) ctx.beginPath(); ctx.setFontSize(16)//设置字体样式 // ctx.setTextBaseline(“middle”);//字体上下居中,绘制时间 for(let i = 1; i < 13; i++) { //利用三角函数计算字体坐标表达式 var x = (r-10) * Math.cos(i * Math.PI / 6 - Math.PI/2); var y = (r-10) * Math.sin(i * Math.PI / 6 - Math.PI/2); let sz = i + ‘’; ctx.fillText(sz, x - 5, y + 5, 15); } ctx.closePath(); // 开始绘制 ctx.draw(); }表盘绘制完毕,再绘制时针,分针,秒针的运动,这里需要新建一个组件来专门管理这个时间运动,在组件中,如下:// 绘制 针表 drawTime = () => { const width = this.props.dataRes.width; const height = this.props.dataRes.height; const ctx = Taro.createCanvasContext(’timeId’, this.$scope); const R = width/2 - 30;//圆半径 //设置坐标轴原点 ctx.translate(width/2, height/2); ctx.save(); const t = new Date();//获取当前时间 let h = t.getHours();//获取小时 h = h>12?(h-12):h;//将24小时制转化为12小时制 const m = t.getMinutes();//获取分针 const s = t.getSeconds();//获取秒 //绘制时针 ctx.beginPath(); ctx.setStrokeStyle(‘green’) ctx.setLineWidth(10); ctx.rotate((Math.PI/6)(h+m/60+s/3600)-Math.PI/2); ctx.moveTo(0,0); ctx.lineTo(R-90,0); ctx.stroke(); ctx.restore(); ctx.save(); // 绘制分针 ctx.beginPath(); ctx.setStrokeStyle(‘gold’) ctx.setLineWidth(5); ctx.rotate((Math.PI/30)(m+s/3600)-Math.PI/2); ctx.moveTo(0,0); ctx.lineTo(R-60,0); ctx.stroke(); ctx.restore(); ctx.save(); // 绘制秒针 ctx.beginPath(); ctx.setStrokeStyle(‘red’) ctx.setLineWidth(1); ctx.rotate((Math.PI/30)*s-Math.PI/2); ctx.moveTo(0,0); ctx.lineTo(R-20,0); ctx.stroke(); ctx.restore(); ctx.save(); ctx.draw(); }结果显示:源码地址:https://gitee.com/hope93/canv… ...

December 18, 2018 · 2 min · jiezi

基于 taro 和 taro-ui 的仿时光网的小程序

本项目是基于 taro 和 taro-ui 的仿时光网的小程序学习项目,后续功能仍在开发中项目中的接口使用的时光网的API,有侵犯时光网权益的嫌疑,若被告知需停止使用,本人会及时删除此页面与整个项目项目运行# 全局安装 taro 开发工具npm install -g @tarojs/cli# 克隆项目git clone https://github.com/calabash519/taro-mtime.gitcd taro-mtime# 安装依赖npm i# 小程序预览(需下载下载并打开微信开发者工具,选择预览项目根目录)npm run dev:weapp# H5 预览npm run dev:h5文件目录├── dist 编译后文件├── config 项目配置项 ├── dev.js ├── index.js └── prod.js └── src ├── assets 外部资源 ├── data mock 数据 └── images 图片资源 └── pages 页面层 ├── coming-soon 即将上映 ├── components 共用组件 ├── hot-showing 正在热映 ├── index 正在售票 ├── user-center 用户中心 ├── user-message 我的消息 └── user-setting 个人资料已开发功能列表正在热映即将上映个人中心个人设置我的消息待开发功能列表电影详情页个人设置项内容页我的消息项内容页页面截图项目地址项目地址:https://github.com/calabash51…,如果对您有帮助,您可以点右上角 “Star” 支持一下 谢谢! ^_^ ...

October 15, 2018 · 1 min · jiezi

【小程序taro 最佳实践】异步action优雅实践(简化流程)

给大家提供思路,可以借鉴哈,有什么问题可以留言taro脚手架后面文章会慢慢讲解更多技巧https://github.com/wsdo/taro-…概要当我们拿到官方项目请求action的时候需要写两个函数(一个返回type,一个dispatch),超级麻烦,如下所示。function articleList(data) { return { type: LIST, payload: data }}export function list() { console.log(’list’) return (dispatch) => { Taro.request({ url: ‘http://api.shudong.wang/v1/article/list', data: { foo: ‘foo’, bar: 10 }, header: { ‘content-type’: ‘application/json’ } }).then((res) => { dispatch(articleList(res.data.article)) // 需要在另一个函数 dispatch }) }}如果每个函数都这样写下去,会极其痛苦的,很多冗余的代码,那么我们应该怎么设计呢?设计参数:type类型,函数(自动dispatch)这样设计后我们可以极其简单的使用action了/** * 创建API Action * * @export * @param actionType Action类型 * @param [func] 请求API方法,返回Promise * @returns 请求之前dispatch { type: ${actionType}_request } * 请求成功dispatch { type: ${actionType}, payload: ${resolveData} } * 请求失败dispatch { type: ${actionType}_failure, payload: ${rejectData} } */export function createApiAction(actionType, func = () => {}) { return ( params = {}, callback = { success: () => {}, failed: () => {} }, customActionType = actionType, ) => async (dispatch) => { try { dispatch({ type: ${customActionType }_request, params }); const data = await func(params); dispatch({ type: customActionType, params, payload: data }); callback.success && callback.success({ payload: data }) return data } catch (e) { dispatch({ type: ${customActionType }_failure, params, payload: e }) callback.failed && callback.failed({ payload: e }) } }}极简使用方式配合上篇文章讲的封装的 api 异步action就变得如此简单了import { createApiAction } from ‘./index’export const list = createApiAction(LIST, params => api.get(’news/list’, params))全部代码如果能帮到你帮忙点个 starhttps://github.com/wsdo/taro-… ...

September 28, 2018 · 1 min · jiezi

【小程序taro最佳实践】http请求封装(方便使用,增加token,统一错误日志记录和上报)

给大家提供思路,可以借鉴哈,有什么问题可以留言taro脚手架后面文章会慢慢讲解更多技巧https://github.com/wsdo/taro-…为什么封装当我们开发小程序的时候,经常会用到http请求,当然官方已经提供了请求的接口,但是我们每次请求的时候,可能会加上token,每次请求都会加上,如果不封装起来,会相当的麻烦,那么又怎么封装呢?特性暴露get方法暴露post方法post自定义contentType增加配置文件token定义添加判断状态,记录异常信息增加异常错误logError日志增加统一日志上报设计需要对外暴露接口:get post参数:url,传输的数据,请求头这样我们可以很方便的使用了在/service/下新建立一个api.jsbaseOptions 方法baseOptions(params, method = ‘GET’) { let { url, data } = params let contentType = ‘application/x-www-form-urlencoded’ contentType = params.contentType || contentType const option = { isShowLoading: false, url: base + url, data: data, method: method, header: { ‘content-type’: contentType, ’token’: token }, // 默认contentType ,预留token success(res) { }, error(e) { logError(‘api’, ‘请求接口出现问题’, e) } } return Taro.request(option) },get方法 get(url, data = ‘’) { let option = { url, data } return this.baseOptions(option) },post 方法增加了contentType 不同的后端框架会要求不同的请求头部 post: function (url, data, contentType) { let params = { url, data, contentType } return this.baseOptions(params, ‘POST’) }这样我们就可以很方便在action里面使用了import api from ‘../service/api’api.get(’news/list’, params)添加判断状态export const HTTP_STATUS = { SUCCESS: 200, CLIENT_ERROR: 400, AUTHENTICATE: 401, FORBIDDEN: 403, NOT_FOUND: 404, SERVER_ERROR: 500, BAD_GATEWAY: 502, SERVICE_UNAVAILABLE: 503, GATEWAY_TIMEOUT: 504}配置export const base = “https://api.github.com/repos/"增加异常错误logError日志export const logError = (name, action, info) => { if (!info) { info = ’empty’ } try { let deviceInfo = wx.getSystemInfoSync() // 这替换成 taro的 var device = JSON.stringify(deviceInfo) } catch (e) { console.error(’not support getSystemInfoSync api’, err.message) } let time = formatTime(new Date()) console.error(time, name, action, info, device) // 如果使用了 第三方日志自动上报 // if (typeof action !== ‘object’) { // fundebug.notify(name, action, info) // } // fundebug.notifyError(info, { name, action, device, time }) if (typeof info === ‘object’) { info = JSON.stringify(info) }配合使用import { HTTP_STATUS } from ‘../const/status’import { base } from ‘./config’import { logError } from ‘../utils’ baseOptions(params, method = ‘GET’) { let { url, data } = params // let token = getApp().globalData.token // if (!token) login() console.log(‘params’, params) let contentType = ‘application/x-www-form-urlencoded’ contentType = params.contentType || contentType const option = { isShowLoading: false, loadingText: ‘正在加载’, url: base + url, data: data, method: method, header: { ‘content-type’: contentType, ’token’: token }, success(res) { if (res.statusCode === HTTP_STATUS.NOT_FOUND) { return logError(‘api’, ‘请求资源不存在’) } else if (res.statusCode === HTTP_STATUS.BAD_GATEWAY) { return logError(‘api’, ‘服务端出现了问题’) } else if (res.statusCode === HTTP_STATUS.FORBIDDEN) { return logError(‘api’, ‘没有权限访问’) } else if (res.statusCode === HTTP_STATUS.SUCCESS) { return res.data } }, error(e) { logError(‘api’, ‘请求接口出现问题’, e) } } return Taro.request(option) },最终版本import Taro from ‘@tarojs/taro’import { HTTP_STATUS } from ‘../const/status’import { base } from ‘./config’import { logError } from ‘../utils’const token = ‘’export default { baseOptions(params, method = ‘GET’) { let { url, data } = params // let token = getApp().globalData.token // if (!token) login() console.log(‘params’, params) let contentType = ‘application/x-www-form-urlencoded’ contentType = params.contentType || contentType const option = { isShowLoading: false, loadingText: ‘正在加载’, url: base + url, data: data, method: method, header: { ‘content-type’: contentType, ’token’: token }, success(res) { if (res.statusCode === HTTP_STATUS.NOT_FOUND) { return logError(‘api’, ‘请求资源不存在’) } else if (res.statusCode === HTTP_STATUS.BAD_GATEWAY) { return logError(‘api’, ‘服务端出现了问题’) } else if (res.statusCode === HTTP_STATUS.FORBIDDEN) { return logError(‘api’, ‘没有权限访问’) } else if (res.statusCode === HTTP_STATUS.SUCCESS) { return res.data } }, error(e) { logError(‘api’, ‘请求接口出现问题’, e) } } return Taro.request(option) }, get(url, data = ‘’) { let option = { url, data } return this.baseOptions(option) }, post: function (url, data, contentType) { let params = { url, data, contentType } return this.baseOptions(params, ‘POST’) }}全部代码如果能帮到你帮忙点个 starhttps://github.com/wsdo/taro-…项目目录 ...

September 28, 2018 · 3 min · jiezi

【taro最佳实践】设置好基础开发字体尺寸

taro脚手架后面文章会慢慢讲解更多技巧https://github.com/wsdo/taro-…设置开发字体尺寸我感觉在一个项目当中,务必一开始就设置好这个尺寸,关系后今后项目的一个统一管理问题。那么应该怎么设置这个呢?设计思路按照惯例,我们开发项目的时候,尽量不要为难自己,如果按照2倍图,来开发的话,处处想着2倍,在开发的过程中,想参考其它项目的样式,发现在浏览器中,看到的只是1倍,我们拿过来样式还得自己换算比较麻烦个人建议统一配置一倍,比较通用一些,如果你的其它项目都是2倍,那就设置2倍。配置方式:默认的只有几个尺寸 如果自己不主动添加的话,编译后悔报:NANpx在 根目录的config.js 文件里面配置 designWidth: 750, deviceRatio:{ ‘750’: 1/2, ‘375’: 1 },这样配置后我们在项目中,写10px 会编译成 20rpx 符合小程序的尺寸,也方便自己写1倍把这个基础尺寸配置好,我们就可以愉快的进行项目了。

September 26, 2018 · 1 min · jiezi