Vue3+TS体验并开发+Vite浅析

本文介绍了作者在学习Vue3+TS与Vite过程中的理解和应用,包括Vue3的setUp、Ref、Reactive、toRefs、生命周期变化、新特性如Teleport和Suspense的使用。此外,还简述了Vite的基本概念、安装使用、工作原理及简单实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

Vue3已经出了一段时间了,本人也一直在学习,但楼主是学生,最近一直在做一些微信小程序和可视化的一些东西,所以更新的有点慢。请大家理解一下。

本文主要讲述一下自己在学习Vue3 + Ts + vite过程中,自己的理解即使用方法

Ts

个人的Ts不能说学的很好,只能说会一部分基础。所以在这里就不 误导大家了,本文会在下面Vue3中进行简单的使用。

如果想深入学习。推荐下面几本书

推荐一本书TypeScript 入门教程,这个是放在Github(可能需要科学上网)。(推荐优先阅读这本书,个人感觉比较通俗易懂)

第二本书就是 官方手册(也可能需要科学上网),相对于第一本我觉得比较难理解,所以首推第一本

Vue3基本环境及语法

配置 vue3 开发环境

// 安装或者升级
npm install -g @vue/cli
yarn global add @vue/cli


// 全局升级vueCli
npm update -g @vue/cli
// 或者
yarn global upgrade --latest @vue/cli



// 保证 vue cli 版本在 4.5.0 以上
vue --version

// 创建项目
vue create my-project

// 或者使用图形化界面创建
vue ui

CSS预处理器推荐

推荐使用dart-sass,(深受node-sass的毒害)。这也是官方所推荐的

有关命令行的步骤。主要是一些配置,大家可以参考一些平常自己的使用。个人推荐Vue ui创建。可能是好看吧

template的变化

Vue2中,每个template节点只能有一个根节点。

而在Vue3中,可以有多个根节点

举例说明

//vue2
<template>
    <div>
        
    </div>
</template>


// vue3
<template>
  <div>
    
  </div>
  <router-view/>
</template>

新增语法

setUp

这个就是代替了原来的data函数,可以接收两个参数,props,context(没有用到的时候可以省略不写)。且只执行一次

示例代码
<template>
  <div>
    <h1>
      num:{{ num }}
    </h1>
  </div>
  <button @click="add">加1</button>
</template>

<script lang="ts">
export default {
  name: 'App',
  setup () {
    let num = 0
    const add = () => {
      num++
    }
    console.log(num)
    return {
      add, num
    }
  }
}
</script>

这就是一个基本的setup的过程,可能大家在这里还看不出来,和Vue2具体有哪些区别,别急。可以试着点击一下button按钮,发现num并没有发生改变。看一下控制台(F12),发现输出的是一个number的值。但是我们点击并没有发生改变。所以接下来,我们将用到第一个 新的API Ref

Ref

首先,我们改造一下上面的代码

<template>
  <div>
    <h1>
      msg:{{ msg }}
    </h1>
    <h1>
      num:{{ num }}
    </h1>
  </div>
  <button @click="add">加1</button>
</template>

<script lang="ts">
import { ref } from 'vue'

export default {
  name: 'App',
  setup () {
    const msg = ref(0)
    let num = 0
    const add = () => {
      msg.value++
      num++
    }
    console.log(msg)
    console.log(num)
    return {
      msg, add, num
    }
  }
}
</script>

