前端框架:vite2+vue3+typescript+axios+vant移动端 框架实战项目详解(三)

2,029 阅读4分钟

☞本系列文章过长会分开消化理解:

☞知识点:vue3

v3.cn.vuejs.org/guide/intro…

Vite 最初是作为 Vue.js 开发工具的未来基础而创建的。尽管 Vite 2.0 版本完全不依赖于任何框架,但官方 Vue 插件仍然对 Vue 的单文件组件格式提供了第一优先级的支持,涵盖了所有高级特性,如模板资源引用解析、<script setup><style module>,自定义块等等。除此之外,Vite 还对 Vue 单文件组件提供了细粒度的 HMR。举个例子,更新一个单文件组件的 <template><style> 会执行不重置其状态的热更新。

vue3 setup 语法糖和避坑事项

v3.vuejs.org/api/sfc-scr…

❓script setup是什么呢?

script setup是vue3中新引入的语法糖,是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖,目的是简化使用Composition API时冗长的模板代码。可以更好的运行时性能(其模板回被编成与其同一作用域的渲染函数,没有任何的中间代理)

例如之前我们不用语法糖时写的:

<script lang="ts">
import { defineComponent, ref } from 'vue'
import { HelloWord } from '@/components'
export default defineComponent({
​
  setup() {
    const count = ref(0)
    const inc = () => count.value++
 
    return {
      count,
      inc,
    }
  },
})
​
</script>

使用script setup语法糖重构后,将变成

<script setup lang="ts">
import { defineComponent, ref } from 'vue'
import { HelloWord } from '@/components'
 
defineProps<{
    name:string
}>()
​
const count = ref(0)
const inc = () => count.value++
 
</script>

script setup语法糖重构后的代码变得可读性更强,优雅了🤭🤭🤭

基本用法

若要使用script setup语法,只需在原vue文件中的script标签加入setup属性。

<script setup lang="ts"></script>

setup执行的时机

  • 在beforeCreate之前执行(一次), 此时组件对象还没有创建
  • this是undefined, 不能通过this来访问data/computed/methods / props,可以在vue模块中导入
  • 其实所有的composition API相关回调函数中也都不可以

使用setup中的参数

<script setup="props, context" lang="ts">
 
</script>

同时也支持解构语法

<script setup="props, { emit }" lang="ts">
 
</script>

暴露变量到模板

如果想在template中使用setup中这个变量

不适用语法糖的需要return , 例如上边的案例

 return {
      count,
      inc,
    }

语法糖中不需要,只要简单声明就好

使用defineProps 、 defineEmits、defineExpose

1、通过defineProps指定当前props类型的同时,获得上下文的props对象 在script中需要props[key]引用,而template中可直接调用key

2、通过defineEmits指定当前组件含有的触发事件 事件通过 defineEmits 返回的上下文 emit 进行触发

3、defineExpose组件暴露出自己的属性,在父组件中可以拿到。

<script setup>
  const props = defineProps({
    foo: String,
  })
  const emit = defineEmits(['change', 'goBack'])
  
  const name = "lee"
  //暴露出去的变量
  defineExpose({
    name
  })
</script>

await 语法支持

在script setup内可以直接使用await语法:

<script setup>
    const data = await fetch(`/api/getUsers`).then((r) => r.json())
</script>

如何获取 slots 和 attrs

使用useContext 获取上下文, useContext 得到的是setup的上下文

<script setup>
  import { useContext } from 'vue'
​
  const { slots, attrs } = useContext()
</script>

😈注意避坑:

  • 1、在setup中不能使用this。为什么呢?

    setup 中应该避免使用this,因为他不会找到组件的示例,这一点与vue2区别很大。setup 的调用发生在 data property\computed\methods 被解析之前,所以它们无法在 setup 中被获取

  • 2、script setup语法糖 defineProps、defineEmits、defineExpose 不需要引入,vue3单组件内部自动暴露的API

  • 3、setup 中 直接导入组件就可以使用,不用像vue2中还需要注册组件

vue tsx 支持

.jsx.tsx 文件同样开箱即用。Vue 用户应使用官方提供的 @vitejs/plugin-vue-jsx 插件,它提供了 Vue 3 特性的支持,包括 HMR,全局组件解析,指令和插槽。

✔安装

npm i @vitejs/plugin-vue-jsx -D

✔在vite.config.ts中引入,使用 tsx支持插件

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx"
import { resolve } from "path";
import styleImport, { VantResolve } from 'vite-plugin-style-import';
import { viteMockServe } from "vite-plugin-mock";
​
​
export default defineConfig({
  resolve: {
    alias: {
      "@": resolve(__dirname, "src"),
      comps: resolve(__dirname, "src/components"),
      apis: resolve(__dirname, "src/apis"),
      views: resolve(__dirname, "src/views"),
      utils: resolve(__dirname, "src/utils"),
      routes: resolve(__dirname, "src/routes"),
      styles: resolve(__dirname, "src/styles"),
    },
  },
​
  plugins: [
    vue(),
    vueJsx(),
    styleImport({
      resolves: [VantResolve()],
    }),
    viteMockServe({
      mockPath: "mock", // ↓解析根目录下的mock文件夹
      supportTs: false, // 打开后,可以读取 ts 文件模块。 请注意,打开后将无法监视.js 文件。
    }),
  ],
  server: {
    host: "0.0.0.0", // 解决  Network: use --host to expose
    // port: 4000, //启动端口
    open: true,
    // proxy: {
    //   "^/api": {
    //     target: "https://baidu.com",
    //     changeOrigin: true,
    //     ws: true,
    //     rewrite: pathStr => pathStr.replace(/^/api/, ""),
    //   },
    // },
    // cors: true,
  },
​
  /**
   * 在生产中服务时的基本公共路径。
   * @default '/'
   */
  base: "./",
});
​

✔tsx 组件:Comp.tsx

创建文件名后缀.tsx

示例:

第一步:src\components\comp\index.module.scss

.helloWorld {
    h1 {
        text-shadow: 2px 4px #ccc;
    }
    button {
        border: none;
        outline: none;
        padding: 5px 20px;
        background: #8bc34a;
        border-radius: 5px;
        border: 2px solid #a5a4a4;
        cursor: pointer;
    }
}

第二步:src\components\comp\Comp.tsx

import { ref, defineComponent } from "vue";
import classes from "./index.module.scss";
​
export default defineComponent({
  name: "Comp",
  props: {
    msg: {
      type: String,
      required: true,
    },
  },
  setup: props => {
    const count = ref(0);
    return () => (
      <div class={classes.helloWorld}>
        <h1>{props.msg}</h1>
        <button
          onClick={() => {
            count.value++;
          }}
        >
         tsx 组件 count is: {count.value}
        </button>
      </div>
    );
  },
});
​

第三步在相关组件中引入

<script setup lang="ts">
import Comp from './comp/Comp'
...
</script><template>
  <h1>{{ showCount }}</h1>
  <button type="button" @click="addBtn">加1</button>
  <van-button type="success" >主要按钮</van-button>
  <Comp msg="HelloWord组件传值Comp"> </Comp>
</template><style scoped>
</style>

image.png

😈注意避坑:

1、所有vue事件不适用:如 .self .stop .prevent 等均失效

2、v-for 不适用:需要使用map遍历替代v-for

3、v-if 不适用:需要使用三元操作符来替代 v-if

Vue3的响应式

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Proxy 与 Reflect</title>
</head>
<body>
  <script>
    
    const user = {
      name: "John",
      age: 12
    };
​
    /* 
    proxyUser是代理对象, user是被代理对象
    后面所有的操作都是通过代理对象来操作被代理对象内部属性
    */
    const proxyUser = new Proxy(user, {
​
      get(target, prop) {
        console.log('劫持get()', prop)
        return Reflect.get(target, prop)
      },
​
      set(target, prop, val) {
        console.log('劫持set()', prop, val)
        return Reflect.set(target, prop, val); // (2)
      },
​
      deleteProperty (target, prop) {
        console.log('劫持delete属性', prop)
        return Reflect.deleteProperty(target, prop)
      }
    });
    // 读取属性值
    console.log(proxyUser===user)
    console.log(proxyUser.name, proxyUser.age)
    // 设置属性值
    proxyUser.name = 'bob'
    proxyUser.age = 13
    console.log(user)
    // 添加属性
    proxyUser.sex = '男'
    console.log(user)
    // 删除属性
    delete proxyUser.sex
    console.log(user)
  </script>
</body>
</html>

自定义hook函数

  • 使用Vue3的组合API封装的可复用的功能函数
  • 自定义hook的作用类似于vue2中的mixin技术
  • 自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂

示例1

src\hooks/useMousePosition.ts

