Viết Shorten URL bằng TALL stack
Tổng quan
Trước khi bắt đầu, mình sẽ giới thiệu cho các bạn những tính năng của web app này. Đây là sẽ là màn hình chính của app. Ở đây sẽ có tên app, một cái input để người dùng có thể thêm url vào. Bên dưới có hai nút, một nút Shorten
để generate ra url ngắn hơn của url mà người dùng đã paste vào và một nút Copy
để copy url mới được generate.
Ví dụ như mình thêm url dài vào và bấm Shorten
thì mình sẽ được một cái url ngắn hơn như bên dưới. Nếu bấm nút Copy
thì url mới này sẽ được copy vào clipboard và mình có thể đem paste đi chỗ khác.
Stack mình sẽ dùng ở đây là TALL stack gồm có Tailwindcss, AlpineJS, Laravel và Livewire. Về db thì mình dùng sqlite3.
Khởi tạo
Laravel
Đầu tiên, mình sẽ khởi tạo dự án bằng Laravel.
laravel new shorten
Sau đó, chúng ta sẽ chạy npm install
để cài những package cho js. Cuối cùng, là thay đổi tên của dự án trong .env ở phần APP_NAME
, đổi thành Shorten
.
Livewire
Để cài đặt livewire, chúng ta chạy lệnh sau.
composer require livewire/livewire
Tiếp theo là tạo layout cho livewire.
php artisan livewire:layout
Các bạn sẽ có một file như thế này trong resources/views/components/layouts/app.blade.php
.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $title ?? 'Page Title' }}</title>
</head>
<body>
{{ $slot }}
</body>
</html>
TailwindCSS
Với tailwindCSS, mình sẽ dùng thêm DaisyUI để đỡ khai báo nhiều class. Đầu tiên mình sẽ cài tailwindcss.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Thêm cái này vào phần content của file tailwind.config.js
"./resources/**/*.blade.php",
"./resources/**/*.js",
"./resources/**/*.vue",
Thêm phần này vào app.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Cuối cùng, thêm đoạn này vào trong phần <head></head> của file layout lúc nãy chúng ta tạo cho livewire.
@vite('resources/css/app.css')
Tiếp theo, chạy câu lệnh này để cài đặt DaisyUI
npm i -D daisyui@latest
Cuối cùng, thêm vào plugins vào file tailwind.config.js
module.exports = {
//...
plugins: [
require('daisyui'),
],
}
Ok như vậy là chúng ta đã cài xong tailwindcss và daisyUI.
AlpineJS
AlpineJS thì có rồi, khỏi cài
Logic
Tiếp theo, chúng ta cần phải xác định về luồng của app. Thì luồng app này cũng khá đơn giản. Khi mà user nhập một cái url vào ví dụ như https://www.example.com, thì app sẽ generate ra một cái unique code, ví dụ như là 12344. Sau đó, app sẽ gắn cái code này với cái url trong database. Cuối cùng, app sẽ dùng code này kết hợp với domain để trả ra kết quả cho người dùng. Thì chúng ta sẽ có http://localhost:8000/12344 -> https://www.example.com. Tức là khi bấm vào http://localhost:8000/12344 thì nó sẽ redirect chúng ta qua https://www.example.com. Ok, vậy là xong logic. Tiếp tục với bước tiếp theo là chúng ta sẽ lên structure cho database.
Database
Chúng ta sẽ có 1 table tên là urls, trong table này sẽ có những field sau:
- id unsignedBigInteger
- code varchar(255)
- url text
- created_at datetime
- updated_at datetime
Sau khi xong phần định nghĩa structure của db thì chúng ta sẽ bắt đầu với phần migration.
Migration
Các bạn chạy lệnh
php artisan make:migration create_urls_table
Laravel sẽ generate ra một file để chạy migration trong thư mục database/migrations. Các bạn thêm code như vầy để có được structure db như mình định nghĩa ở trên.
return new class() extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('urls', function (Blueprint $table) {
$table->id();
$table->string('code');
$table->text('url');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('urls');
}
};
Tạo livewire component
Giờ chúng ta sẽ chạy câu lệnh
php artisan make:livewire ShortenUrl
Sau khi chạy câu lệnh này, chúng ta sẽ có 2 file. Một file là php class tên là ShortenUrl.php
nằm trong thư mục app/Livewire
. Một file là file blade template shorten-url.blade.php
nằm trong thư mục resources/views/livewire/shorten-url.blade.php
. Tiếp theo, chúng ta sẽ vào file web.php trong thư mục routes để trỏ route /
đến component này.
Route::get('/', ShortenUrl::class);
Giờ khi truy cập vào đường dẫn /
thì app sẽ load ra component ShortenUrl
.
Implement UI
Giờ mình sẽ implement UI follow theo thiết kế. Code mình sẽ như thế này.
<div class="flex justify-center flex-wrap flex-col h-screen">
<div class="w-full">
<h1 class="text-center text-6xl font-bold text-indigo-700">Shorten Urls Easily</h1>
</div>
<div class="w-full text-center my-10">
<input type="url" placeholder="Add Url here" class="input input-bordered w-full max-w-md" wire:model="url" />
<p class="text-error">
@error('url')
{{ $message }}
@enderror
</p>
</div>
<div class="flex justify-center text-center mx-auto mb-3">
@if ($result)
<div role="alert" class="alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<a target="_blank" class="link link-primary text-white" href="{{ $result }}"
id="result">{{ $result }}</span>
</div>
@endif
</div>
<div class="w-full text-center">
<button wire:click="submit" class="btn btn-active btn-accent text-white">Shorten</button>
<button
@click.prevent="
navigator.clipboard.writeText(document.getElementById('result').innerText)
.then(() => {
alert('Copied');
})
.catch(err => {
alert('Failed to copy: ', err);
})
"
class="btn btn-active btn-accent text-white">Copy</button>
</div>
</div>
Và class ShortenUrl.php của mình sẽ như thế này:
class ShortenUrl extends Component
{
public string $url;
public string $result;
public function rules()
{
return [
'url' => [
'required',
'url'
],
];
}
public function submit()
{
$this->validate();
$code = Str::random(7);
$url = Url::firstOrCreate([
'url' => $this->url,
], [
'code' => $code
]);
$this->result = $url->shortUrl;
}
public function render()
{
return view('livewire.shorten-url');
}
}
Ok như vậy là mình xong phần UI và handle logic của livewire component. Có những phần mình cần giải thích như sau:
<input type="url" placeholder="Add Url here" class="input input-bordered w-full max-w-md" wire:model="url" />
Ở đây chúng ta dùng wire:model="url"
để gán giá trị của input này vào property url
trong class ShortenUrl.
@error('url')
{{ $message }}
@enderror
Khi có lỗi khi validate property url thì chúng ta sẽ hiện lỗi ra ở đây.
<button wire:click="submit" class="btn btn-active btn-accent text-white">Shorten</button>
Ở đây, chúng ta có wire:click="submit"
. Cái này để handle khi mà click vào nút này, thì sẽ chạy function submit
trong class ShortenUrl. Trong function submit
này thì chúng ta sẽ validate xem url có hợp lệ không. Nếu hợp lệ thì tạo url short mới và gán vào kết quả.
<button
@click.prevent="
navigator.clipboard.writeText(document.getElementById('result').innerText)
.then(() => {
alert('Copied');
})
.catch(err => {
alert('Failed to copy: ', err);
})
"
class="btn btn-active btn-accent text-white">Copy</button>
Đây là phần chúng ta xử lý trong AlpineJS. Khi bấm vào nút Copy này, thì app sẽ kiếm đến nội dung của thẻ id result và copy nội dung vào Clipboard và show ra cho người dùng biết là nội dung đã được copy. Nếu mà fail thì log ra là fail. Với tính năng này, thì khi vào trang khác hoặc là vào một app khác, khi bấm Ctrl + V thì url mới sẽ được paste vào trang hoặc app mà người dùng đang dùng. Như vậy là chúng ta đã làm xong một app shorten url đơn giản bằng TALL stack.
Chạy app
Để chạy app, chúng ta sẽ chạy 2 lệnh cùng với nhau
php artisan serve
Để chạy Laravel app và chạy lệnh:
npm run dev
Để chạy mấy cái file trong resources.
All Rights Reserved