Sử dụng ReactJs trong project Laravel

Trong thời gian gần đây, Single Page App (SPA) đang rất được các dev ưa chuộng vì nó làm tăng trải nghiệm của người dùng rất là nhiều. Từ trước tới giờ mình cũng rất ít khi tìm hiểu về các framework js. Nhưng để bắt kịp xu thế thì mình cũng đã tự mày mò tìm hiểu chút ReacJs 😄. Và với nên tảng là một người học Laravel và Laravel cũng hỗ trợ việc sử dụng ReactJs, sau khi tìm hiểu một chút thì mình cũng có thể sử dụng được ReactJs để tạo một SPA đơn giản trong một project Laravel. Hôm nay viết bài này chia sẻ với mọi người chút, nếu bạn nào muốn tìm hiểu thì có thể đọc qua, còn ai thấy mình sai ở đâu thì mình mong các bạn góp ý để mình viết 😄

Một vài yêu cầu

Để thực hiện được demo này thì có chút yêu cầu về hệ thống và kiến thức mà mình nghĩ các bạn sẽ cần phải có

  • Có kiến thức cơ bản về Laravel, ReactJs
  • Máy đã cài PHP, npm, Composer, Laravel, MySQL, PHP MyAdmin (có thể là những trình quản lý tương đương hoặc bạn có thể sử dụng command nếu bạn nắm vững)

Mô tả demo

Ở demo này, mình sẽ làm 1 SPA đơn giản bao gồm hiển thị danh sách, thêm, sửa, xóa bài viết

Bắt đầu

Đầu tiên chúng ta cần tạo 1 project Laravel:

laravel new demo-reactjs

Sau đó, bạn hãy truy cập vào source code của project, sử dụng lệnh preset để chỉ định framework js muốn sử dụng:

php artisan preset react

Và sau đó chạy:

npm install

Tiếp tới thì chúng ta cần config file .env. Mọi người có kiến thức cơ bản về Laravel đều biết điều này rồi nên mình sẽ không nói lại nữa nhé. À còn một việc nữa, đó là ở đây mình sẽ sử dụng arrow function khi viết code js nên cần phải tạo 1 file .babelrc trong source code của project. Nội dung file sẽ như sau:

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ],
    "plugins": [
        "@babel/plugin-proposal-class-properties"
    ]
}

Nếu không có đoạn "@babel/plugin-proposal-class-properties" mà bạn sử dụng arrow function thì sẽ gặp lỗi khi run watch (Cái này lúc làm thử mình cũng mới biết 😄)

Tạo model và migration

Những thiết lập cơ bản đã xong, giờ chúng ta cần chuẩn bị db để có thể lưu dữ liệu. Nhưng đã giới thiệu thì mình sẽ chỉ làm chức năng thêm sửa xóa bài viết. Vậy ở đây mình sẽ chỉ cần 1 bảng posts

php artisan make:model Post -m

Vì không cần những bảng khác nên mình sẽ xóa tất cả các file trong migrations, chỉ giữ lại file để migrate bảng posts

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->text('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

Model Post của mình sẽ như sau:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = [
        'title',
        'content',
    ];
}

Xong xuôi mọi thứ thì migrate thôi:

php artisan migrate

Tạo API

Ở đây mình sẽ tạo api đơn giản, không có authenticate đâu nhé 😄 Nếu bạn muốn chăm chút hơn cho project này thì có thể xem cách tạo authenticate api với laravel của mình ở bài này nhé : API Authentication với passpost trong Laravel 5.8

Trong file api.php, mình sẽ tạo nhưng route để xử lý những hành động cơ bản:

Route::prefix('posts')->name('posts.')->group(function () {
    Route::get('', '[email protected]')->name('index') //Danh sách;
    Route::post('', '[email protected]')->name('store') //Lưu;
    Route::get('{post}', '[email protected]')->name('show') //Chi tiết;
    Route::post('{post}', '[email protected]')->name('update') //Cập nhập;
    Route::post('delete/{post}', '[email protected]')->name('delete') //Xóa;
});

Giờ thì phải tạo PostController:

php artisan make:controller PostController
<?php

namespace App\Http\Controllers;

use App\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::orderBy('id', 'desc')->get();

        return response()->json($posts, 200);
    }

    public function store(Request $request)
    {
        $data = $request->all();
        $post = Post::create($data);

        return response()->json($post, 200);
    }

    public function show(Post $post)
    {
        return response()->json($post, 200);
    }

    public function update(Request $request, Post $post)
    {
        $data = $request->all();
        $post->update($data);

        return response()->json($post, 200);
    }

    public function delete(Post $post)
    {
        $post->delete();
        $posts = Post::orderBy('id', 'desc')->get();

        return response()->json($posts, 200);
    }
}

Do làm demo nhanh nên mình sẽ không validate dữ liệu ở backend 😄 các bạn thông cảm nha. Ở đây vì chúng ta đang tạo api nên tất các kết quả trả về đều phải để dưới dạng json

Tạo wildcard route

Ở file web.php, mình sẽ viết như sau:

