Hướng dẫn đầy đủ về tìm kiếm thời gian thực trong Laravel

Cập nhật lần cuối: 5 Tháng Mười Hai 2025
  • Laravel cho phép bạn triển khai mọi thứ, từ công cụ tìm kiếm đơn giản với AJAX đến tìm kiếm toàn văn bản nâng cao bằng Laravel Scout và các công cụ tìm kiếm bên ngoài như Algolia, Meilisearch hoặc Elasticsearch.
  • Đối với các tìm kiếm nhẹ, việc lọc ở giao diện người dùng bằng Alpine.js hoặc bằng các yêu cầu tìm nạp gốc sẽ tránh làm quá tải máy chủ và cải thiện trải nghiệm của người dùng trong các danh sách nhỏ.
  • Laravel Scout tập trung tích hợp với nhiều công cụ tìm kiếm khác nhau và giúp dễ dàng đánh dấu các mô hình có thể tìm kiếm, quản lý chỉ mục và khởi chạy truy vấn thống nhất.
  • Việc lựa chọn công cụ (SaaS, mã nguồn mở hoặc cơ sở dữ liệu) phải dựa trên khối lượng dữ liệu, mức độ phức tạp của tìm kiếm và yêu cầu về hiệu suất và bảo trì của dự án.

tìm kiếm thời gian thực trong Laravel

Khi bạn bắt đầu làm việc với Laravel và cần một công cụ tìm kiếm thời gian thực phản hồi ngay lập tứcThật dễ dàng để bị lạc lối giữa hàng ngàn cách tiếp cận có thể có: AJAX với fetch, jQuery, Alpine.js, Scout với Algolia hoặc Meilisearch, lọc giao diện, v.v. Tin tốt là hệ sinh thái Laravel đã cung cấp hầu như mọi thứ bạn cần để thiết lập tìm kiếm mượt mà và nhanh chóng mà không gặp lỗi khi thực hiện.

Trong bài viết này bạn sẽ thấy cách lắp ráp các loại tìm kiếm thời gian thực khác nhau trong LaravelTừ tính năng tự động hoàn thành AJAX cổ điển đến tìm kiếm toàn văn bản với Laravel Scout và các công cụ tìm kiếm như Algolia, Meilisearch, cơ sở dữ liệu, hoặc thậm chí cả Elasticsearch. Bạn cũng sẽ thấy các giải pháp thay thế nhẹ hơn với Alpine.js để lọc dữ liệu trực tiếp trong trình duyệt khi khối lượng dữ liệu nhỏ.

Tìm kiếm thời gian thực trong Laravel là gì và hoạt động cơ bản như thế nào?

Ý tưởng đằng sau tìm kiếm thời gian thực là, khi người dùng nhập vào trường văn bảnMột truy vấn được kích hoạt và kết quả được cập nhật mà không cần tải lại trang. Về mặt kỹ thuật, điều này bao gồm ba thành phần chính: backend Laravel, JavaScript của trình duyệt và việc trao đổi dữ liệu ở định dạng JSON.

Một mặt, Laravel hoạt động như lớp máy chủ Nó chịu trách nhiệm tiếp nhận yêu cầu, diễn giải các tham số tìm kiếm (văn bản đã nhập), truy vấn cơ sở dữ liệu và trả về phản hồi có cấu trúc, thường ở định dạng JSON. Phản hồi này có thể cho biết kết quả thành công, lỗi hoặc không tìm thấy kết quả nào.

Ở một kết thúc khác, JavaScript có trách nhiệm lắng nghe các sự kiện của người dùng. Trên đầu vào tìm kiếm, hãy gửi các yêu cầu bất đồng bộ (AJAX) đến backend và hiển thị dữ liệu trả về trên trang mà không cần trình duyệt phải làm mới hoàn toàn. Điều này có thể được thực hiện bằng cách sử dụng native fetch, jQuery AJAX hoặc các thư viện phản ứng nhỏ như Alpine.js.

Với cơ chế cơ bản này bạn có thể xây dựng từ một Tự động hoàn thành đơn giản với một vài bản ghi, lên đến công cụ tìm kiếm toàn văn bản nâng cao với tính liên quan, phân trang và bộ lọc, được hỗ trợ bởi các thư viện như Laravel Scout và các công cụ tìm kiếm bên ngoài được tối ưu hóa cho tìm kiếm.

Mô hình, tuyến đường và bộ điều khiển cho công cụ tìm kiếm thời gian thực cơ bản

Trước khi tìm hiểu sâu về JavaScript, bạn cần đảm bảo Laravel được tổ chức tốt: một mô hình Eloquent để tìm kiếm, các tuyến đường rõ ràng và một bộ điều khiển chuyên dụng để quản lý logic tìm kiếm theo thời gian thực.

Bước đầu tiên là phải có một mô hình Eloquent đại diện cho bảng dữ liệu mà bạn sẽ tìm kiếm. Hãy tưởng tượng một bảng dữ liệu các quốc gia và một mô hình được gọi là Đất nước Rất đơn giản, không có dấu thời gian và cho phép gán hàng loạt:

Ví dụ về mô hình Eloquent tối thiểu cho tìm kiếm:

namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Pais extends Model
{
use HasFactory;
protected $guarded = [];
public $timestamps = false;
}

Ở đây chỉ ra rằng mô hình Pais nằm trong không gian tên Laravel chuẩnNó kế thừa từ Model và cho phép bạn gán bất kỳ trường nào bằng create() bằng cách để mảng được bảo vệ trống. Bằng cách vô hiệu hóa dấu thời gian với public $timestamps = false, bạn sẽ tránh được sự cố nếu bảng không có các cột created_at và updated_at.

Bước tiếp theo là xác định các tuyến đường sẽ xử lý cả hiển thị công cụ tìm kiếm và các yêu cầu AJAXMột lược đồ rất phổ biến kết hợp tuyến GET để hiển thị chế độ xem và tuyến POST được thiết kế để nhận các truy vấn thời gian thực:

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BuscadorController;

Route::get('/', function () {
return view('welcome');
});

Route::get('buscador', [BuscadorController::class, 'index']);
Route::post('buscador', [BuscadorController::class, 'buscar']);

Tuyến gốc trả về chế độ xem chào mừng, trong khi URL /search được dành riêng cho chức năng tìm kiếmPhương thức index() của bộ điều khiển hiển thị biểu mẫu và đầu vào tìm kiếm, trong khi phương thức search() xử lý các yêu cầu không đồng bộ được gửi từ trình duyệt.

Trong bộ điều khiển, bạn có thể triển khai một mẫu rất thực tế: Chuẩn bị một mảng phản hồi mặc định trong trường hợp có lỗi và chỉ ghi đè lên khi đó thực sự là một yêu cầu AJAX hợp lệ và truy vấn được thực thi mà không có vấn đề gì.

Bộ điều khiển có thể có một cấu trúc tương tự điều này:

namespace App\Http\Controllers;

use App\Models\Pais;
use Illuminate\Http\Request;

class BuscadorController extends Controller
{
public function index()
{
return view('welcome');
}

public function buscar(Request $request)
{
$response = [
'success' => false,
'message' => 'Hubo un error',
];

if ($request->ajax()) {
$data = Pais::where('nombre', 'like', $request->texto.'%')
->take(10)
->get();

$response = [
'success' => true,
'message' => 'Consulta correcta',
'data' => $data,
];
}

return response()->json($response);
}
}

Đến thời điểm này bạn đã có chu trình backend hoàn chỉnh: yêu cầu AJAX đến, kiểm tra xem đó có phải là AJAX không, truy vấn với where like và giới hạn kết quả đến một số lượng hợp lý bằng cách sử dụng take(10) để tránh quá tải cơ sở dữ liệu. Phản hồi luôn được gửi dưới dạng JSON, giúp đơn giản hóa đáng kể công việc của giao diện người dùng.

Chế độ xem Blade và JavaScript để tìm kiếm phản ứng

Khi mô hình, tuyến đường và bộ điều khiển đã sẵn sàng, đã đến lúc xây dựng phần hiển thị: một biểu mẫu có trường tìm kiếm và một khối để hiển thị kết quả, cộng với JavaScript chịu trách nhiệm thực hiện các yêu cầu ở chế độ nền.

Chế độ xem Blade có thể rất đơn giản, dựa vào Mã thông báo CSRF Laravel đưa vào để xác thực các yêu cầu POST và trong mục nhập tìm kiếm thuận tiện để sử dụng:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<strong><meta name="csrf-token" content="{{ csrf_token() }}"></strong>
<title>Laravel</title>
</head>
<body>

<form action="" method="post">
<input type="search" name="texto" id="buscar">
</form>

<div id="resultado"></div>

<script>
window.addEventListener('load', function () {
const buscar = document.getElementById('buscar');
const resultado = document.getElementById('resultado');

buscar.addEventListener('keyup', function () {
fetch('/buscador', {
method: 'post',
body: JSON.stringify({ texto: buscar.value }),
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': document.head.querySelector('[name~="csrf-token"][content]').content,
},
})
.then(response => response.json())
.then(data => {
let html = '';
if (data.success) {
html += '<ul>';
for (let i in data.data) {
html += '<li>' + data.data[i].nombre + '</li>';
}
html += '<ul>';
} else {
html += 'No existen resultados';
}
resultado.innerHTML = html;
});
});
});
</script>

</body>
</html>

Trong ví dụ này, tập lệnh Lắng nghe sự kiện keyup trên đầu vào tìm kiếmMỗi lần nhấn phím sẽ kích hoạt một yêu cầu tìm nạp đến đường dẫn /search. Văn bản hiện tại của trường được gửi ở định dạng JSON, và các tiêu đề khóa như X-Requested-With được bao gồm để chỉ ra rằng đó là AJAX, cùng với mã thông báo CSRF để bỏ qua cơ chế bảo vệ gốc của Laravel.

Khi phản hồi đến, nó sẽ được chuyển đổi thành JSON và được tạo ra một cách động. một danh sách HTML nhỏ với các kết quảhoặc thông báo như "Không tìm thấy kết quả" khi truy vấn không trả về dữ liệu. Tất cả những điều này diễn ra mà không cần tải lại trang, theo cách tự nhiên cho người dùng.

Mẫu này có thể được tinh chỉnh thêm bằng các chi tiết UX nhỏ, chẳng hạn như thêm trì hoãn (giảm độ nảy) giữa các lần nhấn phím, hiển thị trình tải hoặc xử lý lỗi mạng để ngăn giao diện bị treo khi có sự cố xảy ra.

Tìm kiếm trực tiếp với Laravel và AJAX sử dụng jQuery

Mặc dù trò chơi tìm đồ vật đã đạt được nhiều thành tựu ngày nay, jQuery AJAX vẫn rất phổ biến trong các dự án cũ hoặc trong các nhóm đã triển khai. Ý tưởng hoàn toàn giống nhau: ghi lại nội dung người dùng nhập, tạo một yêu cầu bất đồng bộ và làm mới DOM.

Quy trình làm việc điển hình với jQuery trong Laravel để tìm kiếm trực tiếp thường bao gồm các bước cơ bản sau: xác định một tuyến đường cụ thể, tạo một bộ điều khiển chuyên dụng, xây dựng chế độ xem Blade với đầu vào tìm kiếm Và cuối cùng, thêm mã jQuery để kích hoạt AJAX khi nhập.

Quá trình này hoạt động như thế này: khi người dùng bắt đầu nhập, jQuery gửi một truy vấn đến máy chủ với chuỗi tìm kiếm. Laravel lọc thông tin trong cơ sở dữ liệu, trả về JSON với kết quả khớp và jQuery cập nhật vùng chứa HTML trên trang để phản ánh các kết quả khớp, tất cả chỉ trong vài mili giây.

Ưu điểm của việc sử dụng jQuery là Nó khá tóm tắt cú pháp của AJAX Và nó rất dễ đọc nếu bạn đã có thư viện trong dự án của mình. Tuy nhiên, bạn đang thêm một dependency bổ sung có thể không cần thiết nếu bạn có thể làm việc với JavaScript hiện đại và tính năng fetch gốc.

Lọc và tìm kiếm theo thời gian thực trên giao diện người dùng với Alpine.js

