快速搭建项目vue3项目

881 阅读5分钟

背景:vue3在2022年2月7日成为新的默认版本。而vite原生 ES 模块导入方式,可以实现闪电般的冷服务器启动,所以优先使用Vite 快速构建 Vue 项目。 构建方式可能随时更新,这里推荐根据官网最新的构建流程来做,传送门

搭建后最原始的样子(vscode记得安装Vetur)

vue3 + vuex + vue-router + vuex-persistedstate(持久化缓存) 等基配

1.package.json

{
  "name": "yc-ui",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"
  },
  "dependencies": {
    "element-plus": "^1.2.0-beta.4",
    "vite-plugin-style-import": "^1.4.0",
    "vite-plugin-svg-icons": "^1.0.5",
    "vue": "^3.2.16",
    "vue-router": "^4.0.12",
    "vuex": "^4.0.2",
    "vuex-persistedstate": "^4.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^1.9.3",
    "sass": "^1.43.5",
    "unplugin-vue-components": "^0.17.2",
    "vite": "^2.6.4"
  }
}

2.router/index.js

import { createRouter, createWebHistory } from 'vue-router';
const Login = () => import('@/views/login.vue');
const p404 = () => import('@/views/error-page/p404.vue');
import views from '@/router/views';
import MenuLayOut from '@/components/MenuLayOut.vue'; // 这个是管理系统左侧菜单
import Store from '@/store/index';
const routes = [
  {
    path: '/login',
    component: Login,
    name: 'login',
    meta: {
      title: 'login',
      icon: '#login',
    },
  },
  {
    path: '/',
    component: MenuLayOut,
    redirect: 'devops',
    children: [...views],
  },
  {
    path: '/404',
    name: '404',
    component: p404,
    meta: {
      title: '404',
    },
  },
  {
    path: '/:pathMatch(.*)',
    redirect: '/404',
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

router.beforeEach(async (to, from, next) => {
  // const token = Cookies.get('token')
  // if (token) {
  if (to.path === '/login') {
    next({ path: '/' });
  } else {
    // if (!store.state.authorized) {
    //     // set authority
    //     await store.dispatch('setAuthority')
    //     // it's a hack func,avoid bug
    //     next({ ...to, replace: true })
    // } else {
    next();
    // }
  }
  // } else {
  //     if (to.path !== '/login') {
  //         next({ path: '/login' })
  //     } else {
  //         next(true)
  //     }
  // }
});

router.afterEach((to, from) => {
  // ...
  Store.commit('common/changeMenu', to.meta.activeMenu);
});
export default router;

3.view.js

const Devops = () =>
  import(/* webpackChunkName: "devops" */ '../views/devops/list.vue');
const Quality = () =>
  import(/* webpackChunkName: "quality" */ '../views/quality/list.vue');
const QualityDetail = () =>
  import(/* webpackChunkName: "quality" */ '../views/quality/detail.vue');
const Performance = () =>
  import(/* webpackChunkName: "performance" */ '../views/performance/list.vue');
const PerformanceDetail = () =>
  import(/* webpackChunkName: "quality" */ '../views/performance/detail.vue');

const router = [
  {
    path: 'devops',
    component: Devops,
    name: 'devops',
    meta: {
      title: 'DevOps看板',
      icon: 'icon_devops', // 看后面的svg配置后,记得创建对应的svg文件
      activeMenu: 'devops',
    },
  },
  {
    path: 'quality',
    component: Quality,
    name: 'quality',
    meta: {
      title: '应用代码质量报告',
      icon: 'icon_quality',
      activeMenu: 'quality',
    },
  },
  {
    path: 'quality/:name',
    component: QualityDetail,
    name: 'quality.detail',
    meta: {
      title: '应用代码质量报告',
      icon: 'icon_quality',
      activeMenu: 'quality',
      hidden: true,
    },
  },

  {
    path: 'performance',
    component: Performance,
    name: 'performance',
    meta: {
      title: '应用性能测试报告',
      icon: 'icon_microsoft',
      activeMenu: 'performance',
    },
  },
  {
    path: 'performance/:name',
    component: PerformanceDetail,
    name: 'performance.detail',
    meta: {
      title: '应用性能测试报告',
      activeMenu: 'performance',
      hidden: true,
    },
  },
];

export default router;

添加eslint

eslint相关依赖

npm i eslint -s
npm i -d @typescript-eslint/parser @vue/eslint-config-prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue eslint-plugin-vuejs-accessibility prettier vue-eslint-parser

创建 eslintignore.js

/build/
/config/
/dist/
**/*.ts
/tests/

创建 eslint.js

//.eslintrc.js
module.exports = {
  parser: 'vue-eslint-parser',
  root: true,
  env: {
    node: true,
  },
  extends: [
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',
    'plugin:prettier/recommended',
  ],
  parserOptions: {
    parser: '@typescript-eslint/parser', // Specifies the ESLint parser
    ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
    sourceType: 'module', // Allows for the use of imports
    ecmaFeatures: {
      jsx: true,
    },
  },
  rules: {
    'import/extensions': ['.js', '.vue', '.json'],
    // allow optionalDependencies
    // 'import/no-extraneous-dependencies': [
    //   'error',
    //   {
    //     optionalDependencies: ['test/unit/index.js'],
    //     sourceType: 'module',
    //     allowImportExportEverywhere: true,
    //   },
    // ],
    'import/extensions': ['error', 'always', { ignorePackages: false }],
    'implicit-arrow-linebreak': ['error', 'beside'],
    'no-shadow': 0,
    // 禁止覆盖受限制的标识符
    'no-shadow-restricted-names': 2,
    // "no-shadow": ["error", { "allow": ["done"] }],
    'no-unused-expressions': [
      'off',
      { allowShortCircuit: true, allowTernary: true },
    ],
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    // https://eslint.org/docs/rules/arrow-parens
    'arrow-parens': ['warn', 'as-needed'],
    // https://eslint.org/docs/rules/arrow-body-style
    'arrow-body-style': ['off', 'as-needed'],
    // https://eslint.org/docs/rules/arrow-spacing
    'arrow-spacing': 'error',
    // https://eslint.org/docs/rules/class-methods-use-this
    'class-methods-use-this': ['off'],
    // https://eslint.org/docs/rules/no-param-reassign
    'no-param-reassign': ['off'],
    'no-mixed-operators': ['off', { allowSamePrecedence: false }],
    // https://eslint.org/docs/rules/no-console
    'no-console': ['warn', { allow: ['warn', 'error', 'log'] }],
    'prefer-template': 'off',
    'linebreak-style': [2, 'unix'],

    // vue lint
    // https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-self-closing.md
    'vue/html-self-closing': [
      'error',
      {
        html: {
          void: 'always',
          normal: 'never',
          component: 'never',
        },
        svg: 'any',
        math: 'always',
      },
    ],
    // https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/max-attributes-per-line.md
    'vue/max-attributes-per-line': [
      0,
      {
        singleline: 2,
        multiline: {
          max: 1,
          allowFirstLine: true,
        },
      },
    ],
    'object-curly-newline': ['off'],
    'function-paren-newline': ['off'],
    camelcase: ['off'],
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
  },
  overrides: [
    {
      files: [
        '**/__tests__/*.{j,t}s?(x)',
        '**/tests/unit/**/*.spec.{j,t}s?(x)',
      ],
      env: {
        mocha: true,
      },
    },
  ],
};

添加 .prettierrc.json

{
    "semi": true,
    "eslintIntegration": true,
    "singleQuote": true,
    "endOfLine": "lf",
    "tabWidth": 2,
    "bracketSpacing": true
}

.setting.json + jsconfig.json 配置 vscode 自动保存elint/prettier格式化

{
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    },
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        "vue",
        "typescript",
        "typescriptreact",
        "json"
    ]
}

配置 模块化store module

这里可以有大体两种,本人更喜欢下面这个

1.index.js

import { createStore, createLogger } from 'vuex';
import createPersistedState from 'vuex-persistedstate';
import auth from 'store/modules/auth';
import common from 'store/modules/common';

const debug = process.env.NODE_ENV !== 'production';
const plugins = [createPersistedState()];
if (debug) {
  plugins.push(createLogger());
}
export default createStore({
  modules: {
    auth,
    common,
  },
  strict: debug,
  plugins,
});

common.js

配置alias

vite.config

resolve: {
      alias: [
        {
          find: /^@\//,
          replacement: pathResolve('src') + '/',
        },
        {
          find: /^store\//,
          replacement: pathResolve('src/store') + '/',
        },
        {
          find: /^~\//,
          replacement: pathResolve('node_modules') + '/',
        },
      ],
    },

jsconfig.json 是告诉vscode知道

