vue3 实战总结

4,842 阅读6分钟

技术基础

使用vue3+ts(x)作为基础

总纲

  • 技术是服务于业务需求的
  • 技术的选型是依据场景的
  • 技术随着业务变化需要迭代的

注意事项与规范

项目工程规范

  • 组件命名方式使用 ls-lint 进行校验,组件名称必须大写开头,修改目录 .git 文件中 config,ignorecase = False 防止出现文件大小写的问题

  • git 提交规范使用,可以使用 git-cz 做 git commit

<type>(<scope>): <subject><BLANK LINE><body><BLANK LINE><footer>
  • 项目中代码使用 gitHooks 进行代码 prettier 和 eslint 保证跨 ide 代码风格一致性(vscode 中配合 prettier-eslint 自动格式化),项目中依赖@commitlint,eslint,prettier,lint-staged,stylelint,@ls-lint/ls-lint 以及项目中各种 es,ts,pre 的配置文件建议参考 ant-pro 初始化项目
{
  "gitHooks": {
    "pre-commit": "npx @ls-lint/ls-lint && lint-staged",
    "commit-msg": "commitlint -e $GIT_PARAMS"
  },
  "lint-staged": {
    "*.ts?(x)": [
      "npm run lint-staged:js",
      "prettier --parser=typescript --write",
      "git add"
    ],
    "*.{js,jsx}": ["vue-cli-service lint --fix", "prettier --write", "git add"],
    "*.{vue,css,scss}": ["stylelint --fix", "prettier --write", "git add"]
  }
}

代码规范

  • 代码方法注释和文件注释(vscode 中可以使用 koroFileHeader)

  • enums 枚举目录(告别魔术数字),composables 组合api目录(数据,事件,公共方法),types 接口目录(接口统一管理与公用),api(请求集合,api能力封装-加入异常和 loading)

  • 项目组件自动注册,业务组件按需加载

// 区分场景 一个是公用组件 一个是项目内置组件,共有组件按需加载,内置组件自动注册,
// 组件类型 标签类型组件 函数类型组件
const context = require.context('./components', true, /\.tsx$/)
const componentMap = new Map()
context.keys().forEach(k => {
  const componentConfig = context(k).default
  if (componentMap.has(componentConfig.name)) {
    console.warn('componentName has used')
  } else {
    app.component(componentConfig.name, componentConfig)
    componentMap.set(componentConfig.name, componentConfig.name)
  }
})
app.mount('#app')
// 函数挂载
app.config.globalProperties = {
  $message: Message,
}
  • 组件通信使用 mitt

  • 业务组件目录定义,组件文件,样式的.module.scss文件,子组件文件 components 文件夹(可选),组合api方法组件功能方法抽离文件(可选)

  • UI 组件行为控制,例如模态框和提示框,使用函数方式调用加载到 body 内部,否则 ui的z-index 受到父节点的z-index影响

vue3 中 tsx 实践

前因后果

why ts

。。。。在大型项目的长期维护与迭代中,ts所有的特性都能很好的满足这个场景

why tsx (jsx 与 模板语法异同)

模板语言特性

  • 模板语法更方便简单易上手 v-if,v-for

  • vue3 本身对模板编译做了很大的优化,通过标记 block 和方法缓存的方式,diff 的优化,示例可以查看 链接vue3 模板,正是因为使用模板语言,其标签的固定性可以容易识别出代码块与vue变量才能进行标记和方法缓存,减少对象创建这是提高diff算法效率的基础,比jsx中变量识别代码块更简单。编译优化还有其他场景例如嵌套节点等等这些建议观看尤大关于性能提升的视频。

  • 劣势:模板语言中 ts 类型支持力度不够,当然官方提供插件也能解决这个问题,实现的原理就是把模板变成 ts,再把 ts 反馈给模板

<!-- 性能编译,样例模板 -->
<div>
  <span>Hello World!</span>
  <span>Hello World!</span>
  <span>{{mes}}</span>
  <span @click="handlClick">click</span>
</div>
// 示例如下 ,自己可以到网站上面试一下
import {
  createVNode as _createVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createBlock as _createBlock,
} from 'vue'

