含泪重新梳理vue3造轮子项目

204 阅读3分钟

vue3 于 2020.9.18 正式发布,2022.2.7 成为新的默认版本

项目能干啥

1. Vue2 过渡到 Vue3

两者90% 的写法完全一致,区别为:

1. Vue3的 template 支持多个根标签,vue2 不支持
2. Vue3的 主函数为 createApp(),而Vue2 的是 new Vue()
3. createApp(组件) 接收的是组件,而new Vue{template, render} 接收的是对象
4. Vue3采用组合式API,而Vue2是选项式API,在对应的属性中写对应的功能模块

组合式API 是组件的另外一个选项;是启动页面后自动执行的函数,位于 createdbeforCreated 钩子之前,取代了这两个钩子。在setup()函数中定义的变量和方法,要return出去,才可使用。setup可直接写在<script>标签中,称为setup语法糖。

2. JS 过渡到 TS

3. 实现 Antd / Element / iView 这样的 UI 框架

项目技术栈

Vue3 / TS / VueRouter / Vite构建工具 / Volar

vetur相同,volar是一个针对vuevscode插件。安装 Volar 后,注意禁用 vetur

环境搭建 windows

  • 安装Node.js稳定版、npm下载加速
npm config set registry https://registry.npmmirror.com/
npm config get registry  // 查看源地址

环境搭建 Mac

  • 卸载掉 Node.js,若其不是通过homebrew安装的
  • 安装 Homebrew
  • 安装 Node.js 终端运行brew install node
  • npm下载加速、安装yarn、安装编辑器 同windows

使用 Vite 搭建官网

yarn global add create-vite-app@1.18.0 
cva gulu-ui-1
cd gulu-ui-1
yarn   // 相当于 yarn install
yarn dev   

image.png

初始化项目

<!-- 项目首页 -->
<!DOCTYPE html>
...
<body>
  <!-- 容器 -->
  <div id="app"></div>  
  <script type="module" src="/src/main.js"></script>
</body>
// src/main.js
import {createApp} from 'vue'  // 用于创建app实例的主函数 vue2 是new Vue()
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')  // createApp接受App组件并将其挂载到div id=app上
// app.vue
<template>
  <img alt="Vue logo" src="./xx" />
  <HelloWorld msg="Hello" />
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
  name: 'App',
  components:{
    HelloWorld:HelloWorld  // 标签名:组件引入 ,可简写为 HelloWorld
  ]
}
</script>
  • 写一个组件
// Frank.vue
// vue 3 Snippets 插件可以快速模板输入代码
<template>
  <div>我的第一个组件</div> 
</template>
// App.vue 改造一下
<template>
  <div>hi</div>
  <Frank />
</template>

<script>
import Frank from './components/Frank.vue'
export default {
  name: 'App',
  components:{
    Frank  // 标签名:组件引入
  }
}
</script>

image.png

安装并初始化 vue-router

  • 用于页面切换
npm info vue-router versions  // 查看版本号
yarn add vue-router@4.0.0-beta.3
  • 初始化 vue-router
// 创建 History 对象 js文件重命名为 ts 将index.html 也改为 main.ts
// main.ts
import {createWebHashHistory, createRouter} from 'vue-router'
import Frank from './components/Frank.vue'
const history = createWebHashHistory()
const router = createRouter({
  history:history,
  routes:[
    {path:'/', component:Frank}  // 此处易错写为 components
  ]
})

const app = createApp(App)
app.use(router)
app.mount('#app')
// src/shims-vue.d.ts
// 解决TS理解.vue文件的问题 
declare module '*.vue'{
  import {ComponentOptions} from 'vue'
  const componentOptions:ComponentOptions
  export default componentOptions
}
  • 指定展示组件的位置:
//App.vue
<template>
  <div>hi</div>
  <router-view />  // 替换掉 <Frank /> 展示的内容见路由表
</template>

<script>
export default {
  name: 'App',
}
</script>
  • 再添加一个组件
// Frank2.vue
<template>
  Frank2
</template>
  • 添加 Frank2 组件的路由表
// main.ts
import Frank2 from './components/Frank2.vue'
routes:[
    {path:'/', component:Frank},
    {path:'/xxx', component:Frank2}
  ]

image.png

  • 添加 router-link 实现在页面直接跳转
// App.vue
<template>
  <div> 导航栏 | 
  <router-link to="/">Frank</router-link> |
  <router-link to="/xxx">Frank2</router-link>
  <hr/>
  <router-view />
  <div>
</template>
<script>
export default {
  name: 'App',
}
</script>

image.png

写官网首页

// src/views/Home.vue
...
<style lang="scss" scoped>
</style>
// 解决找不到 sass 模块
yarn add -D sass@1.26.10
  • 封装 Topnav 导航栏
// Topnav.vue
<template>
  <div class="topnav">
    <div class="logo">LOGO</div>
    <ul class="menu">
      <li>菜单1</li>
      <li>菜单2</li>
    </ul>
  </div>
</template>
<script lang="ts">
export default {
}
</script>
  • 在首页中引入 Topnav 组件
// src/views/Home.vue
...
<template>
  <div>
    <Topnav/>
  </div>
</template>

<script lang="ts">
import Topnav from '../components/Topnav.vue'
export default {
  components:{Topnav}
}
</script>

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

用依赖provide注入inject实现组件间数据通信

vue提供的provideinject这两个api,实现点击切换aside,点击一次LOGO显示组件列表,再点一次LOGO隐藏。具体的实现过程是,用provide标记menuVisible变量是可以被所有后代组件访问的,在后代组件中用inject可以访问到menuVisible变量。

// 01 在App中放入一个 menuVisible 的变量
const menuVisible = ref(false)  // ref是个盒子,值为false
// 02 把 menuVisible 变量标记为所有后代都可以使用
provide(’xxx‘, menuVisible)
// 03 子组件获取 menuVisible 变量 
const menuVisible = inject<Ref<boolean>>('xxx')
// 04 当用户点击LOGO时,切换显示隐藏列表
<div class="logo" @click="toggleMenu">LOGO</div>
const toggleMenu = ()=>{
      // ref类型的变量只是容器,取值需加.value属性
      menuVisible.value = !menuVisible  // 取反后赋值给变量
    }
    return {toggleMenu}
// 05 显示的区域随着menuVisible的值变化
<aside v-if="menuVisible"> 

image.png

更多setup函数的介绍,详见我的另一篇博文:熬夜梳理一下Vue 3 中setup函数及周边

  • provide标记变量
// App.vue
<template>
  <router-view />
</template>
<script lang="ts">
import {ref, provide} from vue  
export default {
  name: 'App',
// setup 函数是处于 生命周期函数 beforeCreate 和 Created 两个钩子函数之间的函数
// setup 中是无法 使用 data 和 methods 中的数据和方法 是 Composition API(组合API)的入口
  setup(){
    // 安装 Auto Import 插件,可以输入 ref 按Tab键自动引入
    const menuVisible = ref(false)  // false为默认值
    // provide 接收两个参数,第一个是string 
    provide(’xxx‘, menuVisible)  // set
  }
}
</script>
  • inject访问
// Topnav.vue
<script lang="ts">
import {Ref, inject} from vue  
export default {
  name: 'App',
  setup(){
    // 01版
    const menuVisible = inject('xxx')      //get
    // 02版 用 ts 标注类型
    const menuVisible = inject<Ref<boolean>>('xxx')  //get
  }
}
</script>
// Doc.vue
setup(){
  const menuVisible = inject<Ref<boolean>>('xxx') 
  return {menuVisible}
}
  • 添加修改menuVisible变量状态的事件
// Topnav.vue
...
<div class="logo" @click="toggleMenu">LOGO</div>
...
export default {
  name: 'App',
  setup(){
    const menuVisible = inject<Ref<boolean>>('xxx')  
    const toggleMenu = ()=>{
      // ref类型的变量只是容器,取值需加.value属性
      menuVisible.value = !menuVisible  // 取反后赋值给变量
    }
    return {toggleMenu}
  }
}
  • v-if来显示切换结果
// Doc.vue
<Topnav />
<div class="content">
<!-- 当 menuVisible 变化时, aside标签显示内容也随之变化 -->
  <aside v-if="menuVisible"> 
...

手机页面上的切换按钮

@media(max-width:500px){
  > .menu{display:none;}
  > .logo{margin:0 auto;}
}

image.png

// App.vue
setup(){
  // 通过获取页面宽度来决定初始值为ture还是false
  const width = document.documentElement.clientWidth;
  const menuVisible = ref(width <= 500 ? false : true)
}

嵌套路由

点击组件,显示对应文档:

// main.ts
import SwitchDemo from './xxx'
routes:[
    {path:'/', component:Home},
    {path:'/doc', component:Doc, children:[
      {path:'switch', component:SwitchDemo},
    ]}
  ]

指定路由页面显示区域:

// Doc.vue
<main>
  <router-view />
</main>

路由间切换

// App.vue
setup(){
  router.afterEach(()=>{
    if(width<=500){
      menuVisible.value = false;
    } 
  })
}

Switch 组件封装

需求分析:开关,外观设计参考组件库;

API设计:组件怎么用 <Switch value="true"/>value为true时;

// lib/Switch.vue 底层组件
<template>
  <div> Switch 组件 </div>
</template>
// components/SwitchDemo.vue 使用组件
<template>
  <div> 
    <Switch />
  </div>
</template>
<script lang="ts">
import Switch from './xx'  
export default {
  components: {Switch},
</script>
  • 控制初始状态、更新状态并显示切换状态
// components/SwitchDemo.vue 使用组件
<template>
  <div> 
    <!-- 通过添加 value 属性和 input 事件让外界知道当前状态是开或关-->
    <Switch :value="y" @input="y = $event"/>  
    <!-- value 属性控制每一次的 value 对应状态,触发事件之后更新一次-->
    <!-- input 事件通过 $event 拿到最新的值,其为 emit 的第二个参数-->
    <!-- emit(事件名, 事件参数)-->
  </div>
</template>
<script lang="ts">
import Switch from './xx'  
export default {
  components: {Switch},
  setup(){
    const y = ref(true)
    return {y}
  }
</script>
  • 底层组件用props接收value
  • 底层组件发出input事件,用context.emit('input', xx)即可
// lib/Switch.vue 底层组件
<template>
  <button @click="toggle" :class="{checked:value}">
  <span></span>
  </button>
  <div>{{value}}</div>
</template>
<script lang="ts">
import {ref} from 'vue'
export default {
  props:{
    value:Boolean
  },
  setup(props, context){
    const toggle = ()=>{
      // toggle 作用是把当前的值取反,通过input事件发给外面 对应使用组件的 @input="y = $event"
      context.emit('input', !props.value)
      // emit 的第二个参数 !props.value 会被当做 $event
    }
    return {toggle}
  }
}

props$event实现父子组件通信

<Switch :value="y" @input="y = $event"/>  
// 接收两个参数