Route::get('{path?}', 'RenderSpaView')->where('path', '[a-zA-Z0-9-/]+');

Đoạn này có nghĩa là sao, tức là tất cả những chuỗi url nào thỏa mạn điều kiện regex trên đều sẽ chạy vào file RenderSpaView. Ở đây là mình đang để tất cả mọi trường hợp

Giờ thì phải tạo file RenderSpaView:

php artisan make:controller RenderSpaView
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class RenderSpaView extends Controller
{
    public function __invoke()
    {
        return view('spa-view');
    }
}

Giờ trong view mình sẽ tạo file spa-view.blade.php

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Laravel & React</title>
    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div id="app"></div>

<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>

Tạo các components

App Component

Giờ bạn hãy mở thư mục resources/js/components sẽ thấy 1 file Example.js, hãy đổi file này thành App.js. Và bạn vào trong file resources/js/app.js, thay đoạn require('./components/Example') thành require('./components/App') nhé. Giờ chúng ta sẽ phải cài thư viện: react-router-dom (là một thư viện định tuyến tiêu chuẩn trong react)

npm install react-router-dom

ant-design (thư viện này sẽ hỗ trợ chúng ta code frontend một cách đơn giản hơn)

npm install antd

Và đây sẽ là code trong file App.js của mình

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import 'antd/dist/antd.css';

export default class App extends Component {
    render() {
        return (
            <BrowserRouter>
                <div>
                    Hello
                </div>
            </BrowserRouter>
        );
    }
}

ReactDOM.render(<App />, document.getElementById('app'));

Giờ bạn hãy chạy

php artisan serve

npm run watch

Truy cập vào đường dẫn http://127.0.0.1:8000/ xem thử kết quả nhé 😄, nếu bạn thấy chữ hello thì tức là đã thành công rồi đó

Header component

Giờ chúng ta sẽ cần một chiếc Header đơn giản nhỉ, mình sẽ vào đây: https://ant.design/components/page-header/ và tham khảo chút. Tạo 1 file Header.js, mình sẽ để nút thêm bài viết để có thể chuyển tới trang tạo bài viết

import React from 'react';
import {Link} from 'react-router-dom'
import {PageHeader, Button} from 'antd';

class Header extends React.Component {
    render() {
        return (
            <div>
                <PageHeader
                    style={{
                        border: '1px solid rgb(235, 237, 240)',
                    }}
                    title="Bài viết"
                    extra={[
                        <Link to='create'>
                            <Button key="1" type="primary">
                                Thêm bài viết
                            </Button>
                        </Link>
                    ]}
                />
            </div>
        );
    }
}

export default Header;

Giờ quay lại App.js và thêm component Header nào, không còn Hello nữa đâu nhé

import Header from './Header';


<BrowserRouter>
    <div>
        <Header />
    </div
</BrowserRouter>

Add component

Giờ chúng ta cần 1 view để thêm bài viết, vậy mình sẽ tạo ra Add.js

import React from 'react';
import axios from 'axios'
import { Form, Input, Button } from 'antd';

const { TextArea } = Input;

class Add extends React.Component {
    constructor(props) {
        super(props)
    }

    handleSubmit = e => {
        e.preventDefault();
        const {form, history} = this.props;

        form.validateFields((err, values) => {
            if (!err) {
                axios.post('/api/posts', values)
                    .then(response => {
                        history.push('/')
                    })
                    .catch(error => {
                       console.log(error);
                    })
            }
        });
    };

    render() {
        const {form} = this.props;
        const {getFieldDecorator} = form;

        return (
            <Form labelCol={{ span: 5 }} wrapperCol={{ span: 12 }} onSubmit={this.handleSubmit}>
                <Form.Item label="Tên bài viết">
                    {getFieldDecorator('title', {
                        rules: [{ required: true, message: 'Vui lòng nhập tên bài viết!' }],
                    })(<Input />)}
                </Form.Item>
                <Form.Item label="Nội dung">
                    {getFieldDecorator('content', {
                        rules: [{ required: true, message: 'Vui lòng nhập nội dung bài viết!' }],
                    })(<TextArea rows={6} />)}
                </Form.Item>
                <Form.Item wrapperCol={{ span: 12, offset: 5 }}>
                    <Button type="primary" htmlType="submit">
                        Thêm
                    </Button>
                </Form.Item>
            </Form>
        );
    }
}
const WrappedAdd = Form.create({ name: 'addForm' })(Add);

export default WrappedAdd;

Về việc tạo ra những thành phần trong form thì bạn có thể đọc ở đây để rõ thêm nhé: https://ant.design/components/form/. Ở trong function handleSubmit, mình cần kiểm tra xem có bất kì lỗi gì không, nếu không có thì sẽ gửi 1 request api để thêm dữ liệu. Nếu các bạn từng làm quen với các frontend framework như angular hay vue thì chắc cũng không xa lạ gì với axios. Còn nếu không thì bạn có thể đọc qua ở đây: Giới thiệu về axios. Ở đây nếu thêm thành công thì mình sẽ quay trở lại trang chủ, còn phát hiện lỗi thì sẽ log lỗi đó ra console.

