33

从0开始,手把手教你用Vue开发一个答题App01之项目创建及答题设置页面开发

 4 years ago
source link: http://www.cnblogs.com/songboriceboy/p/13262901.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

项目演示

项目演示

项目源码

项目源码

配套讲解视频

配套讲解视频

教程说明

本教程适合对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 访问地址。 打开浏览器,输入其中任何一个地址进行访问

JbUnEjn.png!web

如果能看到该页面,恭喜你,项目创建成功了。

1.2 初始目录结构

项目创建好以后,下面我们来了解一下初始目录结构:

f6fERnM.png!web

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,定义导航部分。

BnEfIjY.png!web

<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,实现游戏初始设置。

beAfm2m.png!web

<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

YBZFJz6.png!web

备注

本系列文章首发于作者的微信公众号[豆约翰],想尝鲜的朋友,请微信搜索关注。

有什么问题也可以加我微信[tiantiancode]一起讨论。

最后,为了将来还能找到我

UzINFzr.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK