[MAUI]深入瞭解.NET MAUI Blazor與Vue的混合開發

2023-10-18 18:00:09

@


.NET MAUI結合Vue的混合開發可以使用更加熟悉的Vue的語法代替Blazor語法,你現有專案不必重寫。之前寫過一篇[MAUI] 在.NET MAUI中結合Vue實現混合開發 ,其中介紹瞭如何建立一個vue應用並將其打包至MAUI專案,這種方式依賴vue-cli建立和打包靜態站點,好處是可以使用Node.js 的構建但MAUI僅僅作為容器。開發應用需要一個獨立的host專案

這次用整合的方式。將vue作為MAUI的一部分,這樣就可以在MAUI專案中直接使用vue了。

Vue在混合開發中的特點

首先要說的是,Vue框架是漸進性的,所謂漸進性,就是Vue不會強求你使用所有的框架特性,你可以根據需要逐步使用。

同樣地,element-ui也可以通過引入樣式和元件庫,配合Vue使用

因此我們不需要Vue Router、Vuex、Vue CLI、單檔案元件這些高階特性,僅僅引入Vue.js即可使用Vue模板語法。我們將利用Blazor引擎的如下功能:

  • 元件化開發
  • 靜態資源管理
  • js程式碼的注入
  • js呼叫C#程式碼
  • C#呼叫js程式碼

由.NET MAUI提供的功能:

  • 路由管理
  • 狀態管理

由Vue提供模板語法,事件處理,計算屬性/偵聽器等,以及Element-UI提供互動元件。

建立MAUI專案

建立一個MAUI專案,這裡使用的是Visual Studio 2022 17.7.3,建立一個Blazor MAUI App專案命名MAUI-Vue-Hybriddev-Integrated,選擇Android和iOS作為目標平臺,選擇.NET 7.0作為目標框架。

Vue官網下載最新的Vue.js

將其放置在wwwroot目錄下,然後在index.html中引入

    <script src="lib/vuejs/vue.js"></script>

建立Vue應用

在Views目錄下建立 HomePage.xaml作為Vue應用的容器,在頁面中建立<BlazorWebView>檢視元素,並設定HostPagewwwroot/index.html,這樣就可以在MAUI中使用Vue了。

<BlazorWebView x:Name="blazorWebView"
               HostPage="wwwroot/index.html">
    <BlazorWebView.RootComponents>
        <RootComponent Selector="#app"
                       x:Name="rootComponent"
                       ComponentType="{x:Type views:HomePageWeb}" />
    </BlazorWebView.RootComponents>
</BlazorWebView>

每個BlazorWebView控制元件包含根元件(RootComponent)定義,ComponentType是在應用程式啟動時載入頁面時的型別,該型別需要繼承自Microsoft.AspNetCore.Components.IComponent,由於我們的導航是由MAUI處理的,因此我們不需要使用Blazor路由,直接使用Razor元件

在Views目錄下建立HomePageWeb.razor,這是Vue應用頁面相當於Vue的單檔案元件,這裡可以使用Vue的模板語法,而不是Blazor的Razor語法。

我們在HomePageWeb.razor中寫下Vue官方檔案中Hello Vue範例程式碼


<div id="vue-app">
    {{ message }}
</div>


<script type="text/javascript">
    var app = new Vue({
        el: '#vue-app',
        data: {
            message: 'Hello Vue!',
        }
    })

</script>

注意:Vue的根元素名稱不要跟Blazor的根元素名稱相同,否則會報錯。

此時更改JavaScript裡的內容,你會發現Blazor頁面不會熱載入。

請勿將 <script> 標記置於 Razor 元件檔案 (.razor) 中,因為 <script> 標記無法由Blazor 動態更新。

於是需要將script部分程式碼放置在外部,此時有兩種方案,一個是放在wwwroot/js目錄下,然後在wwwroot/index.html中引入,還有一種是使用並置的js檔案,這種方式是所謂的"CodeBehind",因為更利於組織程式碼,這裡我們使用並置的js檔案。

建立一個HomePageWeb.razor.js檔案,將script部分程式碼放置在其中,然後在HomePageWeb.razor中引入

protected override async Task OnAfterRenderAsync(bool firstRender)
{

    if (firstRender)
    {
        await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./Views/HomePageWeb.razor.js");
    }
}

釋出應用後,框架會自動將指令碼移動到 Web 根目錄。 在此範例中,指令碼被移動到./wwwroot/Views/HomePageWeb.razor.js

使用element-ui元件庫

