vue3学习

距离vue2学习已经一年度了,现在开始vue3的学习。

一、webpack

(1)创建列表隔行变色项目及webpack使用:

新建项目空白目录,并运行npm init -y命令,初始化包管理配置文件package.json;

新建src源代码目录,新建src -> index.html首页和src -> index.js脚本文件,初始化首页基本的结构
运行npm install jquery -s命令,安装jQuery,通过ES6模块化的方式导入jQuery,实现列表隔行变色效果。其中ul-li快速创建:ul>li{这是第$个li}*9回车。

webpack可以帮助用户将兼容性代码转为非兼容性代码

在代码文件根目录执行命令:

cnpm i webpack-dev-server webpack-cli webpack -D

(2)webpack使用

①在项目根目录创建名为webpack.config.js的webpack配置文件,并初始化如下的配置文件:

module.exports = {
  mode: 'development' // mode 用来制定构建模式,可选值有development 和 production
}

②在package.json的scripts节点下,添加dev脚本如下:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack" 
  },

③在终端中运行npm run dev命令,启动webpack进行项目的打包构建。会生成dist文件夹(里面是main.js),index.html引入的js替换为../dist/main.js,这样html就可以正常运行了。

(3)webpack.config.js 文件的作用

webpack.config.js 是 webpack的配置文件。webpack在真正开始打包构建之前,会先读取这个配置文件,从而基于给定的配置,对项目进行打包。
注意:由于webpack是基于node.js 开发出来的打包工具,因此在它的配置文件中,支持使用node.js 相关的语法和模块进行webpack的个性化配置。

(4)webpack中的默认约定

》在webpack中有如下的默认约定:
①默认的打包入口文件为src -> index.js ②默认的输出文件路径为dist -> main.js
注意:可以在webpack.config.js中修改打包的默认约定

》自定义打包的入口与出口
在webpack.config.js配置文件中,通过entry节点指定打包的入口。通过output节点指定打包的出口。示例代码如下:

const path = require('path')
module.exports = {
  mode: 'development', // mode 用来制定构建模式,可选值有development 和 production
  entry: path.join(__dirname, './src/index.js'), // 打包入口文件的路径
  output: {
    path: path.join(__dirname, './dist'), //输出文件的存放路径
    filename: 'bundle.js' // 输出文件的名称
  }
}

(5)webpack插件的作用

通过安装和配置第三方的插件,可以拓展webpack的能力,从而让webpack用起来更方便。最常用的webpack插件有如下两个: 
①webpack-dev-server
●类似于node.js阶段用到的nodemon工具
●每当修改了 源代码, webpack会自动进行项目的打包和构建,即package.json的scripts节点的dev值改为“webpack serve”

②html-webpack-plugin
●webpack 中的HTML插件( 类似于于一个模板引擎插件)
●可以通过此插件自定制index.html页面的内容

cnpm i html-webpack-plugin -D

配置方式:

 效果如下:

const path = require('path')
// 导入插件,得到构造函数
const HtmlPlugin = require('html-webpack-plugin')
// 创建插件的实例对象
const htmlPlugin = new HtmlPlugin({
  template: './src/index.html',
  filename: './index.html'
})

module.exports = {
  mode: 'development', // mode 用来制定构建模式,可选值有development 和 production
  entry: path.join(__dirname, './src/index.js'), // 打包入口文件的路径
  output: {
    path: path.join(__dirname, './dist'), //输出文件的存放路径
    filename: 'bundle.js' // 输出文件的名称
  },
  plugins: [htmlPlugin] // 挂载插件的实例对象
}

注意:通过HTML插件复制到项目根目录中的index.html页面,也被放到了内存中;HTML插件在生成的index.html页面的底部,自动注入了打包的bundle.js 文件。

(6)devServer节点

在webpack.config.js配置文件中,可以通过devServer节点对webpack-dev-server插件进行更多的配置,open(初次打包之后,直接打开浏览器)、host(实时打包所用的主机地址)、port(d端口号)

(7)loader

在实际开发过程中,webpack默认只能打包处理以.js后缀名结尾的模块。其他非.js后缀名结尾的模块,webpack 默认处理不了,需要调用loader加载器才可以正常打包,否则会报错!

7.1 处理css的loader

在index.js里面导入css:import './css/index/css'

①:npm i style-loader css-loader -D,安装css的loader;②在webpack.config.js的module->rules数组里面添加规则,其中test表示匹配的文件烈性,use表示要调用的loader,loader顺序不能错。

module: {
    rules: [{ test: /\.css$/, use: ['style-loader', 'css-loader'] }]
  }

 7.2 处理less的loader

 ①:运行npm i less-loader less -D命令;②在webpack.config.js的moudle->rules数组添加规则:

rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] }
    ]

其中安装的less是less-loader的内置依赖项。

7.3 打包处理样式表中与url相关的文件

在webpack5中url-loader、file-loader已经弃用。

二、vue3简介

1. vue3的优势

更容易维护:组合式API,更好的TypeScript支持;更快的速度:重写diff算法,模版变异优化,更高效的组件初始化;更小的提价:良好的TreeShaking,按需引入;更优的数据响应式:Proxy