Khi dữ liệu được hiển thị tương đối nhỏ (ví dụ, ít hơn 50 mặt hàng), không phải lúc nào cũng đáng để thiết lập một hệ thống phụ trợ với các tìm kiếm phức tạp. Trong những trường hợp đó, một lựa chọn rất tiện lợi là Lọc trực tiếp trong trình duyệt với Alpine.js, mà không cần gửi yêu cầu đến máy chủ trong khi người dùng nhập.

Ý tưởng là tính toán trước chuỗi tìm kiếm cho từng phần tử (ví dụ: tên, mô tả và danh mục viết thường), lưu trữ trong thuộc tính data-search-text và để Alpine.js xử lý phần còn lại. hiển thị hoặc ẩn các thành phần theo văn bản đã viết trong trường tìm kiếm.

Thành phần Alpine.js có thể có cấu trúc tương tự như sau: mục lọc

{
search: '',
hasResults: true,
selectedValue: '',
init() {
this.$watch('search', () => this.filterItems());
this.$nextTick(() => this.$refs.searchInput?.focus());
},
filterItems() {
const searchLower = this.search.toLowerCase().trim();
const cards = this.$el.querySelectorAll('.item-card');
let visibleCount = 0;

cards.forEach(card => {
const text = card.dataset.searchText || '';
const isVisible = searchLower === '' || text.includes(searchLower);
card.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
});

this.hasResults = visibleCount > 0;
},
}

Trong chế độ xem, mỗi thẻ hoặc hàng dữ liệu sẽ mang một thuộc tính data-search-text với văn bản đã được chuẩn bị ở dạng chữ thườngDo đó, bộ lọc được rút gọn thành hàm includes() trong JavaScript, rất nhanh đối với danh sách ngắn:

<input type="search" x-model="search" x-ref="searchInput" placeholder="Buscar..." />
<div>
<div class="item-card" data-search-text="formulario contacto simple">
<h3>Formulario de contacto</h3>
<p>Formulario de contacto simple</p>
</div>
</div>

Ngoài ra, bạn chỉ có thể hiển thị khối trạng thái trống khi Không có kết quả nào cho từ khóa tìm kiếm hiện tại.mời người dùng sửa đổi văn bản hoặc xóa trường bằng một nút đơn giản để thiết lập lại tìm kiếm thành chuỗi trống.

Cách tiếp cận này có những ưu điểm rõ ràng: Không có cuộc gọi máy chủ nào trong quá trình tìm kiếm.Tương tác gần như tức thời, logic vẫn mang tính cục bộ cao và dễ gỡ lỗi. Nó hoàn hảo cho các bộ chọn nhanh, hộp thoại chọn mục hoặc danh mục nhỏ được nhúng trong trang Laravel.

Laravel Scout: Tìm kiếm toàn văn bản với các công cụ chuyên dụng

Khi mọi thứ trở nên nghiêm trọng và bạn cần tìm kiếm toàn văn nhanh chóng, có liên quan và có thể mở rộngĐường dẫn tự nhiên trong Laravel là Laravel Scout. Scout là một lớp tích hợp cho phép bạn dễ dàng kết nối các mô hình Eloquent của mình với các công cụ tìm kiếm như Algolia, Meilisearch, cơ sở dữ liệu của riêng bạn, các bộ sưu tập trong bộ nhớ hoặc thậm chí là Elasticsearch thông qua các bộ điều khiển bên ngoài.

Để bắt đầu với Scout, điều thông thường là tạo một dự án Laravel mới hoặc sử dụng lại một dự án hiện cóĐể khởi chạy, hãy sử dụng Docker (ví dụ: với Laravel Sail) và sau đó cài đặt thư viện bằng Composer. Sau khi hoàn tất, hãy xuất bản tệp cấu hình scout.php và điều chỉnh các biến môi trường theo trình điều khiển bạn muốn sử dụng.

Một quy trình làm việc điển hình sẽ là cài đặt Scout với Composer, xuất bản cấu hình của nó và kích hoạt hàng đợi lập chỉ mục với SCOUT_QUEUE=true Trong tệp .env, hãy đảm bảo các hoạt động tốn nhiều tài nguyên được xử lý ở chế độ nền, cải thiện thời gian phản hồi của ứng dụng. Ngoài ra, bạn phải đảm bảo DB_HOST trỏ đến cơ sở dữ liệu bạn đang sử dụng, điều này đặc biệt quan trọng nếu bạn đang sử dụng container Docker.

Để một mô hình có thể tham gia tìm kiếm Hướng đạo sinh, cần phải Để đánh dấu rõ ràng là có thể tìm kiếm bằng cách thêm đặc điểm Có thể tìm kiếmVí dụ, nếu bạn có mô hình Train biểu diễn một bảng các chuyến tàu có trường tiêu đề, bạn có thể định nghĩa nó như sau:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class Train extends Model
{
use Searchable;

protected $fillable = ['title'];

public function searchableAs()
{
return 'trains_index';
}
}

Phương thức searchableAs cho phép tùy chỉnh tên chỉ mục trong công cụ tìm kiếmThay vì sử dụng tên mặc định được lấy từ mô hình, Scout sẽ tiếp quản. Từ đây, Scout sẽ xử lý việc đồng bộ hóa các thao tác tạo, cập nhật và xóa với chỉ mục từ xa hoặc cục bộ, tùy thuộc vào trình điều khiển được chọn.

Laravel Scout với Algolia: Tìm kiếm SaaS nhanh như chớp

Algolia là một dịch vụ SaaS tập trung vào để cung cấp các tìm kiếm rất nhanh và có liên quan trên khối lượng dữ liệu lớnNó có bảng điều khiển web để quản lý chỉ mục, quy tắc liên quan, từ đồng nghĩa, v.v. và tích hợp rất tốt với Laravel thông qua Scout và trình khách PHP chính thức.

Để sử dụng Algolia với Scout, bạn sẽ cần cài đặt trình khách PHP của nó với Composer, đăng ký thông tin đăng nhập của bạn trong tệp .env (ID ứng dụng và Khóa API quản trị) và đặt SCOUT_DRIVER=algolia để yêu cầu Scout sử dụng công cụ này. Từ bảng điều khiển Algolia, bạn có thể lấy cả ID ứng dụng và khóa quản trị.

Sau khi môi trường được cấu hình, bạn có thể sử dụng các phương pháp như Train::search('text')->paginate(6) trực tiếp vào bộ điều khiển của bạn để thực hiện tìm kiếm trên các trường được lập chỉ mục, nhận kết quả theo định dạng Eloquent được phân trang sẵn sàng để chuyển sang chế độ xem Blade.

Ví dụ:Bạn có thể có một bộ điều khiển chỉ số liệt kê tất cả các chuyến tàu hoặc thực hiện tìm kiếm nếu nhận được tham số titlesearch và phương thức create để chèn các chuyến tàu mới vào chỉ mục:

public function index(Request $request)
{
if ($request->has('titlesearch')) {
$trains = Train::search($request->titlesearch)->paginate(6);
} else {
$trains = Train::paginate(6);
}

return view('Train-search', compact('trains'));
}

public function create(Request $request)
{
$this->validate($request, ['title' => 'required']);
Train::create($request->all());
return back();
}

Trong chế độ xem tương ứng, bạn có thể kết hợp một mẫu đơn để đăng ký tàu mới và một biểu mẫu GET khác với trường tìm kiếm tiêu đề sẽ kích hoạt tìm kiếm khi gửi. Sau đó, bạn chỉ cần lặp qua bộ sưu tập các chuyến tàu và hiển thị các trường của chúng trong một bảng, tận dụng các liên kết phân trang do Laravel tạo ra.

Trinh sát với Meilisearch, cơ sở dữ liệu và bộ sưu tập

Nếu bạn muốn tránh các dịch vụ bên ngoài, Meilisearch là một công cụ tìm kiếm mã nguồn mở mà bạn có thể triển khai cục bộ hoặc trên cơ sở hạ tầng của mình. Scout tích hợp với Meilisearch theo cách rất giống với Algolia, chỉ cần thay đổi trình điều khiển và thêm các biến MEILISEARCH_HOST và MEILISEARCH_KEY vào tệp .env.