同樣,我們在element-ui官方CDN下載樣式檔案和元件庫,首先在index.html中引入樣式和元件庫

<link href="css/app.css" rel="stylesheet" />
...
<script src="lib/element-ui/index.js"></script>

然後在HomePageWeb.razor中使用元件

<div id="vue-app">
    {{ message }}
    <el-input v-model="input" placeholder="請輸入內容"></el-input>
    <el-button @click="showDialog = true">提交</el-button>
    <el-dialog :visible.sync="showDialog" title="訊息">
        <p>{{input}}</p>
        <p>提交成功</p>
    </el-dialog>
</div>

CodeBehind中引入元件

var app = new Vue({
    el: '#vue-app',
    data: {
        message: 'Hello Vue!',
        showDialog: false,
        input: 'text message from vue'
    }
})

執行效果如下:

JavaScript和原生程式碼的互動

Blazor元件中的程式碼可以通過注入IJSRuntime來呼叫JavaScript程式碼,JavaScript程式碼可以通過呼叫DotNet.invokeMethodAsync來呼叫C#程式碼。

傳遞根元件引數

如果被呼叫的程式碼位於其他類中,需要給這個Blazor元件傳遞範例,還記得剛才提及的根元件(RootComponent)嗎?我們用它來傳遞這個範例,稱之為根元件引數,詳情請檢視官方檔案 在 ASP.NET Core Blazor Hybrid 中傳遞根元件引數

建立SecondPage.xaml,根據剛才的步驟建立一個BlazorWebView並注入vuejs程式碼
html部分建立一個el-dialog元件,當訊息被接收時,顯示對話方塊


@using Microsoft.Maui.Controls
@inject IJSRuntime JSRuntime

<div id="vue-app">
    {{ message }}
    <el-dialog :visible.sync="showDialog" title="Native device msg recived!">
        <p>{{msg}}</p>
    </el-dialog>
</div>

在@code程式碼段中建立SecondPage物件。


@code {

    [Parameter]
    public SecondPage SecondPage { get; set; }

    ...
}

回到SecondPage.xaml.cs,在建構函式中將自己傳遞給根元件引數

public SecondPage()
{
    InitializeComponent();
    rootComponent.Parameters =
        new Dictionary<string, object>
        {
            { "SecondPage", this }
        };
}


從裝置呼叫Javascript程式碼

在SecondPage.xaml中,建立一個Post按鈕,點選按鈕後將文字方塊PostContentEntry的內容傳遞給Vue程式碼

<StackLayout Grid.Row="1">
    <Entry x:Name="PostContentEntry" Text="Hello,this is greetings from native device"></Entry>
    <Button Text="Post To Vue"
            HorizontalOptions="Center"
            VerticalOptions="End"
            HeightRequest="40"
            Clicked="Post_Clicked"></Button>

</StackLayout>

在SecondPage.razor.js中, 建立greet方法,用於接收從原生程式碼傳遞過來的引數,並顯示在對話方塊中。