2. 认识create-vue

creat-vue是Vue官方新的脚手架工具,底层切换到了vite(下一代构建工具)。

前提环境条件:node 版本高于16.0。创建一个vue应用:npm init vue@lastest,这一指令将会安装并执行Create-vue。安装慢的话需要用到:npm 使用快速的安装源(nrm)npm 使用快速的安装源(nrm) - 编程教程

 

按照绿字的提示输入命令,这样就可以打开一个vue3的项目了。使用vscode打开该项目,查看项目结构和目录。

vue2项目的main.js使用new vue()创建一个应用实例,vue3使用createApp() / createRouter() createStore(),将创建实例进行了封装,保证每个实例的独立封闭性,mount设置挂载点,#app(id魏app的盒子,即index.html里面id为app的div)。vue3需要vscode安装volar插件,具体如下:

3. 组合式api

3.1 setup

3.1.1、setup选项的执行时机:beforeCreate钩子之前自动执行
3.1.2.、setup写代码的特点:定义数据+函数然后以对象方式return
3.1.3、<script setup>解决了什么问题?经过语法糖的封装更简单的使用组合式API
3.1.4、setup中的this还指向组件实例吗?指向undefined

3.2 reactive和ref函数

推荐:声明数据,统一使用ref。

3.2.1 reactive()

作用:接受对象函数类型数据的参数传入并返回一个响应式的对象

步骤:从vue包导入reactive函数;在<script setup>中执行reactive函数并传入类型为对象的初始值,并使变量接收返回的值。

3.2.2 ref()

作用:接受简单类型或复杂类型,返回一个响应式的对象,本质上是在原有传入数据的基础上,外层包了一层对象,包成了复杂类型。

注意点:script访问数据,需要通过.value;template里面不需要.value

3.3、computed计算属性函数

计算属性基本思想和Vue2的完全一致,组合式API下的计算属性只是修改了写法。

核心步骤:1.导入computed函数;2.执行函数在回调参数中return基于响应式数据做计算的值,用变量接收。

注意:(1)计算属性中不应该有“副作用”,比如异步请求/修改dom;(2)避免直接修改计算属性的值,计算属性应该是只读的,特殊情况可以配置get set,参见响应式 API:核心 | Vue.js

3.4、watch

3.4.1 侦听单个数据

(1)导入watch函数;(2)执行watch函数传入要侦听的响应式数据(ref对象 )和回调函数。

3.4.2 侦听多个数据

同时侦听多个响应式数据的变化,不管哪个数据变化都要触发回调。

3.4.3 immediate

说明:在侦听器创建时立即触发回调,响应式数据变化之后继续执行回调。

3.4.4 deep:true

说明:深度监视,默认watch进行的是浅层监视;const ref1 = ref(简单类型)可以直接监视;const ref2 = ref(复杂类型)监视不到复杂类型内部数据的变化。

如果不开启deep的前提,要精确侦听对象的某个属性,如下写法:

3.4.5 总结

(1)作为watch函数的第一个参数,ref对象要添加.value吗?不要,第一个参数就是传ref 对象;

(2)watch只能侦听单个数据吗?单个或者多个;

(3)不开启deep,直接监视复杂类型,修改属性能触发回调吗?不能,默认是浅层侦听。

(4)不开启deep,精确侦听对象的某个属性?可以把第一个参数写成函数的写法,返回要监听的具体属性。

3.5、生命周期函数

比如:

3.6、父子通信

3.6.1 父传子

基本思想:父组件给子组件绑定属性,子组件内部通过props选项接收。

子组件由于写了setup,所以无法直接配置 props选项,所以此处需要借助于“编译器宏”函数接收子组件传递的数据;对于props传递过来的数据,模板中可以直接使用。

3.6.2 子传父

基本思想:父组件中给子组件标签通过@绑定事件;子组件内部通过emit方法触发事件。

3.6.3 总结
3.6.3.1 父传子

(1)父传子的过程中通过什么方式接收props?
        defineProps({属性名:类型})
(2)setup语法糖中如何使用父组件传过来的数据?
        const props = defineProps({属性名:类型})   props.xxx

3.6.3.2 子传父