const _hoisted_1 = /*#__PURE__*/ _createVNode(
  'span',
  null,
  'Hello World!',
  -1 /* HOISTED */,
)
const _hoisted_2 = /*#__PURE__*/ _createVNode(
  'span',
  null,
  'Hello World!',
  -1 /* HOISTED */,
)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _hoisted_1,
      _hoisted_2,
      _createVNode('span', null, _toDisplayString(_ctx.mes), 1 /* TEXT */),
      _createVNode(
        'span',
        {
          onClick:
            _cache[1] ||
            (_cache[1] = (...args) =>
              _ctx.handlClick && _ctx.handlClick(...args)),
        },
        'click',
      ),
    ])
  )
}

// Check the console for the AST

jsx语法特性

  • 为什么使用jsx这个疑问不仅仅是我的疑问,也是 react-hook 开源作者的疑问
  • 官方的表述 ui 和逻辑的一致性
  • 白话版翻译 js 本身提供更灵活的使用方式在 jsx 中不仅仅可以 v-show,还可以添加更多的 style,color 等等
  • 理解一致性 map 遍历比 v-for 在 js 本身的逻辑里面,显然map更容易理解一点点
  • 劣势:在 react 中使用 jsx 很容易会出现父组件渲染,子组件重复渲染的问题,当组件层级很深的时候,数据变化时 render 函数重复触发对浏览器的性能就是极大的消耗,官方也提供了例如 useMemo,useCallback 这样的hook实现手动缓存

技术选型为jsx原因

  • 在语法检查,函数式编程,以及单元测试的角度来说jsx是存在相对优势的
  • 本身灵活性在大型项目中确实很常见也很好用,原子化与复用在ant-design-vue 这样的项目中体现很好
  • 一致性,前端现在生态最为人吐槽的一点就是框架方言化,一个框架一个方言,这不是一件很好的事情。为什么 vue3 推出和 react-hook 比较相似的 api,开源作者也注意到了前端生态共建的这样的一个场景,对于团队个人发展也是好事,如果从 vue3 的 tsx 转换成 react-hook 认知成本是很低的,反之亦然

vue3 正式开始

核心模块

  • 响应式模型
  • 编译模型
  • 渲染模型

重点变化

  • 性能提升,编译渲染性能优化,proxy 可以劫持先知道是属性就直接去属性中获取
  • tree-shaking 支持,vue 包模块按需引入
  • composition api, 组合 api 提高复用
  • fragment 不用写根节点了,这个 angular 和 react 很早就有了
  • 更好的 ts 支持 ,大型项目诉求越来越多
  • render api(没懂)

关键 api - setup

  • 为什么使用新的 option 就是为了承接 vue2 中的写法可以不使用 setup 也能把 vue3 实现,这个做法在 react 提出 hook 的时候,官方的说明也是如此不建议把之前 class 全部改下成 hook,在项目已经成熟的情况下。不管是 hook 还是 setup 只要使用了新的写法确实在大型项目中能减少很多代码,实现了方法公用,减少了生命周期中的操作

  • react比较代码量和方法使用简易程度

// 新写法在代码量和逻辑切割上面更有优势
function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  // 相当于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用浏览器的 API 更新页面标题
    document.title = `You clicked ${count} times`;
  });
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}
// 旧class用在bind this和生命周期中的操作显得隆余
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
    // 为了在回调中使用 `this`,这个绑定是必不可少的
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick(){
    this.setState(state => ({
      count: state.count+1
    }));
  }
  componentDidMount() {
  }
  componentWillUnmount() {
  }
  render() {
    return (
      <div>
        <h1 onClick={this.handleClick}>Hello, world!</h1>
      </div>
    );
  }
}
  • vue3-setup

    setup在vue3 是第一位置,在这个钩子函数中不能使用 data 等等其他option 这就是故意为之,在其内部不能使用 this 的原因也正是如此,用this.去获取data属性时data还没有执行。 如果需要使用挂载方法可以通过下面的实例讲解中的方法,这样就可以实现在setup中获取this,使用this.&message({})

watchEffect

  • watchEffect 是没有中间状态的,可以监听所有属性变化,立即执行,vue2中复杂的写法在现在的api中可以更少的代码量实现
// vue2 需要定义三次代码块实现监听属性id的变化
export default {
  props:["id"]
  methods:{
    handleChange(){
      console.log("xxx")
    }
  }
  watch:{
    id:'handleChange'
  }
}
// vue3 只需要少量代码
export default defineComponent({
  setup(props) {
  watchEffect(()=>{
    console.log(props.id)
   })
  },
})

watch

  • watch 具有中间状态,最终状态没变不会触发回调,具有惰性,可以接受多个值,可以获取 oldvalue,但是接受参数必须具有响应性或者是是个执行函数
export default defineComponent({
  setup() {
    // 侦听一个getter
    const state = reactive({ count: 0 })
    watch(
      () => state.count,
      (count, prevCount) => {
        /* ... */
      }
    )

    // 直接侦听一个ref
    const count = ref(0)
    watch(count, (count, prevCount) => {
      /* ... */
    })
  },
})

ref

  • 响应式对象建议使用

reactive

  • 区分场景使用,在组合 api 中 return 返回中不合适使用,下面会详细介绍原因

toRefs

  • 当从合成函数返回响应式对象时,toRefs 非常有用,这样虽然可以变回响应式,但是在多个组合 api 组合使用时也失去了意义,后面详见
function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })
  // 逻辑运行状态
  // 返回时转换为ref
  return toRefs(state)
}
export default {
  setup() {
    // 可以在不失去响应性的情况下破坏结构
    const { foo, bar } = useFeatureX()
    return {
      foo,
      bar
    }
  }
}

核心问题为什么使用组合api,怎么使用组合api

最大的问题 why use composition api

  • 方法复用在大型项目中可以共享代码逻辑,减少对象创建
  • 代码块分布问题:当代码行数很多时,数据,属性,计算属性,watch都分布在不同区域,明明是操作同一个数据却要来回切换,开发体验不好
  • 在vue2中 mixins,extend,原型挂载,组件注册的方式都是实现公用方法,但是变量命名和开发体验不好,跟之前的reactive一个道理,虽然也有解决方法例如命名规则,v-slot等等终究不是很方便(实现方式就不举例了不是这次重点)
// vue2 的实现
export default {
  mixins:[minxA,minB],
  render() {
    const {x,y} = this
    // 根本不知道x和y来自哪,还有命名冲突的问题
    return <>{x}{y}</>
  },
}
// vue3 的实现
export default {
  setup(){
    const {x} = UseX()
    // 这样可以解决命名冲突的问题
    const {x:y} = UseY()
    return{
      x,
      y
    }
  },
  render() {
    const {x,y} = this
    // 现在清晰可知 x,y 来自哪里
    return <>{x}{y}</>
  },
}
  • 使用规范

    业务组件中将相关联的组合api合并,在新建文件导出方法,或者当前在组件函数外部声明

    全局和模块共用方法抽离在composables目录下面

// 不规范示例 代码混乱a,b,c到处混用代码解构不清晰
export default defineComponent({
  setup(){
    const a = ref(null)
    const b = ref(null)
    const changeA = ()=>{
    }
    const c = ref(null)
    const changeB = ()=>{
    }
    const changeC = ()=>{
    }
    watchEffect(()=>{
      console.log(a.value)
    })
    return{
      dom
    }
  },
})
// 好的规范示例 代码清晰
/**
 * @name: UseA
 * @msg: a相关操作
 */
function UseA(){
  const a = ref(null)
  const changeA = ()=>{

  }
  watchEffect(()=>{
      changeA()
  })
  return{
    a
  }
}
/**
 * @name: UseB
 * @msg: b相关操作
 */
function UseB(){
  const b= ref(null)
  const changeB = ()=>{

  }
  watchEffect(()=>{
      changeB()
  })
  return{
    b
  }
}
export default defineComponent({
  setup(){
    // 结构清晰
    const {a} = UseA()
    const {b} = UseB()
    return{
      a,
      b
    }
  },
})

vue3中实战示例

  • 获取 dom
