Vue 3.0 (CompostionAPI + VueRouter + Axios + Bootstrap + Sass) 文章管理项目

1. Vue 3.0 介绍和脚手架文档

2. Vue 3.0 快速入门指南

2.1 Vue 3.0 搭建脚手架项目

  • 创建项目
    • vue create 项目名
    • 根据个人情况设置配置

2.2 Vue 3.0 文件结构梳理

在这里插入图片描述

  • node_modules: 这个目录存放的是项目的所有依赖,即 npm install 命令下载下来的文件
  • public: 静态资源
  • index.html: 主页
  • src: 这个目录下存放项目的源码,即开发者写的代码放在这里
  • assets: 静态文件
  • components: 目录用来存放组件(一些可复用,非独立的页面)
  • App.vue: 是一个Vue根组件,也是项目的第一个Vue组件
  • main.js: 整个项目的入口文件
  • package.json: 定义了项目的所有依赖,包括开发时依赖和发布时依赖
  • .babel.config.js: 该文件是babel的配置文件

2.3 Vue 3.0 组件的组成

  • 组件组成
    • html 结构部分
    • script 逻辑部分
    • style 样式部分
<template>

</template>

<script>
export default {
     
     

}
</script>

<style>

</style>

2.4 Vue 3.0 CompositionAPI

<template>
	<div class="my-app">
		<h1>{
   
   {counter}}</h1>
		<button @click="increment(1)">increment</button>
		<button @click="increment(-1)">decrement</button>
	</div>
</template>

<script>
import {
     
      ref } from 'vue'
/*
Options API Vue2 Class
Composition API Vue3 Function
*/
export default {
     
     
	setup(){
     
     
		const counter = ref(1000);
		// console.log(counter.value);
		
		// 增加方法
		const increment = (num) => {
     
     
			counter.value += num;
		};

		return {
     
      counter, increment };
	}
}
</script>

<style>
</style>

3. Vue 3.0 组件

3.1 Vue 3.0 组件拆分

  • CounterView.vue
<template>
	<h1>1000</h1>
</template>

<script>
export default {
     
     };
</script>

<style>
</style>
  • App.vue
<template>
	<div class="my-app">
		<!-- <CounterView></CounterView> -->
		<!-- <counter-view></counter-view> -->
		<CounterView/>
		<button @click="increment(1)">increment</button>
		<button @click="increment(-1)">decrement</button>
	</div>
</template>

<script>
import {
     
      ref } from 'vue'
import CounterView from '@/components/CounterView.vue";
/*
Options API Vue2 Class
Composition API Vue3 Function
*/
export default {
     
     
	// 组件注册
	components: {
     
     
		CounterView,
		// "zx-counter-view": CounterView,
	},
	setup(){
     
     
		const counter = ref(1000);
		// console.log(counter.value);
		
		// 增加方法
		const increment = (num) => {
     
     
			counter.value += num;
		};

		return {
     
      counter, increment };
	}
}
</script>

<style>
</style>

3.2 Vue 3.0 属性传值

  • CounterView.vue
<template>
	<h1>{
   
   {counter}}</h1>
</template>

<script>
export default {
     
     
	// props: ['counter'],
	props: {
     
     
		counter: {
     
     
			type: Number, // 类型
			required: true, // 可以传也可以不传
			default: 500, // 默认值
		}
	}
};
</script>

<style>
</style>
  • App.vue
<template>
	<div class="my-app">
		<!-- <CounterView></CounterView> -->
		<!-- <counter-view></counter-view> -->
		<CounterView :counter="counter"/>
		<button @click="increment(1)">increment</button>
		<button @click="increment(-1)">decrement</button>
	</div>
</template>

<script>
import {
     
      ref } from 'vue'
import CounterView from '@/components/CounterView.vue";
/*
Options API Vue2 Class
Composition API Vue3 Function
*/
export default {
     
     
	// 组件注册
	components: {
     
     
		CounterView,
		// "zx-counter-view": CounterView,
	},
	setup(){
     
     
		const counter = ref(1000);
		// console.log(counter.value);
		
		// 增加方法
		const increment = (num) => {
     
     
			counter.value += num;
		};

		return {
     
      counter, increment };
	}
}
</script>

<style>
</style>

3.3 Vue 3.0 事件注册

  • CounterController.vue
<template>
	<button @click="increment(1)">increment</button>
	<button @click="increment(-1)">decrement</button>
</template>
<script>
export default {
     
     
	emits: ["onIncrement"],
	setup(props, context){
     
     
		// 增加方法
		const increment = (num) => {
     
     
			// 注册事件 emit
			// 传入事件名、值
			context.emit("onIncrement", num)
		};

		return {
     
      increment };
	}
}
</script>

