Cách xây dựng một ứng dụng toàn cầu với Nuxt.js và Django
Sự ra đời của các thư viện JavaScript hiện đại như React.js và Vue.js đã thay đổi việc phát triển web front-end trở nên tốt hơn. Các thư viện này đi kèm với các tính năng bao gồm SPA (Ứng dụng trang đơn), là tính năng tải động nội dung trong các trang web mà không cần reload đầy đủ vào trình duyệt.Khái niệm đằng sau hầu hết các Ứng dụng Trang Đơn là kết xuất phía client . Trong kết xuất phía client , phần lớn nội dung được hiển thị trong trình duyệt bằng JavaScript; khi tải trang, nội dung không tải ban đầu cho đến khi JavaScript được download đầy đủ và hiển thị phần còn lại của trang web.
Chứng thực từ phía khách hàng là một khái niệm tương đối gần đây và có những đánh đổi liên quan đến việc sử dụng nó. Một mặt tiêu cực đáng chú ý là, vì nội dung không được hiển thị chính xác cho đến khi trang được cập nhật bằng JavaScript, SEO cho trang web sẽ bị ảnh hưởng vì sẽ khó có bất kỳ dữ liệu nào để các công cụ tìm kiếm thu thập dữ liệu.
Mặt khác, kết xuất phía server là cách thông thường để hiển thị các trang HTML trên trình duyệt. Trong các ứng dụng được kết xuất phía server cũ hơn, ứng dụng web được xây dựng bằng ngôn ngữ phía server như PHP. Khi một trang web được trình duyệt yêu cầu, server từ xa sẽ thêm nội dung (động) và cung cấp một trang HTML được điền.
Cũng như có những nhược điểm đối với kết xuất phía client , kết xuất phía server khiến trình duyệt gửi yêu cầu server quá thường xuyên và thực hiện lặp lại các lần reload toàn trang cho dữ liệu tương tự. Có những khung công tác JavaScript ban đầu có thể tải trang web bằng giải pháp SSR (Kết xuất phía server ), sau đó sử dụng khung công tác để xử lý định tuyến động hơn nữa và chỉ tìm nạp dữ liệu cần thiết. Các ứng dụng kết quả được gọi là Ứng dụng chung .
Tóm lại, một ứng dụng phổ quát được sử dụng để mô tả mã JavaScript có thể thực thi trên client và phía server . Trong bài viết này, ta sẽ xây dựng một ứng dụng Universal Recipe bằng Nuxt.js.
Nuxt.js là một khung cấp cao hơn để phát triển các ứng dụng Universal Vue.js. Sự sáng tạo của nó được lấy cảm hứng từ Next.js của React và nó giúp tóm tắt những khó khăn (cấu hình server và phân phối mã client ) nảy sinh trong việc cài đặt các ứng dụng Vue.js được kết xuất phía server . Nuxt.js cũng đi kèm với các tính năng hỗ trợ phát triển giữa phía client và phía server như dữ liệu async, phần mềm trung gian, bố cục, v.v.
Lưu ý: Ta có thể tham khảo ứng dụng ta xây dựng dưới dạng kết xuất Phía server (SSR) vì Vue.js đã triển khai kết xuất Phía client theo mặc định khi ta tạo Ứng dụng một trang. Trên thực tế, ứng dụng này là một ứng dụng Universal.
Trong bài viết này, ta sẽ xem cách tạo một ứng dụng Universal bằng Django và Nuxt.js. Django sẽ xử lý các hoạt động back-end và cung cấp các API bằng cách sử dụng (DRF) Django Rest Framework, trong khi Nuxt.js sẽ tạo frontend.
Đây là bản demo của ứng dụng cuối cùng:
Ta thấy rằng ứng dụng cuối cùng là một ứng dụng công thức nấu ăn thực hiện các hoạt động CRUD.
Mã nguồn cho hướng dẫn này có sẵn tại đây trên GitHub .
Yêu cầu
Để làm theo hướng dẫn này, bạn cần cài đặt những thứ sau trên máy của bạn :
Pipenv là một công cụ sẵn sàng production nhằm mục đích mang lại những gì tốt nhất trong tất cả các thế giới đóng gói cho thế giới Python. Nó khai thác Pipfile, pip và virtualenv thành một lệnh duy nhất.
Hướng dẫn giả định người đọc có những điều sau đây:
- Kiến thức làm việc cơ bản của Django và Django Rest Framework .
- Kiến thức làm việc cơ bản của Vue.js.
Cài đặt chương trình backend
Trong phần này, ta sẽ cài đặt phần backend và tạo tất cả các folder mà ta cần để cài đặt và chạy mọi thứ, vì vậy hãy chạy một version mới của terminal và tạo folder của dự án bằng cách chạy lệnh sau:
- mkdir recipes_app
Tiếp theo, ta sẽ chuyển vào folder :
- cd recipes_app
Bây giờ ta sẽ cài đặt Pipenv bằng Pip và kích hoạt một môi trường ảo mới:
- pip install pipenv
- pipenv shell
Lưu ý: Bạn nên bỏ qua lệnh đầu tiên nếu bạn đã cài đặt Pipenv trên máy tính của bạn .
Hãy cài đặt Django và các phụ thuộc khác bằng Pipenv:
- pipenv install django django-rest-framework django-cors-headers
Lưu ý: Sau khi kích hoạt môi trường ảo mới bằng Pipenv, mỗi dòng lệnh trong terminal sẽ được đặt trước tên của folder làm việc hiện tại. Trong trường hợp này, nó là (công thức_ ứng dụng).
Bây giờ, ta sẽ tạo một dự án Django mới được gọi là api
và một ứng dụng Django có tên là core
:
- django-admin startproject api
- cd api
- python manage.py startapp core
Hãy đăng ký ứng dụng core
, cùng với rest_framework
và rest_framework
cors-headers,
để dự án Django nhận ra nó. Mở file api/settings.py
và cập nhật nó cho phù hợp:
# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', # add this 'corsheaders', # add this 'core' # add this ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # add this 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] # add this block below MIDDLEWARE CORS_ORIGIN_WHITELIST = ( 'localhost:3000', ) # add the following just below STATIC_URL MEDIA_URL = '/media/' # add this MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # add this
Ta đã thêm localhost: 3000 vào danh sách trắng vì ứng dụng client sẽ được phân phối trên cổng đó và ta muốn ngăn lỗi CORS . Ta cũng đã thêm MEDIA URL và MEDIA ROOT vì ta cần chúng khi cung cấp hình ảnh trong ứng dụng.
Xác định mô hình Công thức
Hãy tạo một mô hình để xác định cách các mục Công thức sẽ được lưu trữ trong database , mở file core/models.py
và thay thế hoàn toàn bằng đoạn mã bên dưới:
from django.db import models # Create your models here. class Recipe(models.Model): DIFFICULTY_LEVELS = ( ('Easy', 'Easy'), ('Medium', 'Medium'), ('Hard', 'Hard'), ) name = models.CharField(maxlength=120) ingredients = models.CharField(max_length=400) picture = models.FileField() difficulty = models.CharField(choices=DIFFICULTY_LEVELS, max_length=10) prep_time = models.PositiveIntegerField() prep_guide = models.TextField() def __str_(self): return "Recipe for {}".format(self.name)
Đoạn mã trên mô tả sáu thuộc tính trên mô hình Công thức:
- Tên
- Thành phần
- Hình ảnh
- Khó khăn
- Prep_time
- Prep_guide
Tạo Serializers cho mô hình Recipe
Ta cần trình tuần tự hóa để chuyển đổi các version mô hình thành JSON để giao diện user có thể hoạt động với dữ liệu đã nhận. Ta sẽ tạo một file core/serializers.py
và cập nhật nó với các thông tin sau:
from rest_framework import serializers from .models import Recipe class RecipeSerializer(serializers.ModelSerializer): class Meta: model = Recipe fields = ("id", "name", "ingredients", "picture", "difficulty", "prep_time", "prep_guide")
Trong đoạn mã ở trên, ta đã chỉ định mô hình hoạt động và các trường ta muốn chuyển đổi thành JSON.
Cài đặt console Quản trị
Django cung cấp cho ta giao diện administrator ; giao diện sẽ giúp dễ dàng kiểm tra các hoạt động CRUD trên mô hình Recipe mà ta vừa tạo, nhưng trước tiên, ta sẽ thực hiện một chút cấu hình.
Mở file core/admin.py
và thay thế hoàn toàn bằng đoạn mã bên dưới:
from django.contrib import admin from .models import Recipe # add this # Register your models here. admin.site.register(Recipe) # add this
Tạo chế độ xem
Hãy tạo một lớp RecipeViewSet
trong file core/views.py
, thay thế hoàn toàn nó bằng đoạn mã bên dưới:
from rest_framework import viewsets from .serializers import RecipeSerializer from .models import Recipe class RecipeViewSet(viewsets.ModelViewSet): serializer_class = RecipeSerializer queryset = Recipe.objects.all()
Các viewsets.ModelViewSet
cung cấp các phương thức để xử lý các hoạt động CRUD theo mặc định. Ta chỉ cần làm rõ lớp serializer và queryset
.
Cài đặt URL
api/urls.py
file api/urls.py
và thay thế hoàn toàn bằng mã bên dưới. Mã này chỉ định đường dẫn URL cho API:
from django.contrib import admin from django.urls import path, include # add this from django.conf import settings # add this from django.conf.urls.static import static # add this urlpatterns = [ path('admin/', admin.site.urls), path("api/", include('core.urls')) # add this ] # add this if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Bây giờ, tạo một urls.py
file trong core
folder và paste vào đoạn mã dưới đây:
from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import RecipeViewSet router = DefaultRouter() router.register(r'recipes', RecipeViewSet) urlpatterns = [ path("", include(router.urls)) ]
Trong đoạn mã trên, lớp router
tạo các mẫu URL sau:
- / cooking / - Các thao tác Tạo và Đọc có thể được thực hiện trên tuyến đường này.
- / cooking / {id} - Các thao tác Đọc, Cập nhật và Xóa có thể được thực hiện trên tuyến đường này.
Chạy di cư
Bởi vì gần đây ta đã tạo mô hình Recipe và xác định cấu trúc của nó, ta cần tạo file Di chuyển và áp dụng các thay đổi trên mô hình vào database , vì vậy hãy chạy các lệnh sau:
- python manage.py makemigrations
- python manage.py migrate
Bây giờ, ta sẽ tạo một account superuser để truy cập vào giao diện quản trị:
- python manage.py createsuperuser
Bạn sẽ được yêu cầu nhập tên user , email và password cho superuser. Đảm bảo nhập các chi tiết mà bạn có thể nhớ vì bạn cần chúng để đăng nhập vào console quản trị trong thời gian ngắn.
Đó là tất cả cấu hình cần thực hiện trên phần backend . Bây giờ ta có thể kiểm tra các API mà ta đã tạo, vì vậy hãy khởi động server Django:
- python manage.py runserver
Sau khi server đang chạy, hãy truy cập http: // localhost: 8000 / api / cooks / đảm bảo nó hoạt động:
Ta có thể tạo một mục Recipe mới bằng giao diện:
Ta cũng có thể thực hiện các thao tác XÓA, XÓA và ĐÓNG trên các mục Công thức cụ thể bằng cách sử dụng khóa chính id
của chúng. Để thực hiện việc này, ta sẽ truy cập một địa chỉ có cấu trúc này /api/recipe/{id}.
Hãy thử với địa chỉ này - http: // localhost: 8000 / api / cooks / 1 :
Đó là tất cả cho phần backend của ứng dụng, bây giờ ta có thể chuyển sang phần bổ sung cho giao diện user .
Cài đặt giao diện user
Trong phần này của hướng dẫn, ta sẽ xây dựng giao diện user của ứng dụng. Ta muốn đặt folder cho mã front-end trong folder root của folder recipes_app
. Vì vậy, hãy chuyển ra khỏi folder api
(hoặc tạo một terminal mới để chạy cùng với phần trước) trước khi chạy các lệnh trong phần này.
Hãy tạo một ứng dụng nuxt
tên là client
bằng lệnh này:
- npx create-nuxt-app client
Lưu ý: Trước tạo-nuxt-app với npx sẽ cài đặt gói nếu nó chưa được cài đặt trên phạm vi global trên máy của bạn.
Sau khi cài đặt xong, create-nuxt-app
sẽ hỏi một số câu hỏi về các công cụ bổ sung sẽ được thêm vào. Ta sẽ trả lời chúng như sau:
- Nhập tên dự án hoặc chỉ cần nhấn enter cho mặc định
- Nhập mô tả dự án hoặc chỉ cần nhấn enter cho mặc định
- Không chọn nào cho khung server tùy chỉnh
- Chọn hỗ trợ PWA cho các tính năng để cài đặt
- Chọn bootstrap cho khung giao diện user
- Không chọn nào cho khung thử nghiệm
- Chọn Universal cho chế độ kết xuất
- Nhập tên tác giả hoặc chỉ cần nhấn enter cho mặc định
- Chọn npm cho trình quản lý gói
Thao tác này sẽ kích hoạt cài đặt các phần phụ thuộc bằng trình quản lý gói đã chọn và cuối cùng, bạn sẽ thấy một màn hình như sau:
Hãy chạy các lệnh sau để khởi động ứng dụng ở chế độ phát triển:
- cd client
- npm run dev
Khi server phát triển đã khởi động, hãy truy cập http: // localhost: 3000 để xem ứng dụng:
Bây giờ ta hãy xem cấu trúc folder của folder client
:
├── client ├── assets/ ├── components/ ├── layouts/ ├── middleware/ ├── node_modules/ ├── pages/ ├── plugins/ ├── static/ └── store/
Dưới đây là bảng phân tích về những folder này dùng để làm gì:
- Nội dung - Chứa các file không được biên dịch như hình ảnh, CSS, SASS và file JavaScript.
- Các thành phần - Chứa các thành phần Vue.js.
- Bố cục - Chứa các bố cục của ứng dụng; Bố cục được sử dụng để thay đổi giao diện của một trang và được dùng cho nhiều trang.
- Phần mềm trung gian - Chứa phần mềm trung gian của ứng dụng; Phần mềm trung gian là các chức năng tùy chỉnh được chạy trước khi một trang được hiển thị.
- Pages - Chứa Chế độ xem và Đường dẫn của ứng dụng. Nuxt.js đọc tất cả các file
.vue
trong folder này và sử dụng thông tin để tạo bộ định tuyến của ứng dụng. - Plugin - Chứa các plugin JavaScript sẽ được chạy trước khi ứng dụng Vue.js root được chạy .
- Tĩnh - Chứa các file tĩnh (các file không có khả năng thay đổi) và tất cả các file này được ánh xạ tới folder root của ứng dụng, là
/
. - Lưu trữ - Chứa các file lưu trữ nếu ta định sử dụng Vuex với Nuxt.js.
Ngoài ra còn có file nuxt.config.js
trong folder client
, file này chứa cấu hình tùy chỉnh cho ứng dụng Nuxt.js. Trước khi ta tiếp tục, hãy download tệp zip này, extract nó và đặt các images/
folder bên trong folder static/
.
Cấu trúc của các trang
Trong phần này, ta sẽ thêm một số file .vue
vào các pages/
folder để ứng dụng của ta sẽ có năm trang:
- Trang chủ
- Tất cả trang danh sách công thức nấu ăn
- Trang xem một công thức
- Trang chỉnh sửa Công thức đơn
- Thêm trang Recipe
Hãy thêm các file và folder .vue
sau vào các pages/
folder để ta có cấu trúc chính xác này:
├── pages/ ├── recipes/ ├── _id/ └── edit.vue └── index.vue └── add.vue └── index.vue └── index.vue
Cấu trúc file ở trên sẽ tạo ra các tuyến sau:
- / → được xử lý bởi
pages/index.vue
- / công thức nấu ăn / thêm → được xử lý bởi
pages/recipes/add.vue
- / công thức nấu ăn / → được xử lý bởi
pages/recipes/index.vue
-
pages/recipes/_id/index.vue
/ {id} / → dopages/recipes/_id/index.vue
- / cooks / {id} / edit → do
pages/recipes/_id/edit.vue
.vue
hoặc folder có dấu gạch dưới sẽ tạo ra một tuyến động. Điều này rất hữu ích trong ứng dụng của ta vì nó sẽ giúp dễ dàng hiển thị các công thức nấu ăn khác nhau dựa trên ID của chúng, ví dụ: công thức nấu ăn / 1 /, công thức nấu ăn / 2 /, v.v.
Tạo trang chủ
Trong Nuxt.js, Bố cục là một trợ giúp đắc lực khi bạn muốn thay đổi giao diện ứng dụng của bạn . Như vậy, mỗi version của ứng dụng Nuxt.js đều có một Bố cục mặc định, ta muốn loại bỏ tất cả các kiểu để chúng không ảnh hưởng đến ứng dụng của ta .
Mở file layouts/default.vue
và thay thế bằng đoạn mã sau:
<template> <div> <nuxt/> </div> </template> <style> </style>
Hãy cập nhật file pages/index.vue
bằng mã bên dưới:
<template> <header> <div class="text-box"> <h1>La Recipes ?</h1> <p class="mt-3">Recipes for the meals we love ❤️ ️</p> <nuxt-link class="btn btn-outline btn-large btn-info" to="/recipes"> View Recipes <span class="ml-2">→</span> </nuxt-link> </div> </header> </template> <script> export default { head() { return { title: "Home page" }; }, }; </script> <style> header { min-height: 100vh; background-image: linear-gradient( to right, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.4) ), url("/images/banner.jpg"); background-position: center; background-size: cover; position: relative; } .text-box { position: absolute; top: 50%; left: 10%; transform: translateY(-50%); color: #fff; } .text-box h1 { font-family: cursive; font-size: 5rem; } .text-box p { font-size: 2rem; font-weight: lighter; } </style>
Từ đoạn mã trên, <nuxt-link>
là một thành phần Nuxt.js được dùng để chuyển giữa các trang. Nó rất giống với thành phần <router-link>
từ Vue Router .
Hãy bắt đầu server phát triển front-end (nếu nó chưa chạy), hãy truy cập http: // localhost: 3000 / và xem Trang chủ trông như thế nào:
- npm run dev
Luôn đảm bảo server Django back-end luôn chạy trong một version khác của terminal vì front-end sẽ sớm bắt đầu giao tiếp với nó để lấy dữ liệu.
Mỗi trang trong ứng dụng này sẽ là một Thành phần Vue
và Nuxt.js cung cấp các thuộc tính và chức năng đặc biệt để giúp cho việc phát triển các ứng dụng được liền mạch. Bạn có thể tìm thấy tài liệu về tất cả các thuộc tính đặc biệt này tại Nuxt.js.
Vì lợi ích của hướng dẫn này, ta sẽ sử dụng hai trong số các chức năng sau:
-
head()
- Phương thức này được sử dụng để đặt các<meta>
cụ thể cho trang hiện tại. -
asyncData()
- Phương thức này được sử dụng để tìm nạp dữ liệu trước khi thành phần trang được tải. Đối tượng trả về sau đó được hợp nhất với dữ liệu của thành phần trang. Ta sẽ sử dụng điều này sau trong hướng dẫn này.
Tạo trang danh sách công thức nấu ăn
Hãy tạo một thành phần Vue.js có tên là RecipeCard.vue
trong folder components/
và cập nhật nó bằng đoạn mã bên dưới:
<template> <div class="card recipe-card"> <img :src="recipe.picture" class="card-img-top" > <div class="card-body"> <h5 class="card-title">{{ recipe.name }}</h5> <p class="card-text"> <strong>Ingredients:</strong> {{ recipe.ingredients }} </p> <div class="action-buttons"> <nuxt-link :to="`/recipes/${recipe.id}/`" class="btn btn-sm btn-success"> View </nuxt-link> <nuxt-link :to="`/recipes/${recipe.id}/edit/`" class="btn btn-sm btn-primary"> Edit </nuxt-link> <button @click="onDelete(recipe.id)" class="btn btn-sm btn-danger">Delete</button> </div> </div> </div> </template> <script> export default { props: ["recipe", "onDelete"] }; </script> <style> .recipe-card { box-shadow: 0 1rem 1.5rem rgba(0,0,0,.6); } </style>
Thành phần trên chấp nhận hai đạo cụ:
- Một đối tượng
recipe
có chứa thông tin về một công thức cụ thể. - Phương thức
onDelete
sẽ được kích hoạt khi nào user nhấp vào nút để xóa công thức.
Tiếp theo, mở các pages/recipes/index.vue
và cập nhật nó bằng đoạn mã bên dưới:
<template> <main class="container mt-5"> <div class="row"> <div class="col-12 text-right mb-4"> <div class="d-flex justify-content-between"> <h3>La Recipes</h3> <nuxt-link to="/recipes/add" class="btn btn-info">Add Recipe</nuxt-link> </div> </div> <template v-for="recipe in recipes"> <div :key="recipe.id" class="col-lg-3 col-md-4 col-sm-6 mb-4"> <recipe-card :onDelete="deleteRecipe" :recipe="recipe"></recipe-card> </div> </template> </div> </main> </template> <script> import RecipeCard from "~/components/RecipeCard.vue"; const sampleData = [ { id: 1, name: "Jollof Rice", picture: "/images/food-1.jpeg", ingredients: "Beef, Tomato, Spinach", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " }, { id: 2, name: "Macaroni", picture: "/images/food-2.jpeg", ingredients: "Beef, Tomato, Spinach", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " }, { id: 3, name: "Fried Rice", picture: "/images/banner.jpg", ingredients: "Beef, Tomato, Spinach", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " } ]; export default { head() { return { title: "Recipes list" }; }, components: { RecipeCard }, asyncData(context) { let data = sampleData; return { recipes: data }; }, data() { return { recipes: [] }; }, methods: { deleteRecipe(recipe_id) { console.log(deleted `${recipe.id}`) } } }; </script> <style scoped> </style>
Hãy bắt đầu server phát triển front-end (nếu nó chưa chạy), hãy truy cập http: // localhost: 3000 / công thức nấu ăn và xem trang danh sách công thức nấu ăn này:
- npm run dev
Từ hình ảnh trên, ta thấy rằng ba thẻ công thức xuất hiện ngay cả khi ta đặt recipes
thành một mảng trống trong phần dữ liệu của thành phần. Giải thích cho điều này là phương thức asyncData
được thực thi trước khi tải trang và nó trả về một đối tượng cập nhật dữ liệu của thành phần.
Bây giờ, tất cả những gì ta cần làm là sửa đổi phương thức asyncData
để thực hiện một yêu cầu api
tới phần backend Django và cập nhật dữ liệu của thành phần với kết quả.
Trước khi làm điều đó, ta phải cài đặt và cấu hình Axios
:
- npm install -s @nuxtjs/axios
Sau khi Axios
được cài đặt, hãy mở file nuxt.config.js
và cập nhật nó cho phù hợp:
/_ ** Nuxt.js modules _/ modules: [, // Doc: https://bootstrap-vue.js.org/docs/ 'bootstrap-vue/nuxt', '@nuxtjs/axios' // add this ], // add this Axios object axios: { baseURL: "http://localhost:8000/api" },
Bây giờ, hãy mở file pages/recipes/index.vue
và thay thế phần <script>
bằng phần bên dưới:
[...] <script> import RecipeCard from "~/components/RecipeCard.vue"; export default { head() { return { title: "Recipes list" }; }, components: { RecipeCard }, async asyncData({ $axios, params }) { try { let recipes = await $axios.$get(`/recipes/`); return { recipes }; } catch (e) { return { recipes: [] }; } }, data() { return { recipes: [] }; }, methods: { async deleteRecipe(recipe_id) { try { await this.$axios.$delete(`/recipes/${recipe_id}/`); // delete recipe let newRecipes = await this.$axios.$get("/recipes/"); // get new list of recipes this.recipes = newRecipes; // update list of recipes } catch (e) { console.log(e); } } } }; </script> [...]
Trong đoạn mã trên, asyncData()
nhận một đối tượng có tên là context
, ta sẽ hủy cấu trúc để lấy $axios
. Bạn có thể kiểm tra tất cả các thuộc tính của context
tại đây .
Ta bọc asyncData () trong một khối try… vì ta muốn ngăn lỗi xảy ra nếu server back-end không chạy và axios không truy xuất được dữ liệu. Thay vào đó, khi nào điều đó xảy ra, công thức nấu ăn chỉ được đặt thành một mảng trống.
Dòng mã này - let recipes = await $axios.$get("/recipes/")
- là version ngắn hơn của:
let response = await $axios.get("/recipes") let recipes = response.data
Phương thức deleteRecipe()
xóa một công thức nấu ăn cụ thể, tìm nạp danh sách các công thức nấu ăn mới nhất từ phần backend Django và cuối cùng cập nhật dữ liệu của thành phần.
Ta có thể khởi động server phát triển front-end (nếu nó chưa chạy) ngay bây giờ và ta sẽ thấy rằng các thẻ công thức hiện đang được điền dữ liệu từ phần backend Django.
Để điều này hoạt động, server backend Django phải đang chạy và phải có một số dữ liệu (được nhập từ giao diện quản trị) có sẵn cho các mục Công thức.
Hãy truy cập http: // localhost: 3000 / công thức nấu ăn và xem qua:
- npm run dev
Bạn cũng có thể thử xóa các mục công thức và xem chúng cập nhật tương ứng.
Thêm công thức mới
Như ta đã thảo luận, ta muốn có thể thêm công thức nấu ăn mới từ giao diện user của ứng dụng, vì vậy hãy mở các pages/recipes/add/
file và cập nhật nó bằng đoạn mã sau:
<template> <main class="container my-5"> <div class="row"> <div class="col-12 text-center my-3"> <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img v-if="preview" class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="preview" alt > <img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" src="@/static/images/placeholder.png" > </div> <div class="col-md-4"> <form @submit.prevent="submitRecipe"> <div class="form-group"> <label for>Recipe Name</label> <input type="text" class="form-control" v-model="recipe.name"> </div> <div class="form-group"> <label for>Ingredients</label> <input v-model="recipe.ingredients" type="text" class="form-control"> </div> <div class="form-group"> <label for>Food picture</label> <input type="file" name="file" @change="onFileChange"> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for>Difficulty</label> <select v-model="recipe.difficulty" class="form-control"> <option value="Easy">Easy</option> <option value="Medium">Medium</option> <option value="Hard">Hard</option> </select> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for> Prep time <small>(minutes)</small> </label> <input v-model="recipe.prep_time" type="number" class="form-control"> </div> </div> </div> <div class="form-group mb-3"> <label for>Preparation guide</label> <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </main> </template> <script> export default { head() { return { title: "Add Recipe" }; }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" }, preview: "" }; }, methods: { onFileChange(e) { let files = e.target.files || e.dataTransfer.files; if (!files.length) { return; } this.recipe.picture = files[0]; this.createImage(files[0]); }, createImage(file) { // let image = new Image(); let reader = new FileReader(); let vm = this; reader.onload = e => { vm.preview = e.target.result; }; reader.readAsDataURL(file); }, async submitRecipe() { const config = { headers: { "content-type": "multipart/form-data" } }; let formData = new FormData(); for (let data in this.recipe) { formData.append(data, this.recipe[data]); } try { let response = await this.$axios.$post("/recipes/", formData, config); this.$router.push("/recipes/"); } catch (e) { console.log(e); } } } }; </script> <style scoped> </style>
Trong submitRecipe()
, sau khi dữ liệu biểu mẫu đã được đăng và công thức được tạo thành công, ứng dụng sẽ được chuyển hướng đến /recipes/
cooking /recipes/
sử dụng this.$router
. this.$router
.
Tạo trang Xem một Công thức
Hãy tạo chế độ xem cho phép user xem một mục Công thức, mở file /pages/recipes/_id/index.vue
và paste vào đoạn mã bên dưới:
<template> <main class="container my-5"> <div class="row"> <div class="col-12 text-center my-3"> <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="recipe.picture" alt > </div> <div class="col-md-6"> <div class="recipe-details"> <h4>Ingredients</h4> <p>{{ recipe.ingredients }}</p> <h4>Preparation time ⏱</h4> <p>{{ recipe.prep_time }} mins</p> <h4>Difficulty</h4> <p>{{ recipe.difficulty }}</p> <h4>Preparation guide</h4> <textarea class="form-control" rows="10" v-html="recipe.prep_guide" disabled /> </div> </div> </div> </main> </template> <script> export default { head() { return { title: "View Recipe" }; }, async asyncData({ $axios, params }) { try { let recipe = await $axios.$get(`/recipes/${params.id}`); return { recipe }; } catch (e) { return { recipe: [] }; } }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" } }; } }; </script> <style scoped> </style>
Ta giới thiệu các params
key nhìn thấy trong asyncData()
phương pháp. Trong trường hợp này, ta đang sử dụng params
để có được ID
của công thức ta muốn xem. Ta extract params
từ URL
và tìm nạp trước dữ liệu của nó trước khi hiển thị nó trên trang.
Bây giờ ta có thể xem một mục Recipe trên trình duyệt web và thấy một màn hình tương tự:
Tạo trang chỉnh sửa công thức duy nhất
Ta cần tạo chế độ xem cho phép user chỉnh sửa và cập nhật một mục Công thức, vì vậy hãy mở file /pages/recipes/_id/edit.vue
và paste vào đoạn mã bên dưới:
<template> <main class="container my-5"> <div class="row"> <div class="col-12 text-center my-3"> <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img v-if="!preview" class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="recipe.picture"> <img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="preview"> </div> <div class="col-md-4"> <form @submit.prevent="submitRecipe"> <div class="form-group"> <label for>Recipe Name</label> <input type="text" class="form-control" v-model="recipe.name" > </div> <div class="form-group"> <label for>Ingredients</label> <input type="text" v-model="recipe.ingredients" class="form-control" name="Ingredients" > </div> <div class="form-group"> <label for>Food picture</label> <input type="file" @change="onFileChange"> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for>Difficulty</label> <select v-model="recipe.difficulty" class="form-control" > <option value="Easy">Easy</option> <option value="Medium">Medium</option> <option value="Hard">Hard</option> </select> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for> Prep time <small>(minutes)</small> </label> <input type="text" v-model="recipe.prep_time" class="form-control" name="Ingredients" > </div> </div> </div> <div class="form-group mb-3"> <label for>Preparation guide</label> <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea> </div> <button type="submit" class="btn btn-success">Save</button> </form> </div> </div> </main> </template> <script> export default { head(){ return { title: "Edit Recipe" } }, async asyncData({ $axios, params }) { try { let recipe = await $axios.$get(`/recipes/${params.id}`); return { recipe }; } catch (e) { return { recipe: [] }; } }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" }, preview: "" }; }, methods: { onFileChange(e) { let files = e.target.files || e.dataTransfer.files; if (!files.length) { return; } this.recipe.picture = files[0] this.createImage(files[0]); }, createImage(file) { let reader = new FileReader(); let vm = this; reader.onload = e => { vm.preview = e.target.result; }; reader.readAsDataURL(file); }, async submitRecipe() { let editedRecipe = this.recipe if (editedRecipe.picture.indexOf("http://") != -1){ delete editedRecipe["picture"] } const config = { headers: { "content-type": "multipart/form-data" } }; let formData = new FormData(); for (let data in editedRecipe) { formData.append(data, editedRecipe[data]); } try { let response = await this.$axios.$patch(`/recipes/${editedRecipe.id}/`, formData, config); this.$router.push("/recipes/"); } catch (e) { console.log(e); } } } }; </script> <style> </style>
Trong đoạn mã trên, phương thức submitRecipe()
có một câu lệnh điều kiện có mục đích là xóa ảnh của một mục Công thức đã chỉnh sửa khỏi dữ liệu sẽ được gửi nếu ảnh không được thay đổi.
Khi mục Công thức nấu ăn đã được cập nhật, ứng dụng được chuyển hướng đến trang danh sách Công thức nấu ăn - /recipes/.
Cài đặt chuyển đổi
Ứng dụng có đầy đủ chức năng, tuy nhiên, ta có thể cung cấp cho nó một giao diện mượt mà hơn bằng cách thêm các hiệu ứng chuyển tiếp, cho phép ta thay đổi trơn tru các giá trị thuộc tính CSS (từ giá trị này sang giá trị khác) trong một khoảng thời gian nhất định.
Ta sẽ cài đặt chuyển tiếp trong file nuxt.config.js
. Theo mặc định , tên chuyển đổi được đặt thành page,
điều này đơn giản nghĩa là các chuyển đổi mà ta xác định sẽ hoạt động trên tất cả các trang.
Hãy bao gồm kiểu dáng cho quá trình chuyển đổi. Tạo một folder có tên css/
trong assets/
folder và thêm một transitions.css
file bên trong. Bây giờ, hãy mở file transitions.css
và paste vào đoạn mã bên dưới:
.page-enter-active, .page-leave-active { transition: opacity .3s ease; } .page-enter, .page-leave-to { opacity: 0; }
Mở file nuxt.config.js
và cập nhật nó cho phù hợp để tải file CSS mà ta vừa tạo:
module.exports = { /_ ** Global CSS _/ css: ['~/assets/css/transitions.css'], // update this }
Bây giờ ứng dụng của ta sẽ thay đổi khung trên mỗi chuyển theo cách đẹp mắt:
Kết luận
Trong bài viết này, ta đã bắt đầu bằng cách tìm hiểu sự khác biệt giữa các ứng dụng được hiển thị phía client và phía server . Ta đã tiếp tục tìm hiểu ứng dụng global là gì và cuối cùng, ta đã biết cách xây dựng một ứng dụng global bằng Nuxt.js và Django.
Mã nguồn cho hướng dẫn này có sẵn tại đây trên GitHub.
Các tin liên quan