Vue3

262 阅读8分钟

[top]

一.课程安排

1.项目结构

(1)了解vue3项目的结构

  • .prettierrc文件的配置
1. 在vscode中下载Prettier - Code formatter和eslint插件.
2. 打开eslint先在项目中建一个.prettierrc文件 写上需要的配置。例如:
{
    "semi": false,
    "singleQuote": true,
    "arrowParens": "always",
    "trailingComma": "all"
}
3. 在设置中搜索format后在'工作区'勾选Format on Save  在项目结构中会生成一个.vscode
4. 在设置中搜索save  Auto Save 设置为 onWindowChange 切换窗口后自动保存文件

(2)学习vue3项目的一些基本开发知识

1.使用jsx开发vue3组件
import { defineComponent, reactive } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
export default defineComponent({
  setup() {
    const state = reactive({
      name: '张三',
    })
    function renderHelloword (num:number){
        return <HelloWorld age={ num }></HelloWorld>
    }
    return () => { 
        console.log(state.name)
      return (
        <div id="app"> 
          <p>{state.name}</p>
          <input type="text" v-model={state.name} />
          {/* <HelloWorld age={222}></HelloWorld> */}//第一种写法
          { renderHelloword(12) }//第二种写法 封装函数
        </div>
      )
    }
  },
})

(3)vue3和vue2的开发区别

2.开发模式讲解

1.确定组件接口和其定义

(1)Props 即接口

  • schema(用来定义数据,定义表单)
  • value(表单的数据结果,可以从外部改变这个value)
  • locale(语言使用‘ajv-i18n’指定错误信息使用的语言)
  • onChange (通知组件库的使用者通知数据改变了,回调方法)
  • uiSchema

2.开发入口组件的实现

3.开发基础渲染的实现

3.vue3的TS定义

4.单元测试 (开发开源组件上传时 一般需先测试再上传

//1.针对ArrayField这一个组件或着匹配it里面描述的某一个组件进行单元测试
npm run test:unit -- -t=ArrayField
npm run test:unit -- -t=multi
  • 为什么要单元测试:(本质:组件做了什么就测什么) 1.检查bug 2.提升回归效率 3.保证代码质量

  • vue-test-utils

  • vue-cli的配置文件 下面代码路径:github上vue-cli/packages/@vue/cli-plugin-unit-jest/presets/default/jest-preset.js

// eslint-disable-next-line node/no-extraneous-require
const semver = require('semver')

let vueVersion = 2
try {
  const Vue = require('vue/package.json')
  vueVersion = semver.major(Vue.version)
} catch (e) {}

let vueJest = null
try {
  vueJest = require.resolve(`@vue/vue${vueVersion}-jest`)
} catch (e) {
  throw new Error(`Cannot resolve "@vue/vue${vueVersion}-jest" module. Please make sure you have installed "@vue/vue${vueVersion}-jest" as a dev dependency.`)
}

module.exports = {
  testEnvironment: 'jsdom',
  moduleFileExtensions: [//自动补全某些文件的后缀名
    'js',
    'jsx',
    'json',
    'vue'
  ],
  transform: {//哪些文件需要怎么样的编译
    // process *.vue files with vue-jest
    '^.+\\.vue$': vueJest,//处理vue文件
    '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|avif)$':
    require.resolve('jest-transform-stub'),
    '^.+\\.jsx?$': require.resolve('babel-jest')//jsx文件通过babel编译
  },
  transformIgnorePatterns: ['/node_modules/'],//哪些文件不需要去编译
  moduleNameMapper: {//把@映射到src目录
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  snapshotSerializers: [//怎么去跑快照测试
    'jest-serializer-vue'
  ],
  testMatch: [//通过正则去匹配单元测试文件的地方
    '**/tests/unit/**/*.spec.[jt]s?(x)',
    '**/__tests__/*.[jt]s?(x)'
  ],
  testURL: 'http://localhost/',//模拟浏览器环境
  watchPlugins: [//改源文件自动再跑测试
    require.resolve('jest-watch-typeahead/filename'),
    require.resolve('jest-watch-typeahead/testname')
  ]
}

用于测试vue的组件,进行单元测试

  • 覆盖率 执行npm run test:unit -- --coverage展示测试覆盖的所有文件 fugailv.png
Stmts:语句覆盖率
Lines:行覆盖率
Branch:分支是否都执行到(if/elseFuncs:是否每个函数都有执行到

在github上找到vue-cli的jest测试 vue-cli/packages/@vue/cli-plugin-unit-jest/presets/typescript-and-babel/jest-preset.js  Go to file

  • 写一个测试 经常用的api
  1. describe(描述统计单元测试)
  2. it(用在describe里面对于某一个套件测试应该符合什么)
  3. test(单独写一个测试)
//describe表示这个函数里面测试只针对HelloWorld.vue这个组件
describe('HelloWorld.vue', () => {
  //it表示对HelloWorld这个组件一些方面去做测试
  it('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld as any, {
      props: { msg },
    })
    expect(wrapper.text()).toMatch(msg)
  })
})

终端上跑测试后运行结果

Test Suites: 1 passed, 1 total //Test Suites统计的是describe这个api所对应的
Tests:       1 passed, 1 total//统计的是it的数量
Snapshots:   0 total
expect(wrapper.text()).toMatch(msg) //expect期望得到什么结果
expect(wrapper.text()).not.toEqual(msg)//期望wrapper.text()这个值不等于msg这个值
  • 预设和清理
  1. beforeEach/afterEach
  2. before/afterAll
  3. 作用域
//每一个it 单元测试都会执行beforeEach,afterEach
beforeEach(() => {
  //某一个单元测试启动前执行
  console.log('beforeEach')
})
afterEach(() => {
  //某一个单元测试启动后执行
  console.log('afterEach')
})
beforeAll(() => {
  //所有单元测试在启动前会执行一次
  console.log('beforeAll')
})
afterAll(() => {
  //所有单元测试在启动后会执行一次
  console.log('afterAll')
})
//beforeEach/afterEach  before/afterAll放在describe里面 只针对这个组件的处理
describe('HelloWorld.vue', () => {
  beforeEach(() => {
    console.log('beforeEach')
  })

  afterEach(() => {
    console.log('afterEach')
  })
  beforeAll(() => {
    console.log('beforeAll')
  })
  afterAll(() => {
    console.log('afterAll')
  })
  //it表示对HelloWorld这个组件一些方面去做测试
  it('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld as any, {
      props: { msg },
    })
    expect(wrapper.text()).toMatch(msg) //expect期望
  })

  it('should work', () => {
    expect(1 + 1).toBe(2)
  })
})
  • 异步测试 1.通过done来告诉测试这是个异步操作,让异步执行完成后再测试
describe('HelloWorld.vue', () => {
  //it表示对HelloWorld这个组件一些方面去做测试
  it('renders props.msg when passed', (done) => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld as any, {
      props: { msg },
    })
    setTimeout(() => {
      expect(wrapper.text()).toMatch(msg) //expect期望
      done()//执行完成后调用done执行测试
    }, 100)
  })

  it('should work', () => {
    expect(1 + 1).toBe(2)
  })
})

2.通过promise来告诉测试 这是异步测试、

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld as any, {
      props: { msg },
    })
    return new Promise((resolve: any) => {
      expect(wrapper.text()).toMatch(msg) //expect期望
      resolve()
    })
  })
})

3.通过 async await的方式告诉这是异步测试

describe('HelloWorld.vue', () => {
  //it表示对HelloWorld这个组件一些方面去做测试
  it('renders props.msg when passed', async () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld as any, {
      props: { msg },
    })
    await wrapper.setProps({
      msg: '123',
    })
    expect(wrapper.text()).toMatch(msg) //expect期望
  })
})

5.高泛用性的API

6.响应式原理

7.完善功能开发

8.自动化发布流程

二.vue3概览

1.Slot API的变化

<!-- old 2.0-->
<foo>
  <bar slot="one" slot-scope="one">
    <div slot-scope="bar">
      {{ one }} {{ bar }}
    </div>
  </bar>

  <bar slot="two" slot-scope="two">
    <div slot-scope="bar">
      {{ two }} {{ bar }}
    </div>
  </bar>
</foo>

<!-- new 3.0 -->
** v-slot 可用 # 代替 **
<foo>
  <template v-slot:one="one">
    <bar v-slot="bar">
      <div>{{ one }} {{ bar }}</div>
    </bar>
  </template>

  <template v-slot:two="two">
    <bar v-slot="bar">
      <div>{{ two }} {{ bar }}</div>
    </bar>
  </template>
</foo>

2.全局API使用规范变化

  • main.js 全局引入组件 vue3通过createApp创建
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

app.config.globalProperties.customProperty = () => {}

app.mount(App, '#app')
  • 使用某些API通过import按需引入
import Vue, { nextTick, observable } from 'vue'

Vue.nextTick // undefined

nextTick(() => {})

const obj = observable({})

3.teleport

含义:渲染元素到某个节点下

<teleport to="#endofbody">
  <div id="content">
    <p>
      this will be moved to #endofbody.<br />
      Pretend that it's a modal
    </p>
    <Child />
  </div>
</teleport>

4.Composition API (重)

(1)defineComponent函数
(2)props的用法
  • 定义config类型
<script lang="ts">
import { defineComponent, PropType } from 'vue'
//定义一个Config类型
interface Config {
  name: string
}
export default defineComponent({
  name: 'App',
  components: {
    HelloWorld,
  },
  props: {
    age: {
      //不是必须传时 this.age的类型为number或undifine
      type: Number as PropType<number>, //age类型为number
    },
    config: {
      type: Object as PropType<Config>,
      required: true, //必传时 this.config类型就是Config 不会有undifine的情况
    },
  },
  data() {
    return {
      name: 'zhangsan',
    }
  },
  mounted() {
    // this.config
    // this.age
  },
})
</script>
  • props的提取
<script lang="ts">
    import { defineComponent } from 'vue'
    //在PropsType对象后加上 as const 告诉 PropsType 为readonly(只读对象)。不然required: true不起作用
    //写在 defineComponent 里面就是只读对象
    const PropsType = {
      msg: String,
      age: {
        type: Number,
        required: true,
      },
    } as const
    export default defineComponent({
      name: 'HelloWorld',
      props: PropsType,
      mounted() {
        this.age
      },
    })
</script>
(3)h函数的用法(vue的template模板就是h函数的调用)
import { createApp, defineComponent, h } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
//后面 加上eslint-disable-line注释表示对这一行代码取消eslint检查
const img = require("./assets/logo.png") //eslint-disable-line 
//import App from './App.vue'
const App = defineComponent({
  render() {
  //这里的h函数可以用createVNode代替
    return h('div', { id: 'app' }, [
      h('img', {
        alt: 'Vue logo',
        src: img,
      }),
      h(HelloWorld, {
        msg: 'Welcome to Your Vue.js + TypeScript App',
        age: 12,
      }),
    ])
  },
})

createApp(App).mount('#app')
(4)setup的使用 (和data()一样只初始化执行一次)
  • reactive 在setup中实现响应式
<template>
  <div id="app">
    {{ state.name }}
  </div>
</template>
import { defineComponent, reactive } from 'vue'
//name每隔1秒会加1
setup(props, { slots, attrs, emit }) {
    let state = reactive({
      name: '张三',
    })
    setInterval(() => {
      state.name += '1'
    }, 1000)
    return { state }
    //return { ...state }  取出reactive里面的对象 这样返回就不会有响应式
},
  • ref 在setup中实现响应式
<template>
  <div id="app">
    {{ name }}
  </div>
</template>
import { defineComponent, ref } from 'vue'
setup(props, { slots, attrs, emit }) {
    const nameRef = ref('李四')
    //ref 类似于 {value:xxx}这样一个对象 会自动调.value属性 所以上面直接可以用name
    setInterval(() => {
      nameRef.value += '1'
    }, 1000)
    return { name: nameRef }
},
  • computed(类似于vue2中的computed)
<template>
  <div id="app">
    {{ name }}:{{ name2 }}
  </div>
</template>
import { defineComponent, ref, computed } from 'vue'
setup(props, { slots, attrs, emit }) {
    const nameRef = ref('李四')
    //ref 类似于 {value:xxx}这样一个对象 会自动调.value属性 所以上面直接可以用name
    setInterval(() => {
      nameRef.value += '1'
    }, 1000)
    //每次当nameRef变化时  computedNameRef都会重新计算
    const computedNameRef = computed(() => {
      return nameRef.value + '2'
    })
    return {
      name: nameRef,
      name2: computedNameRef,
    }
},
  • watchEffect
//只监听在watchEffect里面使用到的nameRef.value值 一变化就会执行watchEffect函数
import { defineComponent, ref, computed, watchEffect } from 'vue'
watchEffect(() => { 
    console.log(nameRef.value) 
})
  • setup返回rander函数的用法
import { createApp, defineComponent, h, reactive, ref } from 'vue'
//后面 加上eslint-disable-line注释表示对这一行代码取消eslint检查
const img = require("./assets/logo.png") //eslint-disable-line 
//import App from './App.vue'
const App = defineComponent({
  setup() {
    const state = reactive({
      name: '张三',
    })
    const numberRef = ref(1)
    setInterval(() => {
      state.name += '1'
      numberRef.value += 1
    }, 1000)
    //const number = numberRef.value//这句话放在这里setup里面是无效的,因为setup只能执行一次 所以number永远是初始值 1
    return () => {
      const number = numberRef.value //放在return函数里面是有效的,因为numberRef的变化会让return的函数从新执行
      return h('div', { id: 'app' }, [
        h('img', {
          alt: 'Vue logo',
          src: img,
        }),
        h('p', state.name + number),
      ])
    }
  },
})

createApp(App).mount('#app')

5.v-model 指令API的变化

三.TypeScript

学习TS的注意点:
1. 任何变量都声明类型
2. 不到万不得已不要用any
3. 给你的对象声明接口

四.主题系统

 1.核心逻辑不变
 2.组件单独打包,减少强依赖

2.不同的样式主题

1.交互可以变化
2.组件的产出可以完全不同
3.统一接口后所有内容可以自定义

五.打包配置

1.通过环境变量配置控制打包文件

//1.通过 yarn add cross-env --save-dev 安装cross-env  
//2.指定build:theme这个命令环境变量为TYPE=lib
//3.--target指定打包文件及路径
//4. --name 指定打包后哪个路径下文件名
"build:theme": "cross-env TYPE=lib vue-cli-service build --target lib --name theme-default/index lib/theme-default/index.tsx",

//4.然后在vue.config.js中
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')//eslint-disable-line
const CircularDependencyPlugin = require('circular-dependency-plugin')//eslint-disable-line

const isLib = process.env.TYPE === 'lib'//获取环境变量等于lib时

module.exports = {
  configureWebpack(config) {
    // console.log(config.plugins)
  },
  chainWebpack(config) {
    if (!isLib) {
       //通过配置环境变量不等于lib时才执行下面这个配置
      config.plugin('monaco').use(new MonacoWebpackPlugin())
    }
    config.plugin('circular').use(new CircularDependencyPlugin())
  },
  pwa: {},
}

2.案例 1643271394(1).png

说明:
打包theme-default下index.tsx
打包后会生成两种js
1.common.js 通过node安装webpack打包后用到的文件
2.umd.js可以直接通过script标签引入的文件

3.分开打包

1643273922(1).png

说明:
先安装 yarn add rimraf -D
//1.rimraf dist 手动删掉dist目录
//2.npm run build:core && npm run build:theme  分开打包build:core和build:theme
"build": "rimraf dist && npm run build:core && npm run build:theme"