來自後端的突襲? --開包即食的教程帶你淺嘗最新開源的C# Web引擎 Blazor
在今年年初, 恰逢新春佳節臨近的時候. 微軟給全球的C#開發者們, 著實的送上了一分驚喜. 微軟正式開源Blazor ,將.NET帶回到瀏覽器.
這個小驚喜, 迅速的在dotnet開發者中間傳開了. 而就在昨天(2018年3月22日) Blazor發布了它的第一次Release. Blazor到底是個什麽樣的東西呢?我們是否真的可以攜著C#語言進入前端的市場中? 不如現在就跟我一起體驗dotnet blazor吧.
首先
獲取最新版的dotnet core 並安裝Blazor模板:
- 安裝 最新的.Net Core(版本需要高於2.1.101)
- 對於簡單的嘗試來說, VS code 已經足夠. 所以筆者並沒有親自安裝Visual Studio.
使用命令行初始化項目:
dotnet new -i Microsoft.AspNetCore.Blazor.Templates dotnet new blazor -o BlazorApp1 cd BlazorApp1 dotnet run
- 如果你需要使用Visual Studio,
- 安裝最新的Visual Studio 2017.
- 安裝 ASP.NET Core Blazor Language Services extension
- 在Visual Studio中創建新的測試項目:
- 選擇 File -> New Project -> Web -> ASP.NET Core Web Application
- 確定在Target Framework裏選擇了 .NET Core and ASP.NET Core 2.0.
- 選擇 Blazor 模板
敵後根據地? 如何在前端渲染cshtml
當我們運行起項目之後, 就可以看到如下提示
這個時候我們在瀏覽器裏打開監聽的端口 http://localhost:17477. 就可以看到我們這個項目的網頁了.
這個簡單的示例項目帶了3個頁面
第一個頁面比較簡單, 但先別急,讓我們打開瀏覽器工具. 先看看頁面在加載頁面過程中都加載了什麽
在初次打開頁面的時候, 我們看到的是這樣一個Loading..的頁面. 這個頁面的代碼是這樣的.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>BlazorDemo</title> <base href="/" /> <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" /> </head> <body> <app>Loading...</app> <script src="css/bootstrap/bootstrap-native.min.js"></script> <script src="_framework/blazor.js" main="BlazorDemo.dll" entrypoint="BlazorDemo.Program::Main" references="Microsoft.AspNetCore.Blazor.Browser.dll,Microsoft.AspNetCore.Blazor.dll,Microsoft.Extensions.DependencyInjection.Abstractions.dll,Microsoft.Extensions.DependencyInjection.dll,mscorlib.dll,netstandard.dll,System.Core.dll,System.Diagnostics.StackTrace.dll,System.dll,System.Globalization.Extensions.dll,System.Net.Http.dll,System.Runtime.Serialization.Primitives.dll,System.Security.Cryptography.Algorithms.dll" linker-enabled="true"></script></body> </html>
可以看到這個頁面加載了兩個js, 第一個是bootstrap的, 第二個叫做blazor.js. 只不過這個js有非常多的參數, 有 main, entrypoint, 和 references. 看看References裏寫的是不是很熟悉? 一看就是.net的dll.
blazor.js 加載了mono.js, mono.js 加載了mono.wasm. 這個是個什麽文件?
wasm代表的就是Web Assembly, 簡單地說它就是編譯好的二進制文件, 可以由瀏覽器直接運行, 源語言可以是C/C++或者任何可以編譯到Web Assembly的文件, 而這裏我們加載的就是mono 編譯好的Web Assembly文件, 它被加載之後, 相當於瀏覽器中啟動了一個mono 運行環境.
隨後的兩個js 是筆者chrome瀏覽器插入的js, 在這裏不要被他們幹擾了. 那麽mono 運行時加載完成之後. 就需要加載dotnet 的Dll了, 首先是入口庫, 接著就是需要的引用庫
好家夥 1.9MB. 當所有的Dll被下載完畢之後, 這個時候我們的瀏覽器就可以運行我們這個dotnet的網頁了. 於是就回到了我們最開始看到的那個應用程序.
所以 總結一下 blazor.js 調用mono.js, mono.js加載mono.wsam, 然後根據寫在script標簽裏的內容繼續的加載dotnet的庫文件. 如果瀏覽器不支持wsam, 就會嘗試使用asm.js加載mono.asm.js
柳暗花明又一村,Blazor的模板究竟是怎樣的.
我們已經知道,經過前面的步驟,瀏覽器裏已經運行了一個.Net 運行時了. 而且加載了項目必須的dll. 那麽這樣一個簡單的程序,它的代碼究竟是怎麽樣的呢?
打開項目代碼,映入眼簾的是一個標準的.net Project
_ViewImports.cshtml包含了項目一些其他頁面中最常使用的namespace
1 2 3 4 5 6 7 |
@ using System.Net.Http
@ using Microsoft.AspNetCore.Blazor
@ using Microsoft.AspNetCore.Blazor.Components
@ using Microsoft.AspNetCore.Blazor.Layouts
@ using Microsoft.AspNetCore.Blazor.Routing
@ using BlazorDemo
@ using BlazorDemo.Shared
|
Program.cs是程序的入口點
using Microsoft.AspNetCore.Blazor.Browser.Rendering; using Microsoft.AspNetCore.Blazor.Browser.Services; using System; namespace BlazorDemo { class Program { static void Main(string[] args) { var serviceProvider = new BrowserServiceProvider(configure => { // Add any custom services here }); new BrowserRenderer(serviceProvider).AddComponent<App>("app"); } } }
在入口點中, 我們註冊了一個瀏覽器渲染服務 BrowserRender,讓他渲染App
App.cshmtl是這樣的
<Router AppAssembly=typeof(Program).Assembly />
這裏的Router對應的是Microsoft.AspNetCore.Blazor.Routing.Router. 當給它一個AppAssembly時, 他就會自動的把當前的Url 和 AppAssembly的其他Pages對應起來.
所以 當我們在瀏覽器裏輸入 /Counter時,他就會加載Pages/Couter.cshtml.
Shared文件夾裏分別是布局文件,導航欄, 還有一個我們自定義的控件 SurveyPrompt.
熟悉Razor引擎的小夥伴們一定很輕車熟路了. 那麽當我們打開網站時, 默認顯示給我們的 就是Index, 這個時候我們會加載Pages/Index.cshtml
Index.cshtml的代碼是這個樣子的
@page "/" <h1>Hello, world!</h1> Welcome to your new app. <SurveyPrompt Title="How is Blazor working for you?" />
@page 可以告訴Router, 當前頁面註冊到 "/"
除了顯示hello world以外, 我們在這裏還看到了剛剛說到的第三方控件. SurveyPrompt. 果然不簡單嘛, 一個看似簡單的頁面, 居然還告訴了我們如何使用自定義控件.
從聲明上看, 我們知道 SunveyPrompt是一個控件,並且有一個屬性Title. 現在我們打開它的代碼
<div class="alert alert-survey" role="alert"> <span class="glyphicon glyphicon-ok-circle" aria-hidden="true"></span> <strong>@Title</strong> Please take our <a target="_blank" class="alert-link" href="https://go.microsoft.com/fwlink/?linkid=870381"> brief survey </a> and tell us what you think. </div> @functions { // This is to demonstrate how a parent component can supply parameters public string Title { get; set; } }
我們可以看到代碼分為兩部分, @functions上面是類似html的東西, 下面是類似C#的東西. 熟悉React或者Vue的夥伴們恐怕不會對這種混寫感到陌生. 這個就是Blazor的語法. Html部分很像使Razor的模板方式. 而最後整個頁面都會被編譯成一個類, 這個類派生自 Component. 如果你編譯過項目, 你會在Debug下面的Shared目錄找到一個叫SurveyPrompt.g.cs的東西
#pragma checksum "/Users/pzhi/SCM/gitHub/zhipu123/BlazorDemo/Shared/SurveyPrompt.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "a2a2ea88635b799343bc6d9647bbb818c8a20c9d" // <auto-generated/> #pragma warning disable 1591 namespace BlazorDemo.Shared { #line hidden using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Net.Http; using Microsoft.AspNetCore.Blazor; using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Layouts; using Microsoft.AspNetCore.Blazor.Routing; using BlazorDemo; using BlazorDemo.Shared; public class SurveyPrompt : Microsoft.AspNetCore.Blazor.Components.BlazorComponent { #pragma warning disable 1998 protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder) { base.BuildRenderTree(builder); builder.OpenElement(0, "div"); builder.AddAttribute(1, "class", "alert alert-survey"); builder.AddAttribute(2, "role", "alert"); builder.AddContent(3, "\n "); builder.OpenElement(4, "span"); builder.AddAttribute(5, "class", "glyphicon glyphicon-ok-circle"); builder.AddAttribute(6, "aria-hidden", "true"); builder.CloseElement(); builder.AddContent(7, "\n "); builder.OpenElement(8, "strong"); builder.AddContent(9, Title); builder.CloseElement(); builder.AddContent(10, "\n\n Please take our\n "); builder.OpenElement(11, "a"); builder.AddAttribute(12, "target", "_blank"); builder.AddAttribute(13, "class", "alert-link"); builder.AddAttribute(14, "href", "https://go.microsoft.com/fwlink/?linkid=870381"); builder.AddContent(15, "\n brief survey\n "); builder.CloseElement(); builder.AddContent(16, "\n and tell us what you think.\n"); builder.CloseElement(); builder.AddContent(17, "\n\n"); } #pragma warning restore 1998 // This is to demonstrate how a parent component can supply parameters public string Title { get; set; } } } #pragma warning restore 1591
我們發現@functions裏面的內容 會作為這個類的成員變量和 成員方法, 而上面的內容則被編譯到了BuildRenderTree方法中.
那麽到了這裏我們大概知道了這個簡單的HomePage都有什麽玄機了. 我們也大概知道了Blazor的語法, 也知道其實我們所有的頁面最終都會是一個Componet.
那麽什麽是Componet呢? 在這裏並不想過多的去筆墨介紹這個概念. 如果你是一個Vue或者React的開發, 你應該對這個模塊化開發不陌生. 一個Componet, 就是滿足一定的功能, 有自己的屬性, 狀態. 可以展示特定數據的元素.
就如同我們這裏的SurveyPrompt, 接受一個Title屬性,並且負責把他展示成這樣子
數據驅動? Blazor的刷新和綁定機制初探
現在我們知道了一個簡單的頁面是如何渲染出來的. 那麽讓我們打開Counter這個配置來看一看. 數據是如何交互的
我們第二個page張這樣子
有一個button, 大聲的叫我們點它. 當我們點擊的時候. 上面的current count 變成了 1
這一切是怎麽發生的呢? 以下是Counter.cshtml的代碼
@page "/counter" <h1>Counter</h1> <p>Current count: @currentCount</p> <button @onclick(IncrementCount)>Click me</button> @functions { int currentCount = 0; void IncrementCount() { currentCount++; } }
我們看到 這個頁面非常簡單, 我們定義了一個CurrentCount的Field, 然後在IncreaseCount方法裏給它加一. 一個叫Click me的button標簽裏 有一個@onclick方法, 將IncreaseCount作為參數
Counter.cshtml編譯後的代碼張這樣
#pragma checksum "/Users/pzhi/SCM/gitHub/zhipu123/BlazorDemo/Pages/Counter.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "05ad2dd449cbc9f09f8b759e1f06e7eb5e9583b4" // <auto-generated/> #pragma warning disable 1591 namespace BlazorDemo.Pages { #line hidden using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Net.Http; using Microsoft.AspNetCore.Blazor; using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Layouts; using Microsoft.AspNetCore.Blazor.Routing; using BlazorDemo; using BlazorDemo.Shared; [Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute(typeof(MainLayout))] [Microsoft.AspNetCore.Blazor.Components.RouteAttribute("/counter")] public class Counter : Microsoft.AspNetCore.Blazor.Components.BlazorComponent { #pragma warning disable 1998 protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder) { base.BuildRenderTree(builder); builder.OpenElement(0, "h1"); builder.AddContent(1, "Counter"); builder.CloseElement(); builder.AddContent(2, "\n\n"); builder.OpenElement(3, "p"); builder.AddContent(4, "Current count: "); builder.AddContent(5, currentCount); builder.CloseElement(); builder.AddContent(6, "\n\n"); builder.OpenElement(7, "button"); builder.AddAttribute(8, onclick(IncrementCount)); builder.AddContent(9, "Click me"); builder.CloseElement(); builder.AddContent(10, "\n\n"); } #pragma warning restore 1998 int currentCount = 0; void IncrementCount() { currentCount++; } } } #pragma warning restore 1591
我們看到 @onclick其實在這裏就是執行了一個Component的一個方法onclick, 顧名思義,當這個Component被點擊的時候就被調用. 我們的IncreaseCount被作為參數傳給了它, 可見onclick會在被點擊的時候執行IncreaseCount.
那麽問題來了,當我們執行了IncreaseCount方法時, 頁面怎麽會知道要不要刷新? 是刷新整個頁面還是刷新所有?
熟悉WPF的同學可能知道, 在WPF中如果我們需要讓一個ViewModel可以被監聽變化, 他就需要實現INotifyChanged事件. 那麽同樣道理, 我們的這個IncreaseCount可能也是類似的嗎?
然而基於編譯後的代碼我們可以發現 CurrentCount作為我們Counter這個類的Field, 並沒有任何機會高速Page自己變化了. 而且這個Field非常普通,也不是什麽WPF中的DP, 所以到目前為止變化是怎麽通知的.並沒有一個合理的解釋. 後面的時間裏我會嘗試閱讀Blazor的代碼搞清楚這件事情.
第一個問題畫個問號, 那麽第二個問題呢?
打開瀏覽器工具, 定位到button, 再次點擊button觀察dom的反應.
我們看到 在點擊Button的時候, button上面的<p>標簽閃動了, 說明它被刷新了, 而其他標簽並沒有. 所以局部刷新的功能是有的. 效率問題不用擔心了.
編輯Click me, 把他的內容變成 "點擊我", 再次點擊按鈕, 我們看到還是只有p變, 而且button也沒有變回原來的內容
所以我們知道, 這個局部刷新不是簡單的拿Dom作比較, 肯定是有Virtual Dom的機制在裏面.
星星之火,可燎原?
在簡單的嘗試了Blazor之後, 還是很興奮的. 可以看到Blazor是一個初具規模的產品. 我們C#開發可以用Blazor在今後寫前端渲染的網頁了!
我很期望這樣一個產品能夠持續的演進下去.
就目前版本看(0.1.0), Blazor尚不能應用到產品中. 主要還是有以下的原因
- 打包大小太大, 1.8M的大小對於網站簡直是致命的.
- 產品還不成熟, 現在Component還只能支持簡單的事件, 筆者測試的時候只有onclick,onchange.
- 兼容性差,使用了WebAssembly,就註定了兩年前的瀏覽器必定不能支持.
當然我們還是不能否認, Blazor為如何讓更多語言進入前端世界打開了一扇新的大門. 也許未來JavaScript將不僅僅是前端唯一可以使用的利器. 我們會看到C/C++, Python, Java寫的前端渲染頁面也不一定呢.
當然在後端語言打入前端世界的道路上, WebAssembly也未必是唯一的路勁, 比如Scala.js就完全使用了js重寫了Scala的庫函數, 類似的還有Kotlin.js. 可以看到雖然JavaScript已經非常Fancy了,但是後端程序員們進軍前端的熱情可謂從未停歇過啊.
祝dotnet的應用越來越廣, 祝廣大後端程序員們新年成就慢慢, 加薪升職.
部分 FAQ(翻譯自Blazor官方)
Q:Is this Silverlight all over again? 這又是一個Silverlight嗎?
No, Blazor is a .NET web framework based on HTML and CSS that runs in the browser using open web standards. It requires no plugin and works on mobile devices and older browsers.
當然不是了, Blazor完全是基於Css 和 Html展現的, 它的運行不需要任何插件.
Q:What features will Blazor support?Blazor都會支持什麽功能?
Blazor會支持現在主流單頁面Web Application所應具有的功能:
- A component model for building composable UI 一個面向組件化UI構建而設計的控件模型
- Routing 路由
- Layouts 布局
- Forms and validation 表單驗證
- Dependency injection 依賴註入
- JavaScript interop 與JavaScript的互操作
- Live reloading in the browser during development 開發過程中瀏覽器的熱重載
- Server-side rendering 後端渲染(前後端均可渲染頁面)
- Full .NET debugging both in browsers and in the IDE 在瀏覽器和IDE中都可以獲得全面的.Net 調試支持
- Rich IntelliSense and tooling 豐富的代碼提示和輔助工具
- Ability to run on older (non-WebAssembly) browsers via asm.js 在老舊瀏覽器上能通過 asm.js加載網頁
- Publishing and app size trimming 進一步縮減發布網站時的大包大小
Q: Can I access the DOM from a Blazor app? 我可以在Blazor中操作DOM嗎
You can access the DOM through JavaScript interop from .NET code. However, Blazor is a component based framework that minimizes the need to access the DOM directly.
你可以使用JavaScript互操作層操縱DOM. 但是請註意, Blazor是一個模塊化渲染的框架, 他的目的就是盡可能減少直接操縱DOM
最後軟廣一波,
別具一格,追求卓越的ThoughtWorks 2018全面招人啦!
不關你是前端, 後端, 開發,測試, 產品經理, BA, 還是運維, 我們這裏有號稱最難的面試就問你敢不敢 來挑戰?
想來的同學們請留言或者發信哈 我的郵箱是 [email protected]
來自後端的突襲? --開包即食的教程帶你淺嘗最新開源的C# Web引擎 Blazor