<style>
</style>
  • App.vue
<template>
	<div class="my-app">
		<!-- <CounterView></CounterView> -->
		<!-- <counter-view></counter-view> -->
		<CounterView :counter="counter" />
		<CounterController @onIncrement="increment($event)"/>
	</div>
</template>

<script>
import {
     
      ref } from 'vue'
import CounterView from '@/components/CounterView.vue";
import CounterController from '@/components/CounterController.vue";
/*
Options API Vue2 Class
Composition API Vue3 Function
*/
export default {
     
     
	// 组件注册
	components: {
     
     
		CounterView,
		// "zx-counter-view": CounterView,
	},
	setup(){
     
     
		const counter = ref(1000);
		// console.log(counter.value);
		
		// 增加方法
		const increment = (num) => {
     
     
			counter.value += num;
		};

		return {
     
      counter, increment };
	}
}
</script>

<style>
</style>

3.4 Vue 3.0 调整项目引入 Bootstrap

<style>
	@import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
</style>

3.5 Vue 3.0 模拟数据和属性传值

  • 模拟列表内容数据,并将数据传入列表组件
  • ResourceHome.vue
<!-- 将数据传入列表组件 -->
<ResourceList :resources="resources" />
...
setup(){
    
    
    const data = reactive({
    
    
        resources: [
          {
    
    
            _id: "1",
            title: "2021 前端面试 | “HTML + CSS + JS”专题",
            description: "BAT面试1000题——数据结构(841~850题)",
            type: "video",
            link: ""
          },
          {
    
    
            _id: "2",
            title: "一篇搞定前端高频手撕算法题(36道)",
            description: "《JavaScript 20 年》中文在线版发布",
            type: "book",
            link: ""
          },
          {
    
    
            _id: "3",
            title: "32个手撕JS,彻底摆脱初级前端(面试高频)",
            description: "56 道高频 JavaScript 与 ES6+ 的面试题及答案",
            type: "video",
            link: ""
          },
          {
    
    
            _id: "4",
            title: "字节跳动2020届秋招提前批前端面经",
            description: "初入WEB前端的新手,掌握这些核心知识点,年薪冲破20W",
            type: "book",
            link: ""
          },
          {
    
    
            _id: "5",
            title: "前方预警!史上最全前端面试题来袭!(附答案)",
            description: "做一个数据可视化项目的难点在什么地方?",
            type: "video",
            link: ""
          },
          {
    
    
            _id: "6",
            title: "JavaScript数据类型详解",
            description: "nodejs的websocket的服务器端是如何实现的?",
            type: "book",
            link: ""
          },
        ]
    })
    return {
    
     
      // 解包
      ...toRefs(data),
    }
}
  • ResourceList.vue
<template>
  <!-- {/* 数据列表 Starts */} -->
  <ul class="list-group mb-3">
    <li v-for="resource in resources" 
        :key="resource._id"
        class="list-group-item d-flex justify-content-between lh-condensed"
    >
      <div>
        <h6 class="my-0">{
   
   { resource.title }}</h6>
        <small class="text-muted">{
   
   { resource.description }}</small>
      </div>
      <span class="text-muted">{
   
   { resource.type }}</span>
    </li>
  
  </ul>
  <!-- {/* 数据列表 Ends */} -->
</template>

<script>
export default {
     
     
  props: {
     
     
    resources: {
     
     
      type: Array,
      default: () => [],

    }
  }
}
</script>

<style>

</style>

3.6 Vue 3.0 方法和计算属性

  • 利用方法实现列表数据的统计
  • ResourceHome.vue
<span class="badge badge-secondary badge-pill">{
    
    {
    
    getResourcesLength()}}</span>
...
// 列表数量统计方法
const getResourcesLength = () => {
    
    
  return data.resources.length
}

// 导出数据
return {
    
     
  getResourcesLength,
}
  • 利用计算属性实现列表数据的统计
  • ResourceHome.vue
<span class="badge badge-secondary badge-pill">{
    
    {
    
    getResourcesLength()}}</span>
...
import {
    
     computed } from 'vue';
...
// 列表数量统计(计算属性实现)
const getResourcesLength = computed(() => {
    
    
  return data.resources.length
})

3.7 Vue 3.0 实现视图切换

  • 使用v-if/v-else实现视图切换
<!-- {
    
    /* 更新数据 Starts */} -->
<div class="col-md-8 order-md-1">
  <h4 class="mb-3">数据<button @click="isDetailView = !isDetailView" class="btn btn-sm btn-success">切换</button></h4>
  <ResourceUpdate v-if="isDetailView" />
  <!-- 数据详情 -->
  <ResourceDetail v-else></ResourceDetail>