(1)子传父的过程中通过什么方式得到emit方法?
        defineEmits(['事件名称])
(2)怎么触发事件
        emit('自定义事件名',参数)

3.7、模板引用

通过ref标识获取真实的dom对象或者组件实例对象。

通过ref函数生成一个ref对象,名称要和模版元素ref一致;通过ref标识绑定ref对象到标签。

 默认情况下在<script setup>语法糖下组件内部的属性和方法是不开放给父组件访问的,可以通过defineExpose编译宏指定哪些属性和方法允许访问。

总结:

获取模板引用的时机是什么?组件挂载完毕
defineExpose编译宏的作用是什么?显式暴露组件内部的属性和方法

3.8、provide和inject

跨层传递普通数据:1.顶层组件通过provide函数提供数据;2.底层组件通过inject函数获取数据。

跨层传递响应式数据:在调用provide函数时,第二个参数设置为ref对象。

跨层传递方法:顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件中的数据

4、vue3.3新特性

背景说明:
        有<script setup>之前,如果要定义props,emits 可以轻而易举地添加一个与setup平级的属性。但是用了<script setup>后,就没法这么干了setup属性已经没有了,自然无法添加与其平级的属性。
        为了解决这一问题,引入了defineProps 与defineEmits 这两个宏。但这只解决了props与emits这两个属性。如果我们要定义组件的name或其他自定义的属性,还是得回到最原始的用法——再添加一个普通的<script>标签。这样就会存在两个<script>标签。让人无法接受。

        所以在Vue 3.3中新引入了defineOptions宏。顾名思义,主要是用来定义Options API的选项。可以用defineOptions定义任意的选项,props, emits, expose, slots 除外(因为这些可以使用defineXXX来做到)

4.1、defineOptions

4.2、defineModel

在Vue3中,自定义组件上使用v-model,相当于传递一个modelvalue属性,同时触发 update:modelvalue 事件。我们需要先定义props,再定义emits。其中有许多重复的代码。如果需要修改此值,还需要手动调用emit 函数。

如果使用defineModel,代码如下,需要修改vite.config.js,开启defineModel,重启:

5、Pinia

Pinia使Vue3的官方状态管理工具,是Vuex的替代品。

(1)提供更加简单的API(去掉了mutation )
(2)提供符合,组合式风格的API(和Vue3新语法统一)
(3)去掉了modules的概念,每一个store都是一个独立的模块
(4)配合TypeScript更加友好,提供可靠的类型推断

5.1、使用

按照官方文档安装pinia到项目。Pinia | Pinia

5.1.1 开始
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia).mount('#app')
5.1.2 定义store
// @/store/counter.js
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  const increment = () => {
    count.value++
  }
  const decrement = () => {
    count.value--
  }

  const double = computed(() => count.value * 2)
  const msg = ref('hello pinia')
  return { count, msg, double, increment, decrement }
})
// @/store/channels.js 支持异步
import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from 'axios'

export const useChannelStore = defineStore('channel', () => {
  // 声明数据
  const channelList = ref([])
  // 声明操作数据的方法
  const getList = async () => {
    const {
      data: { data }
    } = await axios.get('http://geek.itheima.net/V1_0/channels')
    channelList.value = data.channels
    console.log(data.channels)
  }
  // 声明getters相关

  return { channelList, getList }
})
5.1.3 使用store
<!-- App.vue,子组件可忽略 -->
<script setup>
import Son1 from '@/components/SonView1.vue'
import Son2 from '@/components/SonView2.vue'

import { useCounterStore } from '@/store/counter'
const counterStore = useCounterStore()
import { useChannelStore } from '@/store/channel.js'
const channelStore = useChannelStore()
</script>

<template>
  <div>
    <h3>APP.vue根组件 -{{ counterStore.count }} -{{ counterStore.msg }} - {{ counterStore.double }}</h3>
    <Son1 />
    <Son2 />
    <hr />

    <button type="button" @click="channelStore.getList">channel</button>
    <ul>
      <li v-for="item in channelStore.channelList" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<style scoped></style>

另一种写法,为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()。它将为每一个响应式属性创建引用。当你只使用 store 的状态而不调用任何 action 时,它会非常有用。请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上。

// 官网代码
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>
5.1.4 pinia持久化插件

官方文档:快速开始 | pinia-plugin-persistedstate

(1)安装插件:npm i pinia-plugin-persistedstate

(2)main.js使用:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia.use(persist)).mount('#app')

(3)store仓库中,pereist:true开启

5.1.5 总结

(1)Pinia是用来做什么的?新一代的状态管理工具,替代vuex
(2)Pinia中还需要mutation吗?不需要,action既支持同步也支持异步
(3)Pinia如何实现getter?computed计算属性函数
(4)Pinia产生的Store如何解构赋值数据保持响应式?storeToRefs
(5)Pinia 如何快速实现持久化?pinia-plugin-persistedstate

6. 项目:大事件管理系统

简介:使用vue3 compositionAPI,Pinia/Pinia持久化处理,Element Plus(表单校验、表格处理、组件封装),pnpm包管理升级,Eslint+prettier更规范的配置,husky(Git hooks工具)代码提交之前进行校验,球球模块设置,VueRouter4路由设计,AI大模型开发一整个项目模块(掌握最新的开发方式)

6.1 pnpm包管理器

一些优势:比同类工具快2倍左右、节省磁盘空间... https://www.pnpm.cn/

安装方式::npm install -g pnpm    创建项目:pnpm create vue。注意不要在磁盘根目录创建项目,否则可能报权限不足。

6.2 Eslint配置代码风格

配置文件.eslintrc.cjs

(1)prettier风格配置:https://pretties.io。禁用prettier code format插件,安装Eslint插件, 修改.eslintrc.cjs:

module.exports = {
 ...
  rules: {
    // 禁用格式化插件,prettier format on save,
    // 安装Eslint插件,并配置保存时自动修复。
    // prettier关注代码美观度
    'prettier/prettier': [
      'warn',
      {
        singleQuote: true, // 单引号
        semi: false, // 无分号
        printWidth: 80, // 每行宽度至多80字符
        trailingComma: 'none', // 不加对象|数组最后逗号
        endOfLine: 'auto' // 换行符号不限制(win mac 不一致)
      }
    ],
    // eslint关注规范
    'vue/multi-word-component-names': [
      'warn',
      {
        ignores: ['index'] // vue组件名称多单词组成(忽略index.vue)
      }
    ],
    'vue/no-setup-props-destructure': ['off'], // 关闭 props 解构的校验
    // 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
    'no-undef': 'error'
  }
}

(2)修改vscode的setting.json:

...
"editor.formatOnSave": false, // #值设置为true时,每次保存的时候自动格式化;
"editor.codeActionsOnSave": {
   "source.fixAll": true
},
...

6.3 提交前做代码检查

6.3.1 husky

husky是一个git hooks工具( git的钩子工具,可以在特定时机执行特定的命令)

(1)初始化git仓库,执行git init即可。

(2)初始化husky工具配置,执行pnpm dlx husky-init && pnpm install 即可、https://typicode.github.io/husky/

(3)修改.husky/pre-commit文件。

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# npm test
pnpm lint

pnpm lint检验的是全部代码。

6.3.2 暂存区eslint校验

1. 安装lint-staged包:pnpm i lint-staged -D
2.  package.json配置lint-staged命令

```jsx
{
  // ... 省略 ...
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix"
    ]
  }
}
{
  "scripts": {
    // ... 省略 ...
    "lint-staged": "lint-staged"
  }
}
```

3. .husky/pre-commit文件修改

```jsx
pnpm lint-staged
```

6.4 目录调整

默认生成的目录结构不满足我们的开发需求,所以这里需要做一些自定义改动。主要是以下工作:
1.删除一些初始化的默认文件;主要是components、assets、router/index.js
2.修改剩余代码内容;主要是App.vue
3.新增调整我们需要的目录结构;比如api文件夹
4.拷贝全局样式和图片,安装预处理器支持:pnpm add sass -D

6.5 Vue-Router4 路由代码解析

6.5.1 路由初始化

以上为Vue2 => Vue3的router变化

(1)创建路由实例由createRouter 实现
(2)路由模式:history模式使用createwebHistory();hash模式使用createWebHashHistory();参数是基础路径,默认/

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

// createRouter创建路由实例
// 配置history模式
// 1. history模式:createwebHistory:地址栏不带#
// 2. hash模式: createwebHashHistory:地址栏带#
// vite环境变量:import.meta.env.BASE_URL
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: []
})

export default router

修改import.meta.env.BASE_URL需要在vite.config.js添加base参数,参看:环境变量和模式 | Vite 官方中文文档

<script setup>
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()

const goList = () => {
  router.push('/list')
  console.log(router, route)
}
</script>

<template>
  <div>APP</div>
  <button @click="$router.push('/home')">跳首页</button>
  <button @click="goList">跳列表页</button>
</template>

<style scoped></style>

一种是直接在html里面使用$router.push(),一种是在script里面用useRouter函数。

6.6 按需引入Element Plus

6.6.1 官网地址

设计 | Element Plus

6.6.2 步骤
// 1. 安装
pnpm install element-plus

// 2. 插件
pnpm add -D unplugin-vue-components unplugin-auto-import

// 3.修改vite.config.js的引入项以及plugins参数
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

// 4. 重启服务,button直接改成el-button即可,无须导入。
 <el-button @click="$router.push('/home')">跳首页</el-button>
 <el-button @click="goList">跳列表页</el-button>

 默认components 下的文件也会被自动注册。

6.7 pinia构建用户仓库和持久化

6.7.1 示例目标:

6.7.2 步骤

(1)新建stores仓库

// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore(
  'big-user',
  () => {
    const token = ref('')
    const setToken = (newToken) => {
      token.value = newToken
    }
    const removeToken = () => {
      token.value = ''
    }

    return {
      token,
      setToken,
      removeToken
    }
  },
  {
    persist: true // 持久化
  }
)

(2)安装持久化插件

pnpm add pinia-plugin-persistedstate -D

(3)使用main.js

```jsx
import persist from 'pinia-plugin-persistedstate'
...
app.use(createPinia().use(persist))
```

(4)配置 stores/user.js

添加persist参数(最终看上面的代码):

...

 { persist: true // 持久化 }

...

6.7.3 pinia仓库统一管理

(1)pinia独立维护:由 stores 统一维护,在 stores/index.js 中完成 pinia 初始化,交付 main.js 使用

(2)仓库统一导出:由 stores/index.js 统一导出,导入路径统一 `./stores`,而且仓库维护在 stores/modules 中。

新建stores/index.js

// stores/index.js
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

const pinia = createPinia().use(persist)

pinia.use(persist)

export default pinia

export * from './modules/user'
export * from './modules/counter'

修改main.js

// main.js
import { createApp } from 'vue'

import pinia from './stores/index.js'
import App from './App.vue'
import router from './router'
import '@/assets/main.scss'

const app = createApp(App)

app.use(pinia)
app.use(router)

app.mount('#app')

修改app.vue

<script setup>
...
import { useUserStore, useCounterStore } from '@/stores'
...
const userStore = useUserStore()
const counterStore = useCounterStore()
</script>

6.8 数据交互-请求工具设计

使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如: 配置基础地址等),一般项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个模块中, 便于使用。

新建 `utils/request.js` 封装 axios 模块,利用 axios.create 创建一个自定义的 axios 来使用,axios中文文档|axios中文网 | axios

import { useUserStore } from '@/stores'
import axios from 'axios'
import router from '@/router'
import { ElMessage } from 'element-plus'

const baseURL = 'http://big-event-vue-api-t.itheima.net'

const instance = axios.create({
  // TODO 1. 基础地址,超时时间
  baseURL,
  timeout: 100000
})
// 请求拦截器
instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带token
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers.Authorization = userStore.token // 根据实际使用修改
    }
    return config
  },
  (err) => Promise.reject(err)
)
// 响应拦截器
instance.interceptors.response.use(
  (res) => {
    // TODO 3. 处理业务失败
    // TODO 4. 摘取核心响应数据
    if (res.data.code === 0) {
      // 根据实际使用修改
      return res
    }
    ElMessage({ message: res.data.message || '服务异常', type: 'error' })
    return Promise.reject(res.data)
  },
  (err) => {
    ElMessage({
      message: err.response.data.message || '服务异常',
      type: 'error'
    })
    console.log(err)
    // TODO 5. 处理401错误 权限不足或token过期
    if (err.response?.status === 401) {
      router.push('/login')
    }
    return Promise.reject(err)
  }
)

export default instance
export { baseURL }

6.9 整体路由设计

6.9.1 实现目标

- 完成整体路由规划【搞清楚要做几个页面,它们分别在哪个路由下面,怎么跳转的.....】
- 通过观察,  点击左侧导航,  右侧区域在切换,  那右侧区域内容一直在变,  那这个地方就是一个路由的出口
- 我们需要搭建嵌套路由
- 把项目中所有用到的组件及路由表, 约定下来。

该项目的路由结构如下:

import { createRouter, createWebHistory } from 'vue-router'

// createRouter创建路由实例
// 配置history模式
// 1. history模式:createwebHistory:地址栏不带#
// 2. hash模式: createwebHashHistory:地址栏带#
// vite环境变量:import.meta.env.BASE_URL
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/login', component: () => import('@/views/login/loginPage.vue') },
    {
      path: '/',
      component: () => import('@/views/layout/LayoutContainer.vue'),
      redirect: '/article/manage',
      children: [
        {
          path: '/article/manage',
          component: () => import('@/views/article/ArticleManage.vue')
        },
        {
          path: '/article/channel',
          component: () => import('@/views/article/ArticleChannel.vue')
        },
        {
          path: '/user/profile',
          component: () => import('@/views/user/UserProfile.vue')
        },
        {
          path: '/user/avatar',
          component: () => import('@/views/user/UserAvatar.vue')
        },
        {
          path: '/user/password',
          component: () => import('@/views/user/UserPassword.vue')
        }
      ]
    }
  ]
})

export default router

6.9.2 登录注册
6.9.2.1 使用element-plus编写模型

(1)结构相关
           el-row表示一行,一行分成24份 
           el-col表示列  
           (1.1) :span="12"  代表在一行中,占12份 (50%)
           (1.2) :span="6"   表示在一行中,占6份  (25%)
           (1.3) :offset="3" 代表在一行中,左侧margin份数
                       el-form 整个表单组件
                       el-form-item 表单的一行 (一个表单域)
                       el-input 表单元素(输入框)

(2)校验相关
            (2.1) el-form => :model="ruleForm" 绑定的整个form的数据对象 { xxx, xxx, xxx }
            (2.2) el-form => :rules="rules" 绑定的整个rules规则对象  { xxx, xxx, xxx }
            (2.3) 表单元素 => v-model="ruleForm.xxx" 给表单元素,绑定form的子属性
            (2.4) el-form-item => prop配置生效的是哪个校验规则 (和rules中的字段要对应)

6.9.2.2 script编写规则

(1)整个表单的校验规则
        (3.1)非空校验 required: true      message消息提示,  trigger触发校验的时机 blur change
        (3.2)长度校验 min:xx, max: xx
        (3.3)正则校验 pattern: 正则规则    \S 非空字符
        (3.4)自定义校验 => 自己写逻辑校验 (校验函数)

(2) validator: (rule, value, callback)
          (4.1) rule  当前校验规则相关的信息
          (4.2) value 所校验的表单元素目前的表单值
          (4.3) callback 无论成功还是失败,都需要 callback 回调
                 - callback() 校验成功
                 - callback(new Error(错误信息)) 校验失败

6.9.2.3 注册功能

封装注册api,进行注册,注册成功切换到登录

(1)新建 api/user.js 封装

import request from '@/utils/request'

export const userRegisterService = ({ username, password, repassword }) =>
  request.post('/api/reg', { username, password, repassword })

(2)页面中调用

