<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于 Laravel + Vue 框架實(shí)現(xiàn)文件異步上傳組件和文章封面圖片功能

          共 25275字,需瀏覽 51分鐘

           ·

          2021-03-14 13:28

          在今天這篇教程中,學(xué)院君將給大家演示如何基于 Laravel + Vue 框架編寫一個(gè)基本的圖片異步上傳組件,并且?guī)ьA(yù)覽功能,然后把這個(gè)圖片上傳組件嵌入到文章發(fā)布/編輯表單中,用于給文章設(shè)置封面圖片,這樣一來,文章列表視圖就不再那么單調(diào)了。

          一、后端接口支持

          開始之前,我們先要在 Laravel 后端準(zhǔn)備好相應(yīng)的數(shù)據(jù)庫字段、圖片上傳接口以及文章封面圖片設(shè)置功能。

          新增文章封面圖片字段

          component-practice 項(xiàng)目根目錄下,運(yùn)行如下 Artisan 命令為 posts 數(shù)據(jù)表生成一個(gè)新的數(shù)據(jù)庫遷移文件:

          php artisan make:migration alter_posts_table_add_image_path --table=posts

          打開這個(gè)剛剛創(chuàng)建的遷移文件,在 posts 表的 title 字段后面新增一個(gè) image_path 字段作為文章封面圖片:

          <?php

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

          class AlterPostsTableAddImagePath extends Migration
          {
              /**
               * Run the migrations.
               *
               * @return void
               */

              public function up()
              
          {
                  Schema::table('posts'function (Blueprint $table) {
                      $table->string('image_path')->nullable()->after('title');
                  });
              }

              /**
               * Reverse the migrations.
               *
               * @return void
               */

              public function down()
              
          {
                  Schema::table('posts'function (Blueprint $table) {
                      $table->dropColumn('image_path');
                  });
              }
          }

          接著運(yùn)行 php artisan migrate 讓這個(gè)新增字段生效:

          編寫圖片上傳接口

          接下來,我們新建一個(gè) ImageController 控制器:

          php artisan make:controller ImageController

          在這個(gè)新生成的控制器中編寫圖片上傳處理代碼如下:

          <?php

          namespace App\Http\Controllers;

          use Illuminate\Http\Request;

          class ImageController extends Controller
          {
              public function upload(Request $request)
              
          {
                  $request->validate([
                      'image' => 'required|image|max:1024',  // 圖片尺寸不能超過 1 MB
                  ]);

                  if ($request->hasFile('image')) {
                      $image = $request->file('image');
                      $local_path = $image->storePublicly('images', ['disk' => 'public']);
                      $image_path = '/storage/' . $local_path;
                      return ['success' => true'path' => $image_path];
                  }

                  return ['success' => false'message' => '圖片上傳失敗'];
              }
          }

          非常簡單,如果你對(duì)此不熟悉的話可以結(jié)合 Laravel 官方文檔驗(yàn)證、請(qǐng)求和文件存儲(chǔ)部分進(jìn)行進(jìn)一步的了解。

          由于我們使用了 public 這個(gè)對(duì)外開放的磁盤路徑,所以需要在運(yùn)行如下 Artisan 命令在項(xiàng)目根目錄下的 public 目錄中創(chuàng)建一個(gè) storage 軟鏈接指向 storage 目錄下的 public 目錄:

          php artisan storage:link

          最后,控制器方法需要通過路由對(duì)外提供服務(wù),因此需要在 routes/web.php 中注冊(cè)對(duì)應(yīng)的 image/upload 路由:

          Route::post('image/upload', [\App\Http\Controllers\ImageController::class, 'upload']);

          這樣一來,用戶就可以通過 image/upload 路由進(jìn)行文件上傳操作了。

          編寫測試用例測試圖片上傳

          我們可以基于 Laravel 內(nèi)置的 HTTP 測試功能編寫一個(gè)測試用例,來測試文件上傳接口是否可以正常工作。

          使用如下 Artisan 命令創(chuàng)建一個(gè) ImageUploadTest 測試類:

          php artisan make:test ImageUploadTest

          然后打開這個(gè)文件編寫文件上傳測試用例:

          <?php
          namespace Tests\Feature;

          use Illuminate\Http\UploadedFile;
          use Tests\TestCase;

          class ImageUploadTest extends TestCase
          {
              /**
               * A basic feature test example.
               *
               * @return void
               */

              public function testImageUpload()
              
          {
                  $response = $this->json('POST''/image/upload', [
                      'image' => UploadedFile::fake()->image('test.jpg')
                  ]);

                  // 斷言響應(yīng)是否正常
                  $response->assertStatus(200);
                  $response->assertJsonPath('success'true);
              }
          }

          這里使用了 UploadedFile::fake() 方法偽造一個(gè) JPG 文件通過 POST 請(qǐng)求上傳到 /image/upload 路由,這樣就完成了模擬用戶在前端上傳文件的操作,非常方便。接下來就可以通過對(duì)返回的響應(yīng)實(shí)例編寫斷言看返回結(jié)果是否符合預(yù)期了,如果符合則測試通過,否則不通過(更多斷言方法參考 Laravel 官方文檔)。

          我們可以通過 php artisan test 命令對(duì)這個(gè)測試用例進(jìn)行測試(PhpStorm 中亦可通過圖形化界面運(yùn)行指定測試用例):

          綠色就表示通過,紅色則不通過。

          文章封面圖片設(shè)置

          接下來,我們來完善文章封面圖片設(shè)置功能。

          Post 模型類中,在批量賦值字段白名單中新增一個(gè) image_path 字段:

          class Post extends Model
          {
              use HasFactory;

              protected $fillable = ['title''content''image_path'];

              ...
          }

          然后打開 PostController 控制器,在發(fā)布文章和更新文章后端處理方法中添加針對(duì) image_path 的驗(yàn)證規(guī)則即可:

          public function store(Request $request)
          {
              $data = $request->validate([
                  'title' => 'required|max:128',
                  'image_path' => 'required',
                  'content' => 'required'
              ]);

              ...

              if ($post->save()) {
                  return ['success' => true'message' => '文章發(fā)布成功''id' => $post->id];
              }
              ...
          }

          ...

          public function update(Request $request, Post $post)
          {
              $data = $request->validate([
                  'title' => 'required|max:128',
                  'image_path' => 'required',
                  'content' => 'required'
              ]);

              ...

              if ($post->save()) {
                  return ['success' => true'message' => '文章更新成功''id' => $post->id];
              }
              ...
          }

          注意到我們?cè)谖恼卤4娉晒蠓祷氐捻憫?yīng)字段中新增了一個(gè) id 字段,用于客戶端處理表單提交成功后的頁面重定向。

          至此,所有的后端代碼就編寫好了,接下來,我們進(jìn)入前端實(shí)現(xiàn)圖片上傳和封面圖片字段設(shè)置。

          二、在前端表單組件中上傳封面圖片

          編寫 InputFile 組件

          首先,我們?cè)?resources/js/components/form 目錄下新建一個(gè) InputFile.vue 文件作為圖片上傳組件,并初始化組件代碼如下:

          <template>
              <div class="form-group">
                  <input class="form-control-file" ref="file" type="file" :name="name" @change="fileUpload">
                  <template v-if="file_path">
                      <InputText type="hidden" :name="field" v-model="file_path"></InputText>
                      <img :src="file_path" class="img-thumbnail" style="width: 50%;" alt="封面圖片預(yù)覽" v-if="name === 'image'">
                  </template>
              </div>
          </template>

          <script>
          import InputText from "./InputText";
          export default {
              components: {InputText},
              props: ['name''field''path'],
              data() {
                  return {
                      file_paththis.path
                  }
              },
              methods: {
                  fileUpload() {
                      this.$emit('clear');
                      let form_data = new FormData();
                      form_data.append(this.name, this.$refs.file.files[0]);
                      axios.post('/' + this.name + '/upload', form_data, {
                          headers: {
                              'Content-Type''multipart/form-data'
                          }
                      }).then(resp => {
                          this.file_path = resp.data.path;
                          this.$emit('success'this.field, this.file_path);
                      }).catch(error => {
                          let errors = {};
                          let error_bag = error.response.data.errors;
                          errors[this.field] = error_bag[this.name];
                          this.$emit('error', errors);
                      });
                  }
              }
          }
          </script>

          在這段模板代碼中,我們引入了一個(gè) Bootstrap 的文件上傳表單元素,如果從父級(jí)作用域傳遞過來的文件路徑不為空,或者在當(dāng)前組件中上傳文件成功的話,還會(huì)渲染一個(gè)包含對(duì)應(yīng)文件路徑值的隱藏字段,如果文件類型是圖片的話,還支持對(duì)該圖片進(jìn)行預(yù)覽。

          顯然我們這個(gè)文件上傳組件是通用的,不僅僅支持圖片上傳,還支持其他類型文件上傳。

          相關(guān)的 props 屬性和數(shù)據(jù)模型屬性綁定就不多做介紹了,我們看下這里監(jiān)聽的文件事件:當(dāng)我們?cè)谖募韱卧厣线x擇一個(gè)本地文件后,會(huì)觸發(fā)該元素的 change 事件,然后執(zhí)行與該事件綁定的 fileUpload 方法。

          在這個(gè)方法中,我們首先會(huì)觸發(fā)父級(jí)作用域上的 clear 事件清空?qǐng)?bào)錯(cuò)消息(馬上會(huì)展示父級(jí)作用域?qū)?yīng)的設(shè)置),然后基于 axios 庫 API 發(fā)起一個(gè)文件上傳異步請(qǐng)求,對(duì)應(yīng)的后端接口正是上面 Laravel 后端新增的 image/upload 接口。注意到這里設(shè)置了額外的請(qǐng)求頭,因?yàn)槲募蟼饕髢?nèi)容類型必須是 multipart/form-data。

          文件上傳成功后,會(huì)覆蓋 file_path 屬性值填充隱藏的文件路徑字段,如果是圖片類型的話會(huì)渲染預(yù)覽圖片,并且還會(huì)觸發(fā)父級(jí)作用域的 success 事件以便執(zhí)行對(duì)應(yīng)的業(yè)務(wù)邏輯,比如填充文章封面圖片路徑字段值。

          文件上傳失敗后,則會(huì)觸發(fā)父級(jí)作用域的 error 事件以便執(zhí)行相應(yīng)的錯(cuò)誤顯示邏輯。

          在表單組件中引入 InputFile

          接下來,我們打開文章發(fā)布/編輯表單組件 PostForm,在其中引入文件上傳組件實(shí)現(xiàn)封面圖片的上傳和設(shè)置:

          <template>
              <FormSection @store="store">
                  <template slot="title">{{ title }}</template>
                  <template slot="input-group">
                      <div class="form-group">
                          <Label name="title" label="標(biāo)題"></Label>
                          ...
                      </div>
                      <div class="form-group">
                          <Label name="title" label="封面圖片"></Label>
                          <InputFile name="image" field="image_path" :path="form.image_path"
                                     @clear="clear('image_path')" @success="uploadSuccess" @error="uploadError">

                          </InputFile>
                          <ErrorMsg :error="form.errors.get('image_path')"></ErrorMsg>
                      </div>
                      ...
                  </template>
                  ...
              </FormSection>

          </template>

          <script>
          import FormSection from './
          form/FormSection';
          import InputText from '
          ./form/InputText';
          import InputFile from "./form/InputFile";
          import TextArea from '
          ./form/TextArea';
          import Button from '
          ./form/Button';
          import ToastMsg from '
          ./form/ToastMsg';
          import Label from "./form/Label";
          import ErrorMsg from "./form/ErrorMsg";

          export default {

              components: {FormSection, InputText, InputFile, TextArea, Label, ErrorMsg, Button, ToastMsg},

              props: ['
          title', 'url', 'action', 'post'],

              data() {
                  let post_data = this.post ? JSON.parse(this.post) : null;
                  return {
                      form: new Form({
                          title: this.post ? post_data.title : '
          ',
                          content: this.post ? post_data.content : '
          ',
                          image_path: this.post ? post_data.image_path : '
          '
                      })
                  }
              },

              methods: {
                  store() {
                      let method = this.action === '
          create' ? 'post' : 'put';
                      this.form[method](this.url)
                          .then(data => {
                              // 發(fā)布/更新成功后都跳轉(zhuǎn)到詳情頁
                              window.location.href = '
          /posts/' + data.id;
                          })
                          .catch(data => console.log(data)); // 自定義表單提交失敗處理邏輯
                  },
                  clear(field) {
                      this.form.errors.clear(field);
                  },
                  uploadSuccess(field, path) {
                      this.form[field] = path;
                  },
                  uploadError(errors) {
                      this.form.errors.set(errors);
                  }
              }
          }
          </script>

          我們?cè)跇?biāo)題之后引入了文件上傳組件,并設(shè)置了一應(yīng)需要傳遞到子組件的 props 屬性,以及三個(gè)事件函數(shù),分別處理子組件上報(bào)的 clear、successerror 事件,我們通過這種機(jī)制實(shí)現(xiàn)子組件和父級(jí)作用域的通信。

          clear 事件函數(shù)前面已經(jīng)介紹過了,就是清理封面圖片字段相關(guān)的錯(cuò)誤信息;success 事件函數(shù)用于圖片上傳成功后設(shè)置 form 實(shí)例的 image_path 屬性,以便后續(xù)提交表單時(shí)使用;error 事件函數(shù)用于設(shè)置 image_path 字段的錯(cuò)誤消息。

          另外,我們還改造了編輯表單數(shù)據(jù)初始化邏輯,因?yàn)樵瓉淼漠惒郊虞d存在延時(shí),相應(yīng)的需要在 views/posts/edit.blade.php 視圖中引入 PostForm 組件的地方傳遞 JSON 格式的 $post 實(shí)例數(shù)據(jù)過來:

          <post-form title="編輯文章" action="update" post="{{ json_encode($post) }}" url="{{ route('posts.update', ['post' => $post->id]) }}">
          </post-form>

          當(dāng)然,PostController 控制器中 edit 方法傳遞進(jìn)視圖的數(shù)據(jù)也要調(diào)整:

          public function edit(Post $post)
          {
              return view('posts.edit', ['pageTitle' => '編輯文章''post' => $post]);
          }

          此外,在 PostForm 中,我們也將文章發(fā)布和文章更新成功后的處理邏輯也統(tǒng)一成跳轉(zhuǎn)到文章詳情頁。

          以上就是 PostForm 表單針對(duì)封面圖片上傳和設(shè)置功能的所有代碼調(diào)整了,此時(shí)打開文章發(fā)布頁面,就可以看到包含封面圖片字段的表單了:

          上傳文章封面圖片

          我們可以在這個(gè)發(fā)布文章表單中測試封面圖片上傳功能,上傳成功后可以立即看到預(yù)覽圖片的渲染,表明圖片上傳成功:

          你還可以在 Vue Devtools 面板中看到 form.image_path 屬性中也成功設(shè)置了圖片路徑:

          點(diǎn)擊「立即發(fā)布」按鈕發(fā)布這篇文章,就可以跳轉(zhuǎn)到文章詳情頁了,點(diǎn)擊編輯鏈接,進(jìn)入該文章的編輯頁面,由于初始 image_path 值不為空,所以可以看到對(duì)應(yīng)的封面圖片預(yù)覽:

          你可以在文章編輯頁面上傳其他圖片覆蓋老的封面圖片:

          發(fā)布成功,則表明文章發(fā)布和編輯表單中的封面圖片上傳和設(shè)置功能可以正常工作。至此,我們就完成了文章封面圖片上傳和設(shè)置的全部工作。

          三、在文章列表頁展示封面圖片

          最后,我們打開 list/CardItem 組件,在文章列表頁卡片視圖中渲染文章封面圖片,讓列表頁看起來更好看一些,這里我們把 Bootstrap 帶封面圖片的 Card 組件 HTML 模板拷貝過來,加上 Vue 條件渲染指令進(jìn)行展示即可(如果有封面圖片的話顯示封面圖片,否則顯示灰色背景):

          <template>
              <div class="col mb-4">
                  <div class="card">
                      <img :src="post.image_path" class="card-img-top" height="180" :alt="'點(diǎn)擊閱讀' + post.title" v-if="post.image_path">
                      <svg class="bd-placeholder-img card-img-top" width="100%" height="180" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false" role="img" v-else>
                          <rect width="100%" height="100%" fill="#868e96"></rect>
                      </svg>
                      <div class="card-body">
                          <h5 class="card-title"><slot name="title"></slot></h5>
                          <p class="card-text">
                              <small class="text-muted">
                                  <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-person-circle" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
                                      <path d="M13.468 12.37C12.758 11.226 11.195 10 8 10s-4.757 1.225-5.468 2.37A6.987 6.987 0 0 0 8 15a6.987 6.987 0 0 0 5.468-2.63z"/>
                                      <path fill-rule="evenodd" d="M8 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
                                      <path fill-rule="evenodd" d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8z"/>
                                  </svg>
                                  {{ post.author.name}}
                              </small>
                              ...
                          </p>
                          ...
                      </div>
                  </div>
              </div>
          </template>

          另外,我們還將卡片中的狀態(tài)字段調(diào)整為了作者字段,相應(yīng)的,需要在后端接口數(shù)據(jù)中添加這段渴求式加載邏輯:

          public function all()
          {
              return Post::with('author')->orderByDesc('created_at')->get();
          }

          為了優(yōu)化用戶體驗(yàn),我們還在異步加載文章列表數(shù)據(jù)期間為列表頁加上了用戶友好的加載提示效果(和文章詳情頁一樣),在 list/ListSection.vue 組件中通過插槽定義加載提示位置:

          <div class="card-body">
              <slot name="loading"></slot>  // 在這里插入加載動(dòng)態(tài)圖
              <ul class="list-group" v-if="view.mode === 'list'">
                  <slot></slot>
              </ul>
              <div class="row row-cols-1 row-cols-md-3" v-else>
                  <slot></
          slot>
              </div>
          </div>

          然后在父級(jí)作用域 PostList 中編寫對(duì)應(yīng)的動(dòng)態(tài)加載效果,以及將默認(rèn)視圖模式切換成卡片視圖:

          <template>
              <div class="post-list">
                  <ListSection :view_mode="view_mode" @view-mode-changed="change_view_mode">
                      ...
                      <template #loading>
                          <div class="spinner-border" role="status" v-if="!loaded">
                              <span class="sr-only">Loading...</span>
                          </div>
                      </template>
                  </ListSection>
              </div>

          </template>

          <script>
          ...

          export default {
              ...
              data() {
                  return {
                      posts: [],
                      view_mode: 'card',
                      loaded: false
                  }
              },
              ...
          }
          </
          script>

          現(xiàn)在,我們?cè)L問文章列表頁,數(shù)據(jù)加載完成之前會(huì)有一個(gè)動(dòng)態(tài)加載中的提示:

          加載完成后,則會(huì)默認(rèn)以卡片模式展示包含封面圖片的文章列表數(shù)據(jù),是不是比原來好看多了:

          關(guān)于文件異步上傳和封面圖片設(shè)置我們就簡單介紹到這里,下篇教程,學(xué)院君將給大家演示如何在 Vue 框架中實(shí)現(xiàn)拖放式圖片上傳組件。

          本系列教程首發(fā)在Laravel學(xué)院(laravelacademy.org),你可以點(diǎn)擊頁面左下角閱讀原文鏈接查看最新更新的教程。

          瀏覽 71
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本三级美国三级久久 | 大雞巴疯狂浓精合集 | 欧美日韩国产在线手机 | 亚洲另类在线观看 | 国产精彩视频免费观看 |