export default defineComponent({
  setup(){
    const dom = ref(null)
    // 必须把dom return
    return{
      dom
    }
  },
  render() {
    return
    <>
    // 定义同名ref
    <div ref="dom">
     <div>
    </>
  },
})
  • v-if 指令
export default defineComponent({
  render() {
    // 三元运算
    const comp = flag ? CompA : CompB
    return <>{comp}</>
  },
})
  • v-for 指令
export default defineComponent({
  render() {
    return (
      <>
        <ul>
          {menuList.map(item => (
            <li
              onClick={() => {
                close(item)
              }}
            >
              <span>{item.name}</span>
            </li>
          ))}
        </ul>
      </>
    )
  },
})
  • v-show 指令
export default defineComponent({
  render() {
    const style = flag? "display:block":"display:none"
    return (
      <div style={style}>
      </div>
    )
  },
})
  • classModule,样式scope 实现样式隔离
// 关键是文件后缀为 module.scss
import styles from './index.module.scss'
export default defineComponent({
  render() {
    return (
     // 使用 styles对象去获取引用
      <div style={style['yxt']}>
      </div>
    )
  },
})
  • classNames
// 多个class
import classNames from 'classnames'
import styles from './index.module.scss'
export default defineComponent({
  render() {
    // 使用classNames 方法进行样式合并
    const content =  classNames(
            styles['item'],
            styles['item-isActive'],
        )
    return (
      <div class ={content}>
      </div>
    )
  },
})
  • 动态 class
// 根据变量渲染不同的class
import classNames from 'classnames'
import styles from './index.module.scss'
export default defineComponent({
  render() {
    //使用函数变量控制样式,在setup中也可与数据关联判断
    let pptClass = ()=>{
        if(flag){
            return styles['item']
        }else{
            return styles['item-isActive']
        }
    }
    return (
      <div class={pptClass()}>
      </div>
    )
  },
})
  • 事件绑定
export default defineComponent({
  render() {
   // 事件或者map 循环的参数都可以传递
    const handleClick = (event,arg?)=>{}
    return (
      <div onClick={(event) => {handleClick()}}>
      </div>
    )
  },
})
  • 函数组件
// 区别之前的Vue.extend()方式
import { createApp } from 'vue'
import message from './Message'
// id 自增计数
let count = 0
export enum Types {
  SUCCESS = 'success',
  ERROR = 'error',
  WARNING = 'warning',
}
// 只需要将Message进行挂载或者单独引用Message 方法进行调用
export const Message = (options: Options) => {
  const div = document.createElement('div')
  const id = `yxt-message-${count + 1}`
  div.setAttribute('id', id)
  document.body.appendChild(div)
  const instance = createApp(message, {
    type: options.type,
    content: options.content,
    id: id,
  })
  // 挂载到 body 下
  document.body.appendChild(instance.mount(`#${id}`).$el as HTMLElement)
  count++
}
  • 插槽的使用与渲染
// 引用组件
export default defineComponent({
    render() {
    const defaults = () => {
      return <div>{pptNote}</div>
    }
    return (
      <>
        <div class={styles.pptDetail}>
          <yxt-tabs>
            <yxt-tab-pane
              title="备注"
              scopedSlots={defaults}
            ></yxt-tab-pane>
          </yxt-tabs>
        </div>
      </>
    )
  },
})
// 子组件渲染
export default defineComponent({
  render() {
    if (this.$props.scopedSlots) {
      return this.$props.scopedSlots()
    } else {
      return <></>
    }
  },
}
  • 在 setup 中获取 this
import { ComponentInternalInstance, getCurrentInstance } from 'vue'
/**
 * @name: UseThis
 * @msg: 在setup中获取this 属性, 方便使用$xx去调用
 */
export function UseThis() {
  const { proxy } = getCurrentInstance() as ComponentInternalInstance
  return {
    proxy: proxy as any,
  }
}

原则

  • 逻辑 UI 用js 控制
  • 父子组件使用 props 传参,渲染,动态绑定(第一优先级)
  • 事件通信使用 mitt
  • 方法复用使用 composition api(只要有两个及以上的重复方法)

资料地址