vite+vue

0.前言

技术栈:【Vue3】【Vite4】【Pinia】【Vue Router】【Element Plus】
node:v18.17.1
npm:9.6.7
pnpm:9.7.0

1.创建项目

Vitejs

pnpm create vite my-vue-app --template vue
cd my-vue-app
npm install
npm run dev

2.代码格式化

Prettier

  1. 安装 Prettier 插件
    在这里插入图片描述
  2. 安装依赖
pnpm install --save-dev --save-exact prettier
pnpm install eslint --save-dev
  1. 配置
    根目录下创建.prettierrc.json格式化配置文件
    可以在 Prettier 中测试效果,然后拷贝配置内容到自己的项目中
    在这里插入图片描述
{
  "arrowParens": "always",
  "bracketSameLine": true,
  "bracketSpacing": true,
  "semi": true,
  "singleQuote": true,
  "jsxSingleQuote": false,
  "quoteProps": "as-needed",
  "trailingComma": "all",
  "singleAttributePerLine": false,
  "htmlWhitespaceSensitivity": "css",
  "vueIndentScriptAndStyle": false,
  "proseWrap": "preserve",
  "insertPragma": false,
  "requirePragma": false,
  "useTabs": false,
  "embeddedLanguageFormatting": "auto",
  "tabWidth": 2,
  "printWidth": 200
}

根目录下创建.prettierignore忽略格式化文件

node_modules
dist

3.ESlint

ESlint 检查代码规范

  1. 安装ESLint
pnpm add -D eslint
  1. 初始化 ESLint 配置
npx eslint --init
  1. 配置初始化选择
? How would you like to use ESLint? ...
  To check syntax only
  To check syntax and find problems
> To check syntax, find problems, and enforce code style
  ---------------------------------------------------------------- 选择强制代码风格
√ How would you like to use ESLint? · style       
? What type of modules does your project use? ... 
> JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these
  ------------------------------------------------------------选择语言模块
? Which framework does your project use? ... 
  React
> Vue.js
  None of these
  ------------------------------------------------------------选择语言框架
? Does your project use TypeScript? » No / Yes
  --------------------------------------------是否使用ts (视自己情况而定,我这里不用选No)
? Where does your code run? ...  (Press <space> to select, <a> to toggle all, <i> to invert selection)
√ Browser
√ Node
  -----------------------------------------------代码在哪里运行 (用空格选中 Browser+Node)
? How would you like to define a style for your project? ... 
  Use a popular style guide
> Answer questions about your style
  -----------------------------------------------用过A&Q来配置规则
? What format do you want your config file to be in? ...
> JavaScript
  YAML
  JSON
  ----------------------------------------------配置文件使用js文件
? What style of indentation do you use? ...
> Tabs
  Spaces
  ----------------------------------------------缩进方式
? What quotes do you use for strings? ... 
  Double
> Single
  -----------------------------------------------字符串用什么引号
? What line endings do you use? ...
> Unix
  Windows
  ---------------------------------------------------
? Do you require semicolons? » No / Yes
  ---------------------------------------------------是否需要分号(视自己情况而定,我这里不用选No)
? Would you like to install them now? » No / Yes
  ---------------------------------------------------选择yes现在立即初始化配置文件
? Which package manager do you want to use? ... 
> npm
  yarn
  pnpm
  ------------------------------------------------包管理器选择npm
  1. 安装完成后(根目录会生成.eslintrc.cjs文件)
module.exports = {
	'env': {
		'browser': true,
		'es2021': true,
		'node': true
	},
	'extends': [
		'eslint:recommended',
		'plugin:vue/vue3-essential'
	],
	'overrides': [
	],
	'parserOptions': {
		'ecmaVersion': 'latest',
		'sourceType': 'module'
	},
	'plugins': [
		'vue'
	],
	'rules': {
		'indent': [
			'error',
			'tab'
		],
		'linebreak-style': [
			'error',
			'unix'
		],
		'quotes': [
			'error',
			'single'
		],
		'semi': [
			'error',
			'never'
		]
	}
}
  1. 安装vite-plugin-eslint(eslint结合vite使用)
// 说明: 该包是用于配置vite运行的时候自动检测eslint规范 不符合页面会报错
pnpm add -D vite-plugin-eslint
  1. 安装eslint-parser
pnpm add -D @babel/core             
pnpm add -D @babel/eslint-parser
  1. 配置vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import eslintPlugin from 'vite-plugin-eslint';

// https://vitejs.dev/config/
export default defineConfig({
	plugins: [
		vue(),
		eslintPlugin({
			include: ['src/**/*.ts', 'src/**/*.vue', 'src/*.ts', 'src/*.vue'],
		}),
	],
	resolve: {
	// 配置别名联想路径
      alias: {
        '@': resolve(__dirname, 'src'),
      },
    },
})

  1. 配合 ESLint 插件检查
    在这里插入图片描述
    ESLint 配置规则文件

  2. .eslintignore配置 eslint 忽略文件

# 忽略public目录下文件的语法检查
public
# 忽略node_modules目录下文件的语法检查
node_modules
# 忽略src/assets目录下文件的语法检查
src/assets
# 忽略dist目录下文件的语法检查
dist
  1. package.json文件添加 lint
 "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint --ext .js,.vue --ignore-path .eslintignore --fix src"
  },
  1. 关闭命名规则校验

.eslintrc.js文件中 rules: 里添加:

"vue/multi-word-component-names":"off",

若不生效,则在 package.json 文件中添加:

  "eslintConfig": {
    "rules": {
      "vue/multi-word-component-names": "off"
    }
  }

或者在vite.config.js文件中添加:

      eslintPlugin({
        include: ['src/**/*.ts', 'src/**/*.vue', 'src/*.ts', 'src/*.vue'],
        overrideConfig: {
          rules: {
            'vue/multi-word-component-names': 'off',
          },
        },
      }),

4.Vite项目配置

Vite文档

  1. 解决 import { ref , reactive ..... } from 'vue' 大量引入的问题
pnpm i -D unplugin-auto-import

vite.config.js

    plugins: [
      vue(),
      // 解决 `import { ref , reactive ..... } from 'vue'` 大量引入的问题
      AutoImport({
        imports: ['vue', 'vue-router'],
      }),
    ],
  1. $ref 语法糖告别

store(pinia版) 中使用 $ref 无法正常持久化数据!!!

pnpm install @vue-macros/reactivity-transform

vite.config.js

    plugins: [
      vue(),
      ReactivityTransform(), // 启用响应式语法糖 $ref ...
    ],

解决ESLint警告: '$ref' is not defined.
.eslintrc.cjs

module.exports = {
  globals: { $ref: 'readonly', $computed: 'readonly', $shallowRef: 'readonly', $customRef: 'readonly', $toRef: 'readonly' },
};

解决ESlint警告:error '$ref' is not defined no-undef

          rules: {
            'no-undef': 'off',
          },

注意:

ref语法糖有其使用限制,特别是在声明响应式变量时。虽然理论上可以使用letconst来声明响应式变量,但在实际使用ref语法糖时,通常建议使用let而不是const

原因在于ref语法糖的目的是为了创建一个响应式引用,而这个引用可能会在组件的生命周期中发生变化。使用let可以确保引用的值可以被改变,而const则保证了变量的值在声明后不会被改变,这包括响应式引用的值。因此,虽然技术上可以使用const来声明使用ref语法糖的目的是为了创建一个响应式引用,而这个引用可能会在组件的生命周期中发生变化。使用let可以确保引用的值可以被改变,而const则保证了变量的值在声明后不会被改变,这包括响应式引用的值。

因此,虽然技术上可以使用const来声明使用ref语法糖的目的是为了创建一个响应式引用,而这个引用可能会在组件的生命周期中发生变化。使用let可以确保引用的值可以被改变,而const则保证了变量的值在声明后不会被改变,这包括响应式引用的值。因此,虽然技术上可以使用const来声明使用ref的变量,但在实践中,为了保持响应式的灵活性,更推荐使用let来声明这些变量。

  1. 环境配置。根目录下创建.env.dev.env.prod文件
