这段时间开发了一个公司内部使用的网站,本着提前探索熟悉(踩坑)未来的全栈UI框架-Flutter的愿望,使用了Flutter for Web作为前端框架,后台部分则循规蹈矩的用了java Spring。目前已经开发完成,在此简要记录一下开发过程中积累的一些经验。
突然发现,我现在不仅会android,又会java后端,又能用flutter写web+android+ios代码,一下子变成了全栈工程师喽!
Flutter的官方入门文档是 https://flutter.dev/docs/get-started/install
现在有中文版可以看,中文链接是: https://flutterchina.club/setup-macos/
Flutter的Github地址是: https://github.com/flutter/flutter
Stack OverFlow上flutter分类地址: https://stackoverflow.com/questions/tagged/flutter
为 Java 开发人员准备的 Dart 教程: https://codelabs.flutter-io.cn/codelabs/from-java-to-dart-cn/index.html#0
很多时候遇到问题,去github的issue里搜一下,都能找到解决方案。在我的开发过程中,大多数问题都是通过翻issue来找到解决方案的,少部分能在Stack Overflow上找到。这两个地方都找不到的时候,一般就代表着该问题没有现成的解决方案,需要自己翻源码排查了。
官网上Flutter的安装方式写了很长一页,但是实际上完全不需要这么麻烦。现在Flutter的安装已经非常简单了。
只需要打开Android Studio,在Preferences-plugins里找到flutter安装即可。
安装好后重启ide,选择创建一个Flutter工程,然后在创建过程中就会提示你下载Flutter Sdk,按照提示一步步下载安装即可。
安装Flutter开发环境,就随着创建第一个Flutter工程的同时自动完成了。
Flutter for Web 和 移动端的Flutter 现在没有在一个仓库里。而是从移动端Flutter仓库分裂出来,并且做了部分修改。
Flutter_web地址: https://github.com/flutter/flutter_web
Flutter web工程的创建同样非常简单,首先要建议下载Intellij idea,然后与Android studio一样,安装flutter插件。
新建一个project,类型选择Dart->Flutter Web App,上面的Dart SDK path路径选择flutter sdk目录下flutter/bin/cache/dart-sdk目录,然后按提示输入工程名即可。
如果不是新建项目,而是引入已经创建好的项目,则连intelij idea都不用下载,直接使用android studio打开已经创建好的项目就会自动识别为flutter_web工程,全自动!
到目前为止,flutter_web项目就已经能正常运行了。直接点这个按钮就能跑起来了。
另外建议安装flutter_web github上的readme所说,安装webdev
。
只需要一行命令flutter pub global activate webdev
即可安装。
运行这条命令,需要先把flutter命令放到环境变量path里,到安装的flutter sdk目录下,把相应的地址写到path内即可。
然后再把$HOME/.pub-cache/bin目录也放到环境变量path里,就能在任意目录的命令行下使用webdev
了。
默认情况下,开发中使用的都是用于开发模式下的编译器Dart Dev Compiler,它专为快速,增量编译和简单调试而设计。
而release包是应该使用Flutter_web的发布编译器 dart2js的。release模式下打出来的包,性能更快、兼容性更好、包大小也小。
编译release包需要先按上一步安装好webdev
命令。
然后运行webdev build
命令即可。
这时会在build目录下,生成对应的html,js等文件。
如果只是想本地测试下release模式下的代码运行情况,直接运行webdev serve -r
即可。
pubspec.yaml
文件写的对不对,是不是有语法错误或者依赖冲突。修改完pubspec.yaml
IDE一般会自动提示要packages get的,这一步相当于gradle的sync,点一下就好了。如果没有提示的话,可以手动在工程目录下执行flutter packages get
命令。pub run build_runner clean
。虽说Flutter for web与Flutter for Android、IOS并不在一个git仓库里,略有不同。但是基本语法都是相通的。
最大的不同有两部分:
import sdk中包的路径不同,Flutter for Android、IOS 中路径是 flutter/xxx
,而Flutter for web中是flutter_web/xxx
.
比如常用的material.dart
包。
在Flutter for Android、IOS 中 这么写:import 'package:flutter/material.dart';
在Flutter for web中这么写:import 'package:flutter_web/material.dart';
部分第三方库,只支持Flutter for Android、IOS,而不支持Flutter for web。
基于以上两点,在没有使用多少第三方库的情况下,Flutter for web项目与Flutter for Android、IOS项目只要批量替换import包中的flutter和flutter_web字符串即可相互迁移。
我测试了写的flutter_web中的部分界面,修改import包中的字符串后,放到手机上,可以完美运行,并且UI上几乎没有区别。
Flutter这种基于Skia重写渲染引擎的模式,确实能实现高度的三端一致性。
具体差异详情可参阅官方文档:
https://github.com/flutter/flutter_web/blob/master/docs/faq.md
https://github.com/flutter/flutter_web/blob/master/docs/migration_guide.md
建议先看一下这篇文章,来熟悉一下Flex布局,这对学习Flutter的布局是很有帮助的。
http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html
学习完Flex之后,就能大概理解常用的几个熟悉的参数:
另外,所有的alignment都是指子控件的摆放方式,对应于android中,就是只有gravity属性,而没有layout_gravity属性。
Scaffold.of(context).showSnackBar(SnackBar(content: const Text("弹出SnackBar")))
:SnackBar控件showDialog( context: context, builder: (context) { return SimpleDialog( children: [ Text("Dialog"), ], ); });
:Dialog弹框需要注意的是,Flutter中没有相对布局,在Android中用相对布局实现的场景,Flutter中可能要找其他方式实现。
对于写UI代码很困难的朋友,也可以尝试下这个拖控件生成flutter代码的在线工具,作为辅助。
https://flutterstudio.app/
Flutter中网络请求库主要有三个:
使用起来非常方便发请求只需要一行代码即可:http.get(url)
。
Response response = await http.get(testUrl);
print(response.body);
对于android和java开发来说,await async关键字比较陌生。但是对于熟悉js和C#的开发者来说,对await async用法可能已经很熟悉了。
await async关键字是一种异步操作的简洁写法,这实际上是一种语法糖。
要明白await async关键字,首先要先看一个Future数据结构,它有一个泛型声明,这个数据结构的对象表示未来某时刻会返回T类型的数据。大家可以把它和RxJava的Observable类比,两者是非常类似的。
比如 这个声明 Future
,就表示这个方法会立即返回一个Future对象,而这个对象未来会返回String类型的一个结果。
这个getUserName方法的调用是可以这么写的:
NetApi.getUserName().then((response) {
print(response);
});
在这种使用方式上,是和RxJava极度类似的。
而await async关键字,在这个基础上,又进一步简化了写法。
使用await关键字,可以这么写。
var response = await NetApi.getUserName();
print(response);
这种写法与上面的实现效果是完全一样的,编译器在编译时对这个语法糖进行降糖解析。
await关键字,可以理解为把当前函数体内,await关键字之后所有的函数调用,都放到了一个隐含的.then(response){ ... }
代码块中。
而当该函数内使用了await关键字后,该函数的返回值,肯定也变成了“未来某时刻才会返回”。这样其他函数调用该函数时也要使用await关键字,或then来使用。dart里规定了,这种使用了await关键字的函数,要在自身声明上标记async关键字,这样其他函数才能明确知道,是否该使用await关键字,或then了。
Flutter中不支持反射,所以java中常用的通过反射来生成json,解析json的方式,全都不能用了。
要在Flutter中使用Json,只能硬编码。
听起来硬编码Json生成、解析,似乎非常恐怖。但是实际上,这种硬编码的代码,都是非常有规律性的,所以完全可以通过工具自动生成。
对于初学一个语言、框架来说,样例代码是重要的学习资料。
Flutter_web给出了几个sample,可做学习之用。
https://flutter.github.io/samples/
对应的源码在这里,https://github.com/flutter/samples/tree/master/web,可以clone下来查看。
其中对各种组件整理的最全的是flutter web gallery
https://flutter.github.io/samples/gallery/
在我的开发过程中,经常会到gallery上找到类似的界面,然后再相应的看其源码学习。
现在Flutter还是一个新生框架,相应的库肯定是没有Java、Android这么全面的,经常有找不到某些控件或者功能的情况。
这个时候一般有这样几种选择:
pubspec.yaml
文件即可引入,非常方便。python
是很像的。platformView
来引入平台组件,对于web来说,平台组件就是html。void main() {
ui.platformViewRegistry.registerViewFactory(
'hello-world-html',
(int viewId) => IFrameElement()
..width = '640'
..height = '360'
..src = 'https://www.youtube.com/embed/IyFZznAk69U'
..style.border = 'none');
runApp(Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 640,
height: 360,
child: HtmlView(viewType: 'hello-world-html'),
),
));
}
这种情况下html和dart的交互要使用EventMessage了,
js方代码
window.parent.postMessage({//发消息
"content": "test"
}, "*");
window.addEventListener("message", function (ev) {//注册消息监听
});
dart方代码
IFrameElement createdView = ui.platformViewRegistry.getCreatedView(viewId);
createdView.contentWindow.postMessage("test", "*");//发消息
window.addEventListener("message", (Event event) {});//注册消息监听
后端部分使用Java Spring开发。Java对于我这样一个Android开发来说,使用起来自然是非常轻松。Spring被使用了这么多年,相关技术早就非常成熟了,几乎可以说是开箱即用。我们并没有遇到多少困难。
使用Spring Initializr配置一下,就可以生成一个基本Spring工程框架,然后在此基础上实现自己要的业务逻辑即可。
在这里,只需要额外选择基本的MyBatis、Spring Web Starter、MySQL Driver即可,其他有需要的框架可以需要的时候再加。
Spring中可以使用注解非常方便的配置各种功能。
写http接口时,也用到了很多注解。
如下面的代码,用很简单的方式,就生成了一个接口。
其中@RestController
表示当前类作为一个rest接口的Controller,@RequestMapping
表示接口的路径,@RequestParam
表示get请求的参数。
@RestController
@RequestMapping("/test/demo")
public class TestController {
@RequestMapping(value = "greet", method = RequestMethod.GET)
Map<String, Object> greet(@RequestParam String name) {
HashMap<String, Object> response = new HashMap<>();
response.put("code", "0");
response.put("msg", "ok");
response.put("body", "hello " + name);
return response;
}
}
上面的实现了常见的http请求,但是还没有数据库相关操作的参与,实际开发中,大多数接口的实际实现,都是数据库的操作。
这里,需要先加上相关配置。
@ImportResource("classpath:applicationContext.xml")
,表示使用applicationContext.xml作为context配置文件。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd">
<context:component-scan base-package="com.example.demo"/>
<import resource="classpath*:application_dao.xml"/>
beans>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://123.123.123.123:3306/dataBaseName?characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="root12345"/>
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*_mapper.xml"/>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.demo"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
bean>
beans>
<mapper namespace="com.example.demo.DemoModelMapper">
<resultMap id="demoModel" type="com.example.demo.demoModel">
<id property="id" column="id"/>
<result property="viewCode" column="view_code"/>
<result property="title" column="title"/>
resultMap>
<sql id="table_name">test_tablesql>
<select id="getDemoModels" resultMap="DemoModel">
select *
from
<include refid="table_name"/>
where
view_code=#{viewCode}
select>
mapper>
做好以上xml配置后,还需要定义Mapper接口类。
@Mapper //表示该类是Mapper类
public interface DemoModelMapper {
List<DemoModel> getDemoModels(String viewCode);
}
之后就可以直接使用该接口操作数据库了,接口的实现,会由Spring框架根据xml配置自动注入。
我们将上面的TestController略微修改,来调用该mapper操作数据库。
@RestController
@RequestMapping("/test/demo")
public class TestController {
@Resource
private DemoModelMapper demoMapper; //不需要手动实例化,Spring会根据注解和配置自动注入
@RequestMapping(value = "greet", method = RequestMethod.GET)
Map<String, Object> greet(@RequestParam String viewCode) {
HashMap<String, Object> response = new HashMap<>();
response.put("code", "0");
response.put("msg", "ok");
response.put("body", demoMapper.getDemoModels(viewCode));
return response;
}
}
如此,就完成了一个可从数据库查找数据的http接口。
我们虽然采用了前后端分离的方式开发,但是在发布时,采用了合并到一起,发布到一台服务器上的形式来上线。这种方式虽然不利于前后端分开迭代,但是在接入我们内部一些权限管理、自动部署等组件时,具有巨大的便利性。
首先使用上面提到过的webdev build
命令,将Flutter_web项目编译成html和js等生成产物。
然后将这些生成产物,统一复制到JavaProject/demo/src/main/resources/static
目录下。这样,java项目运行后,直接访问http://127.0.0.1:8080
地址,就可以访问到网站主页面了。