Udemy - Nuxt JS with Laravel API - Building SSR Vue JS Apps 笔记14 Laravel Nuxt - Create and Read
Moving to CRUD
Topic Model and Post Model Migration
执行:
php artisan make:model Topic -m
php artisan make:model Post -m
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateTopicsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('topics', function (Blueprint $table) { $table->id(); $table->string('title'); $table->unsignedBigInteger('user_id')->index(); $table->timestamps(); //user 删除的时候 删除属于该用户的topics $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('topics'); } }
<?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->id(); $table->string('body'); $table->unsignedBigInteger('topic_id')->index(); $table->unsignedBigInteger('user_id')->index(); $table->timestamps(); $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); $table->foreign('topic_id')->references('id')->on('topics')->cascadeOnDelete(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('posts'); } }
执行:
php artisan migrate
Topic/User/Posts Relationships
Topic.php:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Topic extends Model { protected $fillable = [ 'title', ]; public function user() { return $this->belongsTo(User::class); } public function posts() { return $this->hasMany(Post::class); } }
Post.php
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { protected $fillable = ['body']; public function user() { return $this->belongsTo(User::class); } public function topic() { return $this->belongsTo(Topic::class); } }
User.php:
<?php namespace App; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Tymon\JWTAuth\Contracts\JWTSubject; class User extends Authenticatable implements JWTSubject { use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'password', 'remember_token', ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'email_verified_at' => 'datetime', ]; // Rest omitted for brevity /** * Get the identifier that will be stored in the subject claim of the JWT. * * @return mixed */ public function getJWTIdentifier() { //return the primary key of the user - user id return $this->getKey(); } /** * Return a key value array, containing any custom claims to be added to the JWT. * * @return array */ public function getJWTCustomClaims() { //return a key value array containing any claims to be added to JWT return []; } public function topics() { return $this->hasMany(Topic::class); } public function posts() { return $this->hasMany(Post::class); } }
Scope Trait
新建app\Traits\Orderable.php:
<?php namespace App\Traits; trait Orderable { public function scopeLatestFirst($query) { return $query->orderBy('created_at', 'desc'); } public function scopeOldestFirst($query) { return $query->orderBy('created_at', 'asc'); } }
然后:
在Post 和 Topic model中使用这个traits
Create a New Topic
先增加api.php中的route
Route::group(['prefix' => 'topics'], function () { Route::post('/', 'TopicController@store')->middleware('auth'); });
创建这个TopicController,执行
php artisan make:controller TopicController
TopicController.php:
<?php namespace App\Http\Controllers; use App\Post; use App\Topic; use Illuminate\Http\Request; class TopicController extends Controller { public function store(Request $request) { $topic = new Topic; $topic->title = $request->get('title'); $topic->user()->associate($request->user()); $post = new Post; $post->body = $request->get('body'); $post->user()->associate($request->user()); $topic->save(); $topic->posts()->save($post); } }
注意这里没有验证,后面我们使用自定义的request 在该request类中进行验证。
打开Postman测试:
请求前请登录获取token 并把token设置到Bearer token
查看数据库中结果:
Topic Resource
执行
php artisan make:resource TopicResource
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class TopicResource extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, 'title' => $this->title, 'created_at' => $this->created_at->diffForHumans(), 'updated_at' => $this->updated_at->diffForHumans(), // 'posts'=> 'user' => $this->user, ]; // return parent::toArray($request); } }
上面可以用$this->user方式加载user 这样就不用with(‘user’) 或者load(‘user’)了。
TopicController.php修改:
<?php namespace App\Http\Controllers; use App\Http\Resources\TopicResource; use App\Post; use App\Topic; use Illuminate\Http\Request; class TopicController extends Controller { public function store(Request $request) { $topic = new Topic; $topic->title = $request->get('title'); $topic->user()->associate($request->user()); $post = new Post; $post->body = $request->get('body'); $post->user()->associate($request->user()); $topic->save(); $topic->posts()->save($post); return TopicResource::make($topic); } }
Postman测试:
Post Resource
执行:
php artisan make:resource PostResource
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class PostResource extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, 'body' => $this->body, 'created_at' => $this->created_at->diffForHumans(), 'updated_at' => $this->updated_at->diffForHumans(), 'user' => $this->user, ]; // return parent::toArray($request); } }
更新TopicResource.php
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class TopicResource extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, 'title' => $this->title, 'created_at' => $this->created_at->diffForHumans(), 'updated_at' => $this->updated_at->diffForHumans(), 'posts' => PostResource::collection($this->posts), 'user' => $this->user, ]; // return parent::toArray($request); } }
Postman测试结果:
Topic Request Validation
执行:
php artisan make:request TopicCreateRequest
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class TopicCreateRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'title' => 'required|string|max:255', 'body' => 'required|string|max:200', ]; } }
更新TopicController.php
<?php namespace App\Http\Controllers; use App\Http\Requests\TopicCreateRequest; use App\Http\Resources\TopicResource; use App\Post; use App\Topic; use Illuminate\Http\Request; class TopicController extends Controller { public function store(TopicCreateRequest $request) { $topic = new Topic; $topic->title = $request->get('title'); $topic->user()->associate($request->user()); $post = new Post; $post->body = $request->get('body'); $post->user()->associate($request->user()); $topic->save(); $topic->posts()->save($post); return TopicResource::make($topic); } }
用postman测试:
注意要设置BearerToken 以及 Headers 中设置 Accept和Content-Type
否则会返回一个404 PageNotFound错误
测试一下不发送title:
正常结果:
=============================================================
下面开始更新前端,
Topic Create Page
把之前创建的pages/profile.vue更为pages/dashboard.vue
<template> <div> <h2>User Dashboard</h2> </div> </template> <script> export default { name: "dashboard" } </script> <style scoped> </style>
pages/login.vue中:
pages/register.vue中:
middleware/guest.js:
执行 npm run dev 启动,打开 http://localhost:3000/ 检查工作是否正常:
登录后跳转Dashboard
正常!
dashboard.vue更新:
<template> <div class="container col-md-6 mt-5"> <h2>User Dashboard</h2> <hr> <h3>Create a new topic</h3> <form @submit.prevent="create"> <div class="form-group"> <label><strong>Topic Title:</strong></label> <input type="text" class="form-control" v-model.trim="form.title" autofocus> <small class="form-text text-danger" v-if="errors.title">{{errors.title[0]}}</small> </div> <div class="form-group"> <label><strong>Topic Body:</strong></label> <textarea class="form-control" rows="5" v-model.trim="form.body"></textarea> <small class="form-text text-danger" v-if="errors.body">{{errors.body[0]}}</small> </div> <button type="submit" class="btn btn-primary">Create</button> </form> </div> </template> <script> export default { name: "dashboard", data() { return { form: { title: '', body: '', } } }, methods: { async create() { try { await this.$axios.post('/topics', this.form); } catch (e) { return; } this.$router.push('/'); } } } </script> <style scoped> </style>
更新下Navbar.vue:
<template> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <nuxt-link to="/" class="navbar-brand">Frontend</nuxt-link> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav"> <li class="nav-item active"> <nuxt-link class="nav-link" to="/">Home</nuxt-link> </li> <li class="nav-item"> <nuxt-link class="nav-link" to="/dashboard">Create</nuxt-link> </li> </ul> <template v-if="!authenticated"> <ul class="navbar-nav ml-auto"> <li class="nav-item"> <nuxt-link class="nav-link" to="/login">Login</nuxt-link> </li> <li class="nav-item"> <nuxt-link class="nav-link" to="/register">Register</nuxt-link> </li> </ul> </template> <template v-else> <ul class="navbar-nav ml-auto"> <li class="nav-item"> <a class="nav-link">{{user.name}}</a> </li> <li class="nav-item"> <a class="nav-link" @click.prevent="logout">Logout</a> </li> </ul> </template> </div> </nav> </template> <script> export default { name: "Navbar", methods: { logout() { this.$auth.logout(); }, } } </script> <style scoped> </style>
创建topic 进入dashboard 应该要认证的用户才可以,所以需要在前端页面加middleware:
dashboard.vue更新为:
<template> <div class="container col-md-6 mt-5"> <h2>User Dashboard</h2> <hr> <h3>Create a new topic</h3> <form @submit.prevent="create"> <div class="form-group"> <label><strong>Topic Title:</strong></label> <input type="text" class="form-control" v-model.trim="form.title" autofocus> <small class="form-text text-danger" v-if="errors.title">{{errors.title[0]}}</small> </div> <div class="form-group"> <label><strong>Topic Body:</strong></label> <textarea class="form-control" rows="5" v-model.trim="form.body"></textarea> <small class="form-text text-danger" v-if="errors.body">{{errors.body[0]}}</small> </div> <button type="submit" class="btn btn-primary">Create</button> </form> </div> </template> <script> export default { name: "dashboard", middleware: ['auth',], data() { return { form: { title: '', body: '', } } }, methods: { async create() { try { await this.$axios.post('/topics', this.form); } catch (e) { return; } this.$router.push('/'); } } } </script> <style scoped> </style>
测试dashboard创建,暂时不输入内容:
提示为:
输入内容测试:
创建点击后,跳转了首页。
因为
数据库中结果:
Return All Posts
打开backend项目,
api.php添加一条路由:
TopicController.php添加index方法:
使用Postman测试:
Get All Posts –Nuxt
新建pages/topics/index.vue文件:
<template> <div class="container"> </div> </template> <script> export default { name: "index.vue", data() { return { topics: [], } }, async asyncData({$axios}) { let response = await $axios.get('/topics'); console.log(response.data); } } </script> <style scoped> </style>
Navbar.vue添加一个link跳转topics
结果:
修一下再看结果:
<template> <div class="container"> <h2>Latest Topics</h2> <pre>{{topics}}</pre> </div> </template> <script> export default { name: "index.vue", data() { return { topics: [], } }, async asyncData({$axios}) { let {data} = await $axios.$get('/topics'); return { topics: data.data, } } } </script> <style scoped> </style>
Showing All Topics
pages/topics/index.vue:
<template> <div class="container"> <h2>Latest Topics</h2> <div v-for="(topic,index) in topics" :key="index" class="bg-light mt-5 mb-5" style="padding: 20px"> <h2>{{topic.title}}</h2> <p class="text-muted">{{topic.created_at}} by {{topic.user.name}}</p> <div v-for="(content,index) in topic.posts" :key="index" class="ml-5 content"> {{content.body}} <p class="text-muted">{{content.created_at}} by {{content.user.name}}</p> </div> </div> </div> </template> <script> export default { name: "index.vue", data() { return { topics: [], } }, async asyncData({$axios}) { let {data} = await $axios.$get('/topics'); return { topics: data, } } } </script> <style scoped> .content { border-left: 10px solid white; padding: 0 10px 0 10px; } </style>
效果:
Pagination
pages/topics/index.vue更新为:
<template> <div class="container"> <h2>Latest Topics</h2> <div v-for="(topic,index) in topics" :key="index" class="bg-light mt-5 mb-5" style="padding: 20px"> <h2>{{topic.title}}</h2> <p class="text-muted">{{topic.created_at}} by {{topic.user.name}}</p> <div v-for="(content,index) in topic.posts" :key="index" class="ml-5 content"> {{content.body}} <p class="text-muted">{{content.created_at}} by {{content.user.name}}</p> </div> </div> <nav> <ul class="pagination justify-content-center"> <li v-for="(key,value) in links" class="page-item"> <a @click="loadMore(key)" class="page-link">{{value}}</a> </li> </ul> </nav> </div> </template> <script> export default { name: "index.vue", data() { return { topics: [], links: [], } }, async asyncData({$axios}) { let {data, links,} = await $axios.$get('/topics'); return { topics: data, links, } }, methods: { async loadMore(key) { console.log(key); } } } </script> <style scoped> .content { border-left: 10px solid white; padding: 0 10px 0 10px; } </style>
然后测试点击:
更新pages/topics/index.vue:
<template> <div class="container"> <h2>Latest Topics</h2> <div v-for="(topic,index) in topics" :key="index" class="bg-light mt-5 mb-5" style="padding: 20px"> <h2>{{topic.title}}</h2> <p class="text-muted">{{topic.created_at}} by {{topic.user.name}}</p> <div v-for="(content,index) in topic.posts" :key="index" class="ml-5 content"> {{content.body}} <p class="text-muted">{{content.created_at}} by {{content.user.name}}</p> </div> </div> <nav> <ul class="pagination justify-content-center"> <li v-for="(key,value) in links" class="page-item"> <a @click.prevent="loadMore(key)" class="page-link">{{value}}</a> </li> </ul> </nav> </div> </template> <script> export default { name: "index.vue", data() { return { topics: [], links: [], } }, async asyncData({$axios}) { let {data, links,} = await $axios.$get('/topics'); return { topics: data, links, } }, methods: { async loadMore(key) { if (key === null) { return; } let {data, links} = await this.$axios.$get(key); return this.topics = {...this.topics, ...data}; } } } </script> <style scoped> .content { border-left: 10px solid white; padding: 0 10px 0 10px; } </style>
Respond Single Topic
backend 项目 增加一个route到api.php:
TopicController.php添加show方法:
可以用PostMan测试一下结果:
Get Single Topic
更新pages/topics/index.vue文件:
需要支持点击跳转到对应topic页面:
而对应的页面新建pages/topics/_id/index.vue:
<template> <div class="container"> <h2>Single Topic Page</h2> </div> </template> <script> export default { name: "index.vue" } </script> <style scoped> </style>
效果:
继续更新pages/topics/_id/index.vue:
<template> <div class="container"> <h2>Single Topic Page</h2> <pre>{{topic}}</pre> </div> </template> <script> export default { name: "index.vue", data() { return { topic: "", } }, async asyncData({$axios, params}) { const {data} = await $axios.$get(`/topics/${params.id}`) return { topic: data, } } } </script> <style scoped> </style>
效果
Show Single Topic
继续更新pages/topics/_id/index.vue:
<template> <div class="container"> <div class="bg-light mt-5 mb-5" style="padding: 20px;"> <h2>{{topic.title}}</h2> <p class="text-muted">{{topic.created_at}} by {{topic.user.name}}</p> <div v-for="(content,index) in topic.posts" :key="index" class="ml-5 content"> {{content.body}} <p class="text-muted">{{content.created_at}} by {{content.user.name}}</p> </div> </div> </div> </template> <script> export default { name: "index.vue", data() { return { topic: '', } }, async asyncData({$axios, params}) { const {data} = await $axios.$get(`/topics/${params.id}`); return { topic: data, } } } </script> <style scoped> </style>
源代码:
FrontEnd部分:
https://github.com/dzkjz/laravel-backend-nuxt-frontend-frontpart
选择:
Backend部分:
https://github.com/dzkjz/laravel-backend-nuxt-frontend
选择: