從0開始,手把手教你用Vue開發一個答題App01之專案建立及答題設定頁面開發

[豆約翰]發表於2020-07-07

專案演示

專案演示

專案原始碼

專案原始碼

配套講解視訊

配套講解視訊

教程說明

本教程適合對Vue基礎知識有一點了解,但不懂得綜合運用,還未曾使用Vue從頭開發過一個小型App的讀者。本教程不對所有的Vue知識點進行講解,而是手把手一步步從0到1,做出一個完整的小專案。目前網上的教程不是隻有零散的知識點講解;就是丟擲一個開源的大專案,初級讀者下載下來後,執行起來都很費勁,更談不上理解這個專案是如何一步步開發出來的了。本教程試圖彌補這個空白。

1. 專案初始化

1.1使用 Vue CLI 建立專案

如果你還沒有安裝 VueCLI,請執行下面的命令安裝或是升級:

npm install --global @vue/cli

在命令列中輸入以下命令建立 Vue 專案:

vue create vue-quiz
Vue CLI v4.3.1
? Please pick a preset:
> default (babel, eslint)
  Manually select features

default:預設勾選 babel、eslint,回車之後直接進入裝包

manually:自定義勾選特性配置,選擇完畢之後,才會進入裝包

選擇第 1 種 default.

安裝結束,命令提示你專案建立成功,按照命令列的提示在終端中分別輸入:

# 進入你的專案目錄
cd vue-quiz

# 啟動開發服務
npm run serve

啟動成功,命令列中輸出專案的 http 訪問地址。 開啟瀏覽器,輸入其中任何一個地址進行訪問

image-20200707121732592

如果能看到該頁面,恭喜你,專案建立成功了。

1.2 初始目錄結構

專案建立好以後,下面我們來了解一下初始目錄結構:

image-20200707122944401

1.3 調整初始目錄結構,實現遊戲設定頁面

預設生成的目錄結構不滿足我們的開發需求,所以需要做一些自定義改動。

這裡主要處理下面的內容:

  • 刪除初始化的預設檔案
  • 新增調整我們需要的目錄結構

刪除預設示例檔案:

  • src/components/HelloWorld.vue
  • src/assets/logo.png