import { ref, onMounted, onUnmounted } from 'vue'
/* 
收集用户鼠标点击的页面坐标
*/
export default function useMousePosition () {
  // 初始化坐标数据
  const x = ref(-1)
  const y = ref(-1)
​
  // 用于收集点击事件坐标的函数
  const updatePosition = (e: MouseEvent) => {
    x.value = e.pageX
    y.value = e.pageY
  }
​
  // 挂载后绑定点击监听
  onMounted(() => {
    document.addEventListener('click', updatePosition)
  })
​
  // 卸载前解绑点击监听
  onUnmounted(() => {
    document.removeEventListener('click', updatePosition)
  })
​
  return {x, y}
}

在组件中引入

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useStore } from '@/store/index';
​
import http from '../utils/server'
import Comp from './comp/Comp'
import useMousePosition from '@/hooks/useMousePosition'
const store = useStore()
​
const count = ref(0)
const showCount = computed(() => {
  return store.getters['getCount']
})
const {x, y} = useMousePosition()
​
const addBtn = () => {
  store.commit('setCount', ++count.value)
}
​
// fetch("/api/getUsers")
//   .then( response => {
//     return  response.json()
//   })
//   .then(data => {
//     console.log('/api/getUsers',data)
//   })
  //  try {
  //    let data = await http.get({url:"/getUsers"})
  //    console.log(data,'data')
  //  } catch (error) {
  //    console.log(error,'error')
  //  }
  
  
</script><template>
  <h1>{{ showCount }}</h1>
  <div class="show-mouse-box">使用/hooks/useMousePosition: x: {{x}}, y: {{y}}</div>
  <br>
  <button type="button" @click="addBtn">加1</button>
  <van-button type="success" >主要按钮</van-button>
  <Comp msg="HelloWord组件传值Comp"> </Comp>
</template><style scoped>
.show-mouse-box{
  font-size: 14px;
}
</style>

image.png

点击时得到不同的x,y鼠标的坐标值

ref、toRefs、reactive

ref

  • 作用: 定义一个数据的响应式

  • 语法: const xxx = ref(initValue):

    • 创建一个包含响应式数据的引用(reference)对象
    • js中操作数据: xxx.value
    • 模板中操作数据: 不需要.value
  • 一般用来定义一个基本类型的响应式数据

ref获取元素

利用ref函数获取组件中的标签元素

示例

<script setup lang="ts">
import { onMounted, ref } from 'vue'
/* 
ref获取元素: 利用ref函数获取组件中的标签元素
功能需求: 让输入框自动获取焦点
*/
​
    const inputRef = ref<HTMLElement|null>(null)
​
    onMounted(() => {
      inputRef.value && inputRef.value.focus()
    })
}
</script>
<template>
  <h2>App</h2>
  <input type="text">---
  <input type="text" ref="inputRef">
</template>

toRefs

把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref

应用: 当从合成函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用

问答时间

🙋‍问: reactive 对象取出的所有属性值都是非响应式的

答: 利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性

reactive

  • 作用: 定义多个数据的响应式
  • const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
  • 响应式转换是“深层的”:会影响对象内部所有嵌套的属性
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的

☞知识点:typescript

因为typescript知识点太多了,没办法写那么多,篇幅已经很长了,后续在出一个专门的typescript的详细讲解。这边主要是了解。

TypeScript 的特点

TypeScript 主要有 3 大特点:

  • 始于JavaScript,归于JavaScript

TypeScript 可以编译出纯净、 简洁的 JavaScript 代码,并且可以运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的JavaScript 引擎中。

  • 强大的类型系统

类型系统允许 JavaScript 开发者在开发 JavaScript 应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构。

  • 先进的 JavaScript

TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。

TypeScript整合

如果我们在前面安装框架的时候 没有选用vue-ts版本,我们想改成ts版本

✔安装

npm install -g typescript

安装完成后,在控制台运行如下命令,检查安装是否成功:

tsc -V 

vite中也可以可直接导入.ts 文件,在SFC中通过<script lang="ts">使用

范例:使用ts创建一个组件

<script lang="ts">
import { defineComponent } from 'vue'interface Course {
  id: number;
  name: string;
}
  
export default defineComponent({
  setup() {
    const state = ref<Course[]>([]);
    setTimeout(() => {
      state.value.push({ id: 1, name: "全栈架构师" });
    }, 1000);
  },
});
</script>
​

ts参考配置,tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
    "baseUrl": ".",
    "paths": {
      "@/*":["src/*"],
      "utils": ["src/utils/*"] ,
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "mock/test.ts"],
  "exclude": ["node_modules"]
}