写给前端工程师的 Flutter 教程
w本文由图雀社区认证作者 Tyler 写作而成,,点击阅读原文查看作者的博客链接,感谢作者的优质输出,让我们的技术世界变得更加美好😆
涉及Dart语言,Flutter原理,Flutter Widget,以及Flutter中的状态管理方案等,堪称最全的Flutter教程😄
最爱折腾的就是前端工程师了,从 jQuery 折腾到 AngularJs,再折腾到 Vue、React。最爱跨端的也是前端工程师,从 phonegap,折腾到 React Native,这不又折腾到了 Flutter。图啥?低成本地为用户带来更优秀的用户体验。目前来说Flutter[1]可能是其中最优秀的一种方案了。
Flutter 是什么?
Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile[2], web[3], and desktop[4] from a single codebase.
Flutter[5]是由原 Google Chrome 团队成员,利用 Chrome 2D 渲染引擎,然后精简 CSS 布局演变而来。或者更详细的版本
-
Flutter 在各个原生的平台中,使用自己的 C++的引擎渲染界面,没有使用 webview,也不像 RN、NativeScript 一样使用系统的组件。简单来说平台只是给 Flutter 提供一个画布。 -
界面使用 Dart 语言开发,貌似唯一支持 JIT,和 AOT 模式的强类型语言。 -
写法非常的现代,声明式,组件化,Composition > inheritance,响应式……就是现在前端流行的这一套 😄 -
一套代码搞定所有平台。
Flutter 为什么快?Flutter 相比 RN 的优势在哪里?
从架构中实际上已经能看出 Flutter 为什么快,至少相比之前的当红炸子鸡 React Native 快的原因了。
-
Skia 引擎,Chrome, Chrome OS,Android,Firefox,Firefox OS 都以此作为渲染引擎。 -
Dart 语言可以 AOT 编译成 ARM Code,让布局以及业务代码运行的最快,而且 Dart 的 GC 针对 Flutter 频繁销毁创建 Widget 做了专门的优化。 -
CSS 的的子集 Flex like 的布局方式,保留强大表现能力的同时,也保留了性能。 -
Flutter 业务书写的 Widget 在渲染之前 diff 转化成 Render Object,对,就像 React 中的 Virtual DOM,以此来确保开发体验和性能。
而相比 React Native:
-
RN 使用 JavaScript 来运行业务代码,然后 JS Bridge 的方式调用平台相关组件,性能比有损失,甚至平台不同 js 引擎都不一样。 -
RN 使用平台组件,行为一致性会有打折,或者说,开发者需要处理更多平台相关的问题。
而具体两者的性能测试,可以看这里[6],结论是 Flutter,在 CPU,FPS,内存稳定上均优于 ReactNative。
Dart 语言
在开始 Flutter 之前,我们需要先了解下 Dart 语言…… Dart 是由 Google 开发,最初是想作为 JavaScript 替代语言,但是失败沉寂之后,作为 Flutter 独有开发语言又焕发了第二春 😂。实际上即使到了 2.0,Dart 语法[7]和 JavaScriptFlutter[8]非常的相像。单线程,Event Loop……当然作为一篇写给前端工程师的教程,我在这里只想写写 JavaScript 中暂时没有的,Dart 中更为省心,也更“甜”的东西。
-
不会飘的 this
-
强类型,当然前端现在有了 TypeScript 😬 -
强大方便的操作符号: -
?.
方便安全的foo?.bar
取值,如果 foo 为null
,那么取值为null
-
??
condition ? expr1 : expr2
可以简写为expr1 ?? expr2
-
=
和其他符号的组合:*=
、~/=
、&=
、|=
…… -
级联操作符(Cascade notation ..)
// 想想这样省了多少变量声明
querySelect('#button')
..text ="Confirm"
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed'))
甚至可以重写操作符
class Vector {
final int x, y;
Vector(this.x, this.y);
Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
// Operator == and hashCode not shown. For details, see note below.
// ···
}
void main() {
final v = Vector(2, 3);
final w = Vector(2, 2);
assert(v + w == Vector(4, 5));
assert(v - w == Vector(0, 1));
}
注:重写==
,也需要重写 Object hashCode
getter
class Person {
final String firstName, lastName;
Person(this.firstName, this.lastName);
// Override hashCode using strategy from Effective Java,
// Chapter 11.
@override
int get hashCode {
int result = 17;
result = 37 * result + firstName.hashCode;
result = 37 * result + lastName.hashCode;
return result;
}
// You should generally implement operator == if you
// override hashCode.
@override
bool operator ==(dynamic other) {
if (other is! Person) return false;
Person person = other;
return (person.firstName == firstName &&
person.lastName == lastName);
}
}
void main() {
var p1 = Person('Bob', 'Smith');
var p2 = Person('Bob', 'Smith');
var p3 = 'not a person';
assert(p1.hashCode == p2.hashCode);
assert(p1 == p2);
assert(p1 != p3);
}
这点在 diff 对象的时候尤其有用。
lsolate
Dart 运行在独立隔离的 iSolate 中就类似 JavaScript 一样,单线程事件驱动,但是 Dart 也开放了创建其他 isolate,充分利用 CPU 的多和能力。
loadData() async {
// 通过spawn新建一个isolate,并绑定静态方法
ReceivePort receivePort =ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// 获取新isolate的监听port
SendPort sendPort = await receivePort.first;
// 调用sendReceive自定义方法
List dataList = await sendReceive(sendPort, 'https://hicc.me/posts');
print('dataList $dataList');
}
// isolate的绑定方法
static dataLoader(SendPort sendPort) async{
// 创建监听port,并将sendPort传给外界用来调用
ReceivePort receivePort =ReceivePort();
sendPort.send(receivePort.sendPort);
// 监听外界调用
await for (var msg in receivePort) {
String requestURL =msg[0];
SendPort callbackPort =msg[1];
Client client = Client();
Response response = await client.get(requestURL);
List dataList = json.decode(response.body);
// 回调返回值给调用者
callbackPort.send(dataList);
}
}
// 创建自己的监听port,并且向新isolate发送消息
Future sendReceive(SendPort sendPort, String url) {
ReceivePort receivePort =ReceivePort();
sendPort.send([url, receivePort.sendPort]);
// 接收到返回值,返回给调用者
return receivePort.first;
}
当然 Flutter 中封装了compute[9],可以方便的使用,譬如在其它 isolate 中解析大的 json[10]。
Dart UI as Code
在这里单独提出来的意义在于,从 React 开始,到 Flutter,到最近的 Apple SwiftUI,Android Jetpack Compose 声明式组件写法越发流行,Web 前端使用 JSX 来让开发者更方便的书写,而 Flutter,SwiftUI 则直接从优化语言本身着手。
函数类的命名参数
void test({@required int age,String name}) {
print(name);
print(age);
}
// 解决函数调用时候,参数不明确的问题
test(name:"hicc",age: 30)
// 这样对于组件的使用尤为方便
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(),
floatingActionButton:FloatingActionButton()
);
}
}
大杀器:Collection If 和 Collection For
// collection If
Widget build(BuildContext context) {
return Row(
children: [
IconButton(icon: Icon(Icons.menu)),
Expanded(child: title),
if (!isAndroid)
IconButton(icon: Icon(Icons.search)),
],
);
}
// Collect For
var command = [
engineDartPath,
frontendServer,
for (var root in fileSystemRoots) "--filesystem-root=$root",
for (var entryPoint in entryPoints)
if (fileExists("lib/$entryPoint.json")) "lib/$entryPoint",
mainPath
];
更多 Dart 2.3 对此的优化看这里[11]。
Flutter 怎么写
到这里终于到正题了,如果熟悉 web 前端,熟悉 React 的话,你会对下面要讲的异常的熟悉。Flutter App 的一切从lib/main.dart
文件的 main 函数开始:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
home: Scaffold(
appBar: AppBar(
title: Text('Welcome to Flutter'),
),
body: Center(
child: Text('Hello World'),
),
),
);
}
}
Dart 类 build 方法返回的便是 Widget,在 Flutter 中一切都是 Widget,包括但不限于
-
结构性元素,menu,button 等 -
样式类元素,font,color 等 -
布局类元素,padding,margin 等 -
导航 -
手势
Widget 是 Dart 中特殊的类,通过实例化(Dart 中new 是可选的[12])相互嵌套,你的这个 App 就是形如下图的一颗组件树(Dart 入口函数的概念,main.dart -> main()
)。
Widget 布局
上说过 Flutter 布局思路来自 CSS,而 Flutter 中一切皆 Widget,因此整体布局也很简单:
-
容器组件 Container -
decoration 装饰属性,设置背景色,背景图,边框,圆角,阴影和渐变等 -
margin -
padding -
alignment -
width -
height -
Padding,Center -
Row,Column,Flex -
Wrap, Flow 流式布局 -
Stack, z 轴布局 -
……
更多可以看这里[13]Flutter 中 Widget 可以分为三类,形如 React 中“展示组件”、“容器组件”,“context”。
StatelessWidget
这个就是 Flutter 中的“展示组件”,自身不保存状态,外部参数变化就销毁重新创建。Flutter 建议尽量使用无状态的组件。
StatefulWidget
状态组件就是类似于 React 中的“容器组件”了,Flutter 中状态组件写法会稍微不一样。
class Counter extends StatefulWidget {
// This class is the configuration for the state. It holds the
// values (in this case nothing) provided by the parent and used by the build
// method of the State. Fields in a Widget subclass are always marked "final".
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
// This call to setState tells the Flutter framework that
// something has changed in this State, which causes it to rerun
// the build method below so that the display can reflect the
// updated values. If you change _counter without calling
// setState(), then the build method won't be called again,
// and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance
// as done by the _increment method above.
// The Flutter framework has been optimized to make rerunning
// build methods fast, so that you can just rebuild anything that
// needs updating rather than having to individually change
// instances of widgets.
return Row(
children: <Widget>[
RaisedButton(
onPressed: _increment,
child: Text('Increment'),
),
Text('Count: $_counter'),
],
);
}
}
可以看到 Flutter 中直接使用了和 React 中同名的setState
方法,不过不会有变量合并的东西,当然也有生命周期[14]。可以看到一个有状态的组件需要两个 Class,这样写的原因在于,Flutter 中 Widget 都是 immmutable 的,状态组件的状态保存在 State 中,组件仍然每次重新创建,Widget 在这里只是一种对组件的描述,Flutter 会 diff 转换成 Element,然后转换成 RenderObject 才渲染。Flutter Widget 更多的渲染流程可以看这里[15]。实际上 Widget 只是作为组件结构一种描述,还可以带来的好处是,你可以更方便的做一些主题性的组件[16], Flutter 官方提供的Material Components widgets[17]和Cupertino (iOS-style) widgets[18]质量就相当高,再配合 Flutter 亚秒级的Hot Reload[19],开发体验可以说挺不错的。
State Management
setState()
可以很方便的管理组件内的数据,但是 Flutter 中状态同样是从上往下流转的,因此也会遇到和 React 中同样的问题,如果组件树太深,逐层状态创建就显得很麻烦了,更不要说代码的易读和易维护性了。
InheritedWidget
同样 Flutter 也有个context
一样的东西,那就是InheritedWidget
,使用起来也很简单。
class GlobalData extends InheritedWidget {
final int count;
GlobalData({Key key, this.count,Widget child}):super(key:key,child:child);
@override
bool updateShouldNotify(GlobalData oldWidget) {
return oldWidget.count != count;
}
static GlobalData of(BuildContext context) => context.inheritFromWidgetOfExactType(GlobalData);
}
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: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: GlobalData(
count: _counter,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
Body(),
Body2()
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class Body extends StatelessWidget {
@override
Widget build(BuildContext context) {
GlobalData globalData = GlobalData.of(context);
return Text(globalData.count.toString());
}
}
class Body2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
GlobalData globalData = GlobalData.of(context);
return Text(globalData.count.toString());
}
具体实现原理可以参考这里[20],不过 Google 封装了一个更为上层的库provider[21],具体使用可以看这里[22]。
BlOC
BlOC[23]是 Flutter team 提出建议的另一种更高级的数据组织方式,也是我最中意的方式。简单来说:Bloc = InheritedWidget + RxDart(Stream)Dart 语言中内置了 Steam,Stream ~= Observable,配合RxDart[24], 然后加上StreamBuilder
会是一种异常强大和自由的模式。
class GlobalData extends InheritedWidget {
final int count;
final Stream<String> timeInterval$ = new Stream.periodic(Duration(seconds: 10)).map((time) => new DateTime.now().toString());
GlobalData({Key key, this.count,Widget child}):super(key:key,child:child);
@override
bool updateShouldNotify(GlobalData oldWidget) {
return oldWidget.count != count;
}
static GlobalData of(BuildContext context) => context.inheritFromWidgetOfExactType(GlobalData);
}
class TimerView extends StatelessWidget {
@override
Widget build(BuildContext context) {
GlobalData globalData = GlobalData.of(context);
return StreamBuilder(
stream: globalData.timeInterval$,
builder: (context, snapshot) {
return Text(snapshot?.data ?? '');
}
);
}
}
当然 Bloc 的问题在于
-
学习成本略高,Rx 的概念要吃透,不然你会抓狂 -
自由带来的问题是,可能代码不如 Redux 类的规整。
顺便,今年 Apple 也拥抱了响应式,Combine[25](Rx like) + SwiftUI 也基本等于 Bloc 了。所以,Rx 还是要赶紧学起来 😬 除去 Bloc,Flutter 中还是可以使用其他的方案,譬如:
-
Flutter Redux [26] -
阿里闲鱼的 Fish Redux [27], 据说性能很好。 -
Mobx [28] -
……
展开来说现在的前端开发使用强大的框架页面组装已经不是难点了。开发的难点在于如何组合富交互所需的数据,也就是上面图中的state
部分。更具体来说,是怎么优雅,高效,易维护地处理短暂数据(ephemeral state)setState()
和需要共享的 App State 的问题,这是个工程性的问题,但往往也是日常开发最难的事情了,引用 Redux 作者 Dan 的一句:
“The rule of thumb is:Do whatever is less awkward[29].”
到这里,主要的部分已经讲完了,有这些已经可以开发出一个不错的 App 了。剩下的就当成一个 bonus 吧。
测试
Flutter debugger,测试都是出场自带,用起来也不难。
// 测试在/test/目录下面
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
包管理,资源管理
类似与 JavaScript 的 npm,Flutter,也就是 Dart 也有自己的包仓库[30]。不过项目包的依赖使用 yaml 文件来描述:
name: app
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
生命周期
移动应用总归需要应用级别的生命周期,flutter 中使用生命周期钩子,也非常的简单:
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.inactive:
print('AppLifecycleState.inactive');
break;
case AppLifecycleState.paused:
print('AppLifecycleState.paused');
break;
case AppLifecycleState.resumed:
print('AppLifecycleState.resumed');
break;
case AppLifecycleState.suspending:
print('AppLifecycleState.suspending');
break;
}
super.didChangeAppLifecycleState(state);
}
@override
Widget build(BuildContext context) {
return Container();
}
}
使用原生能力
和 ReactNative 类似,Flutter 也是使用类似事件的机制来使用平台相关能力。
Flutter Web, Flutter Desktop
这些还在开发当中,鉴于对 Dart 喜欢,以及对 Flutter 性能的乐观,这些倒是很值得期待。还记得平台只是给 Flutter 提供一个画布么,Flutter Desktop 未来更是可以大有可为 😄,相关可以看这里[31]。最后每种方案,每种技术都有优缺点[32],甚至技术的架构决定了,有些缺陷可能永远都没法改进,所以 🤔
最后的最后,强烈推荐闲鱼团队的Flutter Blog[33]👍,常常拜读,收益良多
参考资料
Flutter: https://flutter.dev/
[2]mobile: https://flutter.dev/docs
[3]web: https://flutter.dev/web
[4]desktop: https://flutter.dev/desktop
[5]Flutter: https://flutter.dev/
[6]这里: https://www.yuque.com/xytech/flutter/gs3pnk
[7]Dart 语法: https://dart.dev/guides/language/language-tour
[8]Flutter: https://flutter.dev/
[9]compute: https://api.flutter.dev/flutter/foundation/compute.html
[10]在其它 isolate 中解析大的 json: https://flutter.dev/docs/cookbook/networking/background-parsing
[11]这里: https://medium.com/dartlang/making-dart-a-better-language-for-ui-f1ccaf9f546c
[12]new 是可选的: https://dart.dev/guides/language/language-tour#using-constructors
[13]更多可以看这里: https://www.yuque.com/xytech/flutter/hc0xq7
[14]生命周期: https://segmentfault.com/a/1190000015211309
[15]这里: https://www.yuque.com/xytech/flutter/tge705
[16]主题性的组件: https://flutter.dev/docs/development/ui/widgets
[17]Material Components widgets: https://flutter.dev/docs/development/ui/widgets/material
[18]Cupertino (iOS-style) widgets: https://flutter.dev/docs/development/ui/widgets/cupertino
[19]Hot Reload: https://flutter.dev/docs/development/tools/hot-reload
[20]这里: https://loveky.github.io/2018/07/18/how-flutter-inheritedwidget-works/
[21]provider: https://pub.dev/packages/provider
[22]这里: https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
[23]BlOC: https://medium.com/flutterpub/architecting-your-flutter-project-bd04e144a8f1
[24]RxDart: https://pub.dev/packages/rxdart
[25]Combine: https://developer.apple.com/documentation/combine
[26]Flutter Redux: https://pub.dev/packages/flutter_redux
[27]Fish Redux: https://github.com/alibaba/fish-redux
[28]Mobx: https://mobx.pub/
[29]Do whatever is less awkward: https://github.com/reduxjs/redux/issues/1287#issuecomment-175351978
[30]包仓库: https://pub.dev/
[31]这里: https://github.com/flutter/flutter/wiki/Desktop-shells
[32]优缺点: https://medium.com/asos-techblog/flutter-vs-react-native-for-ios-android-app-development-c41b4e038db9
[33]Flutter Blog: https://www.yuque.com/xytech/flutter
·END·
汇聚精彩的免费实战教程
喜欢本文,点个“在看”告诉我
本文分享自微信公众号 - 图雀社区(tuture-dev)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
【AWS征文】AWS安全加固-Fortinet AWS 安全解决方案
作者:昱坤 在公有云方案日益火爆的今天,公有云应用越来越广泛。随之而来的,公有云也遇到了一些挑战: 传统数据中心产品不一定支持云环境 传统的产品不一定支持云弹性的部署以及模块化的部署 云架构的部署思路与传统物理环境部署环境完全不同 传统的安全防护很难在多云环境提供统一安全解决方案 遇到最多的问题就是:我们云环境一定要和物理数据中心架构一样 而在企业进行公有云迁移时对于安全和自动化的要求,AWS上有了新的安全自动部署方案: 在该方案中,可以将防火墙部署在Transit VPC中,以实现VPC与VPC间/VPC与Internet间等的安全加固需求。此文仅介绍Fortinet AWS 的安全解决方案。 Fortinet云部署实例要求 如果要在AWS上部署FortiGate-VM,那么实例需要满足以下要求: Fortinet常见云部署模式 针对AWS云上部署,Fortinet提供两种常见的部署方案: NGFW 和ELB部署形式 两个AZ的FW与WAF工作在AA模式 入向流量通过ELB负载分摊 FW开启NGFW威胁检测功能 FW将流量映射至内部ELB FQDN NGFW以及...
- 下一篇
你不知道的 GraphQL
本文由 kazaff 翻译而成,点击阅读原文可以查看作者的博客,感谢作者的优质输出,让我们的技术世界更加美好✌️ 很久之前其实就关注过这个技术,记得当时还是React刚刚崭露头角的时期吧。总之那时候,GraphQL感觉还只是概念完备阶段,除了FB自己内部大量使用外,好像社区并不是很健全,不过大家应该都在疯狂的讨论和跟进吧。过了2年,如今再回过头来看,已经涌现出各种开源或商用服务专注于这个领域,各种语言的框架和工具也都很完备了,感觉是时候重新接触GraphQL了。如果你的项目正处于技术选型,你正在犹豫选择一种接口风格的时刻,不妨了解一下这个神奇而强大的玩意儿~~ 本文打算翻译一篇感觉很解惑的文章,主要围绕着GraphQL的server端实现,因为相比client端,server端包含了更多的内容。后面如果有机会,也会尝试提供关于client端相关的内容,不过前端同学可以先看一下这里:howtographql[1],这里有各种最佳实践,应该总会找到和你正在使用相关的前端框架的整合方案,好像有个对应的中文版[2]~ 关于GraphQL概念的内容,这篇文章并没有涉及太多,不过假如你用搜索引擎去...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Hadoop3单机部署,实现最简伪集群
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果