前端开发是离用户最近的工程领域,需要在开发时间和体验上不断作出选择和权衡, 就像著名的论断 “php是最好的计算机语言"一样, js也能依靠(node, react native)一统天下. 我们都想要一个统一的框架搞定一切.
而目前的情况是即便是同一个app的界面, 我们也在糅合这些不同的框架, 用来快速迭代,适应变化。最近抽了点时间把app开发领域人气比较高的框架凑到了一块而,对比体会了一下,其间也有一些小的收获。
缘起reactive
首先对于这些框架解决的根本问题, 我特意查了下他们官网的简介, flutter我想将它称之为reactive app, 加上react native和native 从三者的名字上看就知到reactive有多重要了. 这些年reactive面向数据流的开发趋向一直是主流, 而react 开发思想最早是从microsoft的c#语言中涌现出来的, dart和typescript是后面我将会使用的2种语言, 它们都借鉴了c#语言的优秀设计思想.
废话不多说, 这次实验实现了实时搜索并动态展示图片的简单单页面app, 先看下这次实现的app在平台上最终的效果, 没有UI设计,没有适配(轻喷)
解释下这个单页面app实现的原则, 在使用reactive开发模式的前提下, 除了必须的平台上基本的http框架,本次实现尽量或者避免使用第三方库.
native平台
首先是native 平台, kotlin实现, 使用了recyclerview+okhttp, 没有用到任何图片加载框架
虽然recyclerview为我们的gridview item提供了回收机制, 图片和view的绑定及其生命周期的处理都需要格外注意:
(search as EditText).addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
val url = "https://anime-pictures.net/pictures/view_posts/0?&lang=zh_CN&search_tag=${s.toString()}"
client.newCall(Request.Builder().get().url(url).build())
.enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
var html = response.body?.string()
println(html)
imageList.clear()
imageList.addAll(Regex("img_cp[\\s\\S]+?src=\"//([\\s\\S]+?)\"").findAll(
html.toString()
)
.map {
it.groupValues[1]
}
.toList())
println(imageList)
runOnUiThread { grid.adapter?.notifyDataSetChanged() }
}
})
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
}
class GalleryAdapter(
private val imageList: LinkedList<String>,
private val client: OkHttpClient
) : RecyclerView.Adapter<GalleryAdapter.GalleryViewHolder>() {
private var loadings: HashMap<String, Bitmap?> = HashMap()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryViewHolder {
val item : View = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
registerAdapterDataObserver(object: AdapterDataObserver(){
override fun onChanged() {
loadings = HashMap()
}
})
return GalleryViewHolder(item)
}
val url = imageList[position]
holder.itemView.image.tag = url
val imageRef : WeakReference<ImageView> = WeakReference(holder.itemView.image)
if(loadings[url] != null){
holder.itemView.image.setImageBitmap(loadings[url])
}else {
client.newCall(Request.Builder().get().url("https://$url").build())
.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
val image = imageRef.get()
println("done:$url")
if(image != null && image.tag == url) {
val input: InputStream? = response.body?.byteStream()
if(input != null) {
val bitmap: Bitmap =
BitmapFactory.decodeStream(input)
input.closeQuietly()
loadings[url] = bitmap
holder.itemView.post {
holder.itemView.image.setImageBitmap(bitmap)
}
}
}else if(image != null && image.tag != url){
//imageview reused
if(loadings[image.tag] != null){
holder.itemView.post {
holder.itemView.image.setImageBitmap(loadings[image.tag])
}
}
}
}
})
这里面展示了其中整个app的核心代码逻辑, 由于没有使用图片加载框架, bitmap存到了一个临时的hashmap中和url绑定, 由于设置图片时时完全异步的, 需要对imageView的回收和复用状态做出判断,这里使用了weakreference和image.tag数据绑定作判断。要写一个做的好的话还要实现threadpool task cancel和memory diskcache等操作, 这里略去n行代码。 native app实现大约140行,如果引入glide等图片框架代码减少到120行左右,但如果加上layout xml代码40行,考虑都是可见即所得的设计算作10行kotlin的话计作 150行, native不出意料应该代码最多,且往后看。
flutter框架
flutter平台的app实现最终如下,
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'reactive'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<String> images = [];
int loadings = 0;
DateTime nows = DateTime.now();
get serviceList => () {
return images.map((url) => watch_image(url)).toList();
};
Image watch_image(String url) {
NetworkImage image = NetworkImage("https://" + url);
image
.resolve(new ImageConfiguration())
.addListener(ImageStreamListener(_handleResolve));
return Image(
image: image,
height: 150.0,
);
}
void _handleResolve(ImageInfo image, bool synchronousCall) {
print(loadings);
if (mounted && loadings > 0) {
loadings = loadings - 1;
if (loadings == 0) {
print("addPostFrameCallback be invoke:" +
(DateTime.now().second - nows.second).toString());
}
}
}
get onchange => (str) {
http
.get(
"https://anime-pictures.net/pictures/view_posts/0?order_by=views&ldate=0&lang=zh_CN&search_tag=" +
str) //TODO
.then((response) {
return RegExp(r'img_cp[\s\S]+?src="\/\/([\s\S]+?)"', multiLine: true)
.allMatches(response.body)
.map((mt) => mt.group(1))
.toList();
}).then((List<String> urls) {
print(urls);
_updateWheel(urls);
});
};
void _updateWheel(List<String> urls) {
setState(() {
images.clear();
images.addAll(urls);
loadings = urls.length;
nows = DateTime.now();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Center(
child: TextField(
autofocus: true,
onChanged: onchange,
maxLines: 1,
)),
Flexible(
flex: 1,
child: GridView.count(
crossAxisCount: 2,
padding: EdgeInsets.symmetric(vertical: 0),
children: serviceList()),
)
]),
);
}
}
需要注意的时, 由于是实时搜索动态刷新, 需要用到stateful widget, flutter提供的 networkimage 控件相当实用, 既简化了native中图片加载的各种缓存,又默认了最佳的display options实现起来相当高效, 其正则表达式的语法和kotlin非常相像,这里抓取的是https://anime-pictures.net主页的动漫图片, 正则规定了首尾的token, 最终截取group index是1的字符串. Flutter框架整个app算上import 总共110行左右, 真实体验是代码少了,自己需要考虑的逻辑也少了不少。
react native框架
最后是最让大家感觉是个谜团的框架, 从实现机制上来说,reactnative还是需要把js的绘制指令翻译成native对应的指令才可以实现逻辑, 而flutter的reactive app是生成了gpu和cpu相关的机器绘制指令,在ui渲染这块应该很占优势才对, 下面。。。
import React, {Component} from 'react';
import {FlatList, StyleSheet, Text, View, Image, TextInput} from 'react-native';
export default function App() {
return (<SearchGridComponent/>)
}
interface IProps {
url: String
}
class SearchGridComponent extends Component{
constructor(props) {
super(props);
this.state = {
dataArray: []
}
}
onChangeText = (text) => {
fetch("https://anime-pictures.net/pictures/view_posts/0?order_by=views&ldate=0&lang=zh_CN&search_tag=" + text,
{
method: "get",
headers: {
"mode": "no-cors"
}
})
.then((response) => response.text())
.then((text) => {
console.log("seach result:" + text)
var items = RegExp("img_cp[\\s\\S]+?src=\"//([\\s\\S]+?)\"", "gi")
[Symbol.match](text)
.flatMap((item) =>
{
return {url: "https:" + (item.substring(item.indexOf("//"), item.lastIndexOf("\"")))}
})
this.setState({dataArray:items})
})
.catch((error) => {
console.warn(error);
});
}
render(){
console.log(this.state["dataArray"])
return (
<View>
<Text style={styles.texts}>React Native</Text>
<TextInput onChangeText={this.onChangeText}
style={styles.inputs}/>
<FlatList
data={this.state["dataArray"]}
extraData={this.state}
numColumns ={2}
keyExtractor={(item, index) => item.url}
renderItem={({item})=><ImageList url={item.url}/>}
/>
</View>
)
}
}
class ImageList extends Component<IProps>{
constructor(props) {
super(props);
}
render() {
return (
<Image source={{uri:this.props.url}} style={styles.img}/>
)
}
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
padding: 5,
},
texts:{
width: "100%",
height: 48,
textAlign:'center',
alignItems:'center',
justifyContent:'center',
textAlignVertical:'center',
},
inputs:{
width: "100%",
height: 40,
backgroundColor: "#f1f2f3"
},
img: {
width: "50%",
height: 180,
},
})
刚好到100行! 我没有特意优化什么, 都是用react的思想去实现动态交互式布局的, 如果用js实现应该比这个还少,向component的props typescript还需要定义接口, 不过这个也让代码自动化和补全有了想象空间, 论ui实现的开发效率, typescript/js 略胜flutter一筹, 10%吧 ;)其实,不过这里我要吐槽一下:
- 环境配置方面:
reactnative 对我这个js生手来说语法啥的到容易,环境搭建,兼容性问题委实费了我一些功夫, 首先是js debug时的跨域问题, cross-origin( 这个是浏览器对运行在其页面的js限制, 在真机上debug时还是需要开代理才能绕过)
- 框架文档一致性:
然后是reactnative expo插件, 这个在debug时是没事的, 我用的官方给的示例demo, 最后原生编译时androidx的库都找不到, 最终是通过jetifier代码反射,修改解决的, npm生态下的安装包和插件也是够全的, 官方demo更新不积极, 但是community的热情还是不错的, 对比而言flutter在文档上比reactnative厉害一些。
- 框架api方面:
这个我没有深入去研究, 其中一个比较坑的地方就是flat component更新界面需要state + extraData同时设置才可以, 另外js的正则还是很弱, 没有extract group的功能,这里只能字符串处理来解决。虽然作为js的超集,typescript/js的语言函数api虽然繁多,但是设计和实用性还是有待考量的。
- 历史遗留问题:
不同平台底层兼容性和相对为人诟病的内存处理机制(特别是复杂项目)
各方面对比一览:
1.开发效率/代码行数:
native(kotlin): flutter: reactnative(typescript)
150: ~110: <100
2.文档一致性(严谨自洽):
native(kotlin)≥ flutter > reactnative(typescript)
3.平均使用内存:
由于每个平台对图片作了自己的缓存和优化策略, 所以直接比较不合理, 我们从系统资源使用的角度去考量用户的UI使用体验, 这里考虑平均使用内存:
native(2.8) << flutter(9.1) ~≥ reactnative(8.8)
4. 使用体验,性能方面
由于动图太大,就不上了。 每个网络加载和缓存默认策略不同, reactnative默认加载平滑一些, flutter默认图片加载的先后顺序有些慢, native默认网络加载偏慢,这块可以有极大的优化空间.
总结
综合以上各方面,以上只是实验性的检验, 具体机器上可能会有差异, 在具体开发的时候需要根据具体场景做出判断, native在资源使用上还是有无可比拟的优势, 开发效率上就另说了。 reactnative坑多,但是开发总体简单, 在追赶reactnative开发体验的同时, flutter平台对硬件性能的压榨又更进一步了。