六. Vue3

715 阅读14分钟

1.前端工程化

1.1 介绍

  • 实现前端开发的4个现代化:
    • 模块化(JS的模块化,css的模块化,其它资源的模块化)
    • 组件化(复用现有的UI结构 样式 行为)
    • 规范化(目录结构的划分 编码规范化 接口规范化 文档规范化)
    • 自动化(自动化构建 自动部署 自动化测试)
  • 什么事前端工程化: 在企业级的前端项目开发中,把前端开发所需要的工具 技术 流程 经验等进行规范化 标准化.最终落实到细节上,就是实现前端的"工程化"
  • 前端工程化的好处: 让前端开发自成体系,覆盖了前端项目从创建到部署的方方面面
  • 提高开发效率,降低技术选型
  • 早期前端工程化解决方案: grunt gulp parcel
  • 目前主流 webpack

1.2 什么是webpack

  • 提供友好的前端模块化开发支持,以及代码压缩混淆 处理浏览器端JavaScript的兼容性 性能优化等强大的功能.
  • webpack 脚本
mode: development,   // production
entry: '入口',
output: {
    path: '出口'
    filename: 'main.js',   // aaa.js
}
"script": {
    "dev": "webpack",   // 只要运行这个脚本,就会运行这个脚本对应的命令
    "build": "webpack --mode production"
}

"module": {   // 所有第三方文件模块的匹配规则
    rules: [  // 文件后缀名的匹配规则
        { test: /\.css&/, use: ['style-loader', 'css-loader'] }   // test表示匹配的文件类型, use表示对应要调用的loader
    ]
}


只要执行webpack,他就会读取项目根目录中的一个配置文件webpack.config.js,
拿到这个配置对象,webpack根据他得到的这个配置对象对咱们的项目做处理

* 当mode: development时, 不会对打包生成的文件进行代码压缩和性能优化,打包速度快
* 当mode: production时, 进行代码压缩和性能优化,打包速度慢

* webpack默认约定: 
    默认的打包入口文件为 src ---> index.js
    默认的输出文件路径为 dist ---> main.js

* webpack 插件
    webpack-dev-server 每当修改了源代码,webpack会自动进行项目的打包和构建
    html-webpack-plugin 自定制index.html的内容 ###
    "script": {
        "dev": "webpack serve",   // 加一个serve
        "build": "xxtxx"
    }
* 在webpack.config.js配置文件中, 可以通过devServer节点对webpack-dev-server 插件进行更多的配置
devServer: {
    open: true,   // 
    host: '127.0.0.1',
    port: 80
}

* webpack中的loader, 在实际开发中,webpack默认只能打包处理以.js后缀名结尾的模块.其他非.js后缀名结尾的模块,webpack默认处理不了,需要调用loader加载器才可以正常打包,否则会报错.
* loader加载器的作用: 协助webpack打包处理特定的文件模块.
    css-loader 可以打包处理 .css文件
    babel-loader可以打包处理webpack无法处理的高级js语法

* 打包处理url路径的问题,  安装url-loader  file-loader,  配置时limit参数项(字符串配置或者 对象配置)

* 打包处理js文件的高级语法, babel-loader进行打包处理 ###

* webpack 打包发布 ###

1.3 SourceMap

  • 对压缩混淆之后的代码除错是一件极其困难的事情
  • 什么是SourceMap? 是一个信息文件,里面存储着位置信息.也就是说,SourceMap文件中存储着代码压缩混淆前后的对应关系. 有了它 出错时,除错工具将直接显示原始代码, 而不是转换后的代码,能够极大的方便后期的调试.
  • 在开发环境下,webpack默认启用了SourceMap功能. 解决行数错位的问题devtool: 'eval-source-map'
  • 在生产环境下,默认关闭SourceMap功能, 如果只想定位报错的行数,且不想暴露源码.此时可以将devtool的值设置为 nosources-source-map. 若行数和源码都暴露devtool: source-map

2.Vue3带来了什么

  • 2020年9月18日,Vue.js发布3.0版本, 代号: OnePiece (海贼王)
  • 耗时2年多, 2600+次提交, 30+个RFC,600+次PR,99位贡献者

2.1 性能的提升

  • 打包大小减少41%
  • 初次渲染快55%,更新渲染快133%
  • 内存减少54%

2.2 源码的升级

  • 响应式原理: 不再是defineProperty, 使用Proxy代替defineProperty
  • 重写虚拟DOM的实现和Tree-Shaking(去掉残枝败叶)

2.3.拥抱TypeScript

  • Vue3可以更好的支持TypeScript

2.4.新的特性

  • Composition API(组合API)
    • setup配置
    • ref与reactive
    • watch与watchEffect
    • provide与inject
    • ......
  • 新的内置组件
    • Fragment
    • Teleport
    • Suspense
  • 其他改变
    • 新的声明周期钩子
    • data选项应始终被声明为一个函数
    • 移除keyCode支持作为v-on的修饰符
    • ......

3.创建Vue3工程

3.1 使用vue-cl创建

  • @vue/cli 的版本确保在4.5.0之上
  • 创建: vue create vue_project

3.2 使用vite创建

  • 新一代的前端构建工具 (本质上相当于webpack)
  • 优势
    开发环境中,无需打包操作,可快速的冷启动.
    轻量快速的热重载(HMR)
    真正的按需编译,不再等待整个应用编译完成
  • 创建步骤
    创建工程: npm init vite-app projectname
    npm install
    npm run dev

4.Vue3 项目分析

(1)
    引入的不再是Vue构造函数了,引入的是createApp的工厂函数
    import { createApp } from 'vue'
    import App from './App.vue'
    createApp(App).mount('#app')
    
(2) Vue3 template标签内部 可以没有根标签

(3)Vue3浏览器开发者工具

5.常用Composition Api

5.1 拉开序幕的setup

  • setup是所有组合式API表演的舞台,它是Vue3中新的配置项,它的值是一个函数.
  • 组建中所用到的: 数据,方法等等,均要配置在setup中.
  • setup函数的两种返回值:
    • 若返回一个对象,则对象中的属性,方法, 在html模板中均可以直接使用.

    • 若返回一个渲染函数,则可以自定义html渲染内容. 直接return

    • () => h('标签名','内容')

    • return () => h('h1', 'ni你好')

  • 在vue2方法里可以读取vue3的变量和方法, 但是vue3的不能读取vue2的变量和方法.
  • vue2 vue3 混用, 如果变量重名,setup优先.
  • setup不能是一个async函数. 因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性.
  • 建议: vue2和vue3不要混用.

5.2 ref函数

  • 作用: 定义一个响应式的数据
  • 语法: let age = ref(14)
    • 创建一个包含响应式数据的引用对象RefImpl(reference:引用对象 简称ref对象, implement:实现)
    • js中修改数据: age.value = 33
    • 模板中读取数据: 不需要age.value, 直接{{age}}
  • 备注:
    • 接收的数据可以是: 基本类型, 也可以是引用类型
    • 基本类型的数据: 响应式依靠的是Object.defineProperty的 get 和set来完成.
    • 对象类型的数据:使用ref完成响应式,内部 求助了vue3中的新函数 reactive. reactive函数 内部用proxy, Proxy是window对象. 对象类型内部的值访问不用.value

5.3 reactive函数

  • 作用:只能定义对象类型的响应式数据, 不能定义基本类型的响应式数据, 基本类型用ref
  • 语法: const aaa = reactive(obj), 返回一个代理对象 proxy对象,
  • reactive定义的响应式数据是深层次的.
  • 内部基于ES6的Proxy实现,通过代理对象 操作 源对象内部数据 进行操作.

5.4 Vue3中的响应式原理

* vue2 的响应式:
    实现原理: 
        对象类型: 通过Object.defineProperty() 对属性的读取 修改 进行拦截(数据劫持).
        数组类型: 通过重写更新数组的一系列方法来实现拦截. (对数组的变更方法进行了包裹).
        Object.defineProperty(data, 'count', {
            get() {},
            set() {}
        })
    存在问题: 
        某个对象, 新增属性 删除属性 界面不会更新, 要用$set 和 $delete来解决
        直接通过下标修改数组 界面不会自动更新, 方法调用或者使用$set


