0%

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

宣佈 Dart 2 穩定版和 Dart Web 平台

今天,我們宣佈 Dart 2 穩定版正式發佈,其中包括 Dart Web 平台的重寫,它提供了生產力、效能和可擴展性的獨特組合。

Flutter 開發者已經享受 Dart 2 的許多優點,因為 Flutter 幾個月來一直在捆綁 Dart 2 SDK 的預發佈版本。然而,由於框架和語言之間的緊密耦合,Dart 2 的穩定版是 Flutter 穩定版發佈的另一個重要里程碑。如果您還沒有安裝 Flutter,請由此開始

隨著 Dart 2 的發佈,Web 開發者現在可以利用相同的語言、函式庫和工具以及許多 Web 專用的增強功能。Web 開發者應從開始使用頁面開始,以獲取有關安裝工具和建立第一個應用程式的說明。

Dart:越來越受歡迎

Dart 2 標誌著 Dart 作為一種主流程式語言的重生,專注於為行動和 Web 應用程式實現快速開發和出色的使用者體驗。我們希望讓構建客戶端應用程式的開發人員提高生產力,使用一種語言、框架和組件來減少樣板程式碼,讓他們專注於業務邏輯,並使用能夠及早識別錯誤、實現強大除錯功能並提供小型、快速執行時程式碼的工具。

在過去的一年中,Dart 經歷了巨大的成長。我們自己的分析表明,外部使用量成長了十倍。在上個季度,根據拉取請求的衡量標準,Dart 是 GitHub 上成長最快的語言之一StackOverflow 問題的成長也生動地展示了 Dart 的發展勢頭:

在內部,Dart 是 Google 用於 Web 應用程式開發的少數幾種語言之一,擁有來自數十個不同專案的數百萬行程式碼,包括 Google Ads、Google Shopping 和我們自己的內部基礎架構團隊。

事實上,您可能已經在使用 Dart 而沒有意識到:流行的 Sass 樣式表預處理器 最近用 Dart 重寫,使其更快、更可攜帶且更容易編寫。Sass 現在作為 Homebrew 和 Chocolatey 上的獨立可執行檔發佈,並在 npm 上編譯為純 JavaScript。Dart 可以輕鬆地在任何地方安裝應用程式,而無需外部相依性,並且可以融入使用 Dart 和不使用 Dart 的人的工作流程中。

Dart 2 專注於三個領域:強化和收緊語言、開發我們對 Web 和行動框架的支援,以及將支援 Google 使用 Dart 的一些工具和組件帶到外部世界。本文的其餘部分將探討這些主題。

Dart 2:客戶端優化的語言

正如我們在二月 強調的Dart 類型系統健全的。這意味著在使用者執行您的應用程式之前(在分析和編譯期間)就會發現一大類問題。當我們將 Dart 擴展到 Google 內部極其龐大的應用程式時,類型系統在開發週期的早期捕獲了更多錯誤,從而提高了生產程式碼的品質。

使用 Dart 2,您無法進入表達式求值結果與表達式的靜態類型不符的狀態。這意味著您可以將程式碼庫擴展到數百萬行,處理大型重構專案,並自信地部署程式碼。

別擔心:健全性並不意味著大量的樣板程式碼。類型系統包括進階推斷,即使是泛型類型參數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final _field = [3.14, 6.28];

void main() {
// 沒有提供類型參數,但推斷為 <int>
print([1, 2, 3].runtimeType); // List<int>

// 如果類型不匹配,則使用它們的共同基類型
// int (1) 和 double (3.14) 都是 `num`
print([1, 3.14].runtimeType); // List<num>

// 推斷超越了變數。
// 您也可以省略欄位上的類型。
print(_field.runtimeType); // List<double>
}

要了解更多關於 Dart 2 類型系統的資訊,請查看此頁面

如先前的部落格文章所述,Dart 2 還減少了幾個關鍵領域的語言儀式:例如,new 關鍵字現在是可選的,而 const 關鍵字在常數環境中是可選的。

最後,我們在幕後做了很多工作來統一各種支援工具,使用一個共同的前端,現在支援我們的編譯器和執行時。這確保了我們使用者的現在的一致性,並承諾在 Dart 持續發展的過程中提高新功能的速度和品質。

Web 上的 Dart:豐富而強大的框架

Web 應用程式從一開始就是 Dart 使命的核心。事實上,Google 的大多數 Dart 開發都是針對 Web 應用程式。其中最大的一個是 Google Ads,它為數十億美元的網路經濟提供動力。在將他們的程式碼遷移到 Dart 的過程中,他們將 UI 程式碼庫的大小減少了 40%,同時提高了開發人員的生產力和應用程式品質。

雖然核心 Dart SDK 提供了存取現代瀏覽器 API 的函式庫,但我們也支援一個強大的受 Angular 啟發的框架,用於構建複雜的 Web 應用程式。作為 Dart 2 的一部分發佈的 AngularDart 5 利用了 Dart 的健全類型系統和新的構建系統(如下所述)在開發過程中提供快速增量構建,並在您準備部署時提供更小的已編譯 JavaScript。

此版本的一個主要重點領域是改進在頁面加載時處理的程式碼量,顯著減少了網頁的「互動時間」。與 AngularDart 4 相比,許多應用程式的程式碼大小減少了一半以上。

為了展示 Dart Web 平台的改進,我們構建了一個基於 Dart 的 HackerNews 網站客戶端實作,作為一個漸進式 Web 應用程式;這與其他範例實作一起發佈在流行的 HNPWA 網站上。使用構建 Dart Web 應用程式的最佳實務,我們能夠提供功能齊全的體驗,在現代設備上在一秒鐘內即可完全互動,在中等行動設備上在緩慢的 3G 網路上在五秒鐘內即可完全互動。即使與針對較小型應用程式進行優化的輕量級 Web 框架相比,這也具有競爭力。

hnpwa.dartlang.org

我們的框架投資延伸到我們的核心組件,我們也更新了這些組件。您現在可以存取 100 個新的類別,包括許多日期、時間和選單 Material 組件。您可以在組件庫中瀏覽我們的所有組件。

Material 日期選擇器 - Web 應用程式可用的豐富組件之一

Dart 2 的彈性工具

使用 Dart 2,Dart 成為一流的編譯為 JavaScript 的語言,具有 Web 開發人員期望的開發週期和出色的執行時效能特徵。這是同時提供大多數以 JS 為目標的語言不提供的優點:健全的類型系統和出色的原生行動應用程式支援。

使用 Dart 2,我們的 Web 工具採用 100% JavaScript 開發模型,並使用兩個互補的 JavaScript 編譯器。開發編譯器 dartdevc 提供快速增量編譯,同時產生易於閱讀和除錯的 JavaScript。這讓我們的生產編譯器 dart2js 可以專注於為行動裝置的漸進式 Web 應用程式和桌面的複雜企業體驗產生高度優化的 JavaScript。這兩種編譯器都利用 Dart 的健全類型系統來優化它們的輸出。

我們的 Web 工具基於新的構建系統構建,該系統旨在快速、可除錯且可擴展。現在,諸如產生序列化程式碼、將 Sass 編譯為 CSS 以及將 Dart 編譯為 JavaScript 之類的任務都發生在一個工具鏈中,該工具鏈支援在您變更應用程式部分內容時進行快速增量更新。構建系統也被設計為支援 Web 以外的用途。例如:Flutter 開發人員正在使用它來產生 JSON 序列化程式碼

使用 Dart 2,我們還擁有一套擴展的開發人員工具。除了支援 Android Studio 和 JetBrains 套件工具(包括 IntelliJ IDEAWebStorm)之外,我們還支援 Visual Studio CodeDart Code 擴展。我們也有一個很棒的實驗暫存區:DartPad,它已針對 Dart 2 進行了完全更新。

最後,Dart SDK 還附帶了一套其他有用的工具:與我們的套件網站一起使用的套件管理器、靜態分析器、您可以從命令列或作為套件使用的 linter,以及用於Web 文件和符合 Dart 樣式指南程式碼格式化的工具。

了解更多

查看開始使用 FlutterDart Web 工具集的說明。

