从0开始,手把手教你用Vue.js开发一个答题App(上)
项目演示
项目演示
项目源码
项目源码
配套讲解视频
配套讲解视频第一节
配套讲解视频第二节
教程说明
本教程适合对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 访问地址。 打开浏览器,输入其中任何一个地址进行访问
如果能看到该页面,恭喜你,项目创建成功了。
1.2 初始目录结构
项目创建好以后,下面我们来了解一下初始目录结构:
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,定义导航部分。
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,实现游戏初始设置。
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等待动画文件,请参考项目源码。
声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!
若转载请注明出处: 从0开始,手把手教你用Vue.js开发一个答题App(上)
本文地址: https://pptw.com/jishu/297693.html