首先,先看引用形式,可以看到这里是 按需导入,好处我也不就一一叙述, 我们直接看 控制台

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H6ZDHj1K-1610456412702)(https://upload.cc/i1/2020/10/15/LpIr9k.png)]

一个是 RefImpl 一个是数值 0。点击按钮发现,msg可以改变,而num不行。

RefImpl是什么呢?我把它理解为 代理对象。就比如我们知道Vue2data的数据是通过Object.defineProperty()来进行拦截。从而达到 数据响应式 的目的。而Vue3是利用了ES6中的proxy,相对于Object.defineProperty()来说,能拦截的方式更多。功能也更加强大。

所以,大家知道为什么msg能改变,num不能改变了吗?因为msgproxy 进行了代理。而num并没有。所以才会造成了上面的局面

解释Ref

ref是一个函数,它接受一个参数,返回的就是一个响应式对象

例子中,我们初始化的这个 0 作为参数包裹到这个对象中去,在未来操作这个值的时候,可以检测到改变并作出对应的相应。

如果说我想创建一个对象呢?这时候就不能时使用这个函数了。就要使用接下来这个函数Reactive

Reactive

创建一个 JavaScript对象 反应式状态,就要使用reactive方法:

示例代码

<template>
  <div>
    {{ data }}
    <h1>
      count:{{ data.count }}
    </h1>
    <h1>
      double:{{ data.double }}
    </h1>
  </div>
  <button @click="data.increase">1</button>
</template>

<script lang="ts">
import { reactive, computed } from 'vue'

export default {
  name: 'App',
  setup () {
    const data = reactive({
      count: 0,
      increase: () => {
        data.count++
      },
      double: computed(() => data.count * 2)
    })
    return {
      data
    }
  }
}
</script>

这时候呢,我们点击按钮就可以改变状态了。

computed计算属性,给Vue2差不多。学过Vue2的同学可以理解。如果不理解,请看后续有专门介绍

这时候可能回有人嫌比较麻烦,为什么不直接{{count}}这样呢。就想到了Es6中的 解构

所以代码换成了如下

<template>
  <div>
    <h1>
      count:{{ count }}
    </h1>
    <h1>
      double:{{ double }}
    </h1>
  </div>
  <button @click="increase">加1</button>
</template>

<script lang="ts">
import { reactive, computed } from 'vue'

export default {
  name: 'App',
  setup () {
    const data = reactive({
      count: 0,
      increase: () => {
        data.count++
      },
      double: computed(() => data.count * 2)
    })
    return {
      ...data
    }
  }
}
</script>

这时候大家会发现一个问题。为什么点击按钮不改变状态了呢?

这是因为,解构会破坏代理,把他编程一个普通值。就跟上面的Ref的例子一样,所以点击按钮并没有发生变化

这时候,就要请出 另外一个新加的api了,toRefs

toRefs

这是引用官网的一句话

toRefs 从组合函数返回反应对象时,此函数很有用,以便使用组件可以对返回的对象进行解构/扩展而不会失去反应性

使用起来 比较简单,就返回的时候加上就可以

<template>
  <div>
    <h1>
      count:{{ count }}
    </h1>
    <h1>
      double:{{ double }}
    </h1>
  </div>
  <button @click="increase">加1</button>
</template>

<script lang="ts">
import { reactive, computed, toRefs } from 'vue'

export default {
  name: 'App',
  setup () {
    const data = reactive({
      count: 0,
      increase: () => {
        data.count++
      },
      double: computed(() => data.count * 2)
    })
    return {
      ...toRefs(data)
    }
  }
}
</script>

生命周期改变

beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

提示(来自官网)

由于setup是围绕beforeCreatecreated生命周期挂钩运行的,因此您无需显式定义它们。换句话说,将在这些挂钩中编写的任何代码都应直接在setup函数中编写。

原来语法

watch

// watch 基本使用   记得引入
watch(data, () => {
  document.title = '更新过后 ' + data.count
})
// watch 的两个参数,代表新的值和旧的值
watch(data, (newValue, oldValue) => {
  console.log('old', oldValue)
  console.log('new', newValue)
  document.title = '更新过后 ' + data.count
})

// watch 多个值,返回的也是多个值的数组
watch([greetings, data], (newValue, oldValue) => {
  console.log('old', oldValue)
  console.log('new', newValue)
  document.title = '更新过后' + greetings.value + data.count
})

// 使用函数 getter写法
watch([greetings, () => data.count], (newValue, oldValue) => {
  console.log('old', oldValue)
  console.log('new', newValue)
  document.title = '更新过后' + greetings.value + data.count
})

特别注意,data是一个对象。要取里面的值

computed

使用方法和vue2一样。可以写在reactive内部,也可写在外部

setup () {
    const data = reactive({
      count: 0,
      increase: () => {
        data.count++
      },
      double: computed(() => data.count * 2)
    })
    const conComputed = computed(() => data.count * 2)
    const number = ref(0)
    watch(data, () => {
      console.log(data)
      document.title = 'updated ' + data.count
    })
    watch(number, () => {
      console.log(number)
    })
    return {
      number,
      conComputed,
      ...toRefs(data)
    }
  }

新增标签

Teleport瞬间移动

Teleport 英文文档地址

平时我们的遮罩层都存在于 某个多级标签下面,这样其实是不合理的。

Teleport出现可以让我们写的组件移动到指定标签下面。

to是要移动到哪个 标签下。支持选择器

示例代码

// model
<template>
  <teleport to="#modal">
    <div id="center">
      <h1>this is a modal</h1>
    </div>
  </teleport>
</template>

<script>
export default {
  name: 'model'
}
</script>

<style scoped lang="scss">
#center {
  width: 200px;
  height: 200px;
  background: red;
  position: fixed;
  left: 50%;
  top: 50%;
  margin-left: -100px;
  margin-top: -100px;
}
</style>


// App.vue
<template>
  <model v-if="show"></model>
  <button @click="show = !show">show</button>
</template>

<script lang="ts">
import { ref } from 'vue'
import model from './components/model.vue'
export default {
  name: 'App',
  components: {
    model
  },
  setup () {
    const show = ref(false)
    return {
      show,
    }
  }
}
</script>

在我们点击了 按钮之后

这就是基本的用法。

Suspense异步请求

这个新增标签是我觉得最实用的一个标签。解决了我们异步请求中,图片返回过慢导致的空白(虽然可以解决。但是没这个方便)

示例代码

// showPic
<template>
  <img :src="result && result.url" alt="">
</template>

<script lang="ts">
import axios from 'axios'
import { defineComponent } from 'vue'
export default defineComponent({
  async setup () {
    const rawData = await axios.get('https://picsum.photos/id/786/200/200')
    console.log(rawData)
    return {
      result: rawData.config
    }
  }
})
</script>
// App
<template>
  <Suspense>
    <template #default>
      <pic-show />
    </template>
    <template #fallback>
      <h1>Loading !...</h1>
    </template>
  </Suspense>
</template>

<script lang="ts">
import ShowPic from './components/ShowPic.vue'
export default {
  name: 'App',
  components: {
    ShowPic
  }
}
</script>