發行說明提供了自 Dart 1 以來進行的許多其他較小改進的詳細技術說明,其中一些是對舊 Dart 程式碼的重大變更。如果您遇到 Google 搜尋無法解決的問題,我們建議您從 StackOverflow開始。訂閱 Dart 公告郵件列表並在 Twitter 上關注我們以獲取更新。我們也希望在我們的社群中看到您,例如 Gitter聊天室和 r/dartlang subreddit

謝謝

Dart 已成為一種針對 Web 和行動開發進行優化的通用語言。我們已經開發 Dart 2 多年,在此期間,它觸及了我們生態系統的各個方面,並且需要遷移數百萬行程式碼和數百個套件。

我們感謝 DartFlutter社群以及數百名幫助我們完成這段旅程的 Google 工程師。沒有你們,我們不可能做到這一點!


宣佈 Dart 2 穩定版和 Dart Web 平台 最初發佈於 Medium 上的 Dart,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

準備迎接 Dart 2,並讓您的套件在 Pub 網站上看起來更棒!

我們發佈了 Dart 套件庫 Pub 的新版本!這次我們專注於為 Dart 2 穩定版發佈做準備。

將您的套件遷移到 Dart 2

我們預計 Dart 2 版本將在不久的將來升級到穩定版!因此,至關重要的是您要遷移您的程式碼——尤其是您已發佈的任何套件——以使其與 Dart 2 相容。這主要有三個方面:

  • 確保您的程式碼通過 Dart 2 分析(詳情)。我們建議您也要注意提示——例如,為棄用做準備。
  • 運行程式碼測試,以確保您的程式碼通過 Dart 2 運行程式碼檢查(詳情)。
  • 將套件的 SDK 約束上限更新為 <3.0.0(詳情)。

我們強烈建議所有套件作者完成這項工作,我們也將在接下來的幾天內努力完成我們擁有的套件。

Pub 網站上改進的分析介面

為了幫助您進行遷移,我們更新了 Pub 網站的分析標籤頁,使程式碼分析結果更易於閱讀。首先,在分析頁面的頂部,我們加入了進度條,讓您可以快速查看套件的狀態:

Pub 網站上顯示的套件評分

進度條下方是詳細的分析結果,按嚴重程度分組:錯誤(紅色圖示)、警告(黃色圖示)和建議問題(藍色圖示),如下圖所示:

Pub 網站上顯示的分析問題

更新的分析評分

我們還對分析評分進行了一些調整。如果符合以下任何條件,我們現在會降低套件的評分:

  • 套件未通過 Dart 2 分析。
  • SDK 約束上限不小於 <3.0.0。
  • API 參考生成(dartdoc)失敗。

對於我們的下一個 Pub 網站版本,我們預計將進一步完善評分,並加入一些關於評分計算方式的文件。

搜尋套件的 API 介面

搜尋是 Pub 網站上最受歡迎的功能——這並不奇怪。在過去的幾個季度中,我們在這一領域進行了許多改進,重點是加入平台過濾器和更好的排名。新版本擴展了搜尋功能,支援搜尋套件的 API 介面。除了目前對套件描述和 README.md 內容的搜尋索引外,搜尋現在還涵蓋了套件的所有公共 API 成員,以及這些成員的 API 文件!當您想要找到描述和 README.md 檔案中未提及的內容時,這非常棒。

例如,假設您正在尋找一個可以使用 ISO 4217 貨幣格式格式化字串的套件。在描述和 README.md 檔案中搜尋會給您一個結果,但包含 API 文件的搜尋會給您幾個結果

1
2
3
4
5
6
7
8
9
10
11
12
13
4217 的 10 個結果

intl
包含處理國際化/本地化訊息、日期和數字格式化和解析、雙向文字和其他國際化問題的程式碼。
v 0.15.2 • 更新時間:2017 年 10 月 19 日 FLUTTER WEB OTHER
API 結果:
intl/NumberFormat-class.html

flutter_billing
一個 flutter plugin,用於與 iOS 和 Android 上的計費系統進行通訊。
v 0.1.2 • 更新時間:2018 年 2 月 10 日 FLUTTER
API 結果:
flutter_billing/BillingProduct-class.html

總結…

Dart 2 穩定頻道版本即將發佈,我們已經對 Pub 網站進行了一系列與 Dart 2、排名和搜尋相關的改進。

如果您已經發佈了套件,現在 是時候更新它們以適應 Dart 2 了:

  1. 確保您的套件通過 Dart 2 分析。
  2. 在 pubspec.yaml 中將 SDK 約束上限更新為 <3.0.0。
  3. 重新發佈您更新的套件。

然後前往您在 pub.dartlang.org 上的套件分析頁面,看看您還可以做些什麼來提高您的排名!


準備迎接 Dart 2,並讓您的套件在 Pub 網站上看起來更棒! 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

http://creativecommons.org/licenses/by/4.0/

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

AngularDart 5 IDE 工具發佈公告

Dart 是一種我們熟知且喜愛的靜態類型語言,而 Dart 2 的類型將更加嚴格。這就是為什麼它成為 Angular 框架如此出色的平台的原因,在 Angular 框架中,組件大多以靜態方式連結到模板中,以建立高效能的 UI。

因此,為了進一步改善這種高效且安全的開發體驗,我們宣布推出新的 IDE 工具,以在您的模板中保留類型安全!它支援 AngularDart 5,並且與 IntelliJ/WebStorm 開箱即用。它也可以設定為在 VSCode、vim 等環境中工作。

如果您已經迫不及待,現在可以直接跳到 設定,或者繼續瀏覽我們新的 IDE 整合所提供的功能!

錯誤

新的分析插件將在您的模板中為您找到許多類型錯誤。運算式會根據您使用的指令、它們包含的輸入以及您繫結的參考(#foo、let item of、…)進行驗證。

這裡有一個拼寫錯誤的成員,因此您不必扮演人工拼寫檢查器的角色!

組件輸入上的類型不匹配也不再是問題。

我們還可以提供與 $event 變數類型相關的錯誤,

我們還會檢查您在指令中嵌入的內容。這是一個我們不僅可以捕捉類型錯誤,還可以捕捉無效程式碼的例子。

我們還可以繼續!我們可以捕捉到許多其他類型的錯誤,無論是在模板中還是在組件定義中。

除了在編輯器中突出顯示外,完整的錯誤列表還會顯示在 Dart Analysis 面板中。

自動完成

我們不僅僅停留在驗證上!雖然大部分工作是讓我們的分析能夠高效能運作,並且在編譯器方面完全正常運作,但大部分價值來自於使用已解析的模板狀態,在您的模板中提供您常用的 Dart 自動完成的優點:

但我們可以完成比普通 Dart 成員更多的內容——輸入、輸出和 HTML 標籤呢?

標籤?可以。

屬性?可以。

星號?可以。

星號內的屬性?可以。

額外加分:在組件嵌入中建議 帶有 屬性的標籤!

嵌入是 Angular 中一個眾所周知的進階主題,因此如果您認為您知道我們建議的是什麼,請為您非常了解 AngularDart 而自豪。

可以這麼說,使用我們的插件後,組件 API 更容易理解了!

導航

這裡的支援因編輯器而異,但在 IntelliJ 中,您可以點擊 Dart 運算式的各個部分。在其他編輯器中,您甚至可以導航更多實體,例如標籤和輸入/輸出。

如何使用

確保您使用的是 AngularDart 5 beta 版本,以及 Dart 2.0.0-dev.31 或更新版本。

只需將此加入到您的 analysis_options.yaml 檔案中:

1
2
3
analyzer:
plugins:
- angular

然後重新啟動您的 IDE。請注意,啟動插件可能需要幾秒鐘的時間。它首先必須為您下載原始程式碼和依賴項,並且第一次分析會比後續分析慢。

您也可以在我們的 分析器插件範例專案 中試用它。

如果您使用的是 IntelliJ,這就是您需要做的所有事情。對於其他編輯器,它們可能不會在沒有額外工作的情況下在 HTML 上運行我們的插件——例如,VSCode 需要一個 標誌

回饋和更多

我們的插件原始程式碼位於 Github 上,其中包含更多關於支援哪些功能的資訊,以及如果您有任何問題或遇到任何錯誤,可以在哪裡提交問題。

新的 IDE 工具將成為 AngularDart (v5) 的下一個穩定版本的一部分。我們非常興奮能夠在您編寫 Angular 網頁應用程式時為您提供更高的生產力!


AngularDart 5 IDE 工具發佈公告 最初發佈於 Medium 上的 Dart,人們在那裡繼續透過醒目顯示和回應這個故事來進行對話。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

宣布 Dart 官方 gRPC 支援

gRPC 是一個高效能、開源的 RPC 框架。它提供簡單的服務定義,建立在 http/2 之上,並支援雙向串流和完全整合的可插拔身份驗證。 gRPC 框架支援 多種語言,我們很高興地宣布 Dart 語言的支援現在已進入 Beta 階段!Dart gRPC 支援適用於 1.24.3 或更高版本的 Dart SDK,目前支援 FlutterVM/伺服器 平台。

建立伺服器和撰寫 gRPC 服務定義

gRPC 服務通常使用 Protocol Buffers v3 描述其端點和資料序列化。以下是一個小型範例服務定義,它定義了一個名為「Greeter」的服務,其中包含一個名為「SayHello」的 rpc 訊息(兩條訊息中的數字「1」指定了 訊息欄位 的唯一 ID):

1
2
3
4
5
6
7
8
9
10
11
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

定義服務後,您可以自動產生伺服器的框架:

1
protoc --dart_out=grpc:generated -Iprotos protos/greeter.proto

這會在 generated 目錄中產生一個 GreeterServiceBase 類別,然後您可以將其子類別化以加入實際的服務實作:

1
2
3
4
5
class GreeterService extends GreeterServiceBase {
@override
Future<HelloReply> sayHello(ServiceCall call, HelloRequest request) =>
Future.value(HelloReply()..message = 'Hello, ${request.name}!');
}

使用 gRPC 用戶端呼叫伺服器

當我們產生上面的服務 stub 時,protoc 編譯器也產生了一個用戶端函式庫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'package:grpc/grpc.dart';
import 'package:helloworld/greeter.pbgrpc.dart';

Future<void> main() async {
final channel = ClientChannel('localhost',
port: 50051,
options: const ChannelOptions(credentials: ChannelCredentials.insecure()));
final stub = GreeterClient(channel);

final name = 'Michael';
final request = HelloRequest()..name = name;
try {
final response = await stub.sayHello(request);
print('Greeter client received: ${response.message}');
} catch (e) {
print('Caught error: $e');
}
await channel.shutdown();
}

這樣,從用戶端層呼叫服務就很簡單了:

當運行時,這將列印以下輸出:

1
Greeter client received: Hello, Michael!

後續步驟

要開始在 Dart 中使用 gRPC,請查看我們新的 Dart gRPC 快速入門,它將引導您運行和擴展 Greeter 範例。接下來,查看 Dart gRPC 教程

如果您遇到任何問題,請 提交 issue。我們也很樂意聽到您對任何您希望看到的變更或新增的回饋;例如,我們已經聽到了一些支援 gRPC-Web 協定 的請求。提供快速回饋的一種具體方法是透過按下 issue 最上方評論中的 GitHub 豎起大拇指 👍 按鈕來「投票」給 issue。

我們期待看到您使用 Dart 的 gRPC 建立的成果!

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

Dart 的不可變性介紹

不可否認,不可變性是程式設計中的一個熱門話題,尤其是在前端程式設計中。像 Immutable.js 這樣的函式庫和其他概念,像是單向資料流,都認為當資料在您不知不覺中沒有發生變化時,更容易理解資料:

在物件導向和函數式程式設計中,不可變物件(不可更改物件)是指在建立後其狀態無法修改的物件。這與可變物件(可更改物件)相反,後者可以在建立後進行修改。

那麼,Dart 呢?我們有一些概念非常適合不可變性和內建的不可變物件,從 const 修飾符和 const 建構函式開始。不要與 ES6 中的 const 混淆,後者只是一個不可變的繫結:

在 Dart 中,const 既是一個不可變的繫結,也 一個不可變的物件:

所有字面量(NullStringintdoublenumboolMapListSymbol)都可以是 const,並且可以使用 const 建構函式建立使用者類型:

讓我們回顧一下 - const 實例既是不可變的繫結,並且在語言層面上被 強制 為深度不可變的 - 並且在編譯時被 規範化 - 也就是說,任何兩個實例都被認為是等效的,並且在運行時只由一個實例表示。例如,以下程式碼相當便宜 - 它在執行時只分配一個實例:

想了解更多嗎?閱讀關於 final 和 const 的 Dart 語言導覽

使用 package:meta 進行進一步的靜態檢查

當然,const 有點限制 - 您必須能夠在編譯時建立一個類別 - 因此您不能例如讀取資料庫並在執行時建立 const 物件。我們最近在 package:meta 中引入了 @immutable 註釋:

您可以使用此註釋來幫助確保開發人員保持您的類別深度不可變。它不會像 const 那樣被規範化,但仍然對開發人員很有幫助。

希望這是一個很好的不可變性介紹。如果您想了解更多關於 Dart 或不可變性的資訊,請在評論中或 Twitter 上告訴我。


Dart 的不可變性介紹 最初發佈在 dartlang 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

演進 Dart REPL 概念驗證

Dart REPL 允許您在 互動式 shell 中評估 Dart 表達式和語句。自從我 第一次發佈關於 Dart REPL 的文章 以來已經有一段時間了(您不需要閱讀它就能享受這篇文章),而且仍然缺少許多功能。特別是,動態導入和對頂級宣告的支援將非常有用,所以讓我們來看看如何支援它們。

免責聲明:我的確在 Google 工作,但這篇文章是關於一個個人專案。我不在 Dart 團隊或相關團隊。本文僅包含我個人的淺見。

tl;dr:關於如何運行 Dart REPL 的程式碼和說明可以在 https://github.com/BlackHC/dart_repl 找到。

熱重載

對於 Flutter,Dart VM 中加入了一項很酷的新功能:熱重載。Dart DevSummit 上有一個有趣的 YouTube 影片,詳細解釋並展示了它:

熱重載允許您在程式運行時更改程式碼。Dart VM 將擷取您所做的更改,並嘗試應用它們,同時保持一切正常運行。如果不能,它會告訴您原因。這非常酷!本著駭入 Dart 以做偉大的事情的精神,讓我們思考如何使用它來實作新功能。

為什麼我們不能在目前版本的 REPL 中導入新的函式庫?

REPL 使用 Dart 的 VM 服務來評估表達式。遺憾的是,導入函式庫在 Dart 中不是表達式,因此我們不能僅在該上下文中評估它。但是,我們可以在 REPL 的沙盒運行時更改其程式碼以導入新的函式庫,然後我們可以觸發熱重載來更新 REPL。這樣可行嗎?實際上可行 \o/

具有運行時導入的 Dart REPL

但是等等:我們透過 VM 服務評估表達式無法做的另一件事是建立新的類別和函數。事實上,由於這個原因,Dart 的任何頂級宣告都不能透過評估表達式來執行。

我們如何允許頂級宣告?

當然,我們可以使用上面描述的相同想法來加入新的類別或全域函數。但是,任何使用過 IPython 或類似工具一段時間的人都知道,您往往會在迭代程式碼時經常重新宣告相同的類別或函數。在您試用程式碼時,您會一遍又一遍地重新執行略微修改過的相同程式碼版本。

如果我們只是將這些宣告加入到我們的沙盒 Dart 函式庫中,則需要我們記錄在檔案中何時何地宣告了什麼,以便在您迭代它時更新宣告。這需要大量的邏輯和聰明的程式碼。遺憾的是,如果對類別的更改與舊程式碼或其他宣告不相容,它也容易損壞。這將阻止 REPL 熱重載,並迫使用戶重新啟動它 :( 這聽起來很複雜且脆弱:我認為這不是一個成功的組合!

Spike 和鏈

相反,如果我們可以多次重新定義相同的頂級宣告而不會發生重新宣告衝突怎麼辦?這在 Dart 中可能嗎?當然可以!但不是在同一個函式庫中 :) Dart 允許您導入一個函式庫,然後宣告一個遮蔽現有宣告的類別、函數或全局變數。

在此範例中,b.dart 的 MyClass 遮蔽 a.dart 的版本不會有任何抱怨,因為它們位於不同的函式庫中,並且 b.dart 中的本地宣告優先於從 a.dart 導入的宣告。

一般來說,當您宣告一個變數來隱藏來自外部作用域的另一個變數時,就會發生遮蔽。例如:

我們可以使用這個嗎?為了研究它,我實作了一個快速 spike 在這裡。它不會產生任何程式碼。相反,這是一個非常愚蠢的範例,用於確保我們認為可行的方法實際上可行。如果花費大量時間使用程式碼生成來實作它,卻發現它永遠不可能工作,那將會令人沮喪!這是它的要點:

這確實可行!我們可以建立一個相互導入的函式庫鏈(並且也相互匯出,因為否則符號將不會在任何地方都可用)。然後,用戶可以根據需要重新定義符號。顯然,這可能導致舊程式碼引用被遮蔽的符號,這可能會使事情稍微混亂,但至少它不會損壞。任何使用過 IPython 或類似工具的人也都學會了忍受它。它不可能那麼糟糕。

上圖顯示了它是如何工作的:當我們加入新的頂級宣告時,會建立新的「單元格」(Dart 函式庫),這些單元格導入(並匯出)前一個單元格。最終單元格被導入到沙盒函式庫中,該函式庫用作普通 Dart 表達式和語句的執行環境。沙盒檔案被就地編輯,然後使用熱重載重新載入。

工作流程願景

此外,如果您想在不遮蔽任何內容的情況下連續更新程式碼,這也是可能的:熱重載已經允許在普通 Dart 程式中使用此工作流程。您可以在 REPL 中執行相同的操作。您可以編輯您的神奇 Dart 函式庫 amazing_dart_library.dart 並將其導入 REPL,試用它,並且在您這樣做的同時,您可以在您選擇的編輯器中編輯程式碼,並讓 REPL 在您想要時透過調用 reload() 熱重載程式碼。兩全其美 \o/

我們如何在實踐中實作它?

好吧,我們在這裡駭入 Dart,所以讓我們看看:熱重載尚未得到 vm_service_client 的支援,因為它是一個如此新的功能,並且 服務規範 尚未完全完成。我開始為 Natalie(維護者)撰寫 一個 pull request 來加入對它的支援,但實際上,正如我的同事所知:生產品質的程式碼不是我的專長,尤其不是在我的業餘時間(對不起,Natalie!)。但是,這並不會阻止我們的駭客冒險。

Pub,Dart 的套件管理系統,不僅支援自動版本約束解析和套件的集中式儲存庫,還允許您 使用本地套件或直接依賴 GitHub。通常不建議這樣做,因為您會失去許多使 pub 變得偉大的東西,但在這裡它可以工作:我只是將 vm_service_client fork 到我自己的 GitHub clone 中,並進行了必要的更改。您可以在 https://github.com/BlackHC/vm_service_client/tree/reload_sources_poc 找到程式碼。之後,我更改了 Dart REPL 的 pubspec.yaml 以連結到我的 GitHub clone,而不是官方版本:

就是這樣!在終端機中簡單的 pub get 現在會更新 Dart REPL 以使用 fork 的版本。

這使得試驗任何東西都非常容易:您可以 fork 其他套件來試用,並且輕鬆地依賴它們。而且很酷的是,我可以發佈它,當您使用 pub 為自己下載 REPL 時,它也會從 GitHub 獲取程式碼。非常容易駭入,但也可以分享!(即使通常不建議用於生產套件 :) )

