基于Vue3+Vite+TS,二次封装element-plus业务组件 笔记第二章

71 min read

2-1 搭建vite项目并配置路由和element-plus

npm init vite@lastest  x-components -- --template vue-ts
node: 16.6.2
@element-plus/icons: 0.0.11,
@fullcalendar/core: 5.10.1,
@fullcalendar/daygrid: 5.10.1,
@fullcalendar/interaction: 5.10.1,
@types/lodash: 4.14.176,
axios: 0.24.0,
element-plus: 1.1.0-beta.20,
lodash: 4.17.21,
vue: 3.2.16,
vue-router: 4.0.12,
wangeditor: 4.7.9
@types/mockjs: 1.0.4,
@vitejs/plugin-vue: 1.9.3,
@vitejs/plugin-vue-jsx: 1.2.0,
mockjs: 1.1.0,
sass: 1.43.2,
sass-loader: 12.2.0,
typescript: 4.4.3,
vite: 2.6.4,
vue-tsc: 0.3.0

2-2 全局注册图标

原始图标组件名为驼峰命名,通过正则替换将其转换为中划线的形式

// 把驼峰转换成横杠连接
export const toLine = (value: string) => {
  return value.replace(/(A-Z)g/, '-$1').toLocaleLowerCase()
}

main.ts 全局注册图标组件

// el-icon-xxx
for (let i in Icons) {
  // 注册全部组件
  app.component(`el-icon-${toLine(i)}`, (Icons as any)[i])
}

2-3 伸缩菜单-完成伸缩菜单基本功能

布局组件 el-container

<template>
  <el-container>
    <el-aside width="auto">
      <nav-side :collapse="isCollapse"></nav-side>  // 菜单
    </el-aside>
    <el-container>
      <el-header>
        <nav-header v-model:collapse="isCollapse"></nav-header> // 头部
      </el-header>
      <el-main>
        <router-view></router-view> // 路由
      </el-main>
    </el-container>
  </el-container>
</template>

el-container的样式属性

element 导航菜单撑满全屏

html,
body,
#app,
.el-container,
.el-menu {
  height: 100%;
}

css 反向选择器, :not() 用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中,它也被称为反选伪类negation pseudo-class)。

/* 选择所有不是段落(p)的元素 */
:not(p) {
  color: blue;
}

2-4 伸缩菜单-抽离头部和侧边栏组件并完善伸缩菜单

子组件内修改父组件的传递的props值

// 父组件
<nav-side :collapse="isCollapse"></nav-side>
// 子组件
<script lang='ts' setup>
let props = defineProps<{
  collapse: boolean
}>()
let emits = defineEmits(['update:collapse'])
let toggle = () => {
  // 需要修改父组件的数据
  emits('update:collapse', !props.collapse)
}
</script>

2-5图标选择器-巧用两次watch控制弹框的显示与隐藏

<template>
  <el-button @click="handleClick" type="primary">
    <slot></slot>
  </el-button>
  <div class="m-choose-icon-dialog-body-height">
    <el-dialog :title="title" v-model="dialogVisible">
      <div class="container">
        <div
          class="item"
          v-for="(item, index) in Object.keys(ElIcons)"
          :key="index"
          @click="clickItem(item)"
        >
          <div class="text">
            <component :is="`el-icon-${toLine(item)}`"></component>
          </div>
          <div class="icon">{{ item }}</div>
        </div>
      </div>
    </el-dialog>
  </div>
</template>

<script lang='ts' setup>
import * as ElIcons from '@element-plus/icons'
import { watch, ref } from 'vue'
import { toLine } from '../../../utils'
import { useCopy } from '../../../hooks/useCopy'

let props = defineProps<{
  // 弹出框的标题
  title: string,
  // 控制弹出框的显示与隐藏
  visible: boolean
}>()
let emits = defineEmits(['update:visible'])
// 拷贝一份父组件传递过来的visible
let dialogVisible = ref<boolean>(props.visible)


// 点击按钮显示弹出框
let handleClick = () => {
  // 需要修改父组件的数据
  emits('update:visible', !props.visible)
}

// 点击图标
let clickItem = (item: string) => {
  let text = `<el-icon-${toLine(item)} />`
  // 复制文本
  useCopy(text)
  // 关闭弹框
  dialogVisible.value = false
}

// 监听visible的变化 只能监听第一次的变化
watch(() => props.visible, val => {
  dialogVisible.value = val
})
// 监听组件内部的dialogVisible变化
watch(() => dialogVisible.value, val => {
  emits('update:visible', val)
})
</script>

<style lang='scss' scoped>
.container {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
}
.item {
  width: 25%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  margin-bottom: 20px;
  cursor: pointer;
  height: 70px;
}
.text {
  font-size: 14px;
}
.icon {
  flex: 1;
}
svg {
  width: 2em;
  height: 2em;
}
</style>

Button 插槽使用

  <el-button @click="handleClick" type="primary">
    <slot></slot>
  </el-button>

watch Element-Plus dialog 双向绑定无效, 主要是因为el-dialog的dialogVisible有内部和外部两种状态,点击组件的关闭时只修改了内部状态的dialogVisible,需要再次传递到外部组件的dialogVisible

// 监听visible的变化 只能监听第一次的变化
watch(() => props.visible, val => {
  dialogVisible.value = val
})
// 监听组件内部的dialogVisible变化
watch(() => dialogVisible.value, val => {
  emits('update:visible', val)
})

vue调试方法 ,直接在页面上输出变量的值

深度拷贝的手动实现

function deepClone(obj){
    let objClone = Array.isArray(obj)?[]:{};
    if(obj && typeof obj==="object"){
        for(key in obj){
            if(obj.hasOwnProperty(key)){
                //判断ojb子元素是否为对象,如果是,递归复制
                if(obj[key]&&typeof obj[key] ==="object"){
                    objClone[key] = deepClone(obj[key]);
                }else{
                    //如果不是,简单复制
                    objClone[key] = obj[key];
                }
            }
        }
    }
    return objClone;
} 

对于父组件传递过来的值, 直接 watch 组件的 props 对应的属性即可,props 本身是一个 reactive,如果某个属性是个对象,它会被 Proxy 深度代理,想要防止相同的引用则需要深拷贝(如果确定只有一层浅拷贝即可)

2-6 图标选择器-巧用component动态组件显示所有的图标

通过Object.keys循环一个对象

<div v-for="(item, index) in Object.keys(ElIcons)"
          :key="index"
          @click="clickItem(item)"
</div>

动态加载组件

  <div class="text">
    <component :is="`el-icon-${toLine(item)}`"></component>
    </div>
    <div class="icon">{{ item }}</div>
</div>

2-7图标选择器-利用命名空间修改dialog样式

>>>/deep/ 已经被弃用了

改成:deep()就没问题了,注意:这里没有空格,这里没有空格,这里没有空格。不然样式不生效

//警告例子
<style scoped>
 
/deep/ .main{
    background: #df2929;
}
>>> .main{
    background: #df2929;
}
 
</style>
 
 
//正确例子
<style scoped>
 
:deep(.main){
    background: #df2929;
}
 
</style>

2-8 图标选择器-通过自定义hooks函数实现复制功能

useCopy,创建临时页面控件, 调用浏览器复制执行命令 document.execCommand('Copy')

import { ElMessage } from 'element-plus'
// 复制文本
export const useCopy = (text: string) => {
  // 创建输入框
  let input = document.createElement('input')
  // 给输入框value赋值
  input.value = text
  // 追加到body里面去
  document.body.appendChild(input)
  // 选择输入框的操作
  input.select()
  // 执行复制操作
  document.execCommand('Copy')
  // 删除加入的输入框
  document.body.removeChild(input)
  // 提示用户
  ElMessage.success('复制成功')
}

2-9 省市区选择组件-利用github获取到省市区数据

省市区编码树形文件文件

https://github.com/pfinal/city/blob/master/pca-code.json

省市区编码树形文件文件

2-10 省市区选择组件-巧用watch来达到三级联动效果

<template>
  <div>
    <el-select clearable placeholder="请选择省份" v-model="province">
      <el-option v-for="item in areas" :key="item.code" :value="item.code" :label="item.name"></el-option>
    </el-select>
    <el-select
      clearable
      :disabled="!province"
      style="margin: 0 10px;"
      placeholder="请选择城市"
      v-model="city"
    >
      <el-option v-for="item in selectCity" :key="item.code" :value="item.code" :label="item.name"></el-option>
    </el-select>
    <el-select clearable :disabled="!province || !city" placeholder="请选择区域" v-model="area">
      <el-option v-for="item in selectArea" :key="item.code" :value="item.code" :label="item.name"></el-option>
    </el-select>
  </div>
</template>

<script lang='ts' setup>
import { ref, watch } from 'vue'
import allAreas from '../lib/pca-code.json'

export interface AreaItem {
  name: string,
  code: string,
  children?: AreaItem[]
}

export interface Data {
  name: string,
  code: string
}

// 下拉框选择省份的值
let province = ref<string>('')
// 下拉框选择城市的值
let city = ref<string>('')
// 下拉框选择区域的值
let area = ref<string>('')
// 所有的省市区数据
let areas = ref(allAreas)

// 城市下拉框的所有的值
let selectCity = ref<AreaItem[]>([])

// 区域下拉框的所有的值
let selectArea = ref<AreaItem[]>([])

// 分发事件给父组件
let emits = defineEmits(['change'])

// 监听选择省份
watch(() => province.value, val => {
  if (val) {
    let cities = areas.value.find(item => item.code === province.value)!.children!
    selectCity.value = cities
  }
  city.value = ''
  area.value = ''
})
// 监听选择城市
watch(() => city.value, val => {
  if (val) {
    let area = selectCity.value.find(item => item.code === city.value)!.children!
    selectArea.value = area
  }
  area.value = ''
})

// 监听选择区域
watch(() => area.value, val => {
  if (val) {
    let provinceData: Data = {
      code: province.value,
      name: province.value && allAreas.find(item => item.code === province.value)!.name
    }
    let cityData: Data = {
      code: city.value,
      name: city.value && selectCity.value.find(item => item.code === city.value)!.name
    }
    let areaData: Data = {
      code: val,
      name: val && selectArea.value.find(item => item.code === val)!.name
    }
    emits('change', {
      province: provinceData,
      city: cityData,
      area: areaData
    })
  }

})

</script>

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

watch的同时需要清空上次的选择,实现clearable的功能

2-11省市区选择组件-完善省市区联动组件并给父组件分发事件

使用emit 分发事件

// 定义分发事件给父组件的事件
let emits = defineEmits(['change'])

// 分发事件
emits('change', {
      province: provinceData,
      city: cityData,
      area: areaData
})

使用 TypeScript 定义 ref 响应式数据类型

let selectCity = ref<AreaItem[]>([])

在TypeScript里面,有3 4个地方会出现问号操作符,他们分别是

三元运算符

// 当 isNumber(input) 为 True 是返回 ? : 之间的部分; isNumber(input) 为 False 时
// 返回 : ; 之间的部分
const a = isNumber(input) ? input : String(input);

参数

// 这里的 ?表示这个参数 field 是一个可选参数
function getUser(user: string, field?: string) {
}

成员

// 这里的?表示这个name属性有可能不存在
class A {
  name?: string
}

interface B {
  name?: string
}

安全链式调用

// 这里 Error对象定义的stack是可选参数,如果这样写的话编译器会提示
// 出错 TS2532: Object is possibly 'undefined'.
return new Error().stack.split('\n');

// 我们可以添加?操作符,当stack属性存在时,调用 stack.split。若stack不存在,则返回空
return new Error().stack?.split('\n');

// 以上代码等同以下代码, 感谢 @dingyanhe 的监督
const err = new Error();
return err.stack && err.stack.split('\n');

什么是!(感叹号)操作符?

在TypeScript里面有3个地方会出现感叹号操作符,他们分别是

一元运算符

// ! 就是将之后的结果取反,比如:
// 当 isNumber(input) 为 True 时返回 False; isNumber(input) 为 False 时返回True
const a = !isNumber(input);

成员

// 因为接口B里面name被定义为可空的值,但是实际情况是不为空的,那么我们就可以
// 通过在class里面使用!,重新强调了name这个不为空值
class A implemented B {
  name!: string
}

interface B {
  name?: string
}

强制链式调用

// 这里 Error对象定义的stack是可选参数,如果这样写的话编译器会提示
// 出错 TS2532: Object is possibly 'undefined'.
new Error().stack.split('\n');

// 我们确信这个字段100%出现,那么就可以添加!,强调这个字段一定存在
new Error().stack!.split('\n');

2-12利用app.use特性全局注册组件

单个组件文件夹结构

 menu tree -L 2
.
├── index.ts      // 入口文件
└── src
    ├── index.vue   // 主文件
    ├── menu.tsx   //jsx文件
    ├── styles     // 样式文件
    └── types.ts  // 类型文件

在组件文件夹的根目录下创建index.ts文件,暴露install方法,接受一个App类型的参数

import { App } from 'vue'
import chooseArea from './src/index.vue'

// 让这个组件可以通过use的形式使用
export default {
  install(app: App) {
    app.component('m-choose-area', chooseArea)
  }
}

创建组件库的入口文件

import { App } from 'vue'
import chooseArea from './chooseArea'
import chooseIcon from './chooseIcon'
import trend from './trend'
import notification from './notification'
import list from './list'
import menu from './menu'
import chooseTime from './chooseTime'
import chooseDate from './chooseDate'
import progress from './progress'
import chooseCity from './chooseCity'
import form from './form'
import modalForm from './modalForm'
import table from './table'
import calendar from './calendar'

const components = [
  chooseArea,
  chooseIcon,
  trend,
  notification,
  list,
  menu,
  chooseTime,
  chooseDate,
  chooseCity,
  progress,
  form,
  modalForm,
  table,
  calendar
]

export default {
  install(app: App) {
    components.map(item => {
      app.use(item)
    })
  }
}

在main.ts中use组件库

import mUI from './components'
app.use(router).use(ElementPlus).use(mUI)