效果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m78Y2Odt-1610456412713)(https://i.loli.net/2020/10/22/f4MhALH36pyIC71.png)]

自定义HOOKS

本部分展示了 关于抽出共用逻辑代码的部分,仅仅是一个例子。 仅仅作为展示如何抽离。

//useMousePosition.ts
import {onMounted, onUnmounted, ref} from "vue";

function useMousePosition() {
    const x = ref(0)
    const y = ref(0)
    const updateMouse = (e: MouseEvent) => {
        x.value = e.pageX
        y.value = e.pageY
    }

    onMounted(() => {
        document.addEventListener('mousemove', updateMouse)
    })

    onUnmounted(() => {
        document.removeEventListener('mousemove', updateMouse)
    })
    return {x, y }
}

export default useMousePosition

// app
import useMousePosition from "@/hooks/useMousePosition";
export default {
    setup(){
        const {x, y} = useMousePosition()
        return {
            x,y
        }
    }
}

结合Ts开发组件

新增语法

defineComponent。创建组件需要用这个包裹。

PropType 类型断言 判断是哪一个类型(Vue2中已经存在)

/**
	定义
*/
// 主要写一下TS  template 和 style 就先不写了  这里展示一个基本的下来框
<script lang="ts">
import {
  defineComponent,
  PropType,
  computed
} from 'vue'

// 把接口类型导出。在使用的过程中导入接口,对接口进行定义
export interface ColumnProps {
  id: number;
  title: string;
  avatar?: string;
  des: string;
}

export default defineComponent({
  name: 'ColumnList',
  props: {
    list: {
      type: Array as PropType<ColumnProps[]>,
      required: true
    }
  },
  setup(props) { //  这里使用到了props
    const ColumnList = computed(() => {
      return props.list.map((item) => {
        if (!item.avatar) {
          item.avatar = require('@/assets/logo.png')
        }
        return item
      })
    })
    return {
      ColumnList
    }
  }
})
</script>


// 使用
<template>
  <div id="container">
    <ColumnList :list="list"></ColumnList>
  </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import ColumnList, { ColumnProps } from '@/components/ColumnList.vue'

const testDate: ColumnProps[] = [
  {
    id: 1,
    title: 'test1',
    des: 'test1',
    avatar: 'https://picsum.photos/id/239/200/200'
  }, {
    id: 2,
    title: 'test1',
    des: 'test1',
    avatar: 'https://picsum.photos/id/239/200/200'
  }, {
    id: 3,
    title: 'test1',
    des: 'test1',
    avatar: 'https://picsum.photos/id/239/200/200'
  }, {
    id: 4,
    title: 'test1',
    des: 'test1',
    avatar: 'https://picsum.photos/id/239/200/200'
  }
]
export default defineComponent({
  name: 'App',
  components: {
    ColumnList,
  },
  setup() {
    return {
      list: testDate,
    }
  }
})
</script>

以上是我在学习和使用过程中有的一些实际体会。如有不对,欢迎各位批评指正

Vite

什么是Vite

github:https://github.com/vitejs/vite

Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup打包。

作者原话: Vite,一个基于浏览器原生 ES Modules 的开发服务器。利用浏览器去解析模块,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。

它主要具有以下特点:

  • 快速的冷启动
  • 即时的模块热更新
  • 真正的按需编译

简单安装和使用

// npm i vite

npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev

安装过后 的文件目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nyViCXNc-1610456412715)(https://i.loli.net/2020/10/24/PA95akNcu4y2xtT.png)]

打开页面为

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fj6BCYJq-1610456412716)(https://i.loli.net/2020/10/24/hsb2NxYjt7qVB6v.png)]

至于集成vue-router等。大家请自行摸索。这里只做一个简单介绍

原理

Vite的实现是个基于浏览器原生支持的 模块功能

script.typemodule 时,通过 srcimport 导入的文件会发送 http 请求。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-52VOWvaW-1610456412716)(https://i.loli.net/2020/10/24/Mp3IwVvuleNmCrJ.png)]

由图可知,通过发送请求,来获取文件,当需要用到的使用,才会过去请求。真正的 按需加载

简单实现

第一步,初始搭建

首先 我先搭建一个基本的框架,在 根目录下创建一个viteText.js文件。 基于Koa进行的实现。如果有不了解的。请访问Koa官网

const fs = require('fs')
const path = require('path')
const Koa = require('koa')


const app = new Koa()

// 这个主要是对请求进行重定向使用的   可以先不管
function rewriteImport(content) {
// 目的是改造.js文件内容, 不是/ ./ ../开头的import,替换成/@modules/开头的
  return content.replace(/ from ['|"]([^'"]+)['|"]/g,function(s0,s1){
    // console.log(s0,s1)
    if(s1[0]!=='.'&&s1[1]!=='/'){
      return ` from '/@modules/${s1}'`
    }else{
      return s0
    }
  })
}

//  这里图了方便 直接使用了中间件。
app.use((ctx, next)=>{
  const { request: { url } } = ctx
  if (url === '/') { // 发现当请求的是根目录是否 返回index 文件
    let content = fs.readFileSync('./index.html', 'utf-8')
    ctx.type = 'text/html'
    ctx.body = content
  }
  next()
})


app.listen(8888, ()=>{
  console.log('http://localhost:8888/')
})

接下来我们启动服务,访问路径。

  1. 我们可以看到。我们成功请求到了localhost

  2. index.html 里面存在的

    <script type="module" src="/src/main.js"></script>没有成功发送了请求。

第二部 集成js

// 在原有代码的基础上 添加    记得一定要写next()方法 否则 不会执行下一个 中间件
app.use((ctx, next)=>{
  const { request: { url } } = ctx
  if (url.endsWith('.js')) {
    console.log(url.slice(1)) // 打印出请求路径
    const p = path.resolve(__dirname, url.slice(1)) // 找到对应文件
    const content = fs.readFileSync(p, 'utf-8') // 读取文件
    ctx.type = 'application/javascript'
    ctx.body = rewriteImport(content) // 吧一些以  路径进行更换
  }
  next()
})

由此 我们可以看到

  1. 但是还有一些红色的。是没有成功得到响应的。也就是main.js里面的一些比如import { createApp } from 'vue'这些。我们没有找到。所以接下里的工作就是要让这些可以找的到

第三步 更改modules路径

大家可以看到 Vue的请求路径是http://localhost:8888/@modules/vue。但是我们文件夹里没有这个 **@**路径。所以我们需要对这个进行一个特殊处理。

app.use((ctx, next)=>{
  const { request: { url } } = ctx
  if (url.startsWith('/@modules/')) {
    const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
    console.log(prefix)  // 打印出来的  是拼接后的路径
    const module = require(prefix + '/package.json').module
    console.log(module) // 获取 vue/module的路径
    const p = path.resolve(prefix, module)
    console.log(p) // 再次拼接读取
    const ret = fs.readFileSync(p, 'utf-8')
    ctx.type = 'application/javascript'
    ctx.body = rewriteImport(ret)
  }
  next()
})

主要注意一点。就是对文件路径的读取加拼接。可能会有点迷。大打印几次。了解每个地方就可以了

这时候。我们就可以请求到 Vue

第四步 解析Vue文件并返回

  • 这里需要使用官方提供的两个模块

    1. @vue/compiler-sfc: 官方的 vue 单文件解析器
    2. @vue/compiler-dom: 经过parser、transform、generate,将虚拟dom渲染成浏览器上的真实dom
    app.use((ctx, next)=>{
      const { request: { url,query } } = ctx
      if (url.indexOf('.vue') > -1) {
        // import xx from 'xx.vue'
        // 1. 单文件组件解析
        console.log(123456)
        const p = path.resolve(__dirname, url.split('?')[0].slice(1))
        // 解析单文佳年组建,需要官方的库
        const { descriptor } = compilerSfc.parse(fs.readFileSync(p, 'utf-8'))
        if (!query.type) {
          // js内容
          ctx.type = 'application/javascript'
          ctx.body = `
    ${ rewriteImport(
              descriptor.script.content.replace('export default ',
                  'const __script = ')) }
    import {render as __render} from "${ url }?type=template"
    __script.render = __render
    export default __script
          `
        } else if (query.type == 'template') {
          // 解析我们的template 编程render函数
          const template = descriptor.template
          const render = compilerDom.compile(template.content, { mode: 'module' }).code
          ctx.type = 'application/javascript'
          ctx.body = rewriteImport(render)
        }
      }
      next()
    })
    

这时候大家就会发现。APP.vue已经可以识别并返回了

但是。缺没有出效果。这是为什么呢?

经过一番百度之后。查出原来是缺少一个process对象。这是在node·才有的。浏览器端没有。所以。我们接改写一下 第一步

app.use((ctx, next)=>{
  const { request: { url } } = ctx
  if (url === '/') {
    let content = fs.readFileSync('./index.html', 'utf-8')
    content = content.replace('<script', `
      <script>
        window.process = {
          env: {NODE_EV:'dev'}
        }
      </script>
      <script
    `)
    ctx.type = 'text/html'
    ctx.body = content
  }
  next()
})

这样。我们给了浏览器一个process属性。就可以识别了

最后一步。解析CSS

跟解析js差不多

app.use((ctx, next)=>{
  const { request: { url } } = ctx
  if (url.endsWith('.css')) {
    const p = path.resolve(__dirname, url.slice(1))
    const file = fs.readFileSync(p, 'utf-8')
    const content = `
      const css = "${ file.replace(/\n/g, '') }"
      const link = document.createElement('style')
      link.setAttribute('type', 'text/css')
      document.head.appendChild(link)
      link.innerHTML = css
      export default css
    `
    ctx.type = 'application/javascript'
    ctx.body = content
  }
  next()
})

然后我们就识别了所有文件

总结

如果想加其他的文件。只需要按照这个方式,做对应的方式加载就行

发现的问题 未解决

  1. 当我们初步搭建完成后。先解析CSScss是不生效的,但引入了Vue官方的这两个库,就起了效果。

    如果有能帮忙解答疑惑的。不胜感激

总结

本篇主要总结了。

  1. Vue3的新增语法及其使用
  2. Vue + TS 开发的一些小例子
  3. Vite 的了解及简单模仿
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值