Và giờ hãy quay lại App.js để bổ sung thêm route này nhé

import Add from './Add';


<Header />
<Switch>
    <Route path='/create' component={Add} />
</Switch>

Bạn hãy thử ấn nút thêm bài viết và tạo thử 1 bài viết xem đã chạy tốt chưa nhé 😄

List component

Thêm được rồi thì phải có danh sách hiển thị chứ đúng không? Mình sẽ tạo List.js và chức năng xóa bài viết nhé

import React from 'react';
import {Link} from 'react-router-dom'
import {Button, List} from 'antd';
import axios from 'axios'

class ListPosts extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            data: []
        }
    }

    componentDidMount() {
        axios.get('/api/posts').then(response => {
            this.setState({
                data: response.data
            })
        })
    }

    deletePost = (id) => {
        axios.post(`/api/posts/delete/${id}`)
            .then(response => {
                alert('Xoa thanh cong');
                this.setState({
                    data: response.data
                });
            })
            .catch(error => {
                console.log(error);
            })
    };

    render() {
        const {data} = this.state;

        return (
            <List
                itemLayout="horizontal"
                dataSource={data}
                renderItem={item => (
                    <List.Item>
                        <List.Item.Meta
                            title={item.title}
                            description={item.content}
                        />
                        <Link to={`edit/${item.id}`}>
                            <Button type="primary">
                                Chỉnh sửa
                            </Button>
                        </Link>
                        <Button type="danger" onClick={() => this.deletePost(item.id)}>
                            Xóa
                        </Button>
                    </List.Item>
                )}
            />
        );
    }
}

export default ListPosts;

Trong componentDidMount mình sẽ gọi api để lấu dữ liệu và lưu data lấy về vào trong state. Còn ở function deletePost thì mình sẽ gọi tới api xóa, xóa thành công thì sẽ có alert, fail sẽ log ra. Và đừng quên thêm route trong App.js nhé và hãy chạy thử các chức năng đã hoàn thiện xem sao

import LitsPosts from './List';

<Switch>
    <Route exact path='/' component={ListPosts} />
    <Route path='/create' component={Add} />
</Switch>

Edit component

Cố lên nào, tới component cuối cùng rồi. Thật ra thì bạn có thể sử dụng lại Add.js và thêm một thuộc tính isUpdate vào trong props để phân biệt giữa add và edit. Nhưng mình cứ tách ra nhé. Tạo file Edit.js

import React from 'react';
import axios from 'axios'
import {Form, Input, Button} from 'antd';

const {TextArea} = Input;

class Edit extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            data: {}
        }
    }

    componentDidMount() {
        const {match} = this.props;

        axios.get(`/api/posts/${match.params.id}`).then(response => {
            this.setState({
                data: response.data
            })
        })
    }

    handleSubmit = e => {
        e.preventDefault();
        const {form, history, match} = this.props;

        form.validateFields((err, values) => {
            if (!err) {
                axios.post(`/api/posts/${match.params.id}`, values)
                    .then(response => {
                        alert('thanh cong');
                    })
                    .catch(error => {
                        console.log(error);
                    })
            }
        });
    };

    render() {
        const {form} = this.props;
        const {getFieldDecorator} = form;
        const {data} = this.state;

        return (
            <Form labelCol={{span: 5}} wrapperCol={{span: 12}} onSubmit={this.handleSubmit}>
                <Form.Item label="Tên bài viết">
                    {getFieldDecorator('title', {
                        rules: [{required: true, message: 'Vui lòng nhập tên bài viết!'}],
                        initialValue: data.title
                    })(<Input/>)}
                </Form.Item>
                <Form.Item label="Nội dung">
                    {getFieldDecorator('content', {
                        initialValue: data.content,
                        rules: [{required: true, message: 'Vui lòng nhập nội dung bài viết!'}],
                    })(<TextArea rows={6}/>)}
                </Form.Item>
                <Form.Item wrapperCol={{span: 12, offset: 5}}>
                    <Button type="primary" htmlType="submit">
                        Sửa
                    </Button>
                </Form.Item>
            </Form>
        );
    }
}

const WrappedEdit = Form.create({name: 'editForm'})(Edit);

export default WrappedEdit;

Ở đây ở trong phần render sẽ khác so với Add một chút là sẽ có thêm initialValue, thuộc tính này sẽ nhận vào giá trị mặc định ban đầu. Trong componentDitMount mình sẽ gọi api lấy dữ liệu, và mình dùng thuộc tính match trong props để xác định xem id của bài viết. Phần handleSubmit thì cũng gần tương tự như Add thôi.

Kết luận

Vậy là demo đã hoàn thành rồi, mong rằng sẽ giúp ích cho các bạn được phần nào 😄 Vì mình cũng không có quá nhiều thời gian làm demo này nên chưa chăm chút cho nó được lắm. Các bạn nên làm chi tiết hơn để tìm hiểu được nhiều thứ hơn nhé

Tham khảo: https://blog.pusher.com/react-laravel-application/


All Rights Reserved