* vue3 的响应式
    实现原理: 
        通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写 添加 删除等
    实现过程:
        person = {name: '李四', age: 15,}
        const p = new Proxy(person, {
            get(target,propName){
                target是源数据
                propName读取的某个属性的名(key值)
                return target[propName]
            },
            set(target, propName, value){  // 修改和新增都调用
                target[propName] = value
            },
            deleteProperty(target, propName) {
                return delete target[propName]
            },
        })

* Reflect反射
    对被代理对象的属性进行操作.
    除了obj点的方式 也可以用 Reflect
    ECMA 正在尝试着 把Object身上的很多有用的api 都移植到Reflect
    
        person = {name: '李四', age: 15,}
        const p = new Proxy(person, {
            get(target,propName){
                target是源数据
                propName读取的某个属性的名(key值)
                return Reflect.get(target, propName)
            },
            set(target, propName, value){  // 修改和新增都调用
                Reflect.set(target, propName, value)
            },
            deleteProperty(target, propName) {
                return Reflect.deleteProperty(target, propName)
            },
        })

5.5 reactive 对比 ref

  • 从定义数据角度对比:
    • ref用来定义基本类型的数据
    • reactive用来定义 对象或数组类型的数据
    • 备注: ref也可以用来定义对像或数组类型数据, 它内部会自动通过reactive转为代理对象
  • 从原理角度对比:
    • ref通过Object.defineProperty()的get与set来实现响应式(数据劫持)
    • reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据.
  • 从使用角度对比:
    • ref定义的数据: 操作数据需要.value, 标签中读取不用.value
    • reactive定义的数据: 操作数据与读取数据均不需.value

5.6 setup注意点

  • setup执行的时机: 在beforeCreate之前执行一次, this是undefined
  • setup的参数
    • props: 对象类型, 包含组件外部传递过来的 并且 组件内部已经声明接收的属性.
    • context: 上下文对象
      • attrs: 对象类型, 包含组件外部传过来的, 但是没在props中声明配置的.
      • slots: 收到的插槽内容,相当于this.$slots, vue3尽量用v-slot:aaa
      • emit: 分发自定义事件的函数,相当于this.$emit, 在子组件中添加 emits:{}配置项

5.7 vue3计算属性 和 watch函数

(1)vue3计算属性: 
* 引入computed
* person.fullName = computed( ()=>{   // 只读
    return person.firstName + '_' + person.lastName
} )

* person.fullName = computed( {   // 读写
    get() {
        return person.firstName + '-' + person.lastName
    },
    set(value) {
    const namearr = value.split('-')
        person.firstName = nameArr[0]
        person.firstName = nameArr[1]
    }
} )

(2)watch函数
* vue2的 watch
    简单写 watch: {
        sum(n, old) {
        }
    }
    复杂写 sum: { 
        immediate: true,
        deep: true,
        handler(n, old) {
        }
       }
* vue3的 watch 函数可以调多次
    情况一: watch(sum, (n,old)=>{ }, {immediate: true})   监视ref所定义的一个基本类型数据不用点value. 
    
    watch(person, (n,old) => {}, {deep:true})   监听ref的对象数据,监听的是对象的地址值. 
    若想监视对象内部属性值变化, 手动开启deep: true.  但是在不改变地址只改变属性值时, 此时无法正确的获取old.
    
    watch(person.value, (n,old) => {})   监听ref的对象数据也可用点value.
    
    
    情况二: watch([sum,name], (n, old)=>{ n和old 都是数组 }, {immediate: true})   监视ref所定义的多个基本类型数据
    
    -------------------------------------------------
    
    情况三: watch( person, (n, old)=>{}, {deep:false} )   监视reactive所定义的全部响应式数据,  此处无法正确获取old.  隐式强制开启了深度监听, deep:false无效.
    
    情况四: watch( () => person.name, (n, old)=>{ } )   监视reactive所定义数据内的某个原始类型的属性值, deep配置有效无意义
    
    情况五: watch(()=>person.job, (n, old)=>{ }, {deep: true})   监视reactive所定义数据内的某个对象类型数据, 无法正确获取old,  此时深度的更改job内部的数据 监听不到, 需要加上deep:true

    情况六: watch( [()=>person.name, ()=>person.age], (n, old)=>{ } )   监视reactive所定义的数据内的多个原始数据,  deep配置有效无意义
    
(3) watchEffect(()=>{})   // 不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性.
    watchEffect有点像computed,但是computed必须有返回值,而watchEffect更注重过程不用写返回值

5.8 生命周期

  • 可以通过配置项的形式写周期函数, 也可以通过组合api, 这两种都用时带on的优先级高
  • beforeCreate() ---> setup()
  • created() ---> setup()
  • beforeMount() ---> onBeforeMount()
  • mounted() ---> onMounted()
  • beforeUpdate() ---> onBeforeUpdate()
  • Updated() ---> onUpdated()
  • beforeUnmount() ---> onBeforeUnmount()
  • unmounted() ---> onUnmounted()

5.9 自定义hook函数

  • 类似于vue2.x 中的mixin
  • 本质是一个函数, 把setup函数中使用的组合式api进行了封装

5.10 toRef

  • 作用: 创建一个ref对象,其value值指向另外一个对象中的某个属性
  • 语法:const name = toRef(person, 'name')
  • toRefs所有属性都变成ref了, ...toRefs(person)

6. 其他组合api

6.1 shallowReactive 和 shallowRef

  • shallowReactive: 只处理第一层的响应式(浅响应式).
  • shallowRef: 只处理基本数据类型的响应式,不进行对象的响应式处理.

6.2 readonly 和 shallowReadonly

  • readonly: 让一个响应数据变为只读的(深只读)
  • shallowReadonly: 让一个响应式数据变为只读的(浅只读)
  • 应用场景: 不希望数据被修改时

6.3 toRaw 与 markRaw

  • toRaw作用: 将一个由reactive生成的响应式对象转为普通对象

  • 使用场景: 用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面的更新.

  • markRaw作用: 标记一个对象,使其永远不会再成为响应式对象.

  • 应用场景: 有些值不应被设置为响应式的,例如复杂的第三方类库. 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能.

6.4 customRef

  • 作用: 创建一个自定义的ref, 并对其依赖想跟踪和更新触发进行显示控制.
  • 实现防抖效果.
function myRef(value) {
    return customRef((track, trigger) => {
        return {
            get(){
                track()   // 追踪: 通知vue追踪value的变化
                return value 
            },
            
            set(newvalue){ 
                value = newvalue
                trigger()   // 触发:  通知vue去重新解析模板
            }
        }
    })
}

6.5 provide 与 inject

  • 作用: 实现祖孙组件间通信
  • 套路: 父组件有一个provide选项来提供数据,子组件有一个inject选项来开始使用这些数据
  • provide('car', car)
  • inject('car')

6.6 响应式数据的判断

  • isRef: 检查一个值是否为ref
  • isReactive: 检查一个对象是否为reactive
  • isReadonly: 检查一个值是否为只读
  • isProxy: 检查一个值是否 为proxy

6.7. Composition API的优势

  • Options API存在的问题: 使用传统OptionsAPI, 新增或者修改一个需求, 就需要分别在data methods computed里修改.
  • composition 组合式API的优势 我们可以更加优雅的组织我们的代码,函数. 让相关功能的代码更加有序的组织在一起.

7 新的组件 和 其他改变

7.1 Fragment

  • 在vue2中:组件必须有一个根标签
  • 在vue3中: 组件可以没有根标签,内部会将多个标签包含在一个Fragment虚拟元素中
  • 好处: 减少标签层级,减少内存占用

7.2 Teleport

  • 传送走 to="body"

7.3 Suspense

  • 引入 defineAsyncComponent
  • const child = defineAsyncCompontent(()=> {import('./components/Child')})
<Suspense></Suspense> 直接用这个组件把<child> 包裹起来

<Suspense>
    <template v-slot:default>
        <Child/>
    </template>
    <template v-slot:fallback>
        加载中......
    </template>
</Suspense>

7.4 其他

  • 全局API的转移
  • Vue3将全局的API,即 Vue.xxx 调整到应用实例 app 上.
Vue2.x全局APIVue3.x实例API
Vue.config.xxxapp.config
Vue.config.productionTip移除
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
Vue.prototypeapp.config.globalProperties
  • data声明始终是个函数, 在根组件中data也要return函数,
  • 过渡动画的类名的更改
  • 移除keyCode作为v-on的修饰符,同时也不再支持config.keyCodes
  • 移除v-on.native修饰符
  • 移除过滤器 filter

