一文入门vue3+vite2+pinia

682 阅读24分钟

vue3.2出来也已经有很长一段时间了, 也零零碎碎学了vue3的一些知识, 做了些简单的小工程, 但是一直没有好好总结一遍, 这次就针对vue3工程中的vite, 组合式API, pinia等核心概念的角度出发, 系统介绍下vue3的入门知识.

安装

这块很简单, 直接参考官网就可以很快实现, 不过个人更加推荐使用pnpm, 是比npm和yarn更加高效简洁的包管理工具, 关于它的优点, 我在vue3源码学习--如何发布代码一文中有详细介绍, 这里就不再展开了.

# node.js >= 12.0.0
pnpm create vite my-vue3-template -- --template vue

# npm 6.x
$ npm init vite@latest my-vue3-template --template vue

# npm 7+,需要加上额外的双短横线
$ npm init vite@latest <project-name> -- --template vue

cd my-vue3-template
pnpm run dev

vite

首先, 我们来看下vite, 为何要从vite说起呢? 因为vue3是基于vite构建的, 而vite的'志向'也非常远大, 号称: 下一代前端开发与构建工具.

image.png

配置概况

vite的配置文件在根目录的vite.config.js, 当我们执行vite命令的时候, 会自动到根目录中去解析该文件

当然, 我们也可以通过--config参数来显示指定配置文件名

vite --config my.config.js

注意, 即使没有在package.json文件中定义"type": "module", 该配置文件也能够使用ESM语法.

配置文件的形式主要有以下几种:

导出方式

vite的配置, 从导出的方式来看, 主要有三种: 对象, 工具函数

对象

这种方式最为简单, 直接导出一个对象,

因为vite本身就是支持typescript的, 所以我们可以通过@type {import('vite').UserConfig}来获取IDE和jsdoc的智能提示! 我们可以在输入配置项的时候, 得到IDE的联想, 这样可以减少写错配置项名称这种低级错误

/**
 * @type {import('vite').UserConfig}
 */
export default {
  base: './'
}

工具函数defineConfig

使用defineConfig工具函数, 这么做的话, 无需jsdoc注解也能获得只能提示, 其中工具函数按照入参的类型不同, 又能够分为两种类型:

  • 对象
import { defineConfig } from 'vite'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
  base: '/',
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
})
  • 函数

使用函数的方式主要好处就在于, 我们可以根据不同的命令/模式来进行配置! 再就是我们可以不需要通过jsdoc配合IDE进行智能提示.

// 通过command的不同,我们可以决定让哪些配置在哪些环境下生效(生产? 开发?)
command: "build" | "serve"

其实就是我们执行vite xx, xx就代表了这里的命令. 在vite中, vite serve 和 vite dev 都是vite命令的别名.当然, 无论你是vite, vite dev 还是vite serve , command都为serve.

// 同步函数配置
export default defineConfig(({ command, mode }) => {
  if (command === 'serve') {
    return {
      // dev 独有配置
    }
  } else {
    // command === 'build'
    return {
      // build 独有配置
    }
  }
})

// 异步函数配置
export default defineConfig(async ({ command, mode }) => {
  const data = await asyncFunction()
  return {
    // 构建模式所需的特有配置
  }
})

base配置

类型: string

默认: /

base, 也就是公共基础路径, 有三种形式

  • 根路径, 例如: /my-public-path/
  • 完整的路径, 例如: foo.com
  • 空字符串或者相对路径 ./(用于开发环境)

alias配置

类型: Record<string, string> | Array<{ find: string | RegExp, replacement: string }>

这个配置熟悉webpack的小伙伴一定知道就是别名

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: [
      {
        find: '@',
        replacement: path.resolve(__dirname, './src')
      }
    ],
    extensions: ['.js']
  }
})

而从类型上我们可以看出, 其配置项有两种写法, 除了以上代码中的写法, 还有一种就是和webpack一样的配置方式

...
export default defineConfig({
...
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
...
})
...

plugins配置

从名字就可以看出,这个是插件的配置项, 和webpack的配置一样

类型: 数组

强制插件排序

通过修饰符enforce, 我们可以强制该插件在核心插件调用之前调用

enforce修饰符

类型: pre | post

默认为post

pre: 在vite核心插件之前调用

enforce默认在核心插件之后调用

post:在vite构建插件之后调用

export default defineConfig(({command, mode}) => {
  console.log(command)
  return {
    base: './',
    plugins: [{
      ...vue(),
      enforce: 'pre'
    }]
  }
})

按需应用

前面说过, 配置使用函数的形式, 可以获取command命令的值, 这里要介绍的apply属性可以让我们在配置为对象形式下, 同样按照command不同而应用不同的插件配置

export default defineConfig({
  base: './',
  plugins: [{
    ...vue(),
    apply: 'build'
  }]
})

常见插件

好了, 说了插件的基本配置规则, 再来介绍下几个常见插件

@vitejs/plugin-vue

主要为用vite构建的vue的sfc文件提供支持.

主要作用在于,

  1. 支持单文件组件的解析.
  2. 支持SFC以自定义元素的形式导入我们的页面.

具体的属性, 我们可以直接参考其源码中的类型声明文件, 为了方便理解, 英文原注释仍然保留

// @vitejs/plugin-vue/dist/index.d.ts
export declare interface Options {
    /** 
    * 哪些文件将被纳入@vitejs/plugin-vue编译的范畴,
    * 如果我们将这个选项设为非.vue结尾, 则vue文件将无法被渲染出来
    * 默认值:  /.vue$/
    */
    include?: string | RegExp | (string | RegExp)[];
    /** 
    * 哪些文件将不被纳入@vitejs/plugin-vue编译的范畴,
    * 如果我们将include和exclude同时设置为/.vue$/, 则最终起作用的
    * 将是exclude
    */
    exclude?: string | RegExp | (string | RegExp)[];
    /** 
    * 是否为生产环境
    */
    isProduction?: boolean;
    script?: Partial<SFCScriptCompileOptions>;
    template?: Partial<SFCTemplateCompileOptions>;
    style?: Partial<SFCStyleCompileOptions>;
    /**
     * Transform Vue SFCs into custom elements.
     * - `true`: all `*.vue` imports are converted into custom elements
     * - `string | RegExp`: matched files are converted into custom elements
     *
     * @default /.ce.vue$/
     * 默认是以.ce.vue结尾的文件
     * 如果此处匹配失败, 或者直接就是false,
     * 则开发环境自定义元素将无样式,
     * 生产环境下自定义元素的SFC的样式,将被单独打包成一个css文件
     */
    customElement?: boolean | string | RegExp | (string | RegExp)[];
    /**
     * Enable Vue reactivity transform (experimental).
     * https://github.com/vuejs/core/tree/master/packages/reactivity-transform
     * - `true`: transform will be enabled for all vue,js(x),ts(x) files except
     *           those inside node_modules
     * - `string | RegExp`: apply to vue + only matched files (will include
     *                      node_modules, so specify directories in necessary)
     * - `false`: disable in all cases
     *
     * @default false 默认为false
     */
    reactivityTransform?: boolean | string | RegExp | (string | RegExp)[];
    /**
     * Use custom compiler-sfc instance. Can be used to force a specific version.
     */
    compiler?: typeof _compiler;
}

SFC中引入并使用自定义元素

// app.vue
<template>
  <div>
    <my-custom-element></my-custom-element>
  </div>
</template>
<script setup>
  import Test from './components/test.ce.vue'
  import { defineCustomElement } from 'vue'
  const myElement = defineCustomElement(Test)
  customElements.define('my-custom-element', myElement)
</script>

自定义元素

// test.ce.vue
<script setup>

defineProps({
  msg: String
})

</script>

<template>
  <h1 class="title">{{ msg }}</h1>
  <div>
    this is my custom element
  </div>
</template>

<style scoped>
.title {
  color: #42b983;
}
</style>

最后还要注意, @vitejs/plugin-vue在1.9.0+和vue3.2.13+中, @vue/compiler-sfc将不再是peerdependency

@vitejs/plugin-vue插件最终导出的数据是

其中, config, configResolved, configureServer, handleHotUpdate 为vite独有的钩子

vite-plugin-mock

vite-plugin-mock, 是一款基于mockjs, 为vite提供本地/生产环境下的mock数据的插件

版本要求: node.js >=12.0.0

属性:

// vite入参
interface ViteMockOptions {
  // mock文件夹地址, 注意是文件夹, 不是文件!
  // 如果watchFiles选项为true, 则将自动监听该文件夹的变化
  // 默认值: mock, 也就是根目录下的mock文件夹
  mockPath?: string;
  configPath?: string;
  ignore?: RegExp | ((fileName: string) => boolean);
  /** 
  * 是否实时监听mockPath指向的那个文件夹里的文件
  * @default: true
  */
  watchFiles?: boolean;
  /** 设置是否本地开启mock
  * @default cammand === 'serve', 也就是开发阶段开启
  * @type: boolean
  * - true: 开发开启本地mock
  * - false: 开发环境不开启, 生产阶段开启
  */
  localEnabled?: boolean;
  /** 设置是否在生产环境开启mock
  * @default cammand !== 'serve'
  * @type: boolean
  * - true: 生产环境开启mock
  * - false: 生产环境关闭mock
  */
  prodEnabled?: boolean;
  /** 生产环境下(prodEnabled = true情况下), injectCode配置的代码将被注入的文件
  * @default path.resolve(process.cwd(),'src/main.{ts,js}')
  */
  injectFile?: string;
  /** 生产环境下引入mock代码
  * @default 'code content'
  */
  injectCode?: string;
  /**
   * Automatic recognition, no need to configure again
   * @deprecated Deprecated after 2.8.0
   */
  supportTs?: boolean;
  /** 是否需要打印请求日志
  * @default true
  * @type: boolean
  */
  logger?: boolean;
}

使用案例:

  • vite.config.js配置文件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteMockServe } from 'vite-plugin-mock'
import path from 'path'

export default defineConfig(({command}) => {
  return {
    base: './',
    plugins: [
      vue(),
      viteMockServe({
        // 这里的意思是: 根目录下的mock文件夹
        mockPath: 'mock',
        watchFiles: true,
        localEnabled: command === 'serve',
        // 加上injectCode和injectFile,并且proEnabled为true的时候, mock数据会被一并打包
        prodEnabled: command === 'build',
        injectCode: `
          import { setupProdMockServer } from './server.ts'
          setupProdMockServer()
        `,
        injectFile: path.resolve(__dirname,'src/main.js')
      })
    ],
    resolve: {
      alias: [{
        find: '@',
        replacement: path.resolve(__dirname, './src')
      }]
    }
  }
})
  • mock数据文件, 这就是前面说过的mockPath文件夹中的文件
// mock/index.ts
import { MockMethod } from 'vite-plugin-mock'

let arr:MockMethod[] = [{
  url: '/api/url',
  method: 'get',
  response: () => {
    return {
      code: 0,
      msg: 'ok',
      data: {
        name: 'jack',
        age: 12
      }
    }
  }
}]
export default arr
  • vue文件调用mock接口
// App.vue
<script setup>
  let value = ref({})
    fetch('/api/post', {
    method: 'post'
  }).then(res => {
    return res.json()
  }).then(res => {
    value.value = res
  })
</script>
  • 效果如下, 我们就可以通过请求mock接口来获取数据了

vite-plugin-style-import

vite-plugin-style-import 是一款按需引入第三方库样式的vite插件, 文档

直观点来看, 就是在按需引入第三方组件库的时候, 简化写法, 这种类似的插件在vue2的组件库中经常见到, 诸如: babel-plugin-component , babel-plugin-import等(如果不知道啥意思, 可以去看看element-ui 或者 vant等组件库的引入说明). 但是进入vue3+vite时代, 这部分工作将由vite-plugin-style-import 取代