Để sử dụng nó, bạn cài đặt máy khách PHP Meilisearch, điều chỉnh SCOUT_DRIVER=meilisearch và trỏ MEILISEARCH_HOST đến URL của phiên bản (ví dụ: http://127.0.0.1:7700). Nếu bạn đã có các bản ghi trước đó, bạn có thể lập chỉ mục chúng bằng lệnh php artisan scout:import "App\Models\Train" để công cụ có thể sử dụng chúng.

Trong các ứng dụng nhỏ hơn hoặc vừa phải, bạn cũng có thể chọn Cơ sở dữ liệu lái xe trinh sátTính năng này tận dụng các chỉ mục toàn văn bản và lệnh LIKE trên cơ sở dữ liệu MySQL hoặc PostgreSQL của bạn. Trong trường hợp này, bạn không cần dịch vụ bên ngoài; chỉ cần đặt SCOUT_DRIVER=database để Scout sử dụng chính cơ sở dữ liệu làm công cụ tìm kiếm.

Một lựa chọn thú vị khác là bộ sưu tập trình điều khiển, hoạt động trên các bộ sưu tập Eloquent trong bộ nhớCông cụ này lọc kết quả bằng mệnh đề WHERE và bộ lọc collection, tương thích với bất kỳ cơ sở dữ liệu nào được Laravel hỗ trợ. Bạn có thể kích hoạt nó bằng `SCOUT_DRIVER=collection` hoặc bằng cách điều chỉnh tệp cấu hình Scout để có các thiết lập cụ thể hơn.

Tích hợp với Elasticsearch bằng Explorer

Nếu nhu cầu tìm kiếm của bạn liên quan đến làm việc với khối lượng dữ liệu khổng lồ và phân tích thời gian thựcElasticsearch là một công cụ kinh điển. Trong hệ sinh thái Laravel, một cách hiện đại để tích hợp nó với Scout là sử dụng bộ điều khiển Explorer, đóng vai trò cầu nối giữa các mô hình của bạn và cụm Elasticsearch.

Để thực hiện điều này, Docker thường được sử dụng cùng với tệp docker-compose phong phú, ngoài các dịch vụ thông thường (Laravel, MySQL, Redis, Meilisearch, v.v.), Container Elasticsearch và KibanaSau đó, bạn cài đặt gói jeroen-g/explorer thông qua Composer và xuất bản tệp cấu hình của gói này để chỉ ra những mô hình nào sẽ được lập chỉ mục.

Trong tệp config/explorer.php, bạn có thể đăng ký các mô hình của mình theo khóa chỉ mục, ví dụ bằng cách thêm Ứng dụng\Mô hình\Train::lớpNgoài ra, bạn thay đổi trình điều khiển Scout thành elastic trong tệp .env bằng SCOUT_DRIVER=elastic để mọi thứ đều trỏ tới Elasticsearch.

Trong mô hình Train, giao diện Explored phải được triển khai và phương thức phải được ghi đè. có thể ánh xạđịnh nghĩa bản đồ các trường sẽ được gửi đến chỉ mục. Một ví dụ đơn giản là:

use JeroenG\Explorer\Application\Explored;
use Laravel\Scout\Searchable;

class Train extends Model implements Explored
{
use Searchable;

protected $fillable = ['title'];

public function mappableAs(): array
{
return [
'id' => $this->id,
'title' => $this->title,
];
}
}

Từ giờ trở đi, Bạn có thể khởi chạy tìm kiếm trên Elasticsearch bằng cùng giao diện Scout., được hưởng lợi từ thời gian phản hồi rất thấp và sức mạnh truy vấn đầy đủ của công cụ này, nhưng không rời khỏi hệ sinh thái Laravel.

Với tất cả các cách tiếp cận này—từ tính năng tự động hoàn thành cơ bản với fetch hoặc jQuery, đến lọc giao diện người dùng với Alpine.js, đến tìm kiếm toàn văn bản với Laravel Scout và nhiều trình điều khiển khác— Laravel cung cấp cho bạn nhiều lựa chọn để triển khai tìm kiếm theo thời gian thực phù hợp với quy mô dự án, hiệu suất bạn cần và cơ sở hạ tầng bạn muốn duy trì.

  Lập trình trò chơi điện tử: Cách bắt đầu - Hướng dẫn từng bước