修改package.json,新增專案依賴:

 "dependencies": {
    "axios": "^0.19.2",
    "bootstrap": "^4.4.1",
    "bootstrap-vue": "^2.5.0",
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "vue-router": "^3.1.5"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.4.0",
    "@vue/cli-plugin-eslint": "~4.4.0",
    "@vue/cli-plugin-router": "~4.4.0",
    "@vue/cli-service": "~4.4.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  },

然後執行yarn install,安裝依賴。

修改專案入口檔案main.js,引入bootstrap-vue。

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.config.productionTip = false

Vue.use(BootstrapVue)

const state = { questions: [] }

new Vue({
  router,
  data: state,
  render: h => h(App)
}).$mount('#app')

定義一個state物件來共享答題資料(答題頁面和結果頁面共享)

const state = { questions: [] }

src目錄下新增eventBus.js訊息匯流排,用來在元件間傳遞訊息,程式碼如下:

import Vue from 'vue'
const EventBus = new Vue()
export default EventBus

修改App.vue,css樣式略,請參考原始碼。

<template>
  <div id="app" class="bg-light">
    <Navbar></Navbar>
    <b-alert :show="dismissCountdown" dismissible variant="danger" @dismissed="dismissCountdown = 0">
      {{ errorMessage }}
    </b-alert>
    <div class="d-flex justify-content-center">
      <b-card no-body id="main-card" class="col-sm-12 col-lg-4 px-0">
        <router-view></router-view>
      </b-card>
    </div>
  </div>
</template>

<script>
import EventBus from './eventBus'
import Navbar from './components/Navbar'

export default {
  name: 'app',
  components: {
    Navbar
  },
  data() {
    return {
      errorMessage: '',
      dismissSecs: 5,
      dismissCountdown: 0
    }
  },
  methods: {
    showAlert(error) {
      this.errorMessage = error
      this.dismissCountdown = this.dismissSecs
    }
  },
  mounted() {
    EventBus.$on('alert-error', (error) => {
      this.showAlert(error)
    })
  },
  beforeDestroy() {
    EventBus.$off('alert-error')
  }
}
</script>

新增components/Navbar.vue,定義導航部分。

image-20200707125506858

<template>
    <b-navbar id="navbar" class="custom-info" type="dark" sticky>
      <b-navbar-brand id="nav-logo" :to="{ name: 'home' }">Vue-Quiz</b-navbar-brand>

      <b-navbar-nav class="ml-auto">
        <b-nav-item :to="{ name: 'home' }">New Game </b-nav-item>
        <b-nav-item href="#" target="_blank">About</b-nav-item>
      </b-navbar-nav>
    </b-navbar>
</template>

<script>
export default {
  name: 'Navbar'
}
</script>

<style scoped>

</style>


src目錄下新增router/index.js,定義首頁路由。

import Vue from 'vue'
import VueRouter from 'vue-router'
import MainMenu from '../views/MainMenu.vue'


Vue.use(VueRouter)

const routes = [
  {
    name: 'home',
    path: '/',
    component: MainMenu
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

src下新增views/MainMenu.vue,MainMenu主要包含GameForm元件。

<template>
<div>
  <b-card-header class="custom-info text-white font-weight-bold">New Game</b-card-header>
  <b-card-body class="h-100">
    <GameForm @form-submitted="handleFormSubmitted"></GameForm>
  </b-card-body>
</div>
</template>

<script>
import GameForm from '../components/GameForm'

export default {
  name: 'MainMenu',
  components: {
    GameForm
  },
  methods: {
    /** Triggered by custom 'form-submitted' event from GameForm child component. 
     * Parses formData, and route pushes to 'quiz' with formData as query
     * @public
     */
    handleFormSubmitted(formData) {
      const query = formData
      query.difficulty = query.difficulty.toLowerCase()
      this.$router.push({ name: 'quiz', query: query })
    }
  }
}
</script>


新增src/components/GameForm.vue,實現遊戲初始設定。

image-20200707125814786

<template>
  <div>
    <LoadingIcon v-if="loading"></LoadingIcon>

    <div v-else>
      <b-form @submit="onSubmit">
        <b-form-group 
          id="input-group-number-of-questions"
          label="Select a number"
          label-for="input-number-of-questions"
          class="text-left"
        >
          <b-form-input
            id="input-number-of-questions"
            v-model="form.number"
            type="number"
            :min="minQuestions"
            :max="maxQuestions"
            required 
            :placeholder="`Between ${minQuestions} and ${maxQuestions}`"
          ></b-form-input>
        </b-form-group>

        <b-form-group id="input-group-category">
          <b-form-select
            id="input-category"
            v-model="form.category"
            :options="categories"
          ></b-form-select>
        </b-form-group>

        <b-form-group id="input-group-difficulty">
          <b-form-select
            id="input-difficulty"
            v-model="form.difficulty"
            :options="difficulties"
          ></b-form-select>
        </b-form-group>

        <b-form-group id="input-group-type">
          <b-form-select
            id="input-type"
            v-model="form.type"
            :options="types"
          ></b-form-select>
        </b-form-group>

        <b-button type="submit" class="custom-success">Submit</b-button>
      </b-form>
    </div>
  </div>
</template>

<script>
import LoadingIcon from './LoadingIcon'
import axios from 'axios'

export default {
  components: {
    LoadingIcon
  },
  data() {
    return {
      // Form data, tied to respective inputs
      form: {
        number: '',
        category: '',
        difficulty: '',
        type: ''
      },
      // Used for form dropdowns and number input
      categories: [{ text: 'Category', value: '' }],
      difficulties: [{ text: 'Difficulty', value: '' }, 'Easy', 'Medium', 'Hard'],
      types: [
        { text: 'Type', value: '' }, 
        { text: 'Multiple Choice', value: 'multiple' }, 
        { text: 'True or False', value: 'boolean'}
      ],
      minQuestions: 10,
      maxQuestions: 20,
      // Used for displaying ajax loading animation OR form
      loading: true
    }
  },
  created() {
    this.fetchCategories()
  },
  methods: {
    fetchCategories() {
      axios.get('https://opentdb.com/api_category.php')
      .then(resp => resp.data)
      .then(resp => {
        resp.trivia_categories.forEach(category => {
          this.categories.push({text: category.name, value: `${category.id}`})
        });
        this.loading = false;
      })
    },
    onSubmit(evt) {
      evt.preventDefault()
       /** Triggered on form submit. Passes form data
        * @event form-submitted
        * @type {number|string}
        * @property {object}
        */
      this.$emit('form-submitted', this.form)
    }
  }
}
</script>

GameForm元件,主要通過axios發起獲取全部題目分類請求:

axios.get('https://opentdb.com/api_category.php')

新增src/components/LoadingIcon.vue,在非同步請求資料未返回時,渲染等待圖示。

<template>
  <div id="loading-icon" class="h-100 d-flex justify-content-center align-items-center">
    <img src="@/assets/ajax-loader.gif" alt="Loading Icon">
  </div>
</template>

<script>
export default {
  name: 'LoadingIcon'
}
</script>

新增src/assets/ajax-loader.gif等待動畫檔案,請參考專案原始碼。

1.4 執行專案

yarn run serve

image-20200707130702456

備註

本系列文章首發於作者的微信公眾號[豆約翰],想嚐鮮的朋友,請微信搜尋關注。

有什麼問題也可以加我微信[tiantiancode]一起討論。

最後,為了將來還能找到我

相關文章