主要的一點是有趣的邏輯是單元格生成器,它實作了一個非常簡單的模板機制,與我們上面討論的一致:

當需要新的導入時,會從 REPL 中調用熱重載功能:

差不多就是這樣!您可以在 pull request 中查看所有更改:https://github.com/BlackHC/dart_repl/pull/2
我承認程式碼有點駭客,而且不整潔。在 pull request 中,REPL 和沙盒之間的消息傳遞也有一些無關的包裝程式碼。遺憾的是,這有點掩蓋了主要的更改。我需要看看我們如何重構所有這些,讓它再次變得更整潔…但有時,快速讓事情運行起來比撰寫最好的程式碼和 pull request 更容易。對此感到抱歉!

帶有頂級宣告的 Dart REPL

Dart REPL 的原始碼可以在 https://github.com/BlackHC/dart_repl 找到。除了支援頂級宣告之外,我還加入了對內建 importloadPackagereload 命令的支援。(請注意:loadPackage 需要即將發佈的 Dart SDK 1.24 開發構建版本。否則它就是一個無操作。)這些內建命令都是使用熱重載的簡單擴展。最後,為了從您的本地 pub 快取中載入新的套件,我使用了優秀的 pub_cache 套件。

要試用它(並假設您已經安裝了 Dart SDK),只需運行:

1
2
pub global activate dart_repl
pub global run dart_repl

感謝您閱讀到本文的結尾!請告訴我您的想法 :)

乾杯,
Andreas


演進 Dart REPL 概念驗證 最初發佈在 dartlang 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

從零到一學習 Flutter,第二部分

如何在跨平台行動應用程式的環境中製作複合圖形物件的動畫?加入一位熱衷於概念挖掘者的行列,學習如何將 tween 概念應用於結構化值的動畫,例如長條圖。提供完整的程式碼範例,電池已包含在內。

您如何進入一個新的程式設計領域?實驗顯然是關鍵,學習和模仿更有經驗的同行編寫的程式也是如此。我個人喜歡用概念挖掘來補充這些方法:試圖從基本原則出發,識別概念,探索它們的優勢,刻意尋求它們的指導。這是一種理性主義的方法,它不能獨立存在,但它會激發智慧,並可能更快地引導您獲得更深層次的見解。

這是 Flutter 及其 Widget 和 tween 概念介紹的第二部分,也是最後一部分。在 第一部分 的結尾,我們得到了一個 Widget 樹,其中包含各種佈局和狀態處理 Widget,

  • 一個用於使用自訂、動畫感知繪圖程式碼繪製單個長條的 Widget,
  • 一個用於啟動長條高度動畫變化的浮動動作按鈕 Widget。

動畫是使用 BarTween 實現的,我聲稱 tween 概念可以擴展以處理更複雜的情況。在第二部分中,我將通過將設計推廣到具有更多屬性的長條,以及包含各種配置的多個長條的長條圖來實現這一主張。

讓我們從為單個長條添加顏色開始。我們在 Bar 類別的 height 欄位旁邊添加一個 color 欄位,並更新 Bar.lerp 以對它們進行 lerp。這種模式很典型:

通過對應的組件進行 lerp 來在複合值之間進行 lerp。

回想一下第一部分,「lerp」是「線性插值」的縮寫。

請注意這裡靜態 lerp 方法慣用語法的效用。沒有 Bar.lerplerpDouble(在道德上是 double.lerp)和 Color.lerp,我們就必須通過為高度建立一個 Tween<double> 和為顏色建立一個 Tween<Color> 來實現 BarTween。這些 tween 將是 BarTween 的實例欄位,由其建構函式初始化,並在其 lerp 方法中使用。我們將在 Bar 類別之外多次複製關於 Bar 屬性的知識。我們的程式碼的維護者可能會覺得這不太理想。

為長條高度和顏色製作動畫。

為了在我們的應用程式中使用彩色長條,我們將更新 BarChartPainter 以從 Bar 獲取長條顏色。在 main.dart 中,我們需要能夠建立一個空的 Bar 和一個隨機的 Bar。我們將為前者使用完全透明的顏色,為後者使用隨機顏色。顏色將取自一個簡單的 ColorPalette,我們將在它自己的檔案中快速介紹它。我們將使 Bar.emptyBar.random 成為 Bar 上的工廠建構函式(程式碼清單)。

長條圖涉及各種配置中的多個長條。為了慢慢引入複雜性,我們的第一個實作將適用於顯示固定類別集的數量的長條圖。範例包括每週的訪客數或每季的銷售額。對於此類圖表,將資料集更改為另一週或另一年不會更改使用的類別,只會更改每個類別顯示的長條。

我們這次將首先更新 main.dart,用 BarChart 替換 Bar,用 BarChartTween 替換 BarTween程式碼清單)。

為了讓 Dart 分析器滿意,我們在 bar.dart 中建立 BarChart 類別,並使用固定長度的 Bar 實例列表來實現它。我們將使用五個長條,每個長條代表一週中的一天。然後,我們需要將建立空實例和隨機實例的職責從 Bar 移動到 BarChart。對於固定類別,一個空的長條圖合理地被視為一個空長條的集合。另一方面,讓隨機長條圖成為隨機長條的集合會使我們的圖表變得相當像萬花筒。相反,我們將為圖表選擇一個隨機顏色,並讓每個長條(仍然是隨機高度)繼承該顏色。

BarChartPainter 將可用寬度均勻分佈在各個長條之間,並使每個長條佔用其可用寬度的 75%。

固定類別長條圖。

請注意 BarChart.lerp 是如何根據 Bar.lerp 實現的,動態重新生成列表結構。固定類別長條圖是複合值,對其進行直接的組件式 lerp 是有意義的,就像對具有多個屬性的單個長條一樣。

這裡有一個模式在起作用。當 Dart 類別的建構函式接受多個參數時,您通常可以單獨對每個參數進行 lerp,並且組合也會看起來不錯。而且您可以任意嵌套此模式:儀表板將通過對其組成長條圖進行 lerp 來進行 lerp,而長條圖將通過對其長條進行 lerp 來進行 lerp,而長條將通過對其高度和顏色進行 lerp 來進行 lerp。顏色是通過對其 RGB 和 alpha 組件進行 lerp 來進行 lerp 的。在這個遞迴的葉子上,我們對數字進行 lerp。

具有數學傾向的人可能會表達這一點,即 lerp 與結構的可交換性,因為對於複合值 C( x, y),我們有

lerp( C( x 1, y 1), C( x 2, y 2), t) == C( lerp( x 1, x 2, t), lerp( y 1, y 2,  t))

正如我們所見,這可以很好地從兩個組件(長條的高度和顏色)推廣到任意多個組件(固定類別長條圖的 n 個長條)。

然而,在某些情況下,這幅美麗的畫面會崩潰。我們可能希望在兩個並非以完全相同的方式組成的值之間製作動畫。舉一個簡單的例子,考慮從一個包含五個工作日資料的長條圖到一個包含週末的圖表之間的動畫。

您可能會很容易地想出幾種不同的臨時解決方案,然後可能會詢問您的 UX 設計師在它們之間進行選擇。這是一種有效的方法,儘管我認為在討論過程中牢記這些不同解決方案的共同基本結構是有好處的:tween。回想一下第一部分:

通過在動畫值從零到一的過程中描繪所有 T 的空間中的路徑來為 T 製作動畫。使用 Tween 對路徑進行建模。

與 UX 設計師要回答的核心問題是:五個長條的圖表和七個長條的圖表之間的中間值是什麼?一個顯而易見的選擇是使用六個長條,但我們需要比這更多的中間值才能使動畫平滑地進行。我們需要以不同的方式繪製長條,超越等寬、均勻間距、適合 200 像素的領域。換句話說,T 值的空間必須被推廣。

通過將具有不同結構的值嵌入到更通用的值空間中來對它們進行 lerp,將兩個動畫端點和所有需要的中間值都包含在內。

我們可以分兩步完成這項工作。首先,我們將 Bar 推廣到包含其 x 坐標和寬度作為屬性:

其次,我們使 BarChart 支援具有不同長條數的圖表。我們的新圖表將適用於長條 i 代表產品發佈後第 i 天的銷售額等系列中的第 i 個值的資料集。作為程式設計師計數,任何此類圖表都涉及一個長條,用於每個整數值 0.._n_,但長條數 n 可能在不同圖表之間有所不同。

考慮兩個分別有五個和七個長條的圖表。它們五個共同類別 0..5 的長條可以像我們上面看到的那樣組合地進行動畫製作。索引為 5 和 6 的長條在另一個動畫端點中沒有對應的長條,但由於我們現在可以自由地為每個長條指定其自己的位置和寬度,我們可以引入兩個不可見的長條來扮演這個角色。視覺效果是長條 5 和 6 在動畫進行時逐漸變為最終外觀。以相反的方向製作動畫,長條 5 和 6 將會縮小或淡出到不可見。

通過 lerping 相應的組件在複合值之間進行 lerp。如果一個端點中缺少一個組件,請在其位置使用一個不可見的組件。

通常有多種方法可以選擇不可見的組件。假設我們友好的 UX 設計師決定使用零寬度、零高度的長條,其 x 坐標和顏色繼承自它們的可見對應物。我們將向 Bar 添加一個方法,用於建立給定實例的這種摺疊版本。

將上述程式碼整合到我們的應用程式中涉及為此新設定重新定義 BarChart.emptyBarChart.random。一個空的長條圖現在可以合理地被視為包含零個長條,而一個隨機的長條圖可能包含一個隨機數量的長條,所有長條都具有相同的隨機選擇的顏色,並且每個長條都具有隨機選擇的高度。但由於位置和寬度現在是 Bar 定義的一部分,因此我們需要 BarChart.random 也指定這些屬性。向 BarChart.random 提供圖表 Size 參數,然後減輕 BarChartPainter.paint 的大部分計算似乎是合理的(程式碼清單)。

Lerp 到/從不可見的長條。

敏銳的讀者可能已經注意到我們上面對 BarChart.lerp 的定義中存在潛在的效率低下問題。我們建立摺疊的 Bar 實例只是為了將它們作為參數提供給 Bar.lerp,並且對於動畫參數 t 的每個值都會重複發生這種情況。以每秒 60 幀的速度,即使對於相對較短的動畫,這也可能意味著大量 Bar 實例被饋送到垃圾收集器。有一些替代方案:

  • 通過在 Bar 類別中只建立一次摺疊的 Bar 實例,而不是在每次調用 collapsed 時都建立一次,可以重複使用它們。這種方法在這裡有效,但並不普遍適用。
  • 可以由 BarChartTween 代為處理重複使用,方法是讓其建構函式建立一個 BarTween 實例列表 _tween,在建立 lerp 長條圖時使用:(i) => _tweens[i].lerp(t)。這種方法打破了始終使用靜態 lerp 方法的慣例。在靜態 BarChart.lerp 中沒有涉及物件來儲存動畫期間的 tween 列表。相比之下,BarChartTween 物件非常適合於此。
  • 可以使用 null 長條來表示摺疊的長條,假設 Bar.lerp 中有適當的條件邏輯。這種方法很巧妙且高效,但確實需要一些小心以避免取消引用或誤解 null。它通常用於 Flutter SDK 中,其中靜態 lerp 方法傾向於接受 null 作為動畫端點,通常將其解釋為某種不可見的元素,例如完全透明的顏色或零大小的圖形元素。作為最基本的例子,lerpDoublenull 視為零,除非兩個動畫端點都是 null

下面的程式碼片段顯示了我們按照 null 方法編寫的程式碼:

我認為可以公平地說,Dart 的 ? 語法非常適合這項任務。但請注意,使用摺疊(而不是例如透明)長條作為不可見元素的決定現在被埋藏在 Bar.lerp 的條件邏輯中。這就是我之前選擇看似效率較低的解決方案的主要原因。與往常一樣,在效能與可維護性的問題上,您的選擇應基於測量結果。

在我們能夠完全概括地處理長條圖動畫之前,還有一步要做。考慮一個使用長條圖來顯示給定年份按產品類別劃分的銷售額的應用程式。使用者可以選擇另一

年,然後應用程式應動畫到該年份的長條圖。如果兩個年份的產品類別相同,或者恰好相同,只是其中一個圖表中右側顯示了一些額外的類別,我們可以使用我們現有的程式碼。但是,如果公司在 2016 年擁有產品類別 A、B、C 和 X,但在 2017 年停止使用 B 並引入了 D,該怎麼辦?我們現有的程式碼將如下所示製作動畫:

1
2
3
4
5
2016  2017
A -> A
B -> C
C -> D
X -> X