</div>
<!-- 更新数据 Ends  -->
...
import {
    
     ref } from 'vue';
...
// 3. 定义视图切换属性
const isDetailView = ref(true)

// 导出数据
return {
    
     
  isDetailView
}

4. Vue 3.0 数据的交互

4.1 Vue 3.0 添加数据

  • ResourceHome.vue
<!-- 添加按钮 -->
<button @click="addResource" class="btn btn-sm btn-primary">
  添加数据
</button>
...
// 4. 添加数据事件
const addResource = () => {
    
    
   // debugger
   // 随机获取id
   const _id = "_" + Math.random().toString(36).slice(2)
   // 随机获取列表内容类型
   const type = ["book","blog","video"][Math.floor(Math.random() * 3)]
   // 新的列表内容
   const newResource = {
    
    
     _id,
     title:`${
      
      _id} title`,
     description:`${
      
      _id} description`,
     link:'',
     type,
   }
   // 添加到数据列表前列
   data.resources.unshift(newResource)
 }

4.2 Vue 3.0 调整样式

  • 修改切换按钮样式
  • ResourceHome.vue
<!-- {
    
    /* 更新数据 Starts */} -->
<div class="col-md-8 order-md-1">
  <h4 class="mb-3">数据
    <button 
      @click="isDetailView = !isDetailView" 
      :class="`btn btn-sm ${togglesBtnClass}` ">
      {
    
    {
    
    !isDetailView ? "更新" : "详情"}}
    </button>
  </h4>
  <ResourceUpdate v-if="isDetailView" />
  <!-- 数据详情 -->
  <ResourceDetail v-else></ResourceDetail>
</div>
<!-- 更新数据 Ends  -->
...
// 5. 切换按钮样式
const togglesBtnClass = computed(() => {
    
    
  return !isDetailView.value ? "btn-primary" : "btn-warning"
})
  • 修改数据列表固定高度
  • ResourceList.vue
<style scope lang="scss">
.resource-list {
     
     
  max-height: 350px;
  overflow-y: auto;
}

// css语法
// .resource-list {
     
     
//   max-height: 350px;
//   overflow-y: auto;
// }
</style>

4.3 Vue 3.0 选中数据显示详情

  • 选中ResourceList组件中的列表数据,将点击事件传入ResourceHome父组件
  • 从ResourceHome父组件传入ResourceDetail数据详情组件
  • ResourceList.vue
<template>
	<!-- {/* 数据列表 Starts */} -->
	<ul class="list-group mb-3 resource-list">
	  <li v-for="resource in resources" 
	      :key="resource._id"
	      :class="list-group-item d-flex justify-content-between lh-condensed resource-list-item"
	      @click="onItemClick(resource)"
	  >
	    <div>
	      <h6 class="my-0">{
   
   { resource.title }}</h6>
	      <small class="text-muted">{
   
   { resource.description }}</small>
	    </div>
	    <span class="text-muted">{
   
   { resource.type }}</span>
	  </li>
	
	</ul>
	<!-- {/* 数据列表 Ends */} -->
</template>

<script>
import {
     
      computed } from 'vue'
export default {
     
     
  props: {
     
     
    resources: {
     
     
      type: Array,
      default: () => [],
    },
  },
  setup(props, context) {
     
     
    // methods
    // 1. 选中列表显示数据事件
    const onItemClick = (resource) => {
     
     
      // 注册事件
      // 事件名、参数
      context.emit("handleItemClick",resource)
    }
    
    return {
     
      onItemClick }
  }
}
</script>

<style>
</style>
  • ResourceHome.vue
<template>
	<!-- 数据详情 -->
    <ResourceDetail :resource="selectedResource" v-else></ResourceDetail>
</template>

<script>
  ...
  setup(){
     
     
      // =================================================================
      // data
      // 6. 定义选中的数据
      const selectedResource = ref(null);

      // =================================================================
      // computed
      // 7. 调用数据
      const activeResource = computed(() => {
     
     
        return selectedResource.value || 
        (getResourcesLength > 0 && data.resources[0]) ||
        null
      })
      // =================================================================
      // methods
      // 6. 选中列表显示数据事件
      const selectResource = (resource) => {
     
     
        // console.log(resource);
        selectedResource.value = resource
        // console.log(selectedResource.value);
      }
  }
};
</script>

<style>
</style>
  • ResourceDetail.vue