# 开发环境
# NODE_ENV='dev'

# 为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。
# js中通过`import.meta.env.VITE_APP_BASE_API`取值
VITE_APP_PORT = 5173
VITE_APP_BASE_API = '/dev-api'
VITE_APP_BASE_FILE_API = '/dev-api/web/api/system/file/upload'

# 后端服务地址
VITE_APP_SERVICE_API = 'http://localhost:888'

package.json

{
  "scripts": {
    "dev": "vite --mode dev",
    "prod": "vite --mode prod",
    "build": "vite build --mode prod"
  }
}
  1. 配置vite.config.js
import eslintPlugin from 'vite-plugin-eslint';
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import * as path from 'path';
import AutoImport from 'unplugin-auto-import/vite';
import ReactivityTransform from '@vue-macros/reactivity-transform/vite';

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  // 获取`.env`环境配置文件
  const env = loadEnv(mode, process.cwd());

  return {
    plugins: [
      vue(),
      eslintPlugin({
        include: ['src/**/*.ts', 'src/**/*.vue', 'src/*.ts', 'src/*.vue'],
      }), // 项目运行时进行eslint检查
      ReactivityTransform(), // 启用响应式语法糖 $ref ...
      // 解决 `import { ref , reactive ..... } from 'vue'` 大量引入的问题
      AutoImport({
        imports: ['vue', 'vue-router'],
      }),
    ],
    // 反向代理解决跨域问题
    server: {
      // host: 'localhost', // 只能本地访问
      host: '0.0.0.0', // 局域网别人也可访问
      port: Number(env.VITE_APP_PORT),
      // 运行时自动打开浏览器
      // open: true,
      proxy: {
        [env.VITE_APP_BASE_API]: {
          target: env.VITE_APP_SERVICE_API,
          changeOrigin: true,
          rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), ''),
        },
      },
    },
    resolve: {
      // 配置路径别名
      alias: [
        // @代替src
        {
          find: '@',
          replacement: path.resolve('./src'),
        },
      ],
    },
    // 引入scss全局变量
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `@import "@/styles/color.scss";@import "@/styles/theme.scss";`,
        },
      },
    },
  };
});

5. sass集成

Sass

pnpm install sass --save-dev

深度选择器

vue3+scss中不要使用下面方式,会有警告

::v-deep .el-input__wrapper {
    background-color: #08c0ee8c;
}

改用

::v-deep(.el-input__wrapper) {
    background-color: #08c0ee8c;
}

6. Vue-Router

Vue-Router官方文档

  1. 安装
pnpm install vue-router@4
  1. 配置

src/router/index.js

import {createRouter, createWebHashHistory} from 'vue-router';

// 本地静态路由
export const constantRoutes = [
  {
    path: '/test',
    component: () => import('@/views/test/index.vue'),
  },
];

// 创建路由
const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes,
  // 滚动行为 eg: 用户希望查看详情页以后,返回列表页刚刚浏览的位置
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition;
    } else {
      return { x: 0, y: 0 };
    }
  },
});

export default router;

src/main.js

// 路由
import router from '@/router';
app.use(router);

src/App.vue

<template>
  <!--使用 router-link 组件进行导航 -->
  <!--通过传递 `to` 来指定链接 -->
  <ol>
    <li><router-link to="/">Go to Home</router-link></li>
    <li><router-link to="/test">Go to Test</router-link></li>
  </ol>
  
  <hr />
  
  <!-- 路由出口 -->
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view/>
</template>
  • js获取当前路由信息 proxy.$route
  • js跳转 proxy.$router.push({ path: '/' });

7. Element Plus

官方文档

  1. 安装
pnpm install element-plus --save
pnpm install @element-plus/icons-vue
  1. 配置
    src/main.js
// element-plus
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
app.use(ElementPlus);
// 注册所有图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}
  1. 使用
<template>
  <h1>hello</h1>
  <el-row class="mb-4">
    <el-button>Default</el-button>
    <el-button type="primary">Primary</el-button>
    <el-button type="success">Success</el-button>
    <el-button type="info">Info</el-button>
    <el-button type="warning">Warning</el-button>
    <el-button type="danger">Danger</el-button>
  </el-row>
  <el-icon :size="100" color="red">
    <Edit />
  </el-icon>
</template>

在这里插入图片描述

8. Mixin 混入

Mixin
全局混入是一种在多个组件中共享相同逻辑的方式,可以将一些通用的方法、生命周期钩子等混入到所有页面和组件中,以简化代码的编写和维护。

等于抽取公共属性、方法…

  1. ./src/utils/mixin.js
// 抽取公用的实例 - 操作成功与失败消息提醒内容等
export default {
  data() {
    return {
      sexList: [
        { name: '不想说', value: 0 },
        { name: '男', value: 1 },
        { name: '女', value: 2 },
      ],
    };
  },
  methods: {
    // 操作成功消息提醒内容
    submitOk(msg, cb) {
      this.$notify({
        title: '成功',
        message: msg || '操作成功!',
        type: 'success',
        duration: 2000,
        onClose: function () {
          cb && cb();
        },
      });
    },
    // 操作失败消息提醒内容
    submitFail(msg) {
      this.$message({
        message: msg || '网络异常,请稍后重试!',
        type: 'error',
      });
    },
  },
};

这里的this.$notify$message使用了Element Plus反馈组件。

  1. 局部混入
<script>
import mixin from '@/utils/mixin.js';
export default {
    mixins: [mixin],
};
</script>


<script setup>
const { proxy } = getCurrentInstance();

async function submit() { 
    proxy.submitOk('保存成功');
}
</script>
  1. 全局混入

src/main.js

// 混入 -- 抽取公用的实例(操作成功与失败消息提醒内容等)
import mixin from '@/utils/mixin';
app.mixin(mixin);

使用

<template>
  <h1>{{ sexList }}</h1>
  <button @click="handleClick">click</button>
</template>

<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();

async function handleClick() {
  proxy.submitOk('保存成功');
  proxy.submitFail('操作失败');
}
</script>

9.全局过滤器

当后端返回的数据格式不是前端想要的时候,可以将返回的数据处理成自己需要的格式。
eg: 时间过滤器
Vue2

import { filters } from '@/utils/filters.js';
Object.keys(filters).forEach((key) => Vue.filter(key, filters[key]));

Vue3
通过 app.config.globalProperties 来注册一个全局都能访问到的属性
/src/utils/filters.js

export const filters = {
  // 获取性别值
  sexName: (sex) => {
    // 拿到mixin混入的属性值
    const { proxy } = getCurrentInstance();
    let result = proxy.sexList.find((obj) => obj.value == sex);
    return result ? result.name : '数据丢失';
  },
};

/src/main.js

// 全局过滤器
import { filters } from '@/utils/filters.js';
app.config.globalProperties.$filters = filters;

使用

<h1>{{ $filters.sexName(1) }}</h1>

sexList: [
{ name: ‘不想说’, value: 0 },
{ name: ‘男’, value: 1 },
{ name: ‘女’, value: 2 },
],

10. Pinia

Pinia文档

  1. 安装
pnpm install pinia
  1. 配置

src/main.js

// pinia
import { createPinia } from 'pinia';
const pinia = createPinia();
app.use(pinia);
// 全局store
import store from '@/store';
app.config.globalProperties.$store = store;
  1. 使用

store 模块化
src/store/index.js

// 拿到modules下的所有文件
const modulesFiles = import.meta.globEager('./modules/*.*');
const modules = {};
for (const key in modulesFiles) {
  const moduleName = key.replace(/(.*\/)*([^.]+).*/gi, '$2');
  const value = modulesFiles[key];
  modules[moduleName] = value;
  // console.log(modules);
}

export default modules;

src/store/modules/test.js

import { defineStore } from 'pinia';

export const useCounterStore = defineStore('test', () => {
  const count = ref(8888);
  function add() {
    count.value++;
  }
  return { count, add };
});

3.1. 定义Store

  • (1) Option Store

可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0, name: 'Eduardo' }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})
  • (2) Setup Store

在 Setup Store 中:

ref() 就是 state 属性
computed() 就是 getters
function() 就是 actions

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, doubleCount, increment }
})

注意:要让 pinia 正确识别 state,必须在 setup store 中返回 state 的所有属性。这意味着,你不能在 store 中使用私有属性。不完整返回会影响 SSR ,开发工具和其他插件的正常运行

3.2 使用

<template>
  <h1>JS中获取count值{{ count }}</h1>
  <button @click="handleClick">click</button>
  <br />
  <h1>模版中直接获取count值{{ $store.test.useCounterStore().count }}</h1>
  <button @click="$store.test.useCounterStore().add">click</button>
</template>

<script setup>
import { getCurrentInstance, toRefs } from 'vue';
const { proxy } = getCurrentInstance();
let useTestStore = proxy.$store.test.useCounterStore();
let { count } = toRefs(useTestStore); // 响应式
let { add } = useTestStore;

function handleClick() {
  add();
}
</script>

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

11. Pinia 持久化

上面的配置浏览器一刷新数据就丢了,所以配置下持久化存储。
pinia-plugin-persistedstate

  1. 安装
pnpm add pinia-plugin-persistedstate
  1. 配置

src/main.js

该配置将会使所有 Store 持久化存储,且必须配置 persist: false 显式禁用持久化。

// 持久化存储
import { createPersistedState } from 'pinia-plugin-persistedstate';
pinia.use(
  createPersistedState({
    auto: true, // 启用所有 Store 默认持久化
  })
)

tips: pinia持久化的无法通过 window.localStorage.clear(); 一键清空数据

window.localStorage.setItem('user2', 'hello');
// window.localStorage.removeItem('user2');

// tips: pinia持久化的无法通过这种方式清空数据,只能删除同样方式存储的值 eg: window.localStorage.setItem('user2', 'hello');
window.localStorage.clear();
window.sessionStorage.clear();

$reset() 清空数据

组合式api中直接使用 $reset() 会报错

解决:

  1. 改用选项式api
  2. 重写 $reset 方法

src/main.js

// 重写 $reset 方法 => 解决组合式api中无法使用问题
pinia.use(({ store }) => {
  const initialState = JSON.parse(JSON.stringify(store.$state));
  store.$reset = () => {
    store.$patch(initialState);
  };
});

使用views/login/index.vue

<script setup>
import { getCurrentInstance, toRefs } from 'vue';
import { ElMessage } from 'element-plus';

let useTestStore = proxy.$store.test.useCounterStore();

function resets() {
  // 重置 state
  useTestStore.$reset();
  // 变更 state
  // useTestStore.$patch({
  //   count: 100,
  // });
  ElMessage({
    message: '重置成功',
    type: 'success',
  });
}
</script>

12. axios 和 api 封装

axios中文文档

  1. 安装
pnpm install axios
  1. axios 工具封装

src/utils/request.js

import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import store from '@/store';
import { localStorage } from '@/utils/storage';

// 创建axios实例
const service = axios.create({
  // baseURL: import.meta.env.VITE_APP_BASE_API,
  baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
  timeout: 50000, // 请求超时时间:50s
  headers: { 'Content-Type': 'application/json;charset=utf-8' },
});

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    if (!config.headers) {
      throw new Error(`Expected 'config' and 'config.headers' not to be undefined`);
    }

    const { isLogin, tokenObj } = toRefs(store.user.useUserStore());

    if (isLogin.value) {
      // 授权认证
      config.headers[tokenObj.value.tokenName] = tokenObj.value.tokenValue;
      // 租户ID
      config.headers['TENANT_ID'] = '1';
      // 微信公众号appId
      config.headers['appId'] = localStorage.get('appId');
    }

    if (config.isFile) {
      // 图片上传
      config.headers['Content-Type'] = 'multipart/form-data';
    }

    return config;
  },
  (error) => {
    return Promise.reject(error);
  },
);

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    const res = response.data;
    const { code, msg } = res;
    if (code === '1') {
      return res;
    } else {
      // token过期
      if (code === -1) {
        handleError();
      } else {
        console.log(msg, 11111111111);

        ElMessage({
          message: msg || '系统出错',
          type: 'error',
          duration: 5 * 1000,
        });
      }
      return Promise.reject(new Error(msg || 'Error'));
    }
  },
  (error) => {
    console.log('请求异常:', error);
    const { msg } = error.response.data;
    // 未认证
    if (error.response.status === 401) {
      handleError();
    } else {
      console.log(msg, 22222);
      ElMessage({
        message: '网络异常,请稍后再试!',
        type: 'error',
        duration: 5 * 1000,
      });
      return Promise.reject(new Error(msg || 'Error'));
    }
  },
);

// 统一处理请求响应异常
function handleError() {
  const { isLogin, logout } = store.user.useUserStore();
  if (isLogin) {
    ElMessageBox.confirm('您的登录账号已失效,请重新登录', {
      confirmButtonText: '再次登录',
      cancelButtonText: '取消',
      type: 'warning',
    }).then(() => {
      logout();
    });
  }
}

// 导出实例
export default service;

  1. api 封装

src/api/index.js

// 拿到所有api
const modulesFiles = import.meta.glob('./test/*.*', { eager: true });
const modules = {};
for (const key in modulesFiles) {
  const moduleName = key.replace(/(.*\/)*([^.]+).*/gi, '$2');
  const value = modulesFiles[key];
  if (value.default) {
    // 兼容js
    modules[moduleName] = value.default;
  } else {
    // 兼容ts
    modules[moduleName] = value;
  }
}
// console.log(666, modules);
export default modules;
  1. 全局配置

src/main.js

// 配置全局api
import api from '@/api'
app.config.globalProperties.$api = api
  1. 测试

src/api/test/demo.js

import request from '@/utils/request';

export const time = () => {
  return request({
    url: '/home/category/head',
    method: 'get',
  });
};

页面

<template>
  <el-button @click="handleAPI">API</el-button>
  <h1>{{ data }}</h1>
</template>

<script setup>
const { proxy } = getCurrentInstance();
let res = $ref(null);
let data = $ref(null);
// 获取数据
const handleAPI = async () => {
  res = await proxy.$api.demo.time();
  data = res.result[0].name;
  console.log(res);
};
</script>

13. 全局组件

  1. 获取所有组件

src/components/index.js

const modulesFiles = import.meta.glob('./*/*.vue', { eager: true });

const modules = {};
for (const key in modulesFiles) {
  const moduleName = key.replace(/(.*\/)*([^.]+).*/gi, '$2');
  const value = modulesFiles[key];
  modules[moduleName] = value.default;
}

// console.log(666, modules);
export default modules;
  1. 全局组件注册

src/main.js

// 全局组件注册
import myComponent from '@/components/index';
Object.keys(myComponent).forEach((key) => {
  app.component(key, myComponent[key]);
});
  1. 组件示例

src/components/base/BaseNoData.vue

<template>
  <div>
    <slot>暂无数据</slot>
  </div>
</template>
  1. 引用组件

src/views/login/index.vue

<base-no-data />
<base-no-data>请先选择数据</base-no-data>

14. 代码模块化

在这里插入图片描述
vue.json

	"vue3": {
		"prefix": "vue3",
		"body": [
			"<template>",
			"<div></div>",
			"</template>",
			"<script setup></script>",
			"<style scoped></style>",
		],
		"description": "vue3 setup模板"
	},
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值