参数名值类型默认值说明
includestring | RegExp | (string | RegExp)[] | null | undefined[**/*.js, **/*.ts, **/*.tsx, **/*.jsx]被纳入插件处理的文件
exlucde同上node_modules/**被排除的文件
rootstringprocess.cwd(), 即终端执行命令的文件夹层级通常是根目录
resolvesLib[], Lib类型见下表数组的元素都是本插件内置好的resolve(可以理解为'预设'), 本插件内置了针对几个大型组件库的resolve(点击可查看对应的组件库):AntdResolveAndDesignVueResolveElementPlusResolveVantResolveNutuiResolveVxeTableResolve
libsLib[], Lib类型见下表当现有的resolve无法满足我们的需求的时候, 这时,我们就需要自己来写一个'resolve', 只不过是放在libs中, 所以, resolves和libs两者写一个就够了

Lib:

参数名值类型默认值说明
libraryNamestring需要引入的第三方组件库的名字
esModulebooleanfalse如果引入的样式文件不以.css结尾, 这个根据具体使用的组件库来决定是否开启
importTestRegExp正则方式匹配第三方库名(未实现)
resolveStyle(name: string) => string简单理解就是, 这个函数返回的就是某个组件的样式文件所在路径! name就是这个组件的名字, 例如: Button这种;
ensureStyleFilebooleanfalse有些库, 不太规范, 样式文件可能不存在, 为了防止报错, 可以将此选项开启,仅生产环境可用
transformComponentImportName(name: string) =>string改变引入组件的组件名, 仅生产环境可用(未实现)

下面我们引入vant组件库试试看

首先我们继续往我们的vite.config.js配置文件添加代码

// vite.config.js
...
import { defineConfig } from 'vite'
import styleImport, { VantResolve } from 'vite-plugin-style-import'
...
// 使用resolve
export default defineConfig(({command}) => {
  return {
    base: './',
    plugins: [
      ...
      styleImport({
        resolves: [
          VantResolve()
        ]
      })
    ]
  }
})

等同于以下写法:

...
styleImport({
  libs: [
    {
      libraryName: 'vant',
      esModule: true,
      resolveStyle(name) {
        return `vant/es/${name}/style` // 单个组件样式文件所在地址
      }
    }
  ]
  // resolves: [VantResolve()]
}),
...

在vue文件中使用

// App.vue
<template>
  <div>
    <Button>按键</Button>
  </div>
</template>
<script setup>
  import { createApp } from 'vue'
  // 未使用插件,如果要按需加载, 需要这样写:
  // import Button from 'vant/es/button/index';
  // import 'vant/es/button/style/index';
  // 使用了插件之后, 我们可以这样引入
  import { Button } from 'vant'
  createApp(Button)
</script>

@vitejs/plugin-vue-jsx

@vitejs/plugin-vue-jsx可以让我们在vue中使用jsx, 例如我们想要开发一个jsx组件的时候, 可以配置这个插件, 文档

// vite.config.js
...
import { defineConfig } from 'vite'
import vueJsx from '@vitejs/plugin-vue-jsx'
...
// 使用resolve
export default defineConfig(({command}) => {
  return {
    base: './',
    plugins: [
      ...
      vueJsx()
      ...
    ]
  }
})

页面使用

// jsxComponent.jsx
import { defineComponent } from 'vue'
export default defineComponent({
  render() {
    return <div>123</div>
  }
})

css 配置

css配置项, 主要用于处理 css预处理器, postcss, css模块化等方面的配置

类型:

Record<string, any>

preprocessorOptions预处理器配置项

preprocessorOptions配置项专门处理各预处理器的配置, 基本的格式就是

预处理器名: { 预处理器的配置项 }

这个配置项用于处理各种css预处理器的配置项, 诸如: scss, less, stylus等, 下面我们以sass为例

npm i sass -D

而我们可以把sass的配置项, 以及less的配置项

...
css: {
    preprocessorOptions: {
      scss: {
        // 全局变量$red
        additionalData: '$red: #333;'
      },
      less: {
        // 此处引入vant样式库的全局样式变量, 如此, 我们就可以在代码中使用这些第三方库
        // 的变量了
        additionalData: '@import "vant/es/style/var.less";'
      }
    }
  }
...

vue文件

<style lang="less">
#app {
  background: @red;
}
</style>

postcss

看名字就知道了, 这就是postcss的配置项

postcss根据官方文档的描述就是一个‘使用javascript工具和插件转换css代码的工具’

那么为何要转换? postcss不同于预处理器(scss, less, stylus等), 它不会提供一些新的特定的更简便的css写法(比如预处理器支持的嵌套写法, 遍历方法等), 而是专注于如何让css代码有更好的可读性, 兼容性, 更少的错误, 让我们能使用更新的css语法!

所以vite中的这个postcss配置项其实就是postcss这个工具的配置项. 其格式参考postcss-load-config格式, 下面我们重点介绍autoprefixer和postcss-modules这两个插件及其配置参数.

  1. autoprefixer

首先我们需要安装Autoprefixer插件, 这款插件简单说就是为了兼容问题, 给一些特定的样式加上一些浏览器厂商的前缀, 比如-ms-, -webkit-, -moz-等等. 先来看看其主要的参数:

参数类型默认值说明
overrideBrowserslistArray-这个配置项并不被官方推荐, 最好是使用package.json中的browserslist 或者根目录下的.browserslistrc配置
flexboxboolean | stringtrue是否为弹性盒子的样式增加前缀, 默认是true, 当值
gridfalse'autoplace'针对grid网格布局做的兼容性操作
addBooleantrue是否增加前缀
cascadeBooleantrue是否压缩css代码
removeBooleantrue是否将过时的前缀删除

下面, 捡几个重点的配置项来介绍:

1.1 overrideBrowserslist

这里需要注意的一个配置项是overrideBrowserslist, 官方文档对它的描述是:

overrideBrowserslist (array): list of queries for target browsers. Try to not use it. The best practice is to use .browserslistrc config or browserslist key in package.json to share target browsers with Babel, ESLint and Stylelint. See Browserslist docs for available queries and default value.

简单来说, 就是你可以配在.browserslistrc文件中, 也可以配置在package.json的browserslist属性中, 总之就是配在哪儿都比配在这里好🐶! 格式参考browserslist文档, 这个配置项可以说是本插件中最重要的一个配置, 因为后续很多配置项其实是根据这个配置来发挥作用的!

本插件在vite.config.js中的配置

...
css: {
  ...
  postcss: {
    plugins: [
      require('autoprefixer')({
        overrideBrowserslist: [
          "Android 4.1",
          "iOS 7.1",
          "Chrome > 31",
          "ff > 31",
          "ie >= 8"
        ]
      })
    ]
  }
}
...

package.json中的browserslist项的配置

...
"browserslist": [
  "Android 4.1",
  "iOS 7.1",
  "Chrome > 31",
  "ff > 31",
  "ie >= 8"
],
...

.browserslistrc中的配置

# 这就是注释
Android 4.1
iOS 7.1 # IOS 7.1自带浏览器
Chrome > 31 # Chrome 31以上的浏览器
ff > 31
ie >= 8 #兼容大于或等于IE8的浏览器
> 1% # 大于1%的使用量的浏览器
last 1 version # 最近一个版本
Firefox ESR # 最新的火狐 ESR (长期支持版) 浏览器

注意, 如果什么都不配, 那么默认配置为

> 0.5% 全球超过.5%的用户使用的浏览器
last 2 versions # 兼容所有浏览器最近的2个版本
Firefox ESR # Firefox 企业版
not dead # 不要淘汰了的浏览器

如果大家不知道自己项目的browserslist配置到底是个啥情况, 可以通过npx browserslist来查看

.browserslistrc
> 0.5%
ie < 8
FireFox > 96
not dead
> npx browserslist
and_chr 99
and_uc 12.12
chrome 98
chrome 97
chrome 96
edge 98
edge 97
firefox 98
firefox 97
firefox 96
ie 11
ios_saf 15.2-15.3
ios_saf 15.0-15.1
ios_saf 14.5-14.8
ios_saf 14.0-14.4
ios_saf 12.2-12.5
op_mini all
opera 83
safari 15.2-15.3
safari 14.1
samsung 16.0

1.2 flexbox

该配置项表示: 是否为flexbox布局添加前缀, 注意,这里的是否添加其实也是依据browserslist配置来的, 如果, 你的浏览器环境选了比较新的版本, 那实际上也是不会添加前缀的

// vite.config.js
...
postcss: {
    plugins: [
      require('autoprefixer')({
        flexbox: true
      })
    ]
 }
...
.browserslist
last 10 versions
// 源码
.flexbox {
  display: flexbox;
  justify-content: space-around;
}

// 编译后
.flexbox[data-v-e3f10b04]{
  display:flexbox;
  -ms-flex-pack:distribute;
  justify-content:space-around
}

1.3 grid

是否为IE填充网格布局, 这个属性是专门为万恶的IE提供的, 由于IE实在太恶心了🐶, 所以特意拿出来讲讲, 椅背不时之需. 当我们使用了display: grid; 注意, 当我们同时设置了grid-template-rowsgrid-template-columns的时候, 该配置项才会生效!

// 源代码
#app {
  display: grid;
  grid-template-rows: auto;
  grid-template-columns: 1fr 1fr;
}
  • grid: 'autoplace'
// 编译代码
#app{
  display:-ms-grid;
  display:grid;
  -ms-grid-rows:auto;
  grid-template-rows:auto;
  -ms-grid-columns:1fr 1fr;
  grid-template-columns:1fr 1fr
}
#app>*:nth-child(1)
  {
    -ms-grid-row:1;
    -ms-grid-column:1
  }
#app>*:nth-child(2)
  {
    -ms-grid-row:1;
    -ms-grid-column:2
  }
  • grid: 'no-autoplace'
// 编译代码
#app{
  display:-ms-grid;
  display:grid;
  -ms-grid-rows:auto;
  grid-template-rows:auto;
  -ms-grid-columns:1fr 1fr;
  grid-template-columns:1fr 1fr
}

关于grid参数的配置, 除了像上面那样在配置文件中配置, 还有另外两种配置方式:

  • 注释配置

如果我们不使用配置, 使用注释, 也能起到效果, 而且, 权重高于Autoprefixer.grid配置

// 相当于grid配置了autoplace
/* autoprefixer grid: autoplace */
#app {
  display: grid;
  grid-template-rows: auto;
  grid-template-columns: 1fr 1fr;
}

// 相当于grid配置了no-autoplace
/* autoprefixer grid: no-autoplace */
#app {
  display: grid;
  grid-template-rows: auto;
  grid-template-columns: 1fr 1fr;
}
  • cross-env配置 我们通过修改script中的命令, 在cross-env后面加上不同的参数来实现配置的效果
// 配置no-autoplace
"build": "cross-env AUTOPREFIXER_GRID=no-autoplace vite build",

// 配置autoplace
"build": "cross-env AUTOPREFIXER_GRID=autoplace vite build",
  1. postcss-modules

一直以来, 模块化的概念始终在js领域不断发展, 诸如: commjs/es Module/AMD等, 都为js的发展和进步,提供了动力, 但是css似乎并没有那么的'风生水起', 全局样式污染始终是一个客观存在的问题, 因此, css也需要模块化解决方案, 而postcss-modules就是其中的解决方案之一. 其存在的核心就是: 为class/id名加上一些独一无二的前后缀, 得到一个新的类名, 以此解决全局样式污染.

postcss-modules常用参数一览表:

参数类型默认值说明
scopeBehaviour'global''local'这个配置项其实可以理解为是否开启防全局污染处理, 如果选择了'global', 则不再为class/id添加局部性标示, 对应的设置, 诸如getJSON,generateScopedName都将无效
getJSON(cssFileName: string, json: Record<string, string>,outputFileName: string) =>void-获得旧类名和新类名的对应关系, 即一个json对象, 你可以在本回调函数中处理这个对象, 比如, 将它存入本地一个json文件中.
generateScopedNamestring((name: string, filename: string, css: string) =>string)-新类名的命令方式, string类型的时候, 可以使用[name][hash]等方式动态定义新类名;函数模式下, 函数的参数: 类名, css文件路径, css文件的内容.

2.1 案例解析

通过getJSON, 我们可以获取新旧样式类/id名的映射文件(.json格式), 并将其生成出来, 我们就可以通过这张映射表, 将正常的class/id名转换为带局部性标示的类, 以防止全局污染!

{
// vite.config.js
  ...
  modules: {
    // generateScopedName生成局部化类/id名
    // 1. generateScopedName为字符串的时候
    // name为文件名, local为具体的类/id名
    generateScopedName: '[name]__[local]___[hash:base64:5]'
    // 2. generateScopedName为函数的时候
    // generateScopedName(name, filePath, cssContent){
    //   console.log('name', name) // 样式表中的class/id名
    //   console.log('filePath', filePath) // 样式文件路径
    //   console.log('cssContent', cssContent) // 样式表具体内容
    //   // 获取文件名,不含后缀
    //   let fileNameNoExt = path.basename(filePath, '.less')
    //   return `${fileNameNoExt}__${name}`
    // },
    // 获取json映射表
    getJSON(cssFileName ,json, outputfile) {
      const path = require('path');
      // 获取样式文件名
      const cssName = path.basename(cssFileName, '.less');
      // json 文件输出路径(/build/样式文件名.json)
      let jsonFilePath = path.resolve('./build/' + cssName + '.json');
      // 写入指定的文件
      fs.writeFileSync(jsonFilePath, JSON.stringify(json))
    }
  }
  ...
}
// index.module.less
.person {
  .name {
    color: black;
  }
  
  .age {
    color: gray
  }
}
// index.module.json
{"person":"_person_o33uf_1","name":"_name_o33uf_1","age":"_age_o33uf_4"}
<template>
  <div :class="transform('person')">
    <div :class="transform('name')">名字</div>
    <div :class="transform('age')">年龄</div>
  </div>
</template>
<script setup>
    import '@/assets/index.module.less'
    import json from '../../build/index.module.json'
    // class/id转换方法
    const transform = (className) => {
      return json[className]
    }
</script>

image.png 2.2 两种配置方式: 值得注意的是, postcss-modules在vite当中, 有两种配置方式, 一种是上面我们展示的, 也是推荐的, 直接在css.modules下配置; 还有一种, 你也可以单独把postcss-modules插件引入进来作为plugins的方式引入

  • css.modules方式配置

本文虽然把postcss-modules分在在postcss标题之下(因为它确实只是postcss的一个插件), 但是, 在vite当中,它对应了一个配置项是css.modules项, 是和css.postcss配置项平级的! 我们可以直接在css.modules配置项之下去配置postcss-modules插件的参数, 而且无需npm i postcss-modules安装! 但是有个条件, 那就是想被模块化的样式文件, 其文件名都必须是xx.module(.css|.less|.scss).

{
// 在vite中, 两者实际上是平级的存在
  ...
  postcss: {
    // 配置项
  },
  modules: {
    // 配置项
  }
  ...

}
  • plugins方式配置

当然, 你也可以不用css.modules配置项, 而选择直接在css.postcss.plugins中去直接使用postcss-modules, 但是如果这样, 那就必须手动安装postcss-modules! 同时, 即使你不在文件名上加xx.module.xx, 插件也可以对其直接起作用, 这种就是完全按照插件的方式来使用了.当然个人觉得没啥必要

npm i postcss-modules -D
{
  ...
  postcss: {
    plugins: [
      require('autoprefixer')({
        grid: 'autoplace',
        add: true
      }),
      require('postcss-modules')({
        ...
      })
    ]
  },
  // modules: {
    // ...
  //}
  ...

}

build 配置

build配置主要负责构建方面的一些功能,也就是我们执行vite build时, 需要用到的配置项

minify

minify非常关键, 会影响后续很多配置项, minify主要是决定使用哪种混淆器进行混淆.

类型: boolean | 'terser' | 'esbuild'

默认: 'esbuild'

false的时候, 则是禁止混淆, 代码将不压缩直接展示.

true的时候, 默认使用的是'esbuild'进行混淆.

比 terser 快 20-40 倍,压缩率只差 1%-2%, 因此, 默认情况下是使用esbuild进行混淆

关于更多esbuild的知识, 可以参考官方文档

target

类型: string | string[]

默认: modules(代码执行环境为支持esModule的浏览器)

设置最终构建的浏览器兼容目标, target的值可以是浏览器版本号或者es的版本号, 可以是单个版本号, 也可以是版本号组成的数组. 但是, modules不能放进数组中(['modules']), 否则报错!

参数值说明
modules默认值, 表示支持esModule的浏览器环境
esnext按最新的javascript和css特性生成代码. 其实在原生的esbuild中, 本选项才是默认选项
浏览器版本注意, 如果minify配置了terser, 则esnext会自动降级为es2019! 这里的所谓版本号的格式为: 浏览器厂商名+版本号, 中间不留空格, 只能是诸如 chrome79, firefox88等形式
defineConfig(({cammand}) => {
  return {
    build: {
      minify: "esbuild",
      target: "modules"
    },
    ...
    plugins: [
      legacy({
        targets: {
          "ie": "8",
          "chrome": "89"
        }
      })
    ]
    ...
  }
})

但是, 在vite中, 只支持ESM! 所以我们需要借助@vitejs/plugin-legacy进一步兼容老浏览器

@vitejs/plugin-legacy主要参数

参数类型默认值说明
targetsstring |string[] | { [key: string]: string } | defaultsdefaults简单来说,这一项, 其实是browserslist和@babel/preset-env的合体:当为string和string[]类型的时候, 其实就是browserslist中的配置项; 当为对象时, 就是@babel/preset-env中targets的入参
polyfillsboolean | string[]true这个其实就是一个polyfill的列表, 默认情况下, 就相当于@babel/preset-env中的useBuiltIns的'usage',也就是用到了才会引入那个polyfill. 当然, 判断是否需要某个polyfill, 还要根据targets中的配置来决定, 如果targets的目标环境已经不需要某个polyfill, useBuiltIns: 'usage'下就不会引入该polyfill
ignoreBrowserslistConfigbooleanfalse是否忽略.browserslistrc和package.json中的browserslist的配置

terserOptions

注意: 只有minify为terser的时候, 本配置项才能起作用, 所以前面介绍minify的时候就强调过该项会影响后续的配置! 如果只配置了terserOptions不配置minify: 'terser', vite会给出警告

build.terserOptions is specified but build.minify is not set to use Terser. Note Vite now defaults to use esbuild for minification. If you still prefer Terser, set build.minify to "terser".

terserOptions其实是terser中的minify这个api的配置项, 和前面的css.modules类似, 都是把第三方插件的配置项直接拿出来作为vite的一个配置项使用.

而在这之前, 我们先来了解下关于terser的一些知识, 关于它的概念,取官方文档的一句话

A JavaScript mangler/compressor toolkit for ES6+.

简单来说, 就是一个js压缩工具, 说到js的压缩工具, 我们肯定会想到uglify-js,不错, 这个工具已经是我们的老熟人了, 但是, 很遗憾它不支持es6, 而uglify-es也不再维护了!

outDir

类型: string

默认: dist

这一项很好理解, 就是构建后输出的文件夹的存放路径

assetsDir

类型: string

默认: assets

构建出的dist中的静态资源存放的文件夹存放路径

esBuild

该配置项继承了esbuild的配置入参

类型: ESBuildOptions | false

默认会将jsx, tsx, ts纳入其转换目标对象

但是, 其转换jsx的函数默认为React.createElement, 我们可以操作来验证下

# 终端输入
echo '<div>hello</div>' | esbuild --loader=jsx
#输出: /* @__PURE__ */ React.createElement("div", null, "hello");

因此我们需要使用jsxFactory配置项将起改为h函数来处理, 如果不这么做, 会报出'React is not define错误'

...  
esbuild: {
  jsxFactory: 'h',
  jsxFragment: ''
}
...

组合式API

什么是组合式API?

概念就不介绍了, 官网写得更清晰专业, 关于vue3的这个新特性, 我个人理解就是, 按我们自己的业务逻辑来自由组织代码, 而不是按照vue框架的选项来组织

vue2中, 我们的SFC代码组织方式都是以选项为标准将我们的代码分割成诸如: data, methods, computed, mounted .... 中, 这种就是选项式API. 而这么做的痛点就是, 我们的逻辑点被分割了! 比如官网案例中, 一个搜索的逻辑, 被分散到了methods, data, mounted, watch, computed等选项中, 如果代码量少还好, 如果页面复杂了, 那么我们每次更新代码或者寻找bug的时候, 都要上下'反复横跳', 很不利于维护.

<script>
// 案例中, 相同的数字代表同一个逻辑点, 这里重点关注1逻辑点, 我们可以看到, 1逻辑点和2/3逻辑点
// 相互交错在一起, 这样的代码维护起来肯定也会有诸多的不便.
export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { 
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // 使用 `this.user` 获取用户仓库
    }, // 1
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}
</script>

那么如果是组合式API呢? 我们可以这样:

<script>
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'

// 在我们的组合式组件中, 搜索的逻辑被聚合在了setup中, 原本的data,methods,mounted
// 中的逻辑被放到了一起
setup (props) {
  ...
  // 上面案例中的逻辑1, 也就是搜索部分
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(props.user)
  }
  onMounted(getUserRepositories) // 在 `mounted` 时调用 `getUserRepositories`
  ...
  return {
    repositories,
    getUserRepositories,
    ...
  }
}
</script>

如何使用组合式API?

通过以上的案例,相信你已经发现了, 所有的组合式API都在一个setup函数中, 不错, 这就是组合式API的入口, 有了他, 组合式API才能起作用, setup是组合式API的一个入口

setup函数

关于setup函数, 我们需要先认识这几个知识点:

  • 调用时机上, setup是在beforeCreated之后, created之前, 所以在setup中无法获取this对象.
  • 参数上接受两个参数: props和context
  • 返回值是一个对象, 这个返回的对象的所有属性, 将被作用到组件模板之上.

props参数

关于props这个参数,我们主要介绍的就是解构, 我们不能直接对props进行解构!而要使用toRefs这个响应式API进行处理

<template>
  <div class="userInfo">
    姓名: <span>{{datas.name}}</span>
    年龄: <span>{{datas.age}}</span>
  </div>
</template>
<script>
import {toRefs} from 'vue'
export default {
  props: ['info'],
  setup(props) {
    // 使用toRefs才能保证响应特性
    const {name, age} = toRefs(props.info)
    // 如果我们直接解构, 数据无法响应
    // const {name, age} = props.info
    return {
      datas: {
        name,
        age
      }
    }
  }
}
</script>

context参数

setup的第二个参数context 包含了四个属性: attrs, slots, emit, expose. 并且由于context是非响应式的, 所以我们可以直接对其进行解构.

<script>
  ...
  setup(props, {attrs, slots, emit, expose}) {
    ...
  }
  ...
</script>

但是要注意, attrs 和 slots都是具有状态的属性, 会随着组件的更新而更新, 所以不建议对他们使用解构, 而是采用attrs.xx的形式调用属性值.

  • attrs

先来说说attrs, 其实attrs也很好理解, 就是属性的意思, 熟悉vue2的小伙伴应该知道,其实就是写在组件上而又不在该组件props上的属性

// App.vue
<template>
 <setup-component :attrsTest="attrsTest"></setup-component>
</template>

attrtsTest属于组件setup-component的一个属性, 却不在其内部定义的props中, 所以, 就被归为了attrs

// setup-component.vue
<script>
  export default {
    props: ['name', 'age'],
    setup (props, {attrs}) {
      ...
      console.log(attrs.attrsTest) // 初始化的时候打印
      ...
    }
  }
</script>

注意, 此时的attrs只在初始化的时候执行一次, 无法做到响应式更行, 如果想要做到实时监听属性值变化,需要使用生命周期钩子onBeforeUpdate

// setup-component.vue
<script>
  import { onBeforeUpdate } from 'vue'
  export default {
    props: ['name', 'age'],
    setup (props, {attrs}) {
      ...
       onBeforeUpdate(() => {
        console.log('🚀 -->', attrs.attrsTest)
      })
      ...
    }
  }
</script>
  • slots

其实就是获取插槽内组件的实例, 作用等同于$slots

<script>
export default {
  setup(props, {slots, attrs, emit, expose}) {
    console.log(slots.default(), 'slots') // 获取匿名插槽组件实例
    return {}
  }
}
</script>
  • emit

作用和$emit相同, 这里就不赘述了

  • expose

介绍这个属性的用法之前, 我们得先来认识一下渲染函数这个知识点. 我们刚才介绍的setup都是返回一个对象, 然后再让对象中的属性作用于组件. 但是如果我们返回一个render函数呢?

// setup-component.vue
<script>
import {toRefs, ref, onBeforeUpdate, h} from 'vue'
export default {
  props: ['name', 'age'],
  setup(props) {
    return () => h('div', [props.name, props.age])
  }
}
</script>

可以看到, 此时组件只有script部分, template已经为空了, 渲染的内容由渲染函数渲染出的内容取代.

此时, 我们面临一个问题: 之前, 如果我们想把setup中的某个属性/方法暴露给组件, 只需要return一个对象即可, 但是现在, 我们return已经被渲染函数'霸占'了, 如果我们想要在父组件通过模版引用的方式(vue2中的this.$refs.xx)来获取该组件上的某些属性/方法(也就是本应该被setup返回的对象中的内容), 那该怎么做呢? 这时候, 就可以用到expose了

// 子组件
<script>
import { ref, h, toRefs} from 'vue'
export default {
  props: ['name', 'age'],
  setup(props, {expose}) {
    const {name, age} = toRefs(props)
    let count = ref(0)
    // 父组件调用本方法
    const addCount = () => {
      ++count.value
    }
    // 暴露addCount方法
    expose({
      addCount
    })
    // 注意, 渲染函数中,还是要使用.value
    return () => h('div', [name.value, age.value, count.value])
  }
}
</script>
// 父组件
<template>
  <button @click="handleAdd">增加</button>
  <setup-component ref="root" name="jack" age="10"></setup-component>
</template>
<script>
import { ref, onMounted } from 'vue'
import setupComponent from '@components/setupComponent.vue'
// 1. 导出对象
export default {
  components: {
    setupComponent
  },
  setup() {
    // 注意, 此处的变量名(root)要和模版引用上的属性值相同!
    const root = ref(null)
    const handleAdd = () => {
      // 子组件中的addCount方法
      root.value.addCount()
    }
    onMounted(() => {
      console.log(root.value, 'onMounted钩子获取了root的值')
    })
    return {
      handleAdd,
      root
    }
  }
}

响应式API

从刚才的诸多案例中,我们也看到了, 我们高频率使用了ref, toRefs等API, 这些就是所谓的响应式API, 当然, 响应式API远远不止这两种,这里我们就来介绍下这些API

响应性基础API

  • reactive, 返回响应式副本

    • 参数必须是引用类型, 如果是基本数据类型, 则会报出警告
    • 如果有一个属性值为ref类型数据, 通过reactive的代理会将该属性值进行解包, 也就是不需要使用.value来访问
    • 修改reactive的ref属性和ref类型的数据中的任何一个, 另一个都会被改动, 因为两者已经关联了
<template>
  <div>
    修改name输入框
    <input type="text" v-model="name">
    info.name的值: {{info.name}}
  </div>
  <div>
    修改info.name输入框
    <input type="text" v-model="info.name">
    name的值: {{name}}
  </div>
</template>
<script>
  import { ref, reactive } from 'vue'
  export default {
    setup() {
      // ref类型的响应式数据
      let name = ref('大明')
      console.log(name.value) // 需要.value来获取值
      // 将name赋值给reactive对象
      let info = reactive({name, age: 12})
      console.log(info.name) // 自动解包,无需.value
      return {
        info,
        name
      }
    }
  }
</script>

无论修改name还是info.name,数据都会保持统一改变, 因为已经关联了

  • readonly, 可以接受一个对象/响应式对象/ref数据作为参数, 返回一个只读代理

    • 如果是对象, 其内部嵌套的所有数据都将变成只读
    • 和reactive一样, 如果属性为ref, 则会自动将其解包
<script>
  ...
  export default {
    setup() {
      let info = reactive({name: '小明'})
      let copyInfo = readonly(info)
      console.log('info.name初始数据', info.name) // 小明
      console.log('copyInfo.name初始数据', copyInfo.name) // 小明
      watchEffect(() => {
        console.log('监听数据-->', copyInfo.name)
      })
      info.name = '小红'
      copyInfo.name = '小红'
     ...
    }
  }
</script>

从结果可以看出, 我们给copyInfo.name这个只读对象赋值, 导致了警告

  • isReactive, 接受一个对象, 返回一个Boolean类型的数据, 来判断该值是否为reactive创建的响应式代理
    • 一个普通对象, 传递给isReactive, 将返回false
    • 一个由reactive创建的响应式代理, 即使让readonly包裹一层, 传递给reactive, 仍为true
<script>
  import { isReactive, reactive, readonly } from 'vue'
  export default {
    setup() {
      let reactiveObj = reactive({name: '小明'})
      console.log('reactiveObj-->', isReactive(reactiveObj))
      let plainObj = {name: '小红'}
      console.log('plainObj-->', plainObj)
      let readonlyRect = readonly(reactiveObj)
      console.log('readonlyRect -->', readonlyRect)
      let readonlyPlain = readonly(plainObj)
      console.log('readonlyPlain -->', readonlyPlain)
      ...
    }
  }
</script>

  • isReadonly, 判断是否是readonly创建的只读代理, 很简单, 就不展开叙述了
  • toRaw, 从字面意思就能理解, 就是将响应式代理对象转为普通对象.

主要的场景就是一些无需更新ui时候, 我们可以安全地修改数据

<template>
  <input type="text" v-model="reactiveObj.name">
  {{reactiveObj.name}}
  {{rawObj.name}}
</template>
<script>
import { reactive, toRaw, ref } from 'vue'

export default {
  setup() {
    let userInfo = {name: '小明'}
    const reactiveObj = reactive(userInfo)
    let rawObj = toRaw(reactiveObj)
    console.log(rawObj === userInfo)
    setTimeout(() => {
      rawObj.name = '11'
      console.log('rawObj->', rawObj) // 11
      console.log('reactiveObj->', reactiveObj) // 11
    }, 1000)

    return {
      reactiveObj,
      rawObj
    }
  }
}
</script>

这里可以看出, 我们的rawObj中的name已经被改变了, 数据层的reactiveObj也被改了. 但是, 视图上的数据, 还是没变!

  • markRaw, 让一个对象永远无法被转为响应代理对象

因为响应式代理开销很大, 所以对于一些不变的数据或者类, 我们无需让他们转为响应式, 这样有利于性能的提升.

<script>
import { reactive, toRaw, ref, markRaw, isReactive } from 'vue'

export default {
  setup() {
    let userInfo = markRaw({name: 'jack'})
    console.log('isReactive1', isReactive(userInfo)) // false
    let reactUserInfo = reactive(userInfo)
    console.log('isReactive2', isReactive(reactUserInfo)) // false
    ...
  }
}
</script>
  • shallowReactive
    • 自身的第一层属性会变为响应式
    • 不执行嵌套对象深层次属性响应式转换
<template>
  <div>
    userInfo.name
    <input type="text" v-model="userInfo.name">
  </div>
  <div>
    userInfo.birthday.date
    <input type="text" v-model="userInfo.birthday.date">
  </div>
  <div>
    userInfo
    {{userInfo}}
  </div>
</template>
<script>
import { reactive, toRaw, ref, markRaw, isReactive, shallowReactive } from 'vue'

export default {
  setup() {
    let userInfo = shallowReactive({name: 'jack', birthday: {
      date: '1622-12-12',
      time: '12:00:00'
    }})
    console.log(isReactive(userInfo)) // true
    console.log(isReactive(userInfo.birthday)) // false
    
    return {
      userInfo
    }
  }
}
</script>

从结果可以看出, userInfo.name是响应式的; 但是userInfo.birthday.date却不是

  • shallowReadonly, 理解了shallowReactive, 理解这个api也就不难了. 所谓的shallowReadonly, 就是不执行嵌套对象深层次属性只读转换, 但是自身的第一层属性会变为只读
<script>
...
export default {
  setup() {
    let userInfo = shallowReadonly({name: 'jack', birthday: {
      date: '1622-12-12',
      time: '12:00:00'
    }})
    userInfo.name++ // 警告
    userInfo.birthday.date++ // 正常
   ...
  }
}
</script>

结果如下:

Refs

refs的响应式API主要包括: ref, unref, toRef, toRefs, isRef, customRef, shallowRef, triggerRef

  • ref, 是最简单的一个, 其作用就是将一个变量转为响应式, 要注意的是
  • 如果在setup以及渲染函数render中要使用它的值, 都必须加上.value属性, 但是如果在template中使用, 则不需要.
<template>
<!-- template中无需使用.value -->
{{name}}
</template>
<script>
import { ref } from 'vue'
export default {
  setup() {
    const name = ref('')
    // 此处访问需要用到.value
    console.log('🚀 ->', name.value)
    return {
      name
    }
  }
}

</script>
  • unref, 如果参数是ref, 则将其转为非响应类型的数据并返回, 否则返回参数本身. 注意这里不是驼峰写法, r是小写!
...
<script>
  ...
  export default {
    setup() {
      let name = ref('2')
      console.log(name) // RefImpl对象
      name = unref(name)
      console.log(name) // 2
      return {
        name
      }
    }
  }
  ...
</script>
  • toRef 主要作用是以响应式对象(第一个参数)的某个属性(第二个参数)为参数, 重新创建为一个ref对象, 并返回.

  • 若将这个新的对象赋值给其他变量, 则这个新变量和源对象中的属性是相互连接的.

  • 即使源对象上的属性不存在(即对象中没有和第二个参数同名的键), 它也会返回一个有用的ref

组件将props中的某个属性转为响应式

// 这个场景在组件获取父级props的时候很常见, 即, 原对象是响应式, 但是我们只是需要其某个属性, 
// 而这个属性又不是响应式的情况下
<script>
import { h, toRef} from 'vue'
export default {
  props: ['name', 'age'],
  setup(props) {
    // 将props中的name提取出来
    const name = toRef(props, 'name')
    return {
      name
    }
  }
}
</script>

新创建的ref对象的修改仍会影响源对象

<template>
  <div>
    <div>
      obj: {{obj}}
    </div>
    <div>
      name: {{name}}
    </div>
  </div>
  <input type="text" v-model="name">
</template>
<script setup>
  import { toRef, reactive } from 'vue'
  let obj = reactive({name: 'jack', age: 44})
  let name = toRef(obj, 'name')
</script>

效果如下:

1111.gif

  • toRefs, 这个在介绍setup的参数的时候已经介绍过了

    • 接受一个响应对象, 返回一个普通对象
    • 普通对象的每个property都和源对象对应的property相连.
    • 综合就是: '在不破坏响应式的前提下, 对响应对象进行解构'; 不过要注意的是, 它只会转换源对象中包含的property; 如果是可选属性可以直接使用toRef来实现取值
<script>
import { h, toRefs} from 'vue'
export default {
  props: ['name', 'age'],
  setup(props) {
    const {name} = toRefs(props)
    return {
      name
    }
  }
}
</script>
  • isRef, 判断是否是一个ref对象
<script>
 import { toRefs, isRef, unref} from 'vue'
 export default {
  props: ['name', 'age'],
  setup(props) {
    ...
    let {name} = toRefs(props)
    console.log(isRef(name)) // true
    name = unref(name)
    console.log(isRef(name)) // false
    ...
  }
}
</script>
  • customRef, 创建一个ref, 并对其修改/获取进行监控; 接受一个工厂函数, 该函数有俩参数: track(负责追踪), trigger(负责触发), 返回一个包含getter/setter的对象, 分别监听ref的获取/赋值;

这里模仿官方写了一个防抖函数

<template>
  <div>
    数据: {{name}}
  </div>
  <input type="text" v-model="name">
</template>
<script>
import { customRef} from 'vue'
export default {
  setup() {
    function debounceRef (value, delay) {
      let timer = null
      return customRef((track, trigger)=> {
        return {
          get() {
            // 追踪依赖
            track()
            return value
          },
          set(newValue) {
            timer && clearTimeout(timer)
            timer = setTimeout(() => {
              value = newValue
              // 触发界面更新
              trigger()
            }, delay)
          }
        }
      })
    }
    return {
      name: debounceRef('', 300)
    }
  }
}
</script>

效果如下

1111.gif

  • shallowRef, 这个api也是将一个值转为响应式

    • 只是浅层地转, 如果是复杂对象, 其属性将不被转换
    • 但是如果传递的是一个基本数据类型, 则和ref无异!
<template>
  <div>
    <!-- name属性正常响应 -->
    <input type="text" v-model="refObj.name">
    <!-- 实时变化 -->
    数据ref: {{refObj.name}}
  </div>
  <div>
    <!-- 无法监听到name属性 -->
    <input type="text" v-model="shallowRefObj.name">
    <!-- 无变化 -->
    数据shallowRef: {{shallowRefObj.name}}
  </div>
</template>
<script>
  import { ref, shallowRef } from 'vue'
export default {
  setup() {
    let refObj = ref({name: ''})
    let shallowRefObj = shallowRef({name: ''})
    return {
      refObj,
      shallowRefObj
    }
  }
}
</script>

  • triggerRef, 既然 shallowRef参数如果是一个对象, 那么其属性不会是响应式, 那么, 假如有时候我们需要在特定的时机更新数据, 怎么办呢? 这时候就要用到triggerRef了. 接上面的案例, 加入我们再绑定一个updateName事件, 看看效果如何:
<template>
...
  <!-- 无法监听到name属性 -->
  <input type="text" v-model="shallowRefObj.name">
  <!-- 无变化 -->
  数据shallowRef: {{shallowRefObj.name}}
  <button @click="updateName">更新</button>
</template>
<script>
  import { ref, shallowRef, triggerRef, watchEffect } from 'vue'
  export default {
    setup() {
      ...
      watchEffect(() => {
        // 此处会触发
        console.log(shallowRefObj.value.name, '监听')
      })
      const updateName = () => {
        triggerRef(shallowRefObj)
      }
      return {
        ...
        updateName
      }
    }
}

pinia

image.png

pinia, 新一代的vue状态管理工具, 实际上呢, 它就是vuex5! 那么我们为何要使用它呢? 或者说, 它相对于之前的vuex, 有哪些改进呢? 这里做了几点总结:

  • 更加扁平的模块化方式.
  • 取消了mutations, 同步异步统一在actions中使用.
  • 支持typescript.
  • 非常轻, 仅1kb.
  • 支持插件, 可以支持更多扩展
  • 更优秀的代码调用机制, 没有用到的store不会被打包, 不会被调用.
  • 兼容在vue2中使用, 也就是说, 它并不是vue3的专属.
  • 更好的代码分割, 没有引用到的store不会被打包进最后的代码中

基本使用

createPinia

通过暴露出来的createPinia方法, 创建pinia对象, 供vue.use入参使用

引入

// /src/store/index.js
import { createPinia } from 'pinia'
const piniaStore = createPinia()
export default piniaStore

挂载到vue实例

import { createApp } from 'vue'
import App from './App.vue'
import store from './store/index'
const app = createApp(App)
app.use(store)
app.mount('#app')

defineStore

通过defineStore方法定义一个可以创建store的use方法, 提供给页面使用

入参的不同形式

参数有两种形式配置对象亦或者是函数形式

  1. 配置对象
属性名类型说明
idstring当前store的唯一标示, 这个值可以单独作为defineStore的第一个参数, 也可以融入该对象入参中
state函数类型相当于options API中的data, 也就是用来存放数据的, 注意, 要用箭头函数, 这样方便TS推导类型
action对象相当于options API 中的methods, 也就是包含了所有能够改变state数据的方法
getters对象相当于options API 中的computed, 也就是计算属性

我们先在/src/store/user.js内, 用defineStore来创建一个useUserStore方法, 注意, 所有的store, 都是平等的, 属于扁平化的结构. 而vuex中, 是一个主store, 其他的子store都是它的modules对象, 属于嵌套结构. 扁平化更易于使用和维护, 更加有利于代码分割等.

// /src/store/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore({
  id: 'user',
  state: () => {
    return {
      name: 'jack',
      age: 19
    }
  },
  actions: {
    updateName (name, age) {
      this.name = name
    }
  },
  getters: {
    title () {
      return this.name + '老师'
    }
  }
})
// id还可以单独作为一个入参
export const useUserStore = defineStore('user', {
  state: () => {
    return {...}
  },
  actions: {...},
  getters: {...}
})
  • 如果要对store进行解构, 一定要使用storeToRefs方法, 和前面我们介绍toRefs一样, 使数据保持响应式!
  • 可以直接调用特定store上的方法, 非常方便直观. 同样的, 也可以通过store对象直接修改states中的数据
<template>
  <div>
    <div>
      姓名: {{name}}
    </div>
    <div>
      职称: {{title}}
    </div>
    <button @click="updateName">更新数据</button>
  </div>
</template>
<script>
import { useUserStore } from '@/store/user.js'
import { storeToRefs } from 'pinia'
export default {
  setup() {
    const userStore = useUserStore()
    const updateName = () => {
      userStore.updateName('rose')
    }
    // 如果是解构, 注意要使用storeToRefs方法, 否则会丢失响应功能!
    const {name, title} = storeToRefs(userStore)
    // 非响应式数据
    // const {name} = userStore
    return {
      updateName,
      name,
      title
    }
  }
}
</script>

  1. 参数除了可以是对象, 还能为函数, 返回一个对象, 而这个对象里, 则包含了所有的变量/方法, 而且不用被包含在state, actions, getters等属性里.

例如: 我们再定义一个business的store, 此时我们的第二个参数形式为函数, 该函数返回了一个对象, 对象里包含store中的数据/方法.

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useBusinessStore = defineStore('business',function() {
  let price = ref(12)
  let count = ref(100)
   
  // 增加数量
  function increaseCount () {
    count.value ++
  }
  // 计算总价
  let totalPrice = computed(()=> price.value * count.value)

  return {
    totalPrice,
    price,
    increaseCount,
    count
  }
})

关于State

  • state, 类似于我们options API当中的data, 也就是全局的数据, 是一个函数形式, 注意必须是箭头函数, 否则, 无法推导出正确的数据!
  • 如何改变state

从上面的案例, 我们可以看出, store可以通过defineStore中定义的actions中的方法来修改state, 除此之外, 我们再介绍几种和state有关的方法

  1. actions方法改变, 前面已经演示.
  2. 直接通过store.xx = xx 进行修改, 直接了当, 当然, 并不推荐这么做, 毕竟这样很容易误改到全局数据
...
 const updateName = () => {
   userStore.name = 'rose'
 }
...
  1. $patch方法, 前两种方法, 要么就只能改一个属性, 要么就必须事先确定好一个固定的方法, 有些场景显得不太便利, 所以pinia还有一个$patch方法, 可以批量修改数据
// xx.vue
const increase = () => {
  businessStore.$patch({price: 123, count: 100000})
}

但是以上的语法更合适于我们要修改整个对象的场景, 如果我们需要修改多个属性, 但是某个属性只需要修改一部分, 我们仍然要写一个完整的对象作为store.$patch的参数传进去, 这样显然也很麻烦.还好, $patch还可以接受一个函数形式的参数:

// store/xx.js
...
let pets = reactive(['cat'])
...
// xx.vue
businessStore.$patch((state) => {
  state.pets.push('dog')
  state.count = 10000
})

// 如果使用对象为参数, 需要把本不需要涉及到的'cat'也写出来
businessStore.$patch({
  pets: ['cat', 'dog'],
  count: 10000,
  ...
})
  1. $reset方法, 顾名思义, 就是重置state的方法, 使用起来也非常简单
...
const handleReset = () => {
  businessStore.$reset()
}
...

有一点要注意, 那就是如果defineStore是函数形式, 则无法使用该方法!vue会报错!

  1. $state方法, 这个也很好理解, 就是通过store.$state = {xx}, 可以将整个state对象进行修改
store.$state = {
  price: 1,
  count: 10
}
  1. $subscribe

说完了那么多能够改变state的方法, 现在再来介绍下$subscribe方法, 该方法可以监听state的改变

参数:

  1. 回调函数, 其入参包括

    mutation对象, 属性有:

    • type, 修改数据的方法, 前面我们介绍了很多修改state数据的方式, 这个参数属性可以为我们区分修改state的方法.

    • $storeId, store的标识id

    • payload, 修改state时候传递过来的数据载荷

    state对象, 也就是store中state返回的对象

  2. 配置对象, 属性有:

    • detached

      类型: Boolean

      默认: false

      说明: 组件被卸载之后, 是否仍然保持监听, true则表示继续监听

    • immediate:

      类型: Boolean

      默认: false

      说明: 初始化是否触发监听事件, 和watch中的immediate是一个道理, true则是初始化就执行监听

返回值:

返回一个方法, 调用之后, 可以取消监听

// 监听
businessStore.$subscribe(function (mutation, state) {
  console.log(mutation.type) // patch function
  console.log(mutation.$storeId) // business, 即defineStore中的id参数
  console.log(mutation.payload) // $patch函数形式下为undefined, 对象形式为该对象本身
  sessionStorage.setItem(mutation.$storeId, JSON.stringify(state))
})

// 通过$patch的函数形式修改了state.price
businessStore.$patch(state => {
  state.price = 0
})

mutation.type的值可以是:

  • patch function , $patch的函数形式
  • patch object, $patch的对象形式
  • direct, 直接使用store.xx = xx的修改形式, 或者actions的方式修改

如何使用Getters

其实这里的Getters和vuex中的getters是一个道理, 就是计算属性, 而且具有缓存功能, 关于这个属性, 我们需要注意几点:

  • 类型声明

getters中的方法, 接受一个可选参数state, 可以通过state.xx来访问state的数据, 也能通过this来访问数据; 注意: 如果是使用this来访问, 需要声明返回类型, 即使不用typescript, 也要用jsDoc的形式来声明, 否则, 类型无法被推导出来!

// 以下两种形式都是正确的
// state
getters: {
  totalPrice (state) {
    return state.price * state.count
  }
}

// this
 getters: {
  /**
   * 通过jsDoc来实现类型推倒
   *  @returns {number}
   */
  totalPrice (state) {
    return this.price * this.count
    // return state.price * state.count
  }
}
  • 返回函数

之前的案例中, 我们的getters的依赖, 都是一些全局的state数据, 例如: state.price, state.count等等, 如果我们想要用我们具体页面中的数据, 去和一个全局数据做一个计算, 该如何做呢? 这里官方文档推荐的就是让getters返回一个函数!

// /src/store/business.js
getters: {
  totalPrice (state) {
    return (count) => {
      return state.price * count
    }
  }
}
// index.vue
<template>
  <!-- 我们在这里可以传递本页面的count -->
  {{businessStore.totalPrice(count)}}
<input type="text" v-model="count">
</template>
<script setup>
  import { ref } from 'vue'
  import { useBusinessStore } from '../store/business'
  let count = ref(0)
  const businessStore = useBusinessStore()
</script>

注意: 如果用以上的方式, 将无缓存效果! 也就是, 当视图层一更新, getters中的数据就会重新计算

  • 引用其他store中的getters
// user.js
...
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => {
    return {
      name: 'jack',
      age: 18
    }
  },
  getters: {
    desc () {
      return this.name + '今年' + this.age + '岁'
    }
  }
})

...

// business.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useBusinessStore = defineStore('business', {
  state: () => {
    return {
      occupation: 'web前端开发'
    }
  },
  getters: {
    introduction (state) {
      const userStore = useUserStore()
      return userStore.desc + '职业是' + state.occupation
    }
  }
})

vue页面中使用

<template>
  <!-- jack今年18岁职业是web前端开发 -->
  {{businessStore.introduction}}
</template>
<script setup>
  import { ref } from 'vue'
  import { useBusinessStore } from '../store/business'
  const businessStore = useBusinessStore()
</script>
  • 非setup中使用

setup出现于vue3, 我们前面的案例, 也都是演示setup中的使用方式, 那如果我们想在非setup中使用呢? 我们没有地方可以执行我们的hook, 例如: const xxStore = useXXStore(). 这时, 我们就可以借助辅助函数

<template>
  <!-- jack今年18岁职业是web前端开发 -->
  <div>{{introduction}}</div>
  <!-- web前端开发 -->
  <div>{{job}}</div>
  <!-- 我的职业是web前端开发 -->
  <div>{{myJob}}</div>
</template>
<script>
import { mapState } from 'pinia'
import { useBusinessStore } from '../store/business'
export default {
  computed: {
    ...mapState(useBusinessStore, ['introduction']),
    ...mapState(useBusinessStore, {
      job: 'occupation',
      myJob: (store) => {
        return '我的职业是' + store.occupation
      }
    })
  }
}
</script>

代码解析: mapState辅助函数第一个参数为我们store中返回的hook函数, 第二个参数可以为数组, 也可以为对象

  • 如果是数组的话, 其元素就是store中的的属性.

  • 如果是对象的话, 其属性值可以是字符串形式, 也可以是函数形式

    • 字符串形式: 就是store中的属性的键(例如: occupation);
    • 函数形式: 这个函数的首个参数就是store对象, 函数返回的值就是计算结果.
    • 无论属性值如何变, 该对象的键都是本页面中该值的变量名(例如: myJob).

再了解下actions

通过前面对于state的改变, 相信大家已经对actions的基本用法都很熟悉了, 无非就是一个methods, 但是有几点还是要注意:

  1. 和之前的vuex(4及其之前版本), 不同, 这里的actions融合了原来的mutations和actions的功能, 也就是能同步, 也能异步.
  2. actions中, 要通过this.xx来访问state中的变量, 所以, 不可以使用箭头函数, 否则this指向会出错.
...
actions: {
  // 不得为箭头函数, 否则报错
  handleChange(value) {
    // 支持异步
    setTimeout(() => {
      this.occupation = value
    }, 1000)
  }
}
...
  1. 如果在非setup的情况下, 可以使用mapGetters辅助函数, 和前面介绍的mapState一样, 这里就不再赘述了.
  2. 监听方法$onAction, 和之前介绍$subscribe方法一样, 都是监听作用, 它和$subscribe不同的是, $subscribe监听的是state的改变. 而$onAction监听的是对actions的调用
// business.js
...
 actions: {
  // 不得为箭头函数, 否则报错
  handleChange(value) {
    // 返回一个Promise对象
    return new Promise(resolve => {
      timer && clearTimeout(timer)
      // 支持异步
      setTimeout(() => {
        this.occupation = value
        resolve(value)
      }, 1000)
    })
  }
}
...
<!-- index.vue -->
<!-- ...视图层省略... -->
<script setup>
import {ref} from 'vue'
import { useBusinessStore } from '../store/business'
const businessStore = useBusinessStore()
// 监听
let useSubscribe = businessStore.$onAction(({name, store, args, after, onError}) => {
  console.log('name-->', name) // 调用的action方法的名字, 本案例中为handleChange
  console.log('store-->', store) // store对象
  console.log('args-->', args) // businessStore.handleChange的入参
  after((result) => {
    console.log('after', result) // Promise中resolve的值, 如果没有Promise, 则是actions方法中return的值
  })
  onError((reason) => {
    console.log('error-->', reason) // 监听抛出的错误
  })
})
let occupation = ref()
// 本页面执行的方法
const go = () => {
  // 调用actions中的方法
  businessStore.handleChange(occupation.value)
}
</script>

代码解析:

$onAction

参数:

  1. 回调函数, 其入参包括

    • name, 调用的actions的方法名

    • store, 也就是当前监听的那个store对象

    • args, actions方法接受的入参

    • after,action执行结束后的callback, 类型为函数, 函数入参为actions方法的返回值, 若返回Promise, 则是resolve的值

    • onError,类型为函数,函数的入参为错误原因

  2. 一个布尔值, 即本组件卸载时, 是否停止监听, 默认为false

返回值:

返回一个函数, 调用之后, 可以取消监听