對,我說真的,今天的內容其實沒有很難。我們只需要稍微把Dart VM叫出來溝通一下,全部程式碼頂多30行,也沒有什麼複雜難理解的邏輯。最重要的是這很有趣,一起來試試看吧!
首先來複習一下整個Hot Reload大致的流程:
source
我們可以看到流程進行到5.才進入Flutter,1~4其實都是純Dart就可以做到的事情。而這個5.雖然講起來簡單,實際上還是牽涉到很多Flutter的複雜邏輯,所以今天我們會實作的是純Dart版的Hot Reload,並在最後稍微偷瞄一下Flutter hot reload的實作。
一開始我們要來寫一個會不停印出Hello, World!
的程式,然後試著在程式執行時,用Hot Reload把它改成Hello, Dart!
。首先建立我們今天要使用的專案資料夾hot_reload
:
mkdir hot_reload
cd hot_reload
接著建立一個main.dart
import 'dart:async';
void main() {
Timer.periodic(Duration(seconds: 3), (timer) {
runApp();
});
}
void runApp() {
print("Hello, World!");
}
Timer
來使它持續執行下去,因為我們想要在程式執行過程中進行Hot Reload。runApp
函數出來,因為main
函數裡的改動無法被Hot Reload。然後試著執行它:
D:\hot_reload>dart --enable-vm-service main.dart
Observatory listening on http://127.0.0.1:8181/xZqIQ1NpRNA=/
Hello, World!
Hello, World!
Hello, World!
...
很好,沒什麼特別的,除了--enable-vm-service
和一串既陌生又熟悉的網址...
--enable-vm-service
讓我們可以透過Observatory,在瀏覽器中觀察Dart VM運作的各種資訊:
裡面有很多有趣的東西,大家可以趁機瀏覽一下。接下來我們進入上圖紅框的Isolate:
右上角的Reload Source,就是Observatory透過http呼叫Dart VM,進行Hot Reload的地方。讓我們回去修改runApp
程式碼,再回來點點看吧:
void runApp() {
print("Hello, Dart!");
}
// output after Reload Source
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, Dart!
Hello, Dart!
Hello, Dart!
果然成功reload了!話說這Observatory也就是個web介面,既然它能呼叫Dart VM,當然我們也可以。
接下來我們就要模仿Observatory,在我們的程式裡導入vm_service
,透過程式去呼叫reload source。
首先在我們的專案資料夾中建立pubspec.yaml
name: hot_reload
dependencies:
watcher:
vm_service:
除了vm_service
之外,我們還須要watcher
來觀察檔案的變化,讓我們可以在每次程式碼有變更時觸發Hot Reload。別忘了執行flutter pub get
,你可以趁這個機會觀察一下實際上到底發生了什麼事。
接下來我們先看看watcher
怎麼運作,回到main.dart
,修改成如下程式碼:
import 'package:watcher/watcher.dart';
void main() {
final watcher = DirectoryWatcher(".");
watcher.events.listen(print);
}
執行之後,每當我們修改hot_reload
中的任何檔案,都會收到event
並被print
出來:
D:\hot_reload>dart main.dart
add .\test.dart
modify .\test.dart
remove .\test.dart
知道watcher
怎麼使用之後,我們再來看看怎麼使用vm_service
建立hotReload
函數:
Future<ReloadReport> hotReload() async{
final Uri serverUri = (await Service.getInfo()).serverUri;
final Uri webSocketUri = convertToWebSocketUrl(serviceProtocolUrl: serverUri);
final VmService service = await vmServiceConnectUri(webSocketUri.toString());
final VM vm = await service.getVM();
final String isolateId = vm.isolates.first.id;
final ReloadReport report = await service.reloadSources(isolateId);
return report;
}
(這邊其實就照程式碼翻譯而已,搞不好你直接看程式碼比較快)
dart:developer
提供的Service
類別,取得目前Dart VM開啟的web server的URI最後把它們合併起來,讓我們在每次watcher
監聽到程式碼修改時,呼叫hotReload
吧:
import 'dart:developer';
import 'package:vm_service/utils.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import 'package:watcher/watcher.dart';
void main() {
final watcher = DirectoryWatcher(".");
watcher.events.listen((event) async {
final report = await hotReload();
if (report.success) {
print("Hot reload succeed.");
runApp();
} else {
print("Hot reload failed.");
print(report.json['notices'][0]['message']);
}
});
runApp();
}
void runApp() {
print("Hello, World!");
}
Future<ReloadReport> hotReload() async{
final Uri serverUri = (await Service.getInfo()).serverUri;
final Uri webSocketUri = convertToWebSocketUrl(serviceProtocolUrl: serverUri);
final VmService service = await vmServiceConnectUri(webSocketUri.toString());
final VM vm = await service.getVM();
final String isolateId = vm.isolates.first.id;
final ReloadReport report = await service.reloadSources(isolateId);
return report;
}
程式完成了,趕快來玩玩看我們自己實作的Hot Reload吧!
D:\hot_reload>dart --enable-vm-service main.dart
Observatory listening on http://127.0.0.1:8181/NT7CNzm-hqU=/
Hello, World!
Hot reload succeed.
Hello, Dart!
Hot reload failed.
main.dart:24:23: Error: Expected ';' after this.
print("Hello, Dart!")
^
Hot reload succeed.
Hello, Dart!
完美!不但程式正確時可以成功執行Hot Reload,錯誤時也有我們熟悉的錯誤訊息。
最後我們來稍微看一下Flutter這邊是怎麼做的。首先開啟flutter_tools
這個專案,位於flutter/packages/flutter_tools
。注意如果你開啟後IDE抱怨找不到Dart SDK,你須要進行設定:
然後請在專案中搜尋FlutterDevice.reloadSource()
函數:
這是不是跟我們的Dart Hot Reload很像呢?同樣透過vmService取得vm,再透過vmService更新某個isolate。
這次我就不深入探索整個Flutter Hot Reload的運作機制了,如果你很有興趣,或跟我一樣常常吃飽太閒的話,可以透過接下來的方式在執行APP時進入flutter_tools。
首先我們須要一個Flutter App:
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: FlutterLogo(),
),
),
);
}
}
接著為flutter_tools專案新增debug config,注意這裡5和7要換成你自己的flutter_tool和測試專案路徑:
用debug mode執行之後,你會在console看到這樣的訊息:
使用r
來進行hot reload吧:
中斷成功!到這裡你就可以開始自行探索整個Flutter Hot Reload的流程細節了,下次可以考慮來發一篇文吧!
這次我們為了瞭解Dart語言的Hot Reload機制是如何運作的,首先使用Observatory進行reload source,接著嘗試自己用簡單的Dart程式來實作Observatory的功能。我們利用watcher
來觀察source code的變化,並呼叫vm_service
來進行reloadSource。我們也看到flutter_tools
中,用了和我們相似的作法來實現Flutter Hot Reload。最後我們為flutter_tools
建立debug config,成功在hot reload時進入中斷點,為大家後續的深入研究做好了準備。