在互联网不断发展的今天,前端程序员们也不断面临着新的挑战,在这个变化无穷、不断革新本身的领域,每年都有新的美好事物在发生。从去年微信小程序的诞生,到今年的逐渐火热,以及异军突起的轻应用、百度小程序等的出现,前端能够延伸的领域已经愈来愈广,固然也意味着业务在不断扩大。这时候,如何经过技术手段来提高开发效率,应对不断增加的业务,就是一个值得探索的话题。本文将对 Taro 诞生的故事,进行深刻浅出地介绍,记录下这个忙碌的春夏之交发生的故事。javascript
自 2017-1-9
微信小程序(如下简称小程序)诞生以来,就伴随着赞誉与争议不断。从发布上线时的不被大多数人看好,到现在的逐渐火热,甚至说是如日中天也不为过,小程序用时间与实践证实了本身的价值。同时于开发者来讲,小程序的生态不断在完善,许多的坑已被踩平,虽然仍是存在一些使人诟病的问题,但已经足见微信的诚意了。这个时候要是尚未上手把玩太小程序,就显得很是OUT了。html
小程序对于前端程序员来讲应该算得上是福音了,用前端相关的技术,得到丝般顺滑的 Native
体验,前端们又能够在产品小姐姐面前硬气一把了。能够说小程序给前端程序员打开了一扇新的大门,你们都应该感谢微信,可是从开发的角度来讲,小程序的开发体验就很是值得商榷了,不只语法上显得有些不三不四,并且有些莫名其妙的坑也常常让人不经意间感叹一下和谐社会,从市面上层出不穷的小程序开发框架就可见一斑。如下就盘点部分小程序开发的痛点。前端
在小程序中,一个页面 page
可能拥有 page.js
、page.wxss
、page.wxml
、page.json
四个文件java
这样在开发的时候就须要来回进行文件切换,尤为是在同时开发模板和逻辑的时候,切来切去会显得尤为麻烦,影响开发效率,但小程序原生只支持这么写,就显得比较尴尬了。react
而在语法上,小程序的语法能够说既像 React
,又像 Vue
,不能说显得有点不三不四吧,但在使用上老是感受有些别扭,对于开发者来讲,等于又要学习一套新的语法,提高了学习成本。并且,小程序的模板因为没有编辑器插件的支持,书写的时候也没有智能提示与 lint 检查,书写起来显得有些麻烦。webpack
在小程序中处处可见规范不统一的状况git
例如组件的属性,以最简单的 <button />
组件为例,在小程序官方文档中,该组件的属性部分截图以下,你们能够感觉下程序员
<button />
组件属性名既有以中划线分割多个单词的状况 session-form
,也有多个单词连写的状况 bindgetphonenumber
。固然这也不是最严重的,你能够说事件绑定的规范就是 bind + 事件名
,而其余属性的规范就是中划线分割单词,我一度觉得小程序就是这个做为标准,直到我看到了 <progress />
组件github
这和说好的不同啊喂!web
一样的状况也出如今 页面
与 组件
的生命周期方法中,页面
的生命周期方法有 onLoad
、onReady
、onUnload
等,但到了 组件
中则是 created
、attached
、ready
等,这样规范又不统一了,为啥 页面
的生命周期方法是 on+Xxx
的格式,但到了 组件
里缺不同了呢,有点费解。
小程序官方提供了 微信开发工具
做为开发编译工具,而对于代码自己没有提供一个相似 webpack
的工程化开发工具,来解决开发中的一些问题,因此小程序原生的开发方式显得不那么现代化,这也是不少小程序开发框架致力于解决的问题。例如,在小程序开发中
npm
管理依赖,在小程序中须要手动把第三方代码文件下载到本地,而后再 reuqire
进行使用,显得不那么优雅以上就是从开发者的角度看到的一些小程序的开发问题,不过纵然有千般困难,咱们总要面对,做为新时代的前端开发工程师,咱们不能一味忍受问题,要保持技术的头脑,以技术做为武器,用技术手段去提高的咱们开发体验。
目前前端界言及前端框架,必离不开依然保持着统治地位的 React
与 Vue
,这两个都是很是优秀的前端 UI 框架,并且在网上也常常能看到两个框架的粉丝之间热情交流,碰撞出一些思想火花,显得社区异常活跃。
而咱们团队也在去年勇敢地抛弃了历史包袱,很是荣幸地引入了 React
开发方式,让咱们团队丢掉了煤油灯,开始通上了电。并且也研发出了一款优秀的类 React
框架 Nerv
,让咱们和 React
开发思想结合得更深。
与小程序的开发方式相比,React
明显显得更加现代化、规范化,并且 React
天生组件化更适合咱们的业务开发,JSX
也比字符串模板有更强的表现力。那么这时候咱们就在思考,咱们能不能用 React
来写小程序?
经过对比体验 小程序和 React
,咱们仍是能发现二者之间类似的地方
小程序的生命周期和 React
的生命周期,在很大程度上是相似的,咱们甚至能找到他们之间的对应关系
app 及页面的生命周期
小程序 | React |
---|---|
onLaunch | componentWillMount |
onLoad | componentWillMount |
onReady | componentDidMount |
onShow | 不支持,须要特殊处理 |
onHide | 不支持,须要特殊处理 |
onUnload | componentWillUnmount |
能够看出,对于 app
及 页面
来讲,除了 onShow
与 onHide
两个方法,其余方法都能在 React
中找到对应。
在 React
中,组件的内部数据是用 state
来进行管理的,而在小程序中组件的内部数据都是用 data
来进行管理,二者具备必定类似性。而同时在 React
中,咱们更新数据使用的是 setState
方法,传入新的数据或者生成新数据的函数,从而更新相应视图。在小程序中,则对应的有 setData
方法,传入新的数据,从而更新视图。
二者都是以数据驱动视图的方式进行更新,并且 api
神似。
小程序中绑定事件使用的是 bind + 事件名
的方式,例如点击事件,小程序中是 bindtap
<view bindtap="handlClick">1</view> 复制代码
而在 React
里,则是 on + 事件名
的方式,例如点击事件, React
web 中是 onClick
<View onClick={this.handlClick}>1</View>
复制代码
虽然看上去不同,但实际上是能够类比的,咱们只须要在编译时将 on + 事件名
的形式编译成 bind + 事件名
的形式就能够了。
如此看来,二者之间有些类似,用
React
来写小程序貌似是可行的,但接下来咱们就发现了巨大的差别。
React
与小程序之间最大的差别就是他们的模板了,在 React
中,是使用 JSX
来做为组件的模板的,而小程序则与 Vue
同样,是使用字符串模板的。这样二者之间就有着巨大的差别了。
JSX
render () { return ( <View className='index'> {this.state.list.map((item, idx) => ( <View key={idx}>{item}</View> ))} <Button onClick={this.goto}>走你</Button> </View> ) } 复制代码
小程序模板
<view class="index"> <view wx:key={idx} wx:for="{{list}}" wx:for-item="item" wx:for-index="idx">{{item}}</view> <view bindtap="goto">走你</view> </view> 复制代码
众所周知,JSX
其实本质上就是 JS
,咱们能够在里面写任意的逻辑代码,这样一来就比字符串模板的表现力与操做性要强多了,何况,小程序的字符串模板功能比较羸弱,只有一些比较基本的功能。那这样的话,要如何来实现用 JSX
来写小程序模板呢。
咱们能够仔细来分析咱们的需求,咱们指望使用 JSX
来书写小程序模板,但小程序显然是不支持执行 JSX
代码的(要是支持的话,Taro 应该也就不存在了吧),咱们也不能指望微信能给咱们开个后门来跑 JSX
。那么这个时候咱们就想,咱们要是可以将 JSX
编译成小程序模板就行了。
事实上在咱们平时的开发中,这种编译的操做处处可见,babel
就是咱们最经常使用的 JS 代码编译器
,通常浏览器是不能支持一些很是新的语法特性的,但咱们又想使用它们,这个时候就能够借助 babel
来将咱们的高版本的 ES 代码,编译成浏览器能够运行的 ES 代码。而咱们像要将 JSX
编译成小程序模板,也是一样的道理。咱们首先来了解一下 Babel
的运行机制。
Babel
做为一个 代码编译器
,可以将 ES6/7/8 的代码编译成 ES5 的代码,其核心利用的就是计算中很是基础的编译原理知识,将输入语言代码,经过编译器执行,输出目标语言的代码。编译原理的通常过程就是,输入源程序,通过词法分析、语法分析,构造出语法树,再通过语义分析,理解程序正确与否,再对语法树作出须要的操做与优化,最终生成目标代码。
Babel
的编译过程亦是如此,主要包含三个阶段
babel
的配置文件 .babelrc
中定义的 preset
、 plugin
就是在这一步中执行并改变 AST 的为了更好地理解这些过程,你们能够利用 Ast Explorer 这个网站接一下本身的代码,感觉一下每一部分代码所对应的 AST 结构。
能够看到,一份源码通过编译器解析后,会变成相似以下的结构
{ type: "Program", start: 0, end: 78, loc: { start, end } sourceType: "module", body: [ { type: "VariableDeclaration", ... }, { type: "VariableDeclaration", ... }, { type: "FunctionDeclaration", ... }, { type: "ExpressionStatement", ... } ] ... } 复制代码
其中,body
里包含的就是咱们示例代码的语法树结构,第一个 VariableDeclaration
对应的是 const a = 1
,第三个 FunctionDeclaration
对应的则是 function sum (a, b) { }
,分别就是 JS 中的变量定义与函数定义,每个树节点里都会包含许多子节点,这样就造成了一个树形结构,更多的节点类型,请参考 babel types。
固然咱们在这儿只是简单介绍下编译原理与 babel
,编译原理是一门很是深奥的课程, babel
也是一个很是优秀的工具,但愿在后续的文章中能和你们再详细探讨这一部份内容。
再次回到咱们的需求,将 JSX
编译成小程序模板,很是幸运的是 babel
的核心编译器 babylon
是支持对 JSX
语法的解析的,咱们能够直接利用它来帮咱们构造 AST,而咱们须要专一的核心就是如何对 AST 进行转换操做,得出咱们须要的新 AST,再将新 AST 进行递归遍历,生成小程序的模板。
JSX
代码
<View className='index'>
<Button className='add_btn' onClick={this.props.add}>+</Button>
<Button className='dec_btn' onClick={this.props.dec}>-</Button>
<Button className='dec_btn' onClick={this.props.asyncAdd}>async</Button>
<View>{this.props.counter.num}</View>
<A />
<Button onClick={this.goto}>走你</Button>
<Image src={sd} />
</View>
复制代码
编译生成小程序模板
<import src="../../components/A/A.wxml" /> <block> <view class="index"> <button class="add_btn" bindtap="add">+</button> <button class="dec_btn" bindtap="dec">-</button> <button class="dec_btn" bindtap="asyncAdd">async</button> <view>{{counter.num}}</view> <template is="A" data="{{...?A}}"></template> <button bindtap="goto">走你</button> <image src="{{sd}}" /> </view> </block> 复制代码
这时候,聪明的你应该就能发现问题的难点所在了,要知道小程序的模板只是字符串,而 JSX
则是真正的 JS 代码扩展,其语法之丰富,显然不是字符串模板所能比,在这一步中,咱们要作的操做,包括但不只限于以下
abc ? : <View>1</View> : <View>2</View>
须要编译成 <view wx:if="{{abc}}">1</view><view wx:else>2</view>
map
语法,例如 map
的使用 abc.map(item => <View>item</View>)
须要编译成 <view wx:for="{{abc}}" wx:for-item="item">item</view>
以上仅仅是咱们转换规则的冰山一角,JSX
的写法极其灵活多变,咱们只能经过穷举的方式,将经常使用的、React 官方推荐的写法做为转换规则加以支持,而一些比较生僻的,或者是不那么推荐的写的写法则不作支持,转而以 eslint
插件的方式,提示用户进行修改。目前咱们支持的 JSX
转换规则,大体能覆盖到 JSX
80% 的写法操做。
关于 JSX 转小程序模板这一部分,咱们将在后续的技术原理分析系列文章中,详细为你们介绍。
通过咱们一次次的探索,以及一波波猛如虎的操做,咱们已经能够将类 React 代码转成小程序能够跑的代码了,也就是说咱们已经能够正式以 React 的方式来写小程序的代码了。喜大普奔!可是咱们激动之余,冷静下来继续思考,咱们还能不能干点别的有意思的事情呢。
咱们发现,在日常的工做中,咱们业务一般有一些多端的需求,就是要求小程序要有,H5 要有,甚至 RN 也能有就最好了,我猜产品经理还看不上快应用,否则确定要求咱们快应用也上一套吧,反正大家不是常常号称代码优秀、高度可复用么。这个时候,你就会发现,差很少的界面和逻辑,你可能须要重复写上好几轮,这时候要是有个多端代码生成工具就行了,只写一份代码,能够多端运行。Write once, run anywhere,相信是全部工程师的梦想。
这时候咱们回忆一下前文的内容,将一份代码编译成多端代码,这不正是编译原理干的事么,咱们能够输入一份源代码,针对不一样的端设定好对应的转换规则,再一键转换出对应端的代码。并且因为咱们已经遵循 React 语法了,那咱们再转成 H5 端(使用 Nerv)与 RN 端(使用 React)也就有了自然的优点。
可是仔细思考咱们又会发现,仅仅将代码按照对应语法规则转换过去后,还远远不够,由于不一样端会有本身的原生组件,端能力 API 等等,代码直接转换过去后,可能不能直接执行。例如,小程序中普通的容器组件用的是 <view />
,而在 H5 中则是 <div />
;小程序中提供了丰富的端能力 API,例如网络请求、文件下载、数据缓存等,而在 H5 中对应功能的 API 则不一致。
因此,为了弥补不一样端的差别,咱们须要订制好一个统一的组件库标准,以及统一的 API 标准,在不一样的端依靠它们的语法与能力去实现这个组件库与 API,同时还要为不一样的端编写相应的运行时框架,负责初始化等等操做。经过以上这些操做,咱们就能实现一份一键生成多端的需求了。在 Taro 最初的设计中,咱们组件库与 API 的标准就是源自小程序的,由于咱们以为既然已经有定义好的组件库与 API 标准,那为啥不直接拿来使用呢,这样不只省去了定制标准的左思右想,同时也省去了为小程序开发组件库与 API 的麻烦,只须要让其余端来向小程序靠齐就好。
可能有些人会有疑问,既然是为不一样的端实现了对应的组件库与端能力 API (小程序除外,由于组件库和 API 的标准都是源自小程序),那么是怎么可以只写一份代码就够了呢?由于咱们有编译的操做,在书写代码的时候,只须要引入标准组件库 @tarojs/components
与运行时框架 @tarojs/taro
,代码通过编译以后,会变成对应端所须要的库。
既然组件库以及端能力都是依靠不一样的端作不一样实现来抹平差别,那么一样的,若是咱们想为 Taro 引入更多的功能支持的话,有时候也须要按照这个套路来。例如,为了提高开发便利性,咱们为 Taro 加入了 Redux
支持,咱们的作法就是,在小程序端,咱们实现了 @tarojs/redux
这个库来做为小程序的 Redux
辅助库,而且以他做为基准库,它具备和 react-redux
一致的 API,在书写代码的时候,引用的都是 @tarojs/redux
,通过编译后,在 H5 端会替换成 nerv-redux
(Nerv
的 Redux
辅助库),在 RN 端会替换成 react-redux
。这样就实现了 Redux
在 Taro 中的多端支持。
以上就是 Taro 的总体设计思路,里面还有不少细节没有展开去阐述,可能你们会以为有些意犹未尽,后续咱们将会产出一系列的文章来阐述 Taro 的技术细节,例如 《Taro 开发工具原理分析》、《Taro 代码编译的背后》、《深刻浅出 JSX 转小程序模板》等等。
Taro 从立项之初到如今已经差很少有了三个月左右的时间,从最初的激烈讨论方案,各类思想的碰撞,到方案逐渐成型,进入火热的开发迭代,再到如今的小程序端和 H5 端顺利支持,从而决定走向开源。这一路走来,收获颇丰,既有跟团队小伙伴一块儿创造的激动,也有无数个日夜加班的苦思。Taro 是凹凸实验室的诚意之做,咱们也将会一直维护下去,但愿 Taro 能愈来愈好,帮助更多人创造更多价值。
项目官网:taro.aotu.io/
项目 GitHub:github.com/NervJS/taro