const register = async () => {
  await form.value.validate()
  await userRegisterService(formModel.value)
  ElMessage.success('注册成功')
  // 切换到登录
  isRegister.value = false
}

(3)eslintrc 中声明全局变量名,  解决 ElMessage 报错问题

module.exports = {
  ...
  globals: {
    ElMessage: 'readonly',
    ElMessageBox: 'readonly',
    ElLoading: 'readonly'
  }
}
6.9.2.4 登录前的预校验

(1)登录请求之前,需要对用户的输入内容,进行校验,校验通过才发送请求。

(2)封装登录API,点击按钮发送登录请求,登录成功存储token,存入pinia 和 持久化本地storage,跳转到首页,给提示

...
import { useUserStore } from '@/stores/'
import { useRouter } from 'vue-router'
...

...
const userStore = useUserStore()
const router = useRouter()
...
6.9.3 登录访问拦截

只有登录页,可以未授权的时候访问,其他所有页面,都需要先登录再访问

// 登录访问拦截 => 默认是直接放行的
// 根据返回值决定,是放行还是拦截
// 返回值:
// 1. undefined / true 直接放行
// 2. false 拦回from的地址页面
// 3. 具体路径或路径对象,拦截到对应的地址
// return '/login' 或 {name: 'login'}
router.beforeEach((to) => {
  const userStore = useUserStore()
  if (!userStore.token && to.path !== '/login') return '/login'
})
6.9.4 用户基本信息获取&渲染

(1)`api/user.js`封装接口

export const userGetInfoService = () => request.get('/my/userinfo')

(2)stores/modules/user.js 定义数据

const user = ref({})
const getUser = async () => {
  const res = await userGetInfoService() // 请求获取数据
  user.value = res.data.data
}

 (3)`layout/LayoutContainer`页面中调用

import { useUserStore } from '@/stores'
const userStore = useUserStore()
onMounted(() => {
  userStore.getUser()
})
6.9.5 退出功能 [element-plus 确认框]
<!-- 注册点击事件 -->
<el-dropdown placement="bottom-end" @command="onCommand">

<el-dropdown-menu>
  <el-dropdown-item command="profile" :icon="User">基本资料</el-dropdown-item>
  <el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
  <el-dropdown-item command="password" :icon="EditPen">重置密码</el-dropdown-item>
  <el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
</el-dropdown-menu>
// 添加退出功能
const onCommand = async (command) => {
  if (command === 'logout') {
    await ElMessageBox.confirm('你确认退出大事件吗?', '温馨提示', {
      type: 'warning',
      confirmButtonText: '确认',
      cancelButtonText: '取消'
    })
    userStore.removeToken()
    userStore.setUser({})
    router.push(`/login`)
  } else {
    router.push(`/user/${command}`)
  }
}
// pinia  user.js 模块 提供 setUser 方法
const setUser = (obj) => {user.value = obj}

三.项目练手

1、vue3知识回顾

1.1、setup选项的手法和执行时机

(1)setup选项的执行时机?beforeCreate钩子之前 自动执行
(2)setup写代码的特点是什么?定义数据 +函数 然后以对象方式return
(3)<script setup>解决了什么问题?经过语法糖的封装更简单的使用组合式API
(4)setup中的this还指向组件实例吗?指向undefined

1.2 reactive 和 ref函数

reactive和ref有什么区别_ref和reactive的区别-CSDN博客

(1)reactive():作用: 接受对象类型数据的参数传入并返回一个响应式的对象

(2)ref():作用:接收简单类型或者对象类型的数据传入并返回一个响应式的对象

①reactive和ref函数的共同作用是什么?用函数调用的方式生成响应式数据
② reactive vs ref ?
1.reactive不能处理简单类型的数据
2.ref参数类型支持更好但是必须通过.value访问修改
3.ref函数的内部实现依赖于reactive函数
③在实际工作中推荐使用哪个?推荐使用ref函数,更加灵活,小免鲜项目主用ref

1.3 计算属性

最佳实践
(1)计算属性中不应该有“副作用。比如异步请求/修改dom
(2)避免直接修改计算属性的值。计算属性应该是只读的

1.4 watch

(1)作为watch函数的第一个参数,ref对象需要添加.value吗?不需要,watch会自动读取
(2)watch只能侦听单个数据吗?单个或者多个
(3)不开启deep,直接修改嵌套属性能触发回调吗?不能,默认是浅层侦听
(4)不开启deep,想在某个层次比较深的属性变化时执行回调怎么做?可以把第一个参数写成函数的写法,返回要监听的具体属性

1.5 生命周期函数

生命周期函数是可以执行多次的,多次执行时传入的回调会在时机成熟时依次执行

(1)组合式API中生命周期函数的格式是什么?on + 生命周期名字
(2)组合式API中可以使用onCreated吗?没有这个钩子函数,直接写到setup中
(3)组合式API中组件卸载完毕时执行哪个函数?onUnmounted

1.6父子组件通信

1.6.1 组合式API下的父传子和子传父

父传子

子传父

父传子
(1)父传子的过程中通过什么方式接收props?defineProps({ 属性名: 类型 })
(2)setup语法糖中如何使用父组件传过来的数据?const props = defineProps({属性名:类型])
子传父
(1)子传父的过程中通过什么方式得到emit方法?defineEmits([事件名称])

