我是歌谣 放弃很容易 但是坚持一定很酷 微信公众号关注前端小歌谣获取前端知识
本文将介绍 Taro 是如何从一个为了解决小程序开发问题的框架演变成一个多端统一开发框架的。
使用 React 语法来写小程序
谈一谈小程序开发
微信小程序为我们的业务提供了一种新的展现形态,但对于开发者来说,开发体验则显得并不那么友好。
首先,从文件组织上看,一个小程序页面或组件,需要同时包含 4 个文件:脚本逻辑、样式、模板以及配置文件,在开发一个功能模块时,就需要在 4 个文件之间切换,而当功能模块多的话,就需要在多个文件之间切换,这样显然非常浪费时间。
其次,从开发方式上看,在前端工程化思想深入人心的今天,小程序的种种开发方式显得有些落后了,主要体现在以下几个方面:
- 没有自定义文件预处理,无法直接使用 Sass、Less 以及较新的 ES.Next 语法;
- 字符串模板太过孱弱,小程序的字符串模板仿的是 Vue,但是没有提供 Vue 那么多的语法糖,当实现一些比较复杂的处理时,写起来就非常麻烦,虽然提供了
wxs
作为补充,但是使用体验还是非常糟糕;
- 缺乏测试套件,无法编写测试代码来保证项目质量,也就不能进行持续集成,自动化打包。
所以,从开发方式上看,小程序开发没有融入目前主流的工程化开发思想,很多业界开发模式与工具没有在小程序开发中得到相应体现,像是从前端工业时代回退到了刀耕火种的年代。
最后,从代码规范上看,小程序的规范有很多不统一的地方,例如内置组件的属性名,有时候是全小写,有时候是 CamelCase
格式,有时候又是中划线分割的形式,这样就导致编码的时候得不时查阅文档才能确定写法。
如何更优雅地开发小程序
在 Taro 的设计之初,我们的想法就是希望能够以一种更加优雅的方式来开发小程序,解决小程序开发上的种种痛点,首先我们希望能使用前端工程化的方式来进行开发,同时在语法上,我们希望能抛弃小程序的四不像语法,遵循一套我们熟悉的框架语法来进行开发,这样不仅能更好地保证开发质量、提升开发体验,同时也能大大降低开发者开发小程序的成本。
于是,在开发方式上,Taro 打造了一套完善编译工具,引入了前置编译的机制,可以自动化地对源文件进行一系列的处理,最终输出小程序上的可执行文件,包括代码的编译转换处理,加入文件预处理功能,支持 NPM 包管理等等,这一部分的原理,将会在后续章节中为大家介绍。而语法标准上,我们把目光投向了市面上流行的三大前端框架。
React、Vue、Angular 是目前前端框架三巨头,他们各有各的风格,关于他们的优劣,在业界也是一直争论不休,这本身也是智者见智仁者见仁的事,所以在本文中就不再评述。Taro 最终采用的是 React 语法来作为自己的语法标准,主要有以下几点考虑:
- React 是一个非常流行的框架,也有广大的受众,使用它也能降低小程序开发的学习成本;
- 小程序的数据驱动模板更新的思想与实现机制,与 React 类似;
- React 采用 JSX 作为自身模板,JSX 相比字符串模板来说更自由,更自然,更具表现力,不需要依赖字符串模板的各种语法糖,也能完成复杂的处理
- React 本身有跨端的实现方案 - React Native,并且非常成熟,社区活跃,对于 Taro 来说有更多的多端开发可能性。
最终,Taro 采用了 React 语法来作为自己的语法标准,配合前端工程化的思想,为小程序开发打造了更加优雅的开发体验。
如何实现优雅
那么如何实现使用 React 来开发小程序呢?在 Taro 中采用的是编译原理的思想,所谓编译原理,就是一个对输入的源代码进行语法分析,语法树构建,随后对语法树进行转换操作再解析生成目标代码的过程。
探索多端可能性
多端统一开发一直是所有开发人员的共同追求。在终端碎片化的大背景下,前有 Hybrid 模式拉开序幕,后有 React Native、Weex 风起云涌,再到如今 Flutter 横空出世,种种这些都是为了能够 Write once, run anywhere
。给每一种终端单独进行开发的成本是昂贵的,所以一个能够尽可能抹平多端开发差异的开发解决方案就显得极为重要。
多端转换原理
开发时我们遵循 React 语法标准,结合编译原理的思想,对代码文件进行一系列转换操作,最终获得可以在小程序运行的代码。而 React 最开始就是为了解决 Web 开发而生的,所以对代码稍加改动,也可以直接生成在 Web 端运行的代码,而同属 React 语法体系下的 React Native,也能够很便捷地提供支持。同理其他平台,如快应用、百度小程序等,将源码进行编译转换操作,也能获得该平台下的对应语法代码。
抹平多端差异
基于编译原理,我们已经可以将 Taro 源码编译成不同端上可以运行的代码了,但是这对于实现多端开发还是远远不够。因为不同的平台都有自己的特性,每一个平台都不尽相同,这些差异主要体现在不同的组件标准与不同的 API 标准以及不同的运行机制上。
可以看出小程序和 Web 端上组件标准与 API 标准有很大差异,这些差异仅仅通过代码编译手段是无法抹平的,例如你不能直接在编译时将小程序的 直接编译成
这一套标准主要以三个部分组成,包括标准运行时框架、标准基础组件库、标准端能力 API,其中运行时框架和 API 对应 @taro/taro,组件库对应 @tarojs/components,通过在不同端实现这些标准,从而达到去差异化的目的。
而在标准的定制上,起初我们想重新定制一套标准规范,但是发现在所有端都得实现这套标准的成本太高,所以我们就思考为什么不以一个端的组件库、API 为标准呢?这样不仅省去了标准定制的时间,而且在这个端上我们可以不用去实现这套标准了。最终在所有端中,我们挑选了微信小程序的组件库和 API 来作为 Taro 的运行时标准,因为微信小程序的文档非常完善,而且组件与 API 也是非常丰富,同时最重要的是,百度小程序以及支付宝小程序都是遵循的微信小程序的标准,这样一来,Taro 在实现这两个平台的转换上成本就大大降低了。
运行时
本文介绍的是 Taro 在编译成小程序和 H5 运行时发生了什么
JSX 转换微信小程序模板的实现
以这段代码为例:
import Taro, { Component } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'class Home extends Component {config = {navigationBarTitleText: '首页'}state = {numbers: [1, 2, 3, 4, 5]}handleClick = () => {this.props.onTest()}render () {const oddNumbers = this.state.numbers.filter(number => number & 2)return (<ScrollView className='home' scrollTop={false}>奇数:{oddNumbers.map(number => <Text
onClick={this.handleClick}>{number}</Text>)}偶数:{numbers.map(number => number % 2 === 0 &&
<Text onClick={this.handleClick}>{number}</Text>)}</ScrollView>)}
}
Taro 的结构主要分两个方面:运行时和编译时。运行时负责把编译后到代码运行在本不能运行的对应环境中,你可以把 Taro 运行时理解为前端开发当中 polyfill
。举例来说,小程序新建一个页面是使用 Page
方法传入一个字面量对象,并不支持使用类。如果全部依赖编译时的话,那么我们要做到事情大概就是把类转化成对象,把 state
变为 data
,把生命周期例如 componentDidMount
转化成 onReady
,把事件由可能的类函数(Class method)和类属性函数(Class property function) 转化成字面量对象方法(Object property function)等等。
但这显然会让我们的编译时工作变得非常繁重,在一个类异常复杂时出错的概率也会变高。但我们有更好的办法:实现一个 createPage
方法,接受一个类作为参数,返回一个小程序 Page
方法所需要的字面量对象。这样不仅简化了编译时的工作,我们还可以在 createPage
对编译时产出的类做各种操作和优化。通过运行时把工作分离了之后,再编译时我们只需要在文件底部加上一行代码 Page(createPage(componentName))
即可。
如果你是从 Taro CLI
的 dist
文件夹看编译后的代码会发现它相当复杂,那是因为代码会再经过 babel
编译为 ES5。
除了 Page
类型之外,小程序还有 Component
类型,所以 Taro 其实还有 createComponent
方法。由于 Component
在小程序里是全局变量,因此我们还得把 import { Component } from '@tarojs/taro'
的 Component
重命名。
回到一开始那段代码,我们定义了一个类属性 config
,config
是一个对象表达式(Object Expression),这个对象表达式只接受键值为标识符(Identifier)或字符串,而键名只能是基本类型。这样简单的情况我们只需要把这个对象表达式转换为 JSON 即可。另外一个类属性 state
在 Page
当中有点像是小程序的 data
,但它在多数情况不是完整的 data
(下文会继续讨论data
)。这里我们不用做过多的操作,babel
的插件 transform-class-proerties
会把它编译到类的构造器中。函数 handleClick
我们交给运行时处理,有兴趣的同学可以跳到 Taro 运行时原理查看具体技术细节。
再来看我们的 render()
函数,它的第一行代码通过 filter
把数字数组的所有偶数项都过滤掉,真正用来循环的是 oddNumbers
,而 oddNumbers
并没有在 this.state
中,所以我们必须手动把它加入到 this.state
。和 React 一样,Taro 每次更新都会调用 render
函数,但和 React 不同的是,React 的 render
是一个创建虚拟 DOM 的方法,而 Taro 的 render
会被重命名为 _createData
,它是一个创建数据的方法:在 JSX 使用过的数据都在这里被创建最后放到小程序 Page
或 Component
工厂方法中的 data
。最终我们的 render
方法会被编译为:
_createData() {this.__state = arguments[0] || this.state || {};this.__props = arguments[1] || this.props || {};const oddNumbers = this.__state.numbers.filter(number => number & 2);Object.assign(this.__state, {oddNumbers: oddNumbers});return this.__state;
}
开发指导
- 引入目录时如果不是在本模块文件下的不要使用相对路径,要使用绝对路径
如果是js 文件的引入,是和mgr 项目一样的,@/ 代表根目录 如果是 scss 文件的引入,styles 文件也做了 alias,引入的时候 `@import "~@/styles/variables.scss"`
- scss写的时候先引入变量文件,颜色字体统一使用定义好的。必选强制看一遍
/variables.scss
文件。
我是歌谣 放弃很容易 但是坚持一定很酷