window.app = new Vue({
    el: '#vue-app',
    data: {
        message: 'Vue Native interop',
        showDialog: false,
        msg: ''
    },
    methods: {
        greet: function (content) {
            this.msg = content;
            this.showDialog = true;
        }

    },

在SecondPage.xaml.cs中,建立一個OnPost事件,當Post按鈕被點選時觸發該事件


public event EventHandler<OnPostEventArgs> OnPost;

private void Post_Clicked(object sender, EventArgs args)
{
    OnPost?.Invoke(this, new OnPostEventArgs(this.PostContentEntry.Text));
}


在SecondPage.razor中,訂閱OnPost事件,當事件被觸發時,呼叫greet方法,將引數傳遞給JavaScript程式碼


public async void Recived(object o, OnPostEventArgs args)
{
    await JSRuntime.InvokeAsync<string>("window.app.greet", args.Content);
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    try
    {
        if (firstRender)
        {
            SecondPage.OnPost += this.Recived;

            await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./Views/SecondPageWeb.razor.js");

        }


    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }

}

在頁面銷燬時,要取消訂閱事件,避免記憶體漏失。


@implements IDisposable

...

public void Dispose()
{
    SecondPage.OnPost -= this.Recived;
}


執行效果如下

從Vue頁面呼叫原生程式碼

原生程式碼指的是.NET MAUI平臺的C#程式碼,比如要在裝置上顯示一個彈窗,需要呼叫Page.DisplayAlert方法,它隸屬於Microsoft.Maui.Controls名稱空間,屬於MAUI元件庫的一部分。

因此需要將MAUI型別的物件通過參照傳遞給JavaScript呼叫,呼叫方式是通過將物件範例包裝在 DotNetObjectReference 中傳遞給JavaScript。使用該物件的invokeMethodAsync從 JS 呼叫 .NET 實體方法。詳情請檢視官方檔案 JavaScript 函數呼叫 .NET 方法

在@code程式碼段中,介面載入時建立DotNetObjectReference物件

@code {
    private DotNetObjectReference<SecondPageWeb>? objRef;


    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

頁面載入完成時,將DotNetObjectReference物件傳遞給JavaScript程式碼


protected override async Task OnAfterRenderAsync(bool firstRender)
{
    try
    {
        if (firstRender)
        {
            SecondPage.OnPost += this.Recived;

            await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./Views/SecondPageWeb.razor.js");
            await JSRuntime.InvokeAsync<string>("window.initObjRef", this.objRef);

        }


    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }

}

window.app = new Vue({
    
    ...

    data: {
        ...
        objRef: null
    },
    
})
window.initObjRef = function (objRef) {
    window.app.objRef = objRef;
}

在SecondPage.razor中,建立el-input元件和el-button元件,當按鈕被點選時,呼叫Post方法,將文字方塊的內容傳遞給原生程式碼

<div id="vue-app">
    {{ message }}
    <el-input v-model="input" placeholder="請輸入內容"></el-input>
    <el-button @click="post">Post To Native</el-button>
    <el-dialog :visible.sync="showDialog" title="Native device msg recived!">
        <p>{{msg}}</p>
    </el-dialog>
</div>

按鈕和對話方塊的顯示邏輯與之前相同,不再贅述。

在SecondPage.razor中,建立Post方法,方法被呼叫時,將觸發MAUI元件庫的原生程式碼

[JSInvokable]
public async Task Post(string content)
{
    await SecondPage.DisplayAlert("Vue msg recived!", content, "Got it!");

}

vue繫結的函數中,呼叫DotNet.invokeMethodAsync將文字方塊的內容傳遞給原生程式碼


window.app = new Vue({
    el: '#vue-app',
    data: {
        message: 'Vue Native interop',
        showDialog: false,
        msg: '',
        input: 'Hi, I am a text message from Vue',
        deviceDisplay: null,
        objRef: null
    },
    methods: {
        greet: function (content) {
            this.msg = content;
            this.showDialog = true;
        },
        post: function () {
            this.objRef.invokeMethodAsync('Post', this.input);
        }


    }
})

執行效果如下

讀取裝置資訊

可以使用Vue的watch屬性監聽資料變化,當MAUI物件載入完成時,呼叫原生程式碼,讀取裝置資訊

<div id="vue-app">

    ...

    <p>Device Display</p>
    <p>{{deviceDisplay}}</p>
</div>

CodeBehind程式碼如下:

watch: {
    objRef: async function (newObjRef, oldObjRef) {
        if (newObjRef) {
            var deviceDisplay = await this.objRef.invokeMethodAsync('ReadDeviceDisplay');
            console.warn(deviceDisplay);
            this.deviceDisplay = deviceDisplay;
        }

    }
},

原生程式碼如下:


[JSInvokable]
public async Task<string> ReadDeviceDisplay()
{
    return await Task.FromResult(SecondPage.ReadDeviceDisplay());

}

在ReadDeviceDisplay方法中,我們讀取裝置解析度、螢幕密度、螢幕方向、螢幕旋轉、重新整理率等資訊

public string ReadDeviceDisplay()
{
    System.Text.StringBuilder sb = new System.Text.StringBuilder();

    sb.AppendLine($"Pixel width: {DeviceDisplay.Current.MainDisplayInfo.Width} / Pixel Height: {DeviceDisplay.Current.MainDisplayInfo.Height}");
    sb.AppendLine($"Density: {DeviceDisplay.Current.MainDisplayInfo.Density}");
    sb.AppendLine($"Orientation: {DeviceDisplay.Current.MainDisplayInfo.Orientation}");
    sb.AppendLine($"Rotation: {DeviceDisplay.Current.MainDisplayInfo.Rotation}");
    sb.AppendLine($"Refresh Rate: {DeviceDisplay.Current.MainDisplayInfo.RefreshRate}");

    var text = sb.ToString();
    return text;
}

當頁面載入時,會在HTML頁面上顯示裝置資訊


專案地址

Github:maui-vue-hybirddev

關注我,學習更多.NET MAUI開發知識!