1.6.2 defineExpose()

默认情况下在<script setup>语法糖下组件内部的属性和方法是不开放给父组件访问的,可以通过defineExpose编译宏指定哪些属性和方法允许访问

(1)获取模板引用的时机是什么?组件挂载完毕
(2)defineExpose编译宏的作用是什么?显式暴露组件内部的属性和方法

1.7 跨层组件通信

顶层组件向任意的底层组件传递数据和方法,实现跨层组件通信:顶层组件通过provide函数提供数据,底层组件通过inject函数获取数据

1.7.1 传递数据

顶层组件:provide('app-key',ref对象);底层组件:const message = inject('app-key')

1.7.2 传递方法

顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件中的数据

顶层组件:provide('app-key',方法名);底层组件:const message = inject('app-key')

(1)provide和inject的作用是什么?跨层组件通信
(2)如何在传递的过程中保持数据响应式?第二个参数传递ref对象
(3)底层组件想要通知顶层组件做修改,如何做?传递方法,底层组件调用方法
(4)一颗组件树中只有一个顶层或底层组件吗?相对概念,存在多个顶层和底层的关系

1.8 Pinia

Pinia 是 Vue 的专属的最新状态管理库 ,是 Vuex 状态管理工具的替代品。

(1)提供更加简单的API (去掉了 mutation );(2)提供符合组合式风格的API (和 Vue3 新语法统一);(3)去掉了 modules 的概念,每一个 store 都是一个独立的模块;(4)搭配 TypeScript 一起使用提供可靠的类型推断;

1.8.1 使用
(1)定义store 和 组件使用

(2)getters实现和action异步

Pinia中的 getters 直接使用 computed函数 进行模拟

action实现异步和组件定义的数据方法的风格一致

(3)storeToRefs

直接结构赋值会导致响应式丢失,使用storeToRefs函数可以辅助保持数据 (state + getter)的响应式解构=>

(4)总结

1.Pinia是用来做什么的?集中状态管理工具,新一代的vuex
2.Pinia中还需要mutation吗?不需要,action既支持同步也支持异步
3. Pinia如何实现getter?computed计算属性函数
4. Pinia产生的Store如何解构赋值数据保持响应式?storeToRefs

2. 小兔鲜项目

2.1 项目起步

2.1.1 别名路径联想提示
在编写代码的过程中,一旦 输入 @/,VSCode会立刻 联想出SrC下的所有子目录和文件,统一文件路径访问不容易出错。

如何进行配置:

(1)在项目的根目录下新增 jsconfig.json 文件
(2)添加json格式的配置项,用以提示,如下:

// jsconfig.json
{
  "compilerOptions" : {
    "baseUrl" : "./",
    "paths" : {
      "@/*":["src/*"]
    }
  }
}

而实际的路径转化是由vite.config.js完成的

// 实际的路径转换 @ => src
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
2.1.2 组件分类

2.1.2.1 element-plus按需导入

安装 | Element Plus 使用安装命令安装element-plus.

快速开始 | Element Plus 安装unplugin-vue-components 和 unplugin-auto-import这两款插件,并配置vite.config.js

2.1.2.2 主题定制

(1)首先安装sass;

(2)新建一个样式文件,例如 styles/element/index.scss

