Hướng dẫn NativePHP: Xây dựng ứng dụng Mac MenuBar
NativePHP, được tạo ra bởi Marcel Pociot tại BeyondCode, cho phép các nhà phát triển Laravel sử dụng TẤT CẢ kiến thức làm việc mà chúng ta đã có với Laravel để xây dựng các ứng dụng native cho Mac, Windows và Linux.
Gần đây, tôi đã thấy Christopher Rumpel đang làm việc trên một ứng dụng cho phép bạn lưu trữ múi giờ của bạn bè để bạn có thể nhìn thấy thời gian một cách nhanh chóng.
Hãy theo dõi cùng tôi khi chúng ta cùng nhau tạo ra một ứng dụng MenuBar cho Mac để biết giờ địa phương của từng thành viên trong nhóm của bạn.
Wait – how does NativePHP even work??
NativePHP cho phép bạn lựa chọn giữa hai công nghệ phổ biến khác nhau để sử dụng bên trong, đó là Electron và Tauri. Cả hai công nghệ này đều cho phép bạn “Xây dựng ứng dụng desktop đa nền tảng bằng JavaScript, HTML và CSS”. Điều này gần như giống như ma thuật nếu bạn nghĩ về nó – sử dụng các công nghệ web để xây dựng một ứng dụng ‘native’. NativePHP cung cấp một API đơn giản với cách xây dựng ứng dụng quen thuộc (Laravel) trong cả hai công nghệ cơ bản này. Trong ví dụ này, tôi sẽ trình bày cách sử dụng gói bọc Electron.
NativePHP Installation and Hello World
Trong một ứng dụng Laravel mới:
laravel new team-time
Hãy bắt đầu bằng việc cài đặt gói:
composer require nativephp/electron
Chạy trình cài đặt:
php artisan native:install
Would you like to install the NativePHP NPM dependencies? - Select 'yes'
Would you like to start the NativePHP development server? - Select 'no'
Tôi muốn bạn bắt đầu ứng dụng thủ công để bạn quen với cách làm như vậy.
php artisan native:serve
Sau một khoảng thời gian, bạn sẽ thấy một ứng dụng desktop native xuất hiện, hiển thị trang chủ Laravel mặc định với lời chào “hello there!”
Show me the code!
Tất nhiên, nhưng hãy bình tĩnh, mọi thứ sẽ được tiết lộ sớm thôi. Di chuyển đến App\Providers\NativeAppServiceProvider.php. Ở đây, bạn có thể thấy một số phần của NativePHP API đã được định nghĩa sẵn cho bạn. Tuy nhiên, trong ví dụ này, chúng ta sẽ không sử dụng mã này. Hãy xóa tất cả mọi thứ trong phương thức “boot” và thay thế nó bằng mã sau đây:
<?php
namespace App\Providers;
use Native\Laravel\Facades\MenuBar;
class NativeAppServiceProvider
{
public function boot(): void
{
Menubar::create();
}
}
Vì NativePHP hỗ trợ hot reloading, chúng ta nên thấy cửa sổ đóng lại và một biểu tượng Menubar xuất hiện ở đầu máy tính của bạn. Nhấp vào nó sẽ hiển thị trang chủ Laravel mặc định giống như trước đó.
Nice! Let’s build something cool!
Ở phía sau, tôi đang cài đặt TailwindCSS theo tài liệu của họ, Laravel Livewire 3 (có chút rủi ro, nhưng đó là lựa chọn của tôi), Blade Heroicons, sau đó thêm mô hình TeamMember, migration và factory với lệnh sau:
php artisan make:model TeamMember -mf
NOTE: I am keeping `npm run dev` running for hot reloading of the ui.
Migration:
public function up(): void
{
Schema::create('team_members', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('timezone');
$table->timestamps();
});
}
Factory
public function definition(): array
{
return [
'name' => $this->faker->name,
'timezone' => $this->faker->randomElement(timezone_identifiers_list())
];
}
Sau đó, tôi cập nhật file App\Database\seeders\DatabaseSeeder.php như sau:
public function run(): void
{
\App\Models\TeamMember::factory(10)->create();
}
Và chạy php artisan migrate
and php artisan db:seed
.
NOTE: The application inside of NativePHP does NOT have access to the database defined in your `.env`. From my experience, it can be useful to seed your database locally and debug in the browser or by using Spatie/Ray.
Let’s Create Our Livewire Classes and Views
php artisan livewire:make TeamMember/Index
php artisan livewire:make TeamMember/Create
php artisan livewire:make TeamMember/Update
Tiếp theo, cập nhật file web.php của chúng ta như sau:
Route::get('/', \App\Livewire\TeamMember\Index::class)->name('index');
Route::get('/team-members/create', \App\Livewire\TeamMember\Create::class)->name('create');
Route::get('/team-members/{teamMember}/edit', \App\Livewire\TeamMember\Update::class)->name('edit');
Và tạo một tệp app.blade.php trong thư mục resources/views/components/layouts với nội dung HTML sau:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
@vite('resources/css/app.css')
</head>
<body class="antialiased bg-gray-900 text-gray-100">
<div class="max-w-md mx-auto px-4 py-6">
{{$slot}}
</div>
</body>
</html>
Listing Our Teammates
Trong lớp App\Livewire\TeamMember\Index, chúng ta cần lấy tất cả các thành viên trong nhóm để hiển thị, ngoài ra, chúng ta nên cung cấp một liên kết để tạo một thành viên mới và các nút cập nhật và xóa cho các thành viên trong nhóm hiện có.
Lớp:
<?php
namespace App\Livewire\TeamMember;
use App\Models\TeamMember;
use Livewire\Component;
class Index extends Component
{
public function deleteMember(TeamMember $member)
{
$member->delete();
}
public function render()
{
$team = TeamMember::get();
return view('livewire.team-member.index', compact('team'));
}
}
View:
<div>
<div class="flex items-center justify-between mb-10">
<h1 class="text-xl font-bold">My Team</h1>
<a href="{{route('create')}}" type="button"
class="rounded-full bg-pink-600 px-2 py-1 text-xs font-bold text-white shadow hover:bg-pink-500">Add Team
Mate</a>
</div>
<div wire:poll>
@foreach($team as $member)
<div wire:key="{{ $member->id }}" class="my-2 flex items-center justify-between">
<div>
<p class="text-xs font-bold text-sky-500">{{$member->name}}</p>
<p class="text-lg">{{now()->tz($member->timezone)->format('h:i:s A')}} <span
class="text-xs text-gray-500">- {{$member->timezone}}</span></p>
</div>
<div class="flex items-center">
<a href="{{route('edit', ['team-member' => $member])}}">
<span class="sr-only">Edit</span>
<x-heroicon-m-pencil class="w-5 h-5 mr-3 hover:text-pink-500 transition-all duration-300" />
</a>
<button wire:click="deleteMember({{$member}})">
<x-heroicon-m-trash class="w-5 h-5 mr-3 hover:text-red-600 transition-all duration-300" />
</button>
</div>
</div>
@endforeach
</div>
</div>
Nếu bạn đã gieo hạt dữ liệu của mình trên máy cục bộ, thì xem trước trong trình duyệt nên trông giống như sau:
Trong ứng dụng native, nó sẽ trông giống như sau vì chúng ta chưa có bất kỳ dữ liệu nào ở đó (hãy đảm bảo chạy npm run build sau đó php artisan native:serve). NativePHP sử dụng cơ sở dữ liệu SQLite cục bộ trong hậu trường, chúng ta không cần bất kỳ thiết lập hoặc cấu hình bổ sung nào cho nó.
Bây giờ, chúng ta hãy xử lý thao tác Tạo (Create), để chúng ta có thể thấy điều này trong ứng dụng native nữa.
Lớp:
<?php
namespace App\Livewire\TeamMember;
use App\Models\TeamMember;
use Livewire\Attributes\Rule;
use Livewire\Component;
class Create extends Component
{
#[Rule(['required', 'string', 'min:3'])]
public string $name;
#[Rule(['required', 'string', 'min:3'])]
public string $timezone;
public function createMember()
{
TeamMember::create($this->validate());
$this->redirectRoute('index');
}
public function render()
{
return view('livewire.team-member.create');
}
}
View:
<div>
<div class="flex items-center justify-between mb-10">
<h1 class="text-xl font-bold">Add Team Member</h1>
<a href="{{route('index')}}" type="button"
class="rounded-full bg-pink-600 px-2 py-1 text-xs font-bold text-white shadow hover:bg-pink-500 flex items-center">
Go Back
</a>
</div>
<form wire:submit="createMember">
<div>
<label for="name" class="block text-sm font-medium leading-6 text-gray-100">What is your team member's
name?</label>
<div class="mt-2">
<input type="text" wire:model="name" id="name"
class="block w-full rounded-md border-0 py-1.5 text-gray-400 shadow-sm bg-gray-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-pink-600 sm:text-sm sm:leading-6"
placeholder="Sarthak">
@error('name')
<div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mt-6">
<label for="timezone" class="block text-sm font-medium leading-6 text-gray-100">What is your team member's
timezone</label>
<select id="timezone" wire:model="timezone"
class="mt-2 block w-full rounded-md border-0 py-1.5 text-gray-400 shadow-sm bg-gray-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-pink-600 sm:text-sm sm:leading-6">
@foreach(timezone_identifiers_list() as $timezone)
<option wire:key="{{ $timezone }}">{{$timezone}}</option>
@endforeach
</select>
@error('timezone')
<div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
@enderror
</div>
<button type="submit"
class="mt-6 rounded bg-pink-600 px-2 py-1 font-bold text-white shadow hover:bg-pink-500 w-full">Add Team
Mate
</button>
</form>
</div>
Giờ thì chúng ta đã xong rồi! Nhưng dường như tôi đã đặt múi giờ sai cho Sarthak, hãy cài đặt lớp và giao diện Chỉnh sửa và đặt điều này vào ngủ.
Lớp:
<?php
namespace App\Livewire\TeamMember;
use App\Models\TeamMember;
use Livewire\Component;
use Livewire\Features\SupportValidation\Rule;
class Update extends Component
{
public TeamMember $teamMember;
#[Rule(['required','min:3', 'string'])]
public $name;
#[Rule(['required','string'])]
public $timezone;
public function mount(TeamMember $teamMember)
{
$this->teamMember = $teamMember;
$this->name = $teamMember->name;
$this->timezone = $teamMember->timezone;
}
public function saveMember()
{
$this->teamMember->update([
'name' => $this->name,
'timezone' => $this->timezone
]);
$this->redirectRoute('index');
}
public function render()
{
return view('livewire.team-member.update');
}
}
View:
<div>
<div class="flex items-center justify-between mb-10">
<h1 class="text-xl font-bold">Update Team Member</h1>
<a href="{{route('index')}}" type="button"
class="rounded-full bg-pink-600 px-2 py-1 text-xs font-bold text-white shadow hover:bg-pink-500 flex items-center">
Go Back
</a>
</div>
<form wire:submit="saveMember">
<div>
<label for="name" class="block text-sm font-medium leading-6 text-gray-100">Name</label>
<div class="mt-2">
<input type="text" wire:model.blur="name" id="name"
class="block w-full rounded-md border-0 py-1.5 text-gray-200 shadow-sm bg-gray-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-pink-600 sm:text-sm sm:leading-6"
placeholder="Sarthak">
@error('name')
<div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mt-6">
<label for="timezone" class="block text-sm font-medium leading-6 text-gray-100">Timezone</label>
<select id="timezone" wire:model="timezone"
class="mt-2 block w-full rounded-md border-0 py-1.5 text-gray-200 shadow-sm bg-gray-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-pink-600 sm:text-sm sm:leading-6">
@foreach(timezone_identifiers_list() as $timezone)
<option {{$teamMember->timezone === $timezone ? 'selected' : ''}}>{{$timezone}}</option>
@endforeach
</select>
@error('timezone')
<div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
@enderror
</div>
<button type="submit"
class="mt-6 rounded bg-pink-600 px-2 py-1 font-bold text-white shadow hover:bg-pink-500 w-full">Add Team
Mate
</button>
</form>
</div>
Cuối cùng, chúng ta đã hoàn thành việc đóng gói (Wrapping It Up)!
Bây giờ khi ứng dụng hoạt động và có giao diện như chúng ta muốn, hãy làm một số việc cuối cùng trước khi triển khai nó. Đầu tiên, hãy cập nhật biểu tượng MenuBar. Tôi đã tạo 2 hình ảnh, một là file png có kích thước 22×22 và file png kia có kích thước 44×44. Bằng cách thêm từ “Template” vào cuối tên của các tệp này, chúng ta sẽ có một số tính năng tuyệt vời. Trên Mac, NativePHP sẽ chuyển đổi các hình ảnh này thành biểu tượng màu trắng với tính trong suốt để phù hợp với cấu trúc màu của thanh MenuBar native.
Hai hình ảnh có tên:
menuBarIconTemplate.png
menuBarIconTemplate@2x.png
Bằng cách thêm các biểu tượng này vào thư mục storage/app, sau đó cập nhật phương thức boot của NativeAppServiceProvider thành:
public function boot(): void
{
Menubar::create()->icon(storage_path('app/menuBarIconTemplate.png'));;
}
Trên lần serve tiếp theo, chúng ta sẽ thấy biểu tượng được cập nhật trong thanh MenuBar của chúng ta.
Cuối cùng, hãy thêm một số mục vào tệp .env của chúng ta để thông báo cho NativePHP một số chi tiết về ứng dụng của chúng ta:
NATIVEPHP_APP_NAME="TeamTime"
NATIVEPHP_APP_VERSION="1.0.0"
NATIVEPHP_APP_ID="com.teamtime.desktop"
NATIVEPHP_DEEPLINK_SCHEME="teamtime"
NATIVEPHP_APP_AUTHOR="Shane D Rosenthal"
NATIVEPHP_UPDATER_ENABLED=false
Build ứng dụng NativePHP của bạn
php artisan native:build
Chạy lệnh này sẽ đóng gói tất cả những gì chúng ta cần để xây dựng ứng dụng cục bộ và tạo ra một tệp native (‘.dmg’, ‘.exe’, v.v.). Sau khi hoàn thành, các tệp sẽ được đặt trong thư mục root/dist của dự án của bạn và bạn có thể phân phối ứng dụng theo ý thích.
Tính đến thời điểm viết, chức năng php artisan native:build vẫn hoạt động, tuy nhiên khi mở tệp .dmg cục bộ, ứng dụng của tôi có vẻ “treo” và ứng dụng thanh MenuBar không khởi động. Một lần nữa, NativePHP vẫn đang ở trạng thái alpha và các vấn đề dự kiến sẽ xảy ra, nhóm BeyondCode đang nỗ lực sửa các vấn đề như vậy và chúng ta nên mong đợi tính năng hoàn chỉnh trong những tuần hoặc tháng tới.
Kết luận
Vậy, bạn nghĩ sao? Rất tuyệt vời khi chúng ta có thể xây dựng các ứng dụng native với Laravel, đúng không? Tôi có thể nghĩ đến rất nhiều trường hợp sử dụng cho tính năng này và tôi không thể chờ đợi để tiếp tục khám phá và chứng kiến Laravel vươn cao lên tầm cao mới. Còn rất nhiều điều khác trong tài liệu NativePHP mà ứng dụng này không đề cập hoặc đi sâu vào, hãy tự xem qua, cảm hứng và xây dựng một điều gì đó tuyệt vời. #laravelforever!
good!!!