配置自动批量导入自定义组件

这个也是一直用一直爽,每次在指定的文件夹(src/components/*.vue)新增组件,不用在全局逐个引入,就能页面里面直接引用

src/components/index.js

const files = import.meta.glob('./*.vue');

const filesJs = import.meta.glob('./*.js');

const componentsGlob = (app) => {
  Object.keys(files).forEach((key) => {
    files[key]().then((res) => {
      const start = key.lastIndexOf('.');
      const end = key.lastIndexOf('/') + 1;
      const cName = key.slice(end, start);
      app.component(cName, res.default);
    });
  });
};

export { componentsGlob };

src/components/MenuLayOut.vue

<template>
  <div class="dashboard-con">
    <!-- <div class="status-bar"></div> -->
    <div :class="{ content: true, opened: isCollapse }">
      <div class="nav">
        <el-menu
          :default-active="defaultActiveMenu"
          class="menu"
          :collapse="isCollapse"
          :default-openeds="defaultOpeneds"
          router
          background-color="#373B41"
          text-color="#e4e7ed"
        >
          <template v-for="e in views">
            <el-sub-menu :index="e.name" :key="e.name" v-if="e.children">
              <template #title>
                <SvgIcon class="icon" :name="e.meta.icon"></SvgIcon>
                <template>
                  <span>{{ e.meta.title }}</span>
                </template>
                <template v-for="c in e.children">
                  <el-menu-item
                    v-if="!c.meta.hidden"
                    :key="c.name"
                    :index="c.name"
                    :route="{
                      name: c.name,
                      params: { scope: c.meta.title },
                    }"
                  >
                    <SvgIcon class="icon" :name="c.meta.icon"></SvgIcon>
                    <span> {{ c.meta.title }}</span>
                  </el-menu-item>
                </template>
              </template>
            </el-sub-menu>
            <el-menu-item
              v-if="!e.children && !e.meta.hidden"
              :key="e.name"
              :index="e.name"
              :route="{ name: e.name }"
            >
              <SvgIcon class="icon" :name="e.meta.icon"></SvgIcon>
              <template #title>
                <span>{{ e.meta.title }}</span>
              </template>
            </el-menu-item>
          </template>
        </el-menu>
        <p class="collapse-btn" @click="toogleCollapse">
          <el-icon class="arrow"> <arrow-left-bold /> </el-icon>
        </p>
      </div>
      <div class="dashboard-content">
        <router-view></router-view>
      </div>
    </div>
  </div>
</template>
<script>
import { mapState } from 'vuex';
import views from '@/router/views.js';
export default {
  components: {},
  data() {
    return {
      views,
    };
  },
  computed: {
    ...mapState({
      defaultActiveMenu: (state) => state.common.defaultActiveMenu,
      isCollapse: (state) => state.common.isCollapse,
    }),

    defaultOpeneds() {
      const temp = [];
      this.views.forEach((e) => {
        e.children && temp.push(e.name);
      });
      return temp;
    },
  },
  methods: {
    toogleCollapse() {
      this.$store.commit('common/changeIsCollapse', !this.isCollapse);
    },
  },
};
</script>
<style lang="scss" scoped>
$icon-default: #909399;
.el-menu {
  border-right: none !important;
}

.icon {
  vertical-align: middle;
  margin-right: 5px;
  text-align: center;
  font-size: 20px;
  width: 24px;
  height: 18px;
  align-self: center;
  color: $icon-default;
  fill: $icon-default;
}

.el-menu--vertical .icon {
  color: #909399;
}

.el-sub-menu__title {
  &:hover {
    background-color: generate-color($blue, -6);
  }

  &.is-active {
    background-color: generate-color($blue, -7);
  }
}

.el-menu-item {
  height: 50px;
  line-height: 50px;

  &:hover {
    background-color: $dark-menu-item-bg;
  }

  &.is-active {
    background-color: $dark-menu-item-bg;
    color: $blue;

    .icon {
      color: $blue;
      fill: $blue;
    }
  }
}

.dashboard-con {
  height: 100%;
  position: relative;

  .status-bar {
    height: $nav-height;
    width: 100%;
    background-color: $dark-menu-bg;
  }

  .content {
    width: 100%;
    height: 100%;

    .nav {
      color: #e4e7ed;
      background-color: $dark-menu-bg;
      width: $nav-width;
      min-width: $nav-width;
      height: calc(100% - 57px);
      padding-bottom: 57px;
      position: absolute;
      overflow-y: scroll;
      transition: all 0.3s;

      .menu {
        width: 100%;
        overflow-y: scroll;
        // height: calc(100% - #{$nav-height});
        height: 100%;
        overflow-y: scroll;
      }

      .collapse-btn {
        border-top: 1px solid #2d3034;
        background-color: $dark-menu-bg;
        width: $nav-width;
        text-align: center;
        height: 56px;
        line-height: 56px;
        position: absolute;
        bottom: 0;
      }
    }

    .dashboard-content {
      width: calc(100% - $nav-width);
      min-width: calc(1280px - $nav-width);
      box-sizing: border-box;
      height: 100%;
      overflow: scroll;
      margin-left: $nav-width;
      position: relative;
      transition: all 0.3s;
    }

    &.opened {
      .nav {
        width: 64px;
        min-width: 64px;
      }

      .dashboard-content {
        margin-left: 64px;
      }

      .collapse-btn {
        width: 64px;
      }
    }
  }
}
</style>

配置svg

svg 在一些菜单栏或者操作按钮上都很常用,你不会还在用图片格式的吧。使用svg加上依稀图标网站上例如icon-font获取一些必要的图表,没有ui也不方,像我现在的广州团队没有ui,我一个前端搞定了。

1.引入依赖

npm i vite-plugin-svg-icons -d

2.vite.config 加上这个配置
import viteSvgIcons from 'vite-plugin-svg-icons';
plugins: [
      vue(),
      viteSvgIcons({
        // 指定需要缓存的图标文件夹
        iconDirs: [resolve(root, 'src/assets/svg')],
        // 指定symbolId格式
        symbolId: 'icon-[dir]-[name]',
      }),
]

一直用一直爽,每次到icon-font复制svg代码到src/assets/svg下,就可以直接在组件中使用了,不用再逐个引入svg文件了

3.svg 组件
<template>
  <svg class="svg" aria-hidden="true">
    <use :xlink:href="symbolId" :fill="color" />
  </svg>
</template>

<script>
import { computed } from 'vue';

export default {
  name: 'SvgIcon',
  props: {
    prefix: {
      type: String,
      default: 'icon',
    },
    name: {
      type: String,
      required: true,
    },
    color: {
      type: String,
    },
  },
  setup(props) {
    const symbolId = computed(() => `#${props.prefix}-${props.name}`);
    return { symbolId };
  },
};
</script>
<style lang="scss" scoped>
.svg {
  display: inline-block;
  vertical-align: middle;
}
</style>

4.使用方式
 <SvgIcon class="svg-icon" name="icon_name"></SvgIcon>
5.可以class改变样式或者大小
.svg-icon{fill:cyan;background-color:blue;}

使用ui组件库 element-plus

1.element-pus 安装

npm i element-plus

2.使用unplugin-vue-components/resolvers按使用情况自动导入对应组件依赖

npm i unplugin-vue-components/resolvers 配置vite.config

      Components({
        resolvers: [ElementPlusResolver()],
      }),
      styleImport({
        libs: [
          {
            libraryName: "element-plus",
            ensureStyleFile: true,
            resolveStyle: (name) => {
              if (name === "locale") return "";
              return `element-plus/packages/theme-chalk/src/${name}.scss`;
            },
            resolveComponent: (name) => {
              return `element-plus/lib/${name}`;
            },
          },
        ],
      }),

3.配置组件为中文版本

<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import zhCn from 'element-plus/lib/locale/lang/zh-cn';
import { ref } from 'vue';
const locale = ref(zhCn);
</script>
<template>
  <div id="app">
    <ElConfigProvider :locale="locale">
      <router-view></router-view>
    </ElConfigProvider>
  </div>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  background-color: #f1f3f6;
  width: 100%;
  height: fit-content;
}
</style>

添加env 信息

.env

# port
VITE_PORT = 5000

# spa-title
VITE_GLOB_APP_TITLE = Daoclou DCS

# spa shortname
VITE_GLOB_APP_SHORT_NAME = DCS

.env.production

# 只在生产模式中被载入

# 网站标题
VITE_APP_TITLE = peoduction-ui


# 网站前缀
VITE_BASE_URL = './'


VITE_OUTPUT_DIR = dist

# 是否删除console
VITE_DROP_CONSOLE = true

# API
VITE_APP_API_URL = http://10.23.12.108:34203

.env.development

# 只在开发模式中被载入

# 网站标题
VITE_APP_TITLE = dev-ui


# 网站前缀
VITE_BASE_URL = '/'

VITE_OUTPUT_DIR = dist

# 是否删除console
VITE_DROP_CONSOLE = true

# API
VITE_APP_API_URL = http://10.23.12.108:34203

整个vite.config

import { defineConfig, loadEnv } from 'vite';
import { resolve } from 'path';
import dayjs from 'dayjs';
import vue from '@vitejs/plugin-vue';
import viteSvgIcons from 'vite-plugin-svg-icons';
import Components from 'unplugin-vue-components/vite';
// 动态导入用到的element组件
import styleImport from 'vite-plugin-style-import';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import pkg from './package.json';
// https://vitejs.dev/config/
const root = process.cwd();
const { dependencies, devDependencies, name, version } = pkg;
const __APP_INFO__ = {
  pkg: { dependencies, devDependencies, name, version },
  lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
};
export default defineConfig(({ mode }) => {
  const isProduction = mode === 'production';
  const { VITE_BASE_URL, VITE_APP_API_URL, VITE_PORT, VITE_OUTPUT_DIR } =
    loadEnv(mode, root);
  function pathResolve(dir) {
    return resolve(root, VITE_BASE_URL, dir);
  }
  return {
    base: VITE_BASE_URL,
    root,
    hmr: { overlay: false },
    resolve: {
      alias: [
        {
          find: /^@\//,
          replacement: pathResolve('src') + '/',
        },
        {
          find: /^store\//,
          replacement: pathResolve('src/store') + '/',
        },
        {
          find: /^~\//,
          replacement: pathResolve('node_modules') + '/',
        },
      ],
    },
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `@import './src/style/variables.scss';@import './src/style/element.scss';`,
        },
      },
    },
    build: {
      outDir: VITE_OUTPUT_DIR, //打包后文件的存放路径
      cssCodeSplit: true,
      sourcemap: false,
      target: 'modules',
      chunkSizeWarningLimit: 2000,
      assetsInlineLimit: 4096,
      minify: 'terser',
      brotliSize: false, // => brotli压缩大小报告
      terserOptions: {
        compress: {
          keep_infinity: true,
          drop_console: true,
          drop_debugger: true,
        },
      },
      rollupOptions: {
        output: {
          chunkFileNames: 'static/js/[name]-[hash].js',
          entryFileNames: 'static/js/[name]-[hash].js',
          assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
        },
      },
    },
    define: {
      // setting vue-i18-next
      // Suppress warning
      // __INTLIFY_PROD_DEVTOOLS__: false,
      __APP_INFO__: JSON.stringify(__APP_INFO__),
    },
    optimizeDeps: {
      include: [
        '@vueuse/core',
        '@vue/runtime-core',
        'element-plus',
        'lodash',
        'jspdf',
        'html2canvas',
        'axios',
        'vuex',
      ],
    },
    server: {
      port: VITE_PORT,
      open: true,
      cors: true,
      proxy: {
        '/v1': {
          target: VITE_APP_API_URL,
          changeOrigin: true,
        },
      },
    },
    plugins: [
      vue(),
      viteSvgIcons({
        // 指定需要缓存的图标文件夹
        iconDirs: [resolve(root, 'src/assets/svg')],
        // 指定symbolId格式
        symbolId: 'icon-[dir]-[name]',
      }),
      Components({
        resolvers: [ElementPlusResolver()],
      }),
      styleImport({
        libs: [
          {
            libraryName: 'element-plus',
            ensureStyleFile: true,
            resolveStyle: (name) => {
              if (name === 'locale') return '';
              return `element-plus/theme-chalk/${name}.css`;
            },
          },
        ],
      }),
    ],
  };
});

引入axios

1.npm i axios

2. src/service/index.js

import axios from 'axios';
import { localGet } from '@/core/storage';
import { ElMessage } from 'element-plus';

const showMsg = (
  message = '请求出错,请联系管理员',
  type = 'error',
  duration = 5 * 1000
) => {
  ElMessage({
    message,
    type,
    duration,
  });
};

const service = axios.create({
  // baseURL: process.env.BASE_API, // api 的 base_url
  timeout: 50000, // 请求超时时间
});

service.interceptors.request.use(
  (config) => {
    // if (store.getters.token) {
    //   config.headers['X-Token'] = getToken(); // TODO:
    // }
    return config;
  },
  (error) => {
    console.log(error); // for debug
    Promise.reject(error);
  }
);

// response 拦截器
service.interceptors.response.use(
  (response) => {
    const { silent } = response.config;
    const res = response.data;
    const codeReg = /^20\d+/;
    if (codeReg.test(response.status)) {
      return Promise.resolve(res);
    } else {
      return Promise.reject(res);
    }
    // !silent && showMsg(res.msg);
    // return Promise.reject(response.data);
  },
  (error) => {
    const errorItems = error?.response?.data;
    const { status } = error.response;
    const { silent } = error.config;
    if (silent) {
      return Promise.reject(error);
    }
    let msg = '';
    if (status === 500) {
      const { error_type = '错误', error_message = '系统错误' } =
        errorItems || {};
      msg = `${error_type} ${error_message}`;
    }
    !silent && showMsg(msg || error.message);
    return Promise.reject(error);
  }
);

export default service;

3. src/service/namespace.js

import api from '@/service/index';
class NameSpaceService {
  constructor() {
    this.api = api;
  }

  nameSpaceList(namespace_id = '') {
    return this.api.get('/api/dcs/namespace', { params: namespace_id });
  }
}

export default new NameSpaceService();

关于vue2的mixins在vue3下使用的语法糖

优化的原因

主要是避免了命名冲突,解决了变量追踪不明确的问题。

使用方法

-例如:src/hooks/TableHook.js

import { reactive, ref } from 'vue';
export default function () {
  const total = ref(0);
  const page = ref(1);
  const pageSizes = [10, 30, 50, 100];
  const pageSize = ref(10);
  const query = reactive({ page, pageSize });
  function handleSizeChange(val) {
    query.pageSize = val;
  }
  return {
    total,
    query,
    pageSizes,
    handleSizeChange,
  };
}

然后用的地方

import TableHook from '@/hooks/TableHook';

const { total, query, pageSizes } = TableHook();

使用lodash并自动引入

npm i lodash npm i @types/lodash -d

支持js新语法可选链「?.」以及逻辑空分配(双问号)「??」

npm i @babel/plugin-proposal-optional-chaining -s


// .babel.config.js
module.exports = {
   presets: [
      '@vue/cli-plugin-babel/preset',
    ],
  	...
}