/* index.scss:只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      // 主色
      'base': #27ba9b,
    ),
    'success': (
      // 成功色
      'base': #1dc779,
    ),
    'warning': (
      // 警告色
      'base': #ffb302,
    ),
    'danger': (
      // 危险色
      'base': #e26237,
    ),
    'error': (
      // 错误色
      'base': #cf4444,
    ),
  )
)

(3)vite.config.js配置

// ...

 Components({
      resolvers: [
        // 1. 配置elementPlus采用sass样式配色系统
        ElementPlusResolver({
          importStyle: 'sass'
        })
      ]
    })

//...
 // 2. 自动导入定制化样式文件进行样式覆盖
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element/index.scss" as *;`
      }
    }
  }

2.1.2.3 axios配置

## 1. 安装:npm i axios

配置基础实例(统一接口配置)>通常包括:

> 1. 实例化 - baseURL + timeout

> 2. 拦截器 - 携带token 401拦截等

## 2. axios文件:

// axios 基础配置
import axios from 'axios'

// 创建axios实例
const httpInstance = axios.create({
  baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
  timeout: 5000
})

// axios请求拦截器
httpInstance.interceptors.request.use(
  config => {
    return config
  },
  e => Promise.reject(e)
)

// axios响应式拦截器
httpInstance.interceptors.response.use(
  res => res.data,
  e => {
    return Promise.reject(e)
  }
)

export default httpInstance

## 3. 封装请求函数并测试

// utils/http.js
import http from '@/utils/http'

export function getCategoryAPI () {
  return http({
    url: 'home/category/head'
  })
}
// main.js里面写测试接口函数
import { getCategory } from './apis/testApi'
getCategory().then(res => {
  console.log(res)
})

## 4. 如果项目里面不同的业务模块需要的接口基地址不同该如何来做?
axios.create() 方法可以执行多次,每次执行就会生成一个新的实例,比如:

2.1.2.4 设计路由

路由设计原则: 找内容切换的区域,如果是页面整体切换,则为一级路由,比如首页是:#/,登录页是:#/login;二级路由设计原则: 找内容切换的区域,如果是在一级路由页的内部切换,则为二级路由。App和带有children的路由都要添加<router-view></router-view>

router/index.js:

// createRouter:创建router实例对象// createHistory:创建你history模式的路由

(1)路由设计的依据是什么?内容切换的方式
(2)默认二级路由如何进行设置?path配置项置空

2.1.2.5 静态资源和erorLen的引入

1. 图片资源和样式资源

资源说明
(1)实际工作中的图片资源通常由 UI设计师 提供,常见的图片格式有png,svg等都是由UI切图交给前端
(2)样式资源通常是指项目初始化的时候进行样式重置,常见的比如开源的 normalize.cSs 或者手写
2. 资源操作
(1)图片资源 - images 文件夹放到 assets 目录下
(2)样式资源- 把 common.scss 文件放到 styles 目录下,并在main.js里面引入。

// 引入初始化样式文件
import '@/styles/common.scss'

2. error lens安装

errorlens是一个实时提供错误警告信息的VScode插件,方便开发

2.1.2.6 scss自动导入

1. 为什么要自动导入
在项目里一些组件共享的色值会以sCS变量的方式统一放到一个名为 var.SCSs 的文件中,正常组件中使用,需要先导入SCSS文件,再使用内部的变量,比较繁琐,自动导入可以免去手动导入的步骤,直接使用内部的变量。

2. 自动导入配置
1.新增一个 var.scss 文件,存入色值变量
2.通过 vite.config.js 配置自动导入文件

 // vite.config.js // 2. 自动导入定制化样式文件进行样式覆盖
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `
          @use "@/styles/element/index.scss" as *;
          @use "@/styles/var.scss" as *;
        `
      }
    }
  }
2.1.2.7 字体图标引入

字体图标采用的是阿里的字体图标库,样式文件已经准备好,在 `index.html`文件中引入即可
```html
  <link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
```

2.2 项目编写

**实现步骤**
1. 封装接口函数;2. 调用接口函数;3. v-for渲染模版;

2.2.1 吸顶导航交互实现

1. 准备组件静态结构;2. 安装VueUse,使用useScroll 获得滚动距离

useIntersectionObserver | VueUse

// vueUse
import { useScroll } from '@vueuse/core'
const { y } = useScroll(window)

3. 根据滚动距离判断当前show类名是否显示,大于78显示,小于78,不显示

<template>
  <div class="app-header-sticky" :class="{ show: y > 78 }">
    <!-- 省略部分代码 -->
  </div>
</template>
2.2.2 pina优化重复请求

由layout页面配置pina,子组件直接调用store

import { useCategoryStore } from '@/stores/category.js'

const categoryStore = useCategoryStore()

onMounted(() => {
  categoryStore.getCategory()
})
2.2.3 Home整体结构搭建和分类实现
2.2.3.1 组件封装

问:组件封装解决了什么问题?
答:(1)复用问题(2)业务维护问题

核心思路:把可复用的结构只写一次,把可能发生变化的部分抽象成组件参数(props / 插槽)

实现步骤
(1)不做任何抽象,准备静态模版
(2)抽象可变的部分:主标题和副标题是纯文本,可以抽象成prop传入;主体内容是复杂的模版,抽象成插槽传入。

2.3.3.2 图片懒加载

只有进入视口区域才发送图片请求
场景:电商网站的首页通常会很长,用户不一定能访问到页面靠下面的图片,这类图片通过懒加载优化手段可以做到。

(1)这里需要用到自定义指令

空指令:

指令逻辑实现:

// main.js
// ...
import { useIntersectionObserver } from '@vueuse/core'
// 定义全局指令
app.directive('img-lazy', {
  mounted(el, binding) {
    // el:指令绑定的那个元素,img
    // binding:binding.value 指令等于号后面绑定的表达式的值,图片url
    const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
      if (isIntersecting) {
        el.src = binding.value
        stop()
      }
    })
  }
})

参考:useIntersectionObserver | VueUse

 (2)代码优化

src/directives/index.js

// 定义懒加载插件
import { useIntersectionObserver } from '@vueuse/core'

export const lazyPlugin = {
  install (app) {
    // 懒加载指令逻辑
    app.directive('img-lazy', {
      mounted (el, binding) {
        // el: 指令绑定的那个元素 img
        // binding: binding.value  指令等于号后面绑定的表达式的值  图片url
        // console.log(el, binding.value)
        const { stop } = useIntersectionObserver(
          el,
          ([{ isIntersecting }]) => {
            console.log(isIntersecting)
            if (isIntersecting) {
              // 进入视口区域
              el.src = binding.value
              stop()
            }
          },
        )
      }
    })
  }
}
// main.js
// ...
// 引入懒加载指令插件并且注册
import { lazyPlugin } from '@/directives'
app.use(lazyPlugin)

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值