8. Vue3 的路由

router/index.js导入
    import { createRouter, createWebHashHistory } from "vue-router"
    history: createWebHashHistory(),
main.js:
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router/index.js'

    const app= createApp(App)
    app.use(router)

    app.mount('#app')
    
使用页面: 
    import { useRoute, useRouter } from 'vue-router'
    export default {
      setup () {
        const route = useRoute()
        const router = useRouter()
        router.push({
            name: 'home'
        })
        
        return {}
      },
    }

9. vue3 安装element-UI

* npm install element-plus -S  或者 yarn add element-plus
* 完全引入,  在main.js 中:
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    app.use(ElementPlus)

10. vue3 安装axios

* 安装 : yarn add axios
* utils / request.ts 中 import axios, { AxiosRequestConfig } from 'axios';

11. vue3 项目架构

* UI组件: element
* vux状态管理
    -   安装 piana
    -   store/index
    -   store/modules/......
* 网络请求
    -   安装axios, ant-design-vue
    -   utils / request.ts
    -   enums/cacheEnum
    -   /utils/Storage
    -   /store/modules/user
    -   api文件夹

* 前端路由 vue-router

* 引入字体图标 ###

* 全局样式 ###

* 自定义指令:

* hocks文件夹: hook本质是函数, 它把setup函数中使用的Composition API进行了封装.

* plugins 插件

* core

* Ueditor

* 各种配置文件: 23个

目录结构

src目录结构

12. Vite???

  • webpack---> 市场份额大,生态相对来说非常成熟
  • vite: 思维比较前卫而且先进的构建工具, 他确实解决了一些webpack解决不了的问题,同时降低了一些心智负担

12.1 什么是构建工具?

  • 企业级项目里都可能会具备哪些功能:
(1) typescript: 如果遇到ts文件我们需要使用tsc将typescript代码转换为js代码.
App.tsx 用tsc转成 App.jsx, 再用React-complier 转成 js件
(2) React/Vue: 安装react-compiler/ vue-complier, 将我们写的jsx文件或者.vue文件转换为render函数
(3) sass/less/postcss/component-style: 我们又需要安装sass-loader等等
(4) babel:  将es6+ 转为es5, 语法降级
(5) 体积优化: uglifyjs ---> 将我们的代码进行压缩,变成体积更好性能更高的文件
    以及代码分割等等

这个过程非常复杂, 我们的构建工具vite, webpack  就是为了 干这个而生的.
它将这些功能全部集中到一起, 我们只需要关心我们写的代码就好了,
我们写的代码一变化,  它就自动全部按个走一遍, 最终给我们几个 js文件就可以了.
这就是构建工具.
  • 构建工具都做了什么
(1) 模块化开发支持: 直接从node_modules里引入代码 + 多种模块化支持
(2) 兼容性和项目性能: 集成各种工具, 实现以上5个功能, 
(3) 构建工具自动帮你监听文件的变化,当你文件变化之后它会自动帮你调用对应的集成工具进行重新编译. 浏览器重新运行.
(4) 开发服务器: 跨域问题


打包: 将我们写的浏览器不认识的代码, 交给构建工具进行处理的过程叫打包.
构建工具他让我们可以不用每次都关心我们的代码在浏览器如何运行,我们只需要首次构建工具提供一个配置文件(config.js)

12.2 vite比webpack的优势

(1)
webpack 有commonjs的引入有es modules的引入
因为webpack支持多种模块化, 他一开始必须要统一模块化代码,所以意味着他需要将所有的依赖全部读一遍. 所以需要很长时间才能启动开发服务器.

vite 只有es modules的导入, 不需要把所有的依赖都读一遍, webpack更多的关注兼容性, 而vite关注浏览器端的开发体验
(2) webpack 是把所有的页面都编译
vite是用到哪个,编译哪个

使用yarn  create vite name 是脚手架create-vite在做的事情,不是vite
使用vue  create name  是脚手架vue-cli在做事情,而不是webpack

create-vite是vite的脚手架, create-vite内置了vite,  而vue-cli会默认内置webpack

12.3 vite启动项目初体验

  • 开箱即用(out of box), 不需要任何额外的配置