<template>
  <!-- {/* 数据详情 Starts */} -->
  <div class="card" v-if="!resource?._id">
    <div class="card-body">No Resource is selected :(</div>
  </div>
  <div class="card" v-else>
    <div class="card-header">{
   
   { resource.title }}</div>
    <div class="card-body">
      <blockquote class="blockquote mb-0">
        <p>{
   
   { resource.description }}</p>
        <footer class="text-muted mb-2">{
   
   { resource.type }}</footer>
      </blockquote>
      <a href="#" class="btn btn-primary">编辑</a>
    </div>
  </div>
  <!-- {/* 数据详情 Ends */} -->
</template>

<script>
export default {
     
     
  props: {
     
     
    resource: {
     
     
      // type: Object,
      validator: (prop) => typeof prop === "object" || prop === null,
      required: true,
    },
  },
};
</script>

<style>
</style>

4.4 Vue 3.0 选中状态

  • 选中列表数据改变其样式
  • ResourceList.vue
<template>
	<!-- {/* 数据列表 Starts */} -->
	<ul class="list-group mb-3 resource-list">
	  <li v-for="resource in resources" 
	      :key="resource._id"
	      :class="`${activeItemClass(resource)} list-group-item d-flex justify-content-between lh-condensed resource-list-item`"
	      @click="onItemClick(resource)"
	  >
	    <div>
	      <h6 class="my-0">{
   
   { resource.title }}</h6>
	      <small class="text-muted">{
   
   { resource.description }}</small>
	    </div>
	    <span class="text-muted">{
   
   { resource.type }}</span>
	  </li>
	
	</ul>
	<!-- {/* 数据列表 Ends */} -->
</template>

<script>
import {
     
      computed } from 'vue'
export default {
     
     
  props: {
     
     
    resources: {
     
     
      type: Array,
      default: () => [],

    },
    activeId: String,
  },
  setup(props, context) {
     
     
    // methods
    // 1. 选中列表显示数据事件
    const onItemClick = (resource) => {
     
     
      // 注册事件
      // 事件名、参数
      context.emit("handleItemClick",resource)
    }

    // computed
    // 2. 选中样式
    const activeItemClass = computed(() => {
     
     
      return (resource) => resource._id === props.activeId ? "is-active" : ""
    })
    
    return {
     
      onItemClick, activeItemClass }
  }

}
</script>

<style scope lang="scss">
.resource-list {
     
     
  max-height: 350px;
  overflow-y: auto;

  &-item {
     
     
    cursor: pointer;

    &:hover {
     
     
      background-color: #f3f3f3;
    }
  }

  .is-active {
     
     
    background-color: #f3f3f3;
  }
}

// css语法
// .resource-list {
     
     
//   max-height: 350px;
//   overflow-y: auto;
// }

// .resource-list-item {
     
     
//   cursor: pointer;
// }

// .resource-list:hover {
     
     
//   background-color: #f3f3f3;
// }
</style>

4.5 Vue 3.0 发送数据请求 axios

  • 安装 axios
    • npm install axis --save
  • ResourceHome.vue
// 生命周期钩子函数
onMounted(async () => {
    
    
	const resources = await fetchResources()
});
  • index.js
import axios from 'axios'

// 请求数据方法
export function fetchResource(){
    
    
    return axios.get("https://vue3-fjord-81553.herokuapp.com/api/resources")
}

4.6 Vue 3.0 代理解决跨域

4.6.1 跨域问题
  • blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
4.6.2 跨域
  • 只要不同源,即为跨域
  • 同源策略: 以下任意一个不同,即为跨域
    • 协议头 http https file
    • 域名 baidu.com www.taobao.com
    • 端口号 8080 441 21 22
  • 域名不同 = 跨域
    • https://www.baidu.com:8888
    • https://www.taobao.com:8888
  • 协议不同 = 跨域
    • http://www.baidu.com:21
    • https://www.baidu.com:21
  • 端口不同 = 跨域
    • http://www.baidu.com:21
    • http://www.baidu.com:22
  • 同源
    • http://www.baidu.com:21
    • http://www.baidu.com:21/map/api/citi
4.6.3 解决跨域
  • jsonp

  • 服务器代理

  • 后端允许跨域

  • vue.config.js

module.exports = {
    
    
  devServer: {
    
    
    // 代理
    proxy: {
    
    
      "^/api": {
    
    
        target: "https://vue3-fjord-81553.herokuapp.com",
        changeOrigin: true,
      },
    },
  },
};

4.7 Vue 3.0 数据赋值

  • 请求接口数据
  • index.js
import axios from 'axios'

// 请求数据方法
export function fetchResources(){
    
    
    return axios.get("/api/resources").then((res) => res.data)
}
  • 数据赋值
  • ResourceHome.vue
// 生命周期钩子函数
onMounted(async () => {
    
    
  const resources = await fetchResources()
  data.resources = resources
});

4.8 Vue 3.0 调整更新组件

  • ResourceUpdate.vue
<template>
  <form>
    <div class="mb-3">
      <label htmlFor="title">标题</label>
      <input
        v-model="uResource.title"
        type="text"
        class="form-control"
        id="title"
        placeholder="title...."
      />
    </div>
    <div class="mb-3">
      <label for="description">描述</label>
      <textarea
        v-model="uResource.description"
        class="form-control"
        id="description"
        placeholder="描述"
      ></textarea>
    </div>
    <div class="mb-3">
      <label htmlFor="type">类型<span class="text-muted">(可选)</span></label>
      <input v-model="uResource.type" type="text" class="form-control" id="type" placeholder="类型..."/>
    </div>
    <div class="mb-3">
      <label htmlFor="link">链接</label>
      <div class="input-group">
        <input
          v-model="uResource.link"
          type="text"
          class="form-control"
          id="link"
          placeholder="链接...."
        />
      </div>
    </div>
    <hr class="mb-4" />
    <button class="btn btn-primary btn-lg btn-block" type="submit">提交</button>
  </form>
</template>

<script>
import {
     
      ref } from 'vue'
export default {
     
     
  props: {
     
     
    resource: Object
  },
  setup(props, context) {
     
     
    const uResource = ref(props.resource)
    
    return {
     
      uResource }
  }
}
</script>

<style>
</style>
  • ResourceHome.vue
<ResourceUpdate :resource="activeResource" v-if="isDetailView" />

4.9 Vue 3.0 watch 监听数据实时切换

  • 利用 watch 监听数据是否变化
  • ResourceUpdate.vue
import {
    
     ref, watch } from 'vue'
export default {
    
    
  props: {
    
    
    resource: Object
  },
  setup(props, context) {
    
    
    const uResource = ref(props.resource)
    
    watch(
      () => props.resource,
      (resource, prevResource) => {
    
    
        uResource.value = resource
      }
    )
    
    return {
    
     uResource }
  }
}

5. Vue 3.0 数据的处理

5.1 Vue 3.0 调整类型选项

  • ResourceUpdate.vue
<div class="mb-3">
  <label htmlFor="type">类型</label>
  <select class="form-control" id="type" v-model="uResource.type">
    <option v-for="(resourceType, index) in types" 
            :key="index" 
            :value="resourceType">{
    
    {
    
    resourceType}}</option>
    
  </select>

</div>
...
// 类型选项
const types = ["blog","video","book"]

5.2 Vue 3.0 数据更新到服务器端

  • index.js
// 更新数据方法
// 传入对应id,及新内容
export function updateResource(id, resource){
    
    
    return axios
        .patch(`/api/resources/${
      
      id}`, resource)
        .then((res) => res.data)
}
  • ResourceUpdate.vue
<button 
      @click="handleUpdate" 
      class="btn btn-primary btn-lg btn-block" 
      type="button">提交
</button>
...
import {
    
     updateResource } from '@/actions'
...
// 提交事件(异步)
const handleUpdate = async () => {
    
    
  // 传入对应id和新内容
  const updatedResource = await updateResource(uResource.value._id, uResource.value)
}


// 将更新的数据传给父组件
context.emit("onUpdateResource", updateResource)
  • ResourceHome.vue
<ResourceUpdate 
    @onUpdateResource="handleUpdateResource($event)" 
     :resource="activeResource" 
     v-if="isDetailView" />
...
// 8. 获取更新数据
const handleUpdateResource = (newResource) => {
    
    
  // 拿到原先存储数据的数组下标
  const index = data.resources.findIndex(
    (resource) => resource._id === newResource._id
  )
  data.resources[index] = newResource

  selectResource(newResource)
}

5.3 Vue 3.0 弹窗提醒

  • ResourceUpdate.vue
<div v-if="alert?.success" class="alert alert-success">{
    
    {
    
     alert.success }}</div>
<div v-if="alert?.error" class="alert alert-danger">{
    
    {
    
     alert.error }}</div>
...
// 封装方法
// 初始化弹窗状态
const initAlert = () => {
    
    
  return {
    
     success: null, error: null }
}

// 弹窗状态
const setAlert = (type, message) => {
    
    
  data.alert = initAlert()
  data.alert[type] = message
}

// 提交事件(异步)
const handleUpdate = async () => {
    
    
	// 抛出异常
	try {
    
    
	  // 传入对应id和新内容
	  const updatedResource = await updateResource(
	    uResource.value._id, 
	    uResource.value
	  )
	
	  // 将更新的数据传给父组件
	  context.emit("onUpdateResource", updatedResource)
	  // 提交成功后弹窗信息
	  setAlert("success","Resource was updated")
	} catch (error) {
    
    
	  setAlert("error", error?.message)
	}
 
}

5.4 Vue 3.0 设置定时器取消提醒

  • ResourceUpdate.vue
setup(props, context) {
    
    
  // 接收数据
  const uResource = ref(props.resource)
  // 类型选项
  const types = ["blog","video","book"]

  // 弹窗信息
  const data = reactive({
    
    
    alert: {
    
     success: null, error: null },
    // 定义定时器
    timeoutId: null
  })
  
  // 监听数据切换
  watch(
    () => props.resource,
    (resource, prevResource) => {
    
    
      // 判断改变的值存不存在
      if(resource && (resource._id !== prevResource._id)){
    
    
        // 关闭定时器
        clearAlertTimeout()
        data.alert = initAlert()
      }

      uResource.value = resource
    }
  )

  // 封装方法
  // 初始化弹窗状态
  const initAlert = () => {
    
    
    return {
    
     success: null, error: null }
  }

  // 钩子函数
  // 离开组件之前调用
  onBeforeUnmount(() => {
    
    
    // 清除定时器方法
    clearAlertTimeout()
  })

  // 清除定时器方法
  const clearAlertTimeout = () => {
    
    
    data.timeoutId && clearTimeout(data.timeoutId)
  }

  // 弹窗状态
  const setAlert = (type, message) => {
    
    
    data.alert = initAlert()
    data.alert[type] = message

    // 设置定时器
    data.timeoutId = setTimeout(() => {
    
    
      data.alert = initAlert()
    },3000)
  }
}

5.5 Vue 3.0 删除数据

<template>
  <button @click="handleDeleteResource()" class="btn btn-sm btn-danger">
    删除
  </button>
</template>

<script>
import {
     
      deleteResource } from "@/actions";
export default {
     
     
  props: {
     
     
    activeId: String,
  },
  setup(props, context) {
     
     
    const handleDeleteResource = async () => {
     
     
      // 请求数据接口
      // 拿到要删除的内容
      const deleteData = await deleteResource(props.activeId);
      // 传递给父组件
      context.emit("onResourceDelete", deleteData);
      
    };
    return {
     
      handleDeleteResource };
  },
};
</script>

<style scoped>
</style>

6. Vue 3.0 路由和搜索功能

6.1 Vue 3.0 配置路由

  • 安装路由
    • yarn add vue-router@next
  • router.js
import {
    
     createRouter, createWebHistory } from "vue-router";
import ResourceHome from "@/views/ResourceHome.vue";
import ResourceNew from "@/views/ResourceNew.vue";

const routes = [
  {
    
     path: "/", component: ResourceHome },
  {
    
     path: "/new", component: ResourceNew}
];

const router = createRouter({
    
    
  history: createWebHistory(),
  //   routes: routes,
  routes,

});

export default router;
  • main.js
import {
    
     createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App);
// 挂载
app.use(router);
app.mount("#app");

6.2 Vue 3.0 子级路由

  • router.js
import {
    
     createRouter, createWebHistory } from "vue-router";
import ResourceHome from "@/views/ResourceHome.vue";
import ResourceNew from "@/views/ResourceNew.vue";
import ResourceRoutes from "@/views/ResourceRoutes.vue"

const routes = [
  // =================================================================
  // { path: "/", component: ResourceHome },
  // { path: "/new", component: ResourceNew}

  // =================================================================
  // 重定向、定义名字
  // http://localhost:8080/resources
  // { 
  //   path: "/", 
  //   name: "base", 
  //   redirect: { name: "resourceHomePage"} 
  // },
  // { 
  //   path: "/resources", 
  //   name: "resourceHomePage", 
  //   component: ResourceHome 
  // },
  // { 
  //   path: "/resources/new", 
  //   name: "resourceNewPage", 
  //   component: ResourceNew
  // }

  // =================================================================
  // 子路由
  {
    
     
    path: "/", 
    name: "base", 
    redirect: {
    
     name: "resourceHomePage"} 
  },
  {
    
     
    path: "/resources", 
    name: "resourceHomePage", 
    component: ResourceRoutes,
    children: [
      {
    
     
        path: "",
        name: "resourceHome",
        component: ResourceHome,
      },
      {
    
     
        path: "new",
        name: "resourceNewPage",
        component: ResourceNew,
      }
    ]
  }
];

const router = createRouter({
    
    
  history: createWebHistory(),
  //   routes: routes,
  routes,
});

export default router;

6.3 Vue 3.0 导航

  • Header.vue
<router-link class="btn btn-outline-primary mr-2" :to="{ name: 'resourceHomePage' }">首页</router-link>
<router-link class="btn btn-outline-primary" :to="{ name: 'resourceNewPage' }">添加</router-link>
  • router.js
import {
    
     createRouter, createWebHistory } from "vue-router";
import ResourceHome from "@/views/ResourceHome.vue";
import ResourceNew from "@/views/ResourceNew.vue";
import ResourceRoutes from "@/views/ResourceRoutes.vue"

const routes = [
  // 重定向、定义名字
  // http://localhost:8080/resources
  {
    
     
    path: "/", 
    name: "base", 
    redirect: {
    
     name: "resourceHomePage"} 
  },
  {
    
     
    path: "/resources", 
    name: "resourceHomePage", 
    component: ResourceHome 
  },
  {
    
     
    path: "/resources/new", 
    name: "resourceNewPage", 
    component: ResourceNew
  }
];

const router = createRouter({
    
    
  history: createWebHistory(),
  //   routes: routes,
  routes,
  linkExactActiveClass: "active",
});

export default router;

6.4 Vue 3.0 重构更新组件

  • ResourceForm.vue
<template>
  <form>
    <div v-if="alert?.success" class="alert alert-success">
      {
   
   { alert.success }}
    </div>
    <div v-if="alert?.error" class="alert alert-danger">{
   
   { alert.error }}</div>
    <div class="mb-3">
      <label for="title">标题</label>
      <input
        v-model="uResource.title"
        type="text"
        class="form-control"
        id="title"
        placeholder="title...."
      />
    </div>

    <div class="mb-3">
      <label for="description">描述</label>
      <textarea
        v-model="uResource.description"
        class="form-control"
        id="description"
        placeholder="描述"
      ></textarea>
    </div>
    <div class="mb-3">
      <label for="type">类型</label>
      <select class="form-control" id="type" v-model="uResource.type">
        <option
          v-for="(resourceType, index) in types"
          :key="index"
          :value="resourceType"
        >
          {
   
   { resourceType }}
        </option>
      </select>
    </div>
    <div class="mb-3">
      <label for="link">链接</label>
      <div class="input-group">
        <input
          v-model="uResource.link"
          type="text"
          class="form-control"
          id="link"
          placeholder="链接...."
        />
      </div>
    </div>
    <hr class="mb-4" />
    <button
      @click="submitForm()"
      class="btn btn-primary btn-lg btn-block"
      type="button"
    >
      提交
    </button>
  </form>
</template>

<script>
import {
     
      ref, watch } from "vue";

export default {
     
     
  props: {
     
     
    resource: Object,
    alert: Object,
  },
  setup(props, context) {
     
     
    const uResource = ref(props.resource);
    const types = ["blog", "video", "book"];

    watch(
      () => props.resource,
      (resource, prevResource) => {
     
     
        uResource.value = resource;
      }
    );

    const submitForm = () => {
     
     
      context.emit("onFormSubmit", uResource);
    };

    return {
     
     
      uResource,
      types,
      submitForm,
    };
  },
};
</script>
  • ResourceUpdate.vue
<template>
  <ResourceForm
    @onFormSubmit="handleUpdate($event)"
    :resource="resource"
    :alert="alert"
  />
</template>

6.5 Vue 3.0 添加组件

  • ResourceNew.vue
<template>
  <ResourceForm
    @onFormSubmit="handleCreate($event)"
    :resource="resource"
    :alert="alert"
  />
</template>

<script>
import ResourceForm from "@/components/ResourceForm.vue";
import {
     
      useRouter } from "vue-router";
import {
     
      createResource } from "@/actions";
import {
     
      reactive, toRefs, onBeforeUnmount } from "vue";
export default {
     
     
  components: {
     
     
    ResourceForm,
  },
  setup() {
     
     
    const router = useRouter();
    const data = reactive({
     
     
      resource: {
     
     
        title: "",
        description: "",
        type: "video",
        link: "",
      },
      alert: {
     
     
        success: null,
        error: null,
      },
      timeoutId: null,
    });

    const initAlert = () => {
     
     
      return {
     
     
        success: null,
        error: null,
      };
    };

    onBeforeUnmount(() => {
     
     
      clearAlertTimeout();
    });

    const clearAlertTimeout = () => {
     
     
      data.timeoutId && clearTimeout(data.timeoutId);
    };

    const setAlert = (type, message) => {
     
     
      data.alert = initAlert();
      data.alert[type] = message;
      data.timeoutId = setTimeout(() => {
     
     
        data.alert = initAlert();
        router.push("/");
      }, 2000);
    };

    const handleCreate = async (resource) => {
     
     
      // console.log(resource.value);
      await createResource(resource.value);
      setAlert("success", "Resource was created");
    };

    return {
     
     
      ...toRefs(data),
      handleCreate,
    };
  },
};
</script>

<style lang="scss" scoped></style>

6.6 Vue 3.0 详情跳转

  • ResourceDetailPage.vue
<template>
  <ResourceDetail :resource="resource">
    <template #buttonLink>
      <button @click="$router.go(-1)" class="btn btn-outline-success">
        返回
      </button>
    </template>
  </ResourceDetail>
</template>

<script>
import {
     
      useRoute } from "vue-router";
import {
     
      fetchResource } from "@/actions";
import {
     
      toRefs, reactive, onMounted } from "vue";
import ResourceDetail from "@/components/ResourceDetail.vue";
export default {
     
     
  components: {
     
     
    ResourceDetail,
  },
  setup() {
     
     
    const route = useRoute();
    const data = reactive({
     
     
      resource: null,
    });
    onMounted(async () => {
     
     
      // console.log(route.params.id);
      const {
     
      id } = route.params;
      data.resource = await fetchResource(id);
    });

    return {
     
     
      ...toRefs(data),
    };
  },
};
</script>

<style scoped>
</style>

6.7 Vue 3.0 显示详情页面

  • ResourceList.vue
<template>
  <!-- {/* 数据列表 Starts */} -->
          <ul class="list-group mb-3 resource-list">
            <li v-for="resource in resources" 
                :key="resource._id"
                :class="`${activeItemClass(resource)} list-group-item d-flex justify-content-between lh-condensed resource-list-item`"
                @click="onItemClick(resource)"
            >
              <div>
                <h6 class="my-0">{
   
   { resource.title }}</h6>
                <small class="text-muted">{
   
   { resource.description }}</small>
              </div>
              <span class="text-muted">{
   
   { resource.type }}</span>
            </li>
          
          </ul>
          <!-- {/* 数据列表 Ends */} -->
</template>

<script>
import {
     
      computed } from 'vue'
export default {
     
     
  props: {
     
     
    resources: {
     
     
      type: Array,
      default: () => [],

    },
    activeId: String,
  },
  setup(props, context) {
     
     
    // methods
    // 1. 选中列表显示数据事件
    const onItemClick = (resource) => {
     
     
      // 注册事件
      // 事件名、参数
      context.emit("handleItemClick",resource)
    }

    // computed
    // 2. 选中样式
    const activeItemClass = computed(() => {
     
     
      return (resource) => resource._id === props.activeId ? "is-active" : ""
    })


    return {
     
      onItemClick, activeItemClass }
  }

}
</script>

<style scope lang="scss">
.resource-list {
     
     
  max-height: 350px;
  overflow-y: auto;

  &-item {
     
     
    cursor: pointer;

    &:hover {
     
     
      background-color: #f3f3f3;
    }
  }

  .is-active {
     
     
    background-color: #f3f3f3;
  }
}

// css语法
// .resource-list {
     
     
//   max-height: 350px;
//   overflow-y: auto;
// }

// .resource-list-item {
     
     
//   cursor: pointer;
// }

// .resource-list:hover {
     
     
//   background-color: #f3f3f3;
// }
</style>

6.8 Vue 3.0 slot 插槽

  • ResourceDetail.vue
<slot name="buttonLink"></slot>
  • ResourceDetailPage.vue
<template>
  <ResourceDetail :resource="resource">
    <template #buttonLink>
      <button @click="$router.go(-1)" class="btn btn-outline-success">
        返回
      </button>
    </template>
  </ResourceDetail>
</template>

6.9 Vue 3.0 搜索数据

  • ResourceSearch.vue
<template>
  <!-- {/* 搜索框 start */} -->
  <form class="card p-2">
    <div class="input-group">
      <input
        @keyup="$emit('onsearch', $event.target.value)"
        type="text"
        class="form-control"
        placeholder="写点啥..."
      />
    </div>
  </form>
  <!-- {/* 搜索框 Ends */} -->
</template>

<script>
export default {
     
     };
</script>

<style></style>
  • index.js
// 搜索功能
export function searchResources(title) {
    
    
  return axios.get(`/api/resources/s/${
      
      title}`).then((res) => res.data);
}

7. 总结

  • 新版 Vue 3.0 实战项目,做个笔记

猜你喜欢

转载自blog.csdn.net/qq_43645678/article/details/108938454
今日推荐