動畫可能很漂亮且流暢,但對使用者來說仍然會感到困惑。為什麼?因為它沒有保留語義。它將代表產品類別 B 的圖形元素轉換為代表類別 C 的圖形元素,而 C 的圖形元素則移動到其他位置。僅僅因為 2016 年的 B 恰好繪製在 2017 年的 C 後來出現的相同位置並不意味著前者應該變形為後者。相反,2016 年的 B 應該消失,2016 年的 C 應該向左移動並變形為 2017 年的 C,而 2017 年的 D 應該出現在它的右側。我們可以使用書中最古老的演算法之一來實現這種混合:合併排序列表。

通過對語義上對應的組件進行 lerp 來在複合值之間進行 lerp。當組件形成排序列表時,合併演算法可以使此類組件處於同等地位,根據需要使用不可見的組件來處理單側合併。

我們只需要使 Bar 實例在線性順序中相互可比較即可。然後我們可以如下所示合併它們:

具體來說,我們將為每個長條分配一個排序鍵,形式為整數等級屬性。然後,等級也可以方便地用於從調色板中為每個長條分配顏色,從而使我們能夠在動畫演示中跟蹤各個長條的移動。

現在,隨機長條圖將基於要包含的等級的隨機選擇(程式碼清單)。

任意類別。基於合併的 lerp。

這很有效,但可能不是最有效的解決方案。我們在 BarChart.lerp 中重複執行合併演算法,每次都是針對 t 的每個值。為了解決這個問題,我們將實現前面提到的將可重複使用的資訊儲存在 BarChartTween 中的想法。

我們現在可以移除靜態 BarChart.lerp 方法。

讓我們總結一下到目前為止我們所學到的關於 tween 概念的知識:

通過在動畫值從零到一的過程中描繪所有 T 的空間中的路徑來為 T 製作動畫。使用 Tween 對路徑進行建模。

根據需要推廣 T 概念,直到它包含所有動畫端點和中間值。

通過對應的組件進行 lerp 來在複合值之間進行 lerp。

  • 對應關係應基於語義,而不是偶然的圖形共置。
  • 如果一個動畫端點中缺少一個組件,請在其位置使用一個不可見的組件,該組件可能源自另一個端點。
  • 當組件形成排序列表時,使用合併演算法使語義上對應的組件處於同等地位,根據需要引入不可見的組件來處理單側合併。

考慮使用靜態 Xxx.lerp 方法實現 tween,以方便在複合 tween 實現中重複使用。如果在對單個動畫路徑的 Xxx.lerp 的調用中發生大量重新計算,請考慮將計算移動到 XxxTween 類別的建構函式中,並讓其實例託管計算結果。

有了這些見解,我們終於可以為更複雜的圖表製作動畫了。我們將快速連續地製作堆疊長條圖、分組長條圖和堆疊 + 分組長條圖:

  • 堆疊長條圖用於類別是二維的資料集,並且將長條高度表示的數值相加是有意義的。一個例子可能是每個產品和地理區域的收入。按產品堆疊可以輕鬆比較全球市場中的產品效能。按區域堆疊顯示哪些區域很重要。
堆疊長條圖。
  • 分組長條圖也用於具有二維類別的資料集,但在這種情況下,堆疊長條沒有意義或不可取。例如,如果數值是每個產品和區域的市場份額百分比,則按產品堆疊沒有意義。即使在堆疊有意義的情況下,分組也可能更可取,因為它可以更輕鬆地同時跨兩個類別維度進行定量比較。
分組長條圖。
  • 堆疊 + 分組長條圖支援三維類別,例如每個產品、地理區域和銷售管道的收入。
堆疊 + 分組長條圖。

在所有三個變體中,都可以使用動畫來視覺化資料集的變化,從而引入額外的維度(通常是時間),而不會使圖表變得混亂。

為了使動畫有用而不仅仅是漂亮,我们需要确保我们只在语义上对应的组件之间进行 lerp。因此,用于表示 2016 年特定产品/区域/管道的收入的条形段应变形为表示 2017 年相同产品/区域/管道收入的条形段(如果存在)。

可以使用合并算法来确保这一点。正如您可能从前面的讨论中猜到的那样,合并将在多个级别上进行,反映类别的维度。我们将在堆叠图中合并堆叠和条形,在分组图中合并组和条形,并在堆叠 + 分组图中合并所有三个。

为了在不大量重复代码的情况下实现这一点,我们将合并算法抽象为一个通用实用程序,并将其放在它自己的文件中 tween.dart 中:

MergeTweenable<T> 接口精確地捕獲了通過合併建立兩個 T 的排序列表的 tween 所需的內容。我們將使用 BarBarStackBarGroup 實例化類型參數 T,並使所有這些類型都實現 MergeTweenable<T>

堆疊分組堆疊 + 分組 實作已編寫為可以直接比較。我鼓勵您使用程式碼進行試驗:

  • 更改 BarChart.random 建立的組、堆疊和長條的數量。
  • 更改調色板。對於堆疊 + 分組長條,我使用了單色調色板,因為我認為這樣看起來更好。您和您的 UX 設計師可能不同意。
  • BarChart.random 和浮動動作按鈕替換為年份選擇器,並從實際資料集中建立 BarChart 實例。
  • 實現水平長條圖。
  • 實現其他圖表類型(餅圖、折線圖、堆疊面積圖)。使用 MergeTweenable<T> 或類似方法為它們製作動畫。
  • 添加圖表圖例和/或標籤和軸,然後也為它們製作動畫。

最後兩個要點的任務非常具有挑戰性。祝您玩得開心。


從零到一學習 Flutter,第二部分 最初發佈在 dartlang 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

讓 Dart 網頁應用程式具備離線功能:只需 3 行程式碼

您是否曾經嘗試載入網頁應用程式(例如遊戲或測量轉換器),卻因為網路斷線而無法使用?這是一個糟糕的體驗,但幸運的是,我們擁有讓此類應用程式可供使用者使用的技術。

對於大多數應用程式和遊戲,只需 3 行 Dart 程式碼和在終端機中輸入 1 個指令即可完成。在這篇簡短的文章中,我將引導您完成這些步驟,並確保您可以隨時玩 Pop, Pop, Win!

[Pop, Pop, Win!](https://dart-lang.github.io/sample-pop_pop_win/) - Dart 中的 [踩地雷實作](https://github.com/dart-lang/sample-pop_pop_win/)

Service Worker

Service Worker 是一個在背景執行的 JavaScript 檔案。它可以控制與之關聯的網頁或網站,攔截和修改導航和資源請求,並以非常精細的方式快取資源。

它是一種非侵入式網頁技術:如果瀏覽器 支援 Service Worker,它們可以改善使用者體驗,但網站即使沒有它們也可以正常運作(使用預設的網頁行為)。這是一個有用的屬性,可以啟用**漸進式網頁應用程式 (PWA)**,您可以為大多數使用者提供更進階的功能,同時確保其餘使用者不被鎖定。

作為背景處理執行緒,Service Worker 可以幫助:

  • 離線模式(在網路斷線時從快取中擷取資源)
  • 快取策略(用於近乎即時的快取回應,稍後可以使用新內容進行更新)
  • 推播通知(如在行動應用程式中)
  • 訊息傳遞(如果應用程式在多個分頁中開啟)

對於我們的離線遊戲體驗來說,重要的功能是:我們希望玩 Pop, Pop, Win!,而不是看到這隻恐龍:

有趣的事實:您可以透過按下向上箭頭鍵來玩這隻恐龍。

使用 Dart 建立漸進式網頁應用程式

支援離線模式大致需要以下步驟:

  1. 確定要將哪些資源放入快取以供離線使用。
  2. 建立一個 Service Worker 來準備這些資源的快取。
  3. 註冊 Service Worker,以便可以從離線快取中提供後續請求(以防網路斷線)。
  4. 在該 Service Worker 中,使用 URL 預先填入離線快取,並從快取或網路中處理適當的擷取請求。
  5. 確保 Service Worker 偵測到應用程式或靜態資源的變更,並將新版本放入快取中。

雖然上面的列表可能聽起來有點嚇人,但 Dart 中有一個 pwa 套件可以為我們完成大部分工作,提供高階 API 並自動化大部分工作。

應用程式中的變更

在您的 pubspec.yaml 中匯入 pwa 套件:

1
2
dependencies:
pwa: ^0.1.2

執行 pub get 後,將客戶端新增到您的 web/main.dart 中:

1
2
3
4
5
6
import 'package:pwa/client.dart' as pwa;

main() {
// 註冊 PWA ServiceWorker 以進行離線快取。
new pwa.Client();
}

上面的程式碼透過註冊 Service Worker(我們將在下一步中建立)來處理上面列表中的第 3 項。目前我們沒有將 Client 實例用於其他任何用途,但隨著 pwa 套件獲得新功能,它可能會用於其他用途。

自動生成的漸進式網頁應用程式

pwa 套件提供程式碼生成,處理上面列表中的第 1-2 項和第 4-5 項。為了確保正確使用快取(包括填入和清空快取),請使用以下工作流程:

  1. 建置您的 Web 應用程式,所有靜態資源都位於 build/web 中:
    pub build
  2. 運行程式碼產生器以掃描(或重新掃描)您的離線資源:
    pub run pwa
  3. 再次建置您的專案,因為您需要編譯(新的)pwa.dart 檔案:
    pub build

這些步驟會產生一個名為 lib/pwa/offline_urls.g.dart 的檔案,其中包含要快取的離線 URL 列表。.g.dart 副檔名表示該檔案是生成的,可能會被 pwa 的程式碼產生器工具自動覆蓋。

第一次執行時,此工作流程會產生 web/pwa.dart 檔案,其中包含具有合理預設值的 Service Worker。您可以修改此檔案(例如,自訂離線 URL 或使用高階 API),因為程式碼產生器不會再次更改或覆蓋它。

注意事項

雖然 Dartium 非常適合大多數 Web 開發,但目前很難與 Service Worker 一起使用。我們建議改用 Chrome 或 Firefox。

快取失效是電腦科學中最難的問題之一。底層的 Web Cache API 提供了一些保證,pwa 函式庫也盡力妥善處理邊緣情況,但不要將快取視為任何真正重要內容的可靠儲存空間。在快取可用時使用它,在不可用時優雅地失敗。

試試看

您現在可以部署應用程式的新版本。或者嘗試 離線 Pop, Pop, Win! 遊戲

開啟遊戲並玩一輪後,關閉您的 Wi-Fi 或拔掉網路線,然後重新載入(或重新輸入 URL)。如果您使用的是 Chrome 或 Firefox,您的遊戲應該可以正常執行。祝您好運,玩得開心!


讓 Dart 網頁應用程式具備離線功能:只需 3 行程式碼 最初發佈在 dartlang 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

使用 AngularDart 整合 Google 地圖

本文將介紹如何將 Google 地圖整合到 AngularDart 應用程式中。該應用程式本身非常簡單:它計算地圖上兩個選定標記之間的大圓距離(球體表面上的最短距離)。

在此過程中,您將:

  • 註冊您自己的 Google 地圖 API 金鑰。
  • 建立一個基本的 Angular 網頁應用程式。
  • 將 Dart 與 Google 地圖 JavaScript API 整合,並處理地圖互動。
  • 學習一些 Angular 元件的優化技巧。

本文的篇幅大約是程式碼的四倍。如果您願意,可以直接查看完整的原始碼和最終的Demo

Demo 的螢幕截圖

Google 地圖 JavaScript API

Google 地圖 JavaScript API允許開發人員在其網站上嵌入和整合 Google 地圖。您可以使用自己的圖像、數據和處理來自訂顯示的地圖內容和處理方式。

要開始使用 Google 地圖 API 進行開發,您必須註冊一個免費的 API 金鑰,該金鑰允許合理的使用量。隨著您的應用程式獲得更多關注,您可以將其升級到付費方案。

訪問JavaScript API 頁面,然後在頁面頂部點擊「取得金鑰」。為您的專案建立名稱,然後點擊「建立並啟用 API」:

您的金鑰將在幾秒鐘內啟用並準備使用:

記下您的 API 金鑰。我們將在擷取地圖 JavaScript 函式庫時使用它:

1
https://maps.googleapis.com/maps/api/js?key=您的金鑰

在 JavaScript 中,要建立地圖實例並在其上放置標記,我們將使用以下程式碼:

1
2
3
4
5
6
7
8
9
10
var hostElement = document.getElementById('map-id');
var map = new google.maps.Map(hostElement, {
zoom: 2,
center: {lat: 47.4979, lng: 19.0402}
});
var marker = new google.maps.Marker({
position: {lat: 47.4979, lng: 19.0402},
map: map,
label: 'A'
});

稍後您將看到,Dart 程式碼將非常相似(具有 Dart 的所有額外好處)。

AngularDart 應用程式

開始使用 AngularDart 應用程式的最簡單方法是遵循入門指南,並在 WebStorm 或 IntelliJ IDEA 的社群版中建立新專案

如果您使用的是不同的編輯器,或者您只想遵循我們的範例程式碼的結構,以下是最低要求。

在 pubspec 檔案 pubspec.yaml 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: google_maps_angular_dart
version: 0.0.1
description: 整合 Google 地圖的 Angular 應用程式

environment:
sdk: '>=1.19.0 <2.0.0'

dependencies:
angular2: ^2.2.0
google_maps: ^3.0.0

dev_dependencies:
dart_to_js_script_rewriter: ^1.0.1

transformers:
- angular2:
platform_directives:
- 'package:angular2/common.dart#COMMON_DIRECTIVES'
platform_pipes:
- 'package:angular2/common.dart#COMMON_PIPES'
entry_points: web/main.dart
- dart_to_js_script_rewriter

在主機頁面 web/index.html 中:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<!-- 其他標題 -->
<script src="https://maps.googleapis.com/maps/api/js?key=您的金鑰"></script>
<script defer src="main.dart" type="application/dart"></script>
</head>
<body>
<map-control></map-control>
</body>
</html>

在應用程式的入口點 web/main.dart 中:

1
2
3
4
5
6
import 'package:angular2/platform/browser.dart';
import 'package:google_maps_angular_dart/component/map_control.dart';

void main() {
bootstrap(MapControl);
}

<map-control> 元件的 Dart 檔案 lib/component/map_control.dart 中:

1
2
3
4
5
6
7
8
9
import 'package:angular2/core.dart';

@Component(
selector: 'map-control',
template: '{{distance}}',
)
class MapControl {
String distance = '尚未計算距離';
}

上面的程式碼不會做太多事情,但這是一個開始,您可以使用 pub serve 執行它,然後在 Dartium 中打開頁面:

1
2
3
4
5
$ pub serve
Loading source assets...
Loading angular2 and dart_to_js_script_rewriter transformers...
Serving google_maps_angular_dart web on http://localhost:8080
Build completed successfully

Google 地圖整合

要開始 Google 地圖整合,請將以下 script 標籤放入您的 web/index.html 中。請注意,您需要設定您的 API 金鑰:

1
<script src="https://maps.googleapis.com/maps/api/js?key=[您的金鑰]"></script>

幸運的是,pub 上有一個現成的 Google 地圖 Dart 套件。將其加入到您的 pubspec.yaml 中:

1
2
3
dependencies:
angular2: ^2.2.0
google_maps: ^3.0.0

然後執行 pub get以下載套件。

我們需要在元件模板中為地圖區域建立一個主機元素。我們將從基本樣式開始,並使用 #mapArea 錨點在下一步中識別元素:

1
<div style="width: 300px; height: 300px" #mapArea>[map]</div>

在元件程式碼中,我們可以注入元素的參考,如下所示:

1
2
@ViewChild('mapArea')
ElementRef mapAreaRef;

MapControl 類別建立後,元素參考不會立即提供,因此我們需要連接到 Angular 的生命週期回調:

1
2
3
4
5
6
7
class MapControl implements AfterViewInit {

@override
void ngAfterViewInit() {
// mapAreaRef 現在可用
}
}

提示:使用 IDE 為您撰寫方法主體。例如,在 IntelliJ 中,按 CMD + N(或 CTRL + N),然後選擇「實作方法…」選單項。它將允許您選擇缺少的方法,您只需要擔心方法主體:

如下面的程式碼所示,Dart API 與 JavaScript API 非常相似,額外的好處是它經過類型檢查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MapControl implements AfterViewInit {
// ...

@override
void ngAfterViewInit() {
GMap map = new GMap(
mapAreaRef.nativeElement,
new MapOptions()
..zoom = 2
..center = new LatLng(47.4979, 19.0402)
// ^ 布達佩斯,匈牙利
);
new Marker(new MarkerOptions()
..map = map
..position = new LatLng(47.4979, 19.0402)
..label = 'A');
}
}

類型檢查好處的一個例子是,當我們監聽事件時,我們不需要考慮如何存取屬性。例如,IDE 可以幫助我們找到包含標記位置的 MouseEvent 屬性 (latLng)。

以下程式碼回應拖動標記:

1
2
3
marker.onDrag.listen((MouseEvent event) {
print('拖動時的新位置:${event.latLng}');
});

在地圖上擷取點擊事件類似:

1
2
3
map.onClick.listen((MouseEvent event) {
print('使用者點擊的位置:${event.latLng}');
});

整合在一起

為了測量兩個坐標之間的距離,我們將在地圖上追蹤兩個標記。使用者應該能夠拖動標記或透過點擊放置它們。

我們需要將地圖和標記作為欄位進行追蹤:

1
2
3
GMap _map;
Marker _aMarker;
Marker _bMarker;

更新初始化以儲存地圖參考並註冊點擊處理程式。點擊處理程式會更新標記位置和距離:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@override
void ngAfterViewInit() {
_map = new GMap(
mapAreaRef.nativeElement,
new MapOptions()
..zoom = 2
..center = new LatLng(47.4979, 19.0402)
// ^ 布達佩斯,匈牙利
);
_map.onClick.listen((MouseEvent event) {
_updatePosition(event.latLng);
_updateDistance();
});
}

這是點擊處理程式第一部分的程式碼:

1
2
3
4
5
6
7
8
9
10
void _updatePosition(LatLng position) {
if (_aMarker == null) {
_aMarker = _createMarker(_map, 'A', position);
} else if (_bMarker == null) {
_bMarker = _createMarker(_map, 'B', position);
} else {
_aMarker.position = _bMarker.position;
_bMarker.position = position;
}
}

標記實例化的程式碼與前面的範例類似:

1
2
3
4
5
6
7
8
9
10
11
Marker _createMarker(GMap map, String label, LatLng position) {
final Marker marker = new Marker(new MarkerOptions()
..map = map
..draggable = true
..label = label
..position = position);
marker.onDrag.listen((MouseEvent event) {
_updateDistance();
});
return marker;
}

借助 dart:math 中的工具函數,我們能夠處理大圓距離計算的數學運算,並在我們的 distance 欄位中設定值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// 地球半徑,單位為公里。
const int radiusOfEarth = 6371;

double _toRadian(num degree) => degree * PI / 180.0;

void _updateDistance() {
if (_aMarker == null || _bMarker == null) return;
LatLng a = _aMarker.position;
LatLng b = _bMarker.position;
double dLat = _toRadian(b.lat - a.lat);
double sLat = pow(sin(dLat / 2), 2);
double dLng = _toRadian(b.lng - a.lng);
double sLng = pow(sin(dLng / 2), 2);
double cosALat = cos(_toRadian(a.lat));
double cosBLat = cos(_toRadian(b.lat));
double x = sLat + cosALat * cosBLat * sLng;
double d = 2 * atan2(sqrt(x), sqrt(1 - x)) * radiusOfEarth;
distance = '${d.round()} 公里';
}

您是否曾經想過您住的地方距離親戚、著名地點或地標有多遠?現在是時候試用該應用程式並親自檢查一下了。事實證明,我家距離山景城的 Google 總部 9815 公里:

加州山景城 Googleplex 與作者在匈牙利布達佩斯的住所之間的距離

優化應用程式

此時,我們 Angular 應用程式中的 Google 地圖整合已完成。我們正在監聽和回應地圖事件,並在地圖上建立物件。在最後一部分中,我們將實作一些功能,為我們的 Demo 增添一些美好的修飾。

分離模板和樣式

將複雜的 UI 模板放在單獨的 .html 檔案中是一個好習慣。我們可以用 CSS 樣式做同樣的事情:

1
2
3
4
5
@Component(
selector: 'map-control',
templateUrl: 'map_control.html',
styleUrls: const <String>['map_control.css'])
class MapControl implements AfterViewInit {

地圖元素有一個 CSS 類別 map-area

1
<div class="map-area" #mapArea>[map]</div>

我們的 CSS 檔案可以像這樣簡單:

1
2
3
4
5
.map-area {
width: 500px;
height: 400px;
margin: 10px;
}

處理距離單位

有些人精通公里 ↔ 英里的轉換,但我們其他人想要一個下拉選單,我們可以在其中選擇距離單位。在 HTML 模板中,下拉選單可以是一個簡單的 <SELECT> 元素,模型綁定到 unit 欄位。

1
2
3
4
5
<label>單位:</label>
<select [(ngModel)]="unit">
<option value="km">公里</option>
<option value="miles">英里</option>
</select>

在 Dart 程式碼中,我們想要儲存單位,並在單位更新時更新距離:

1
2
3
4
5
6
7
8
String _unit = 'km';

String get unit => _unit;

set unit(String value) {
_unit = value;
_updateDistance();
}

並且別忘了在距離計算中更新先前硬編碼的公里:

1
2
3
4
5
6
7
8
/// 將公里轉換為英里的常數值。
const double milesPerKm = 0.621371;

// ... 與之前的程式碼相同
if (unit == 'miles') {
d *= milesPerKm;
}
distance = '${d.round()} $unit';

格式化坐標

如果我們想發佈標記的位置怎麼辦?最簡單的方法是在 Dart 中公開位置值,這樣我們就可以在模板中使用 {{a}}{{b}}

1
2
3
// 公開位置值。
LatLng get a => _aMarker?.position;
LatLng get b => _bMarker?.position;

當標記尚未初始化時,模板可以隱藏標籤,防止潛在的空值問題:

1
2
<div *ngIf="a != null">A: {{a}}</div>
<div *ngIf="b != null">B: {{b}}</div>

但是,{{a}} 將會轉換為調用 LatLng.toString(),它會給我們兩個非常長的 double 值,而幾個數字就足夠了。一種解決方案是在模板中使用管道:

1
2
3
<div *ngIf="a != null">A: {{a.lat | number : '1.4-4'}}, {{a.lng | number : '1.4-4'}}</div>
<div *ngIf="b != null">B: {{b.lat | number : '1.4-4'}}, {{b.lng | number : '1.4-4'}}</div>

模板語法指南建議將該邏輯放在控制器類別中,以便更好地進行測試:

1
2
3
4
5
6
7
8
9
10
11
/// 'A' 標記的格式化位置。
String get aPosition => _formatPosition(a);

/// 'B' 標記的格式化位置。
String get bPosition => _formatPosition(b);

String _formatPosition(LatLng pos) {
if (pos == null) return null;
return '${pos.lat.toStringAsFixed(4)}, '
'${pos.lng.toStringAsFixed(4)}';
}

這樣,模板就可以更簡單了:

1
2
<div *ngIf="a != null">A: {{aPosition}}</div>
<div *ngIf="b != null">B: {{bPosition}}</div>

清理模板

最後一步,將所有剩餘的程式碼部分從模板移動到控制器:

1
2
3
4
5
6
7
8
/// 是否應顯示 'A' 標記的位置
bool get showA => a != null;

/// 是否應顯示 'B' 標記的位置
bool get showB => b != null;

/// 是否應顯示 'distance' 標籤
bool get showDistance => distance != null;

這樣做,模板只會參考 getter:

1
2
3
4
5
6
<div *ngIf="showA">A: {{aPosition}}</div>
<div *ngIf="showB">B: {{bPosition}}</div>

<p *ngIf="showDistance">
距離:{{distance}}
</p>

結語

如您所見,使用 Google 地圖實作雙向整合很容易:原始碼乾淨且易讀。在 Dart 工具鏈的全力支援下,它很容易擴展,而無需擔心我們可能會在其他地方造成破壞。

讀者可以練習一下,使用 Dart 中的相同 API 將熱圖視覺化新增到地圖中。查看 google_maps 套件以了解所有可能性。


使用 AngularDart 的 Google 地圖 最初發佈在 Medium 的 dartlang 上,人們在那裡透過突出顯示和回應這個故事來繼續討論。