vue3 构建

1,050 阅读14分钟

vite 安装 vue3+ts

终端运行

pnpm create vite vue3 -- --template vue-ts

配置 tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
    "skipLibCheck": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "noImplicitAny": false,
    "allowJs": true,
    "removeComments": true,
    "types": ["vite/client"],
    "baseUrl": ".",
    "paths": {
      "/@/*": ["src/*"],
      "/#/*": ["types/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

配置 vite.config.ts

安装 @types/node, 因为node不支持ts

pnpm add @types/node -D
import type { ConfigEnv, UserConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";

function pathResolve(dir: string) {
  return resolve(process.cwd(), ".", dir);
}

export default ({ command, mode }: ConfigEnv): UserConfig => {
  return {
    resolve: {
      alias: [
        {
          find: /\/@\//,
          replacement: pathResolve("src") + "/",
        },
        {
          find: /\/#\//,
          replacement: pathResolve("types") + "/",
        },
      ],
    },
    plugins: [vue()],
  };
};

配置 eslint 和 prettier

安装依赖

pnpm add eslint eslint-plugin-vue vue-eslint-parser @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier eslint-plugin-prettier eslint-config-prettier -D
  • eslint: 是检查 js 代码的依赖
  • eslint-plugin-vue: 是检查 vue 文件 js 代码的依赖
  • vue-eslint-parser: 是对 vue 文件 <template> 中的 js 语法进行检查
  • @typescript-eslint/eslint-plugin : ts 语法的默认规则
  • @typescript-eslint/parser: 解析器的默认规则
  • prettier: 格式化代码
  • eslint-plugin-prettier: 检查格式化规则
  • eslint-config-prettier: 默认格式化规则配置

配置 eslint

.eslintrc.js

创建 .eslintrc.cjs 文件

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
    es6: true,
  },
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 2020,
    sourceType: 'module',
    jsxPragma: 'React',
    ecmaFeatures: {
      jsx: true,
    },
  },
  extends: [
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  rules: {
    'vue/script-setup-uses-vars': 'error',
    '@typescript-eslint/ban-ts-ignore': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/no-var-requires': 'off',
    '@typescript-eslint/no-empty-function': 'off',
    'vue/custom-event-name-casing': 'off',
    'no-use-before-define': 'off',
    '@typescript-eslint/no-use-before-define': 'off',
    '@typescript-eslint/ban-ts-comment': 'off',
    '@typescript-eslint/ban-types': 'off',
    '@typescript-eslint/no-non-null-assertion': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-unused-vars': [
      'error',
      {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
      },
    ],
    'no-unused-vars': [
      'error',
      {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
      },
    ],
    'space-before-function-paren': 'off',

    'vue/attributes-order': 'off',
    'vue/one-component-per-file': 'off',
    'vue/html-closing-bracket-newline': 'off',
    'vue/max-attributes-per-line': 'off',
    'vue/multiline-html-element-content-newline': 'off',
    'vue/singleline-html-element-content-newline': 'off',
    'vue/attribute-hyphenation': 'off',
    'vue/require-default-prop': 'off',
    'vue/require-explicit-emits': 'off',
    'vue/html-self-closing': [
      'error',
      {
        html: {
          void: 'always',
          normal: 'never',
          component: 'always',
        },
        svg: 'always',
        math: 'always',
      },
    ],
    'vue/multi-word-component-names': 'off',
  },
};

.eslintignore

创建 .eslintignore 文件

node_modules
*.md
.vscode
dist
/public
.husky

配置 prettier

prettier.config.js

创建 .prettierrc.cjs 文件

module.exports = {
  printWidth: 100,
  semi: true,
  vueIndentScriptAndStyle: true,
  singleQuote: true,
  trailingComma: 'all',
  proseWrap: 'never',
  htmlWhitespaceSensitivity: 'strict',
  endOfLine: 'auto',
};

.prettierignore

创建 .prettierignore 文件

/dist/*
/node_modules/**
**/*.svg
**/*.sh
/public/*

脚本运行

{
  "script": {
    "/": "对src目录下的vue, ts, tsx 文件进行 eslint 检查并修复",
    "lint:eslint": "eslint --cache --max-warnings 0  \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
    "/": "对src目录下的 js,json,tsx,css,less,scss,vue,html,md 文件进行 prettier 格式化并修复",
    "lint:prettier": "prettier --write  \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\""
  }
}

配置 style-lint

安装依赖

pnpm add stylelint stylelint-config-standard stylelint-config-standard-scss stylelint-order postcss postcss-html stylelint-config-recommended stylelint-config-recommended-scss stylelint-config-recommended-vue stylelint-config-html -D
  • stylelint: 检查样式
  • stylelint-config-standard: stylelint 的常用约定配置
  • stylelint-config-standard-scss: 兼容scss的常用约定配置
  • stylelint-order: 对 stylelint 错误文件进行修复
  • postcss: css 转换工具
  • postcss-html: 解析 html postcss 语法
  • stylelint-config-recommended: stylelint 的基础配置
  • stylelint-config-recommended-scss: 兼容scss的基础配置
  • stylelint-config-recommended-vue: vue stylelint 的基础配置
  • stylelint-config-html

stylelint.config.js

创建 .stylelintrc.cjs 文件

module.exports = {
  extends: [
    'stylelint-config-standard',
    'stylelint-config-html/vue',
    'stylelint-config-standard-scss',
    'stylelint-config-recommended-vue/scss',
  ],
  plugins: ['stylelint-order'],
  rules: {
    'indentation': 2,
    'function-no-unknown': null,
    'selector-class-pattern': null,
    'selector-pseudo-class-no-unknown': [
      true,
      {
        ignorePseudoClasses: ['global'],
      },
    ],
    'selector-pseudo-element-no-unknown': [
      true,
      {
        ignorePseudoElements: ['v-deep'],
      },
    ],
    'at-rule-no-unknown': [
      true,
      {
        ignoreAtRules: [
          'tailwind',
          'apply',
          'variants',
          'responsive',
          'screen',
          'function',
          'if',
          'each',
          'include',
          'mixin',
        ],
      },
    ],
    'no-empty-source': null,
    'string-quotes': null,
    'named-grid-areas-no-invalid': null,
    'unicode-bom': 'never',
    'no-descending-specificity': null,
    'font-family-no-missing-generic-family-keyword': null,
    'declaration-colon-space-after': 'always-single-line',
    'declaration-colon-space-before': 'never',
    // 'declaration-block-trailing-semicolon': 'always',
    'rule-empty-line-before': [
      'always',
      {
        ignore: ['after-comment', 'first-nested'],
      },
    ],
    'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
    'order/order': [
      [
        'dollar-variables',
        'custom-properties',
        'at-rules',
        'declarations',
        {
          type: 'at-rule',
          name: 'supports',
        },
        {
          type: 'at-rule',
          name: 'media',
        },
        'rules',
      ],
      { severity: 'warning' },
    ],
    'order/properties-order': [
      'position',
      'top',
      'right',
      'bottom',
      'left',
      'z-index',
      'display',
      'justify-content',
      'align-items',
      'float',
      'clear',
      'overflow',
      'overflow-x',
      'overflow-y',
      'margin',
      'margin-top',
      'margin-right',
      'margin-bottom',
      'margin-left',
      'padding',
      'padding-top',
      'padding-right',
      'padding-bottom',
      'padding-left',
      'width',
      'min-width',
      'max-width',
      'height',
      'min-height',
      'max-height',
      'font-size',
      'font-family',
      'font-weight',
      'border',
      'border-style',
      'border-width',
      'border-color',
      'border-top',
      'border-top-style',
      'border-top-width',
      'border-top-color',
      'border-right',
      'border-right-style',
      'border-right-width',
      'border-right-color',
      'border-bottom',
      'border-bottom-style',
      'border-bottom-width',
      'border-bottom-color',
      'border-left',
      'border-left-style',
      'border-left-width',
      'border-left-color',
      'border-radius',
      'text-align',
      'text-justify',
      'text-indent',
      'text-overflow',
      'text-decoration',
      'white-space',
      'color',
      'background',
      'background-position',
      'background-repeat',
      'background-size',
      'background-color',
      'background-clip',
      'opacity',
      'filter',
      'list-style',
      'outline',
      'visibility',
      'box-shadow',
      'text-shadow',
      'resize',
      'transition',
    ],
  },
  ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
  overrides: [
    {
      files: ['*.vue', '**/*.vue', '*.html', '**/*.html'],
      extends: ['stylelint-config-recommended'],
      rules: {
        'keyframes-name-pattern': null,
        'selector-pseudo-class-no-unknown': [
          true,
          {
            ignorePseudoClasses: ['deep', 'global'],
          },
        ],
        'selector-pseudo-element-no-unknown': [
          true,
          {
            ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'],
          },
        ],
      },
    },
    {
      files: ['*.less', '**/*.less'],
      customSyntax: 'postcss-less',
      extends: ['stylelint-config-standard', 'stylelint-config-recommended-vue'],
    },
  ],

}

.stylelintignore

创建 .stylelintignore 忽略文件

/dist/*
/public/*
public/*

脚本运行

{
  "script": {
    "/": "对src目录下的 vue,less,postcss,css,scss文件进行 stylelint 样式检查并修复",
    "lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/"
  }
}

脚本删除文件

安装依赖

pnpm add rimraf -D

脚本运行

{
  "script": {
    "/": "删除缓存文件",
    "clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
    "/": "删除依赖文件",
    "clean:lib": "rimraf node_modules",
    "/": "删除安装依赖文件, 并重新安装",
    "reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap"
  }
}

规范代码提交

安装依赖

pnpm add husky lint-staged @commitlint/cli @commitlint/config-conventional cz-conventional-changelog-zh conventional-changelog-cli  -D
  • husky: 可以在 Git 的钩子函数中执行脚本
  • lint-staged: 针对暂存文件进行 lint 操作
  • @commitlint/cli: 对 commit 的消息进行格式检查
  • @commitlint/config-conventional: commit 的消息检查格式传统配置
  • cz-conventional-changelog-zh: 中文提交
  • conventional-changelog-cli : 生成 git 提交记录

配置 husky

官网: url[typicode.github.io/husky/#/?id…]

脚本运行

{
  "script": {
    "/": "初始化git钩子函数",
    "prepare": "husky install",
    "/": "对暂存文件进行 lint 检查",
    "lint:lint-staged": "lint-staged",
    "/": "生成 git 提交记录",
    "log": "conventional-changelog -p angular -i CHANGELOG.md -s"
  }
}

初始化

yarn prepare
npx husky add .husky/commit-msg
npx husky add .husky/pre-commit

common.sh

创建 common.sh 文件

#!/bin/sh
command_exists () {
  command -v "$1" >/dev/null 2>&1
}

# Workaround for Windows 10, Git Bash and Yarn
if command_exists winpty && test -t 1; then
  exec < /dev/tty
fi

commit-msg

修改 commit-msg 文件

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx --no-install commitlint --edit "$1"

pre-commit

修改 pre-commit 文件

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"

[ -n "$CI" ] && exit 0

npm run lint:lint-staged

commitlint.config.js

创建 .commitlintrc.cjs 文件, 配置提交规则

module.exports = {
  ignores: [(commit) => commit.includes('init')],
  extends: ['@commitlint/config-conventional'],
  rules: {
    'body-leading-blank': [2, 'always'],
    'footer-leading-blank': [1, 'always'],
    'header-max-length': [2, 'always', 108],
    'subject-empty': [2, 'never'],
    'type-empty': [2, 'never'],
    'subject-case': [0],
    'type-enum': [
      2,
      'always',
      [
        'feat',
        'fix',
        'perf',
        'style',
        'docs',
        'test',
        'build',
        'ci',
        'chore',
        'revert',
        'conflict',
        'font',
        'delete',
        'stash'
      ],
    ],
  },
};

配置 git-cz

中文

{
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog-zh"
    }
  },
  "lint-staged": { 
    "*.{js,jsx,ts,tsx}": [ 
      "eslint --fix", 
      "prettier --write" 
    ], 
    "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [ 
      "prettier --write--parser json" 
    ], 
    "package.json": [ 
      "prettier --write" 
    ], 
    "*.vue": [ 
      "eslint --fix", 
      "prettier --write", 
      "stylelint --fix" 
    ], 
    "*.{scss,less,styl,html}": [ 
      "stylelint --fix", 
      "prettier --write" 
    ], 
    "*.md": [ 
      "prettier --write"
    ] 
  }
}

注意

修改 package.json 才能通过 commit 校验

{
  "name": "xx",
  "private": true,
  "version": "0.0.0",
  // 需要删除 type 属性
  // "type": "module",
}

配置 vue-router

官网: url[router.vuejs.org/zh/introduc…]

安装依赖

pnpm add vue-router

文件目录

├── src # 主目录
│   ├── router # 路由
│   │   ├── guard # 路由守卫
│   │   │   ├── index.ts # 路由守卫配置
│   │   ├── routes # 路由地址
│   │   │   ├── modules # 路由模块
│   │   │   │   ├── xx.ts # 对应模块路由配置
│   │   │   ├── index.ts # 路由模块统一导出
│   │   ├── index.ts # 创建路由
│   │   ├── types.ts # 路由类型

配置 router

  • src/router/index.ts

    import { createRouter, createWebHistory } from 'vue-router';
    import type { App } from 'vue';
    import routeModuleList from './routes';
    
    /**
     * 创建路由
     */
    export const router = createRouter({
      history: createWebHistory(),
      routes: routeModuleList,
    });
    
    /**
     * 挂载路由
     * @param app
     */
    export function setupRouter(app: App<Element>) {
      app.use(router);
    }
    
  • src/router/types.ts

    import type { RouteRecordRaw, RouteMeta } from 'vue-router';
    import { defineComponent } from 'vue';
    
    export type Component<T = any> =
      | ReturnType<typeof defineComponent>
      | (() => Promise<typeof import('*.vue')>)
      | (() => Promise<T>);
    
    // app 路由类型
    export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
      name: string;
      meta: RouteMeta;
      component: Component | string;
      components?: Component;
      children?: AppRouteRecordRaw[];
    }
    
    export type AppRouteModule = AppRouteRecordRaw;
    
  • src/router/routes/modules/**.ts

    import type { AppRouteModule } from '/@/router/types';
    
    const Home: AppRouteModule = {
      path: '/',
      name: '首页',
      component: () => import('/@/views/home/index.vue'),
      meta: {
        title: '首页',
      },
    };
    
    export default Home;
    
  • src/router/routes/index.ts

    import type { AppRouteModule } from '/@/router/types';
    
    /**
     * 获取到模块里的参数
     */
    const modules: Record<string, () => Promise<any>> = import.meta.glob('./modules/*.ts');
    
    /**
     * 获取到每个模块的路径
     */
    let routeModuleList: AppRouteModule[] = [];
    const paths = Object.keys(modules);
    const list = Promise.all(
    paths.map(async (path) => {
        const mod = await modules[path]();
        return mod.default;
      }),
    );
    routeModuleList = await list;
    export default routeModuleList as any;
    
  • src/router/routes/guard/index.ts

    import type { Router } from 'vue-router';
    
    /**
     * 路由 导航守卫方法
     * @param router
     */
    export function setupRouterGuard(router: Router) {
      createPageGuard(router);
    }
    
    /**
     * 处理页状态钩子
     * @param router
     * @returns
     */
    function createPageGuard(router: Router) {
      const loadedPageMap = new Map<string, boolean>();
      // 全局前置守卫
      router.beforeEach(async (to) => {
        // 页面已经加载,重新打开会更快,您不需要进行加载和其他处理
        to.meta.loaded = !!loadedPageMap.get(to.path);
        return true;
      });
      // 全局后置守卫
      router.afterEach((to) => {
        loadedPageMap.set(to.path, true);
      });
    }
    

配置 main.ts

import { createApp } from 'vue';
import App from './App.vue';
import { router, setupRouter } from '/@/router';
import { setupRouterGuard } from '/@/router/guard';

async function bootstrap() {
  const app = createApp(App);
  setupRouter(app);
  setupRouterGuard(router);
  app.mount('#app');
}

bootstrap();

配置 pinia

官网: url[pinia.vuejs.org/introductio…]

安装依赖

pnpm add pinia

文件目录

├── src # 主目录
│   ├── store # pinia
│   │   ├── modules # store 模块
│   │   │   ├── xx.ts # 对应模块 store 配置
│   │   ├── index.ts # store 配置

配置 pinia

  • src/store/modules/xx.ts

    import { defineStore } from 'pinia';
    
    export const useCounterStore = defineStore('counter', {
      state: () => ({ count: 0 }),
      getters: {
        // 计算属性
      },
      actions: {
        // 异步操作
        increment() {
          this.count++;
        },
      },
    });
    
  • src/store/index.ts

    import type { App } from 'vue';
    import { createPinia } from 'pinia';
    
    /**
     * 创建 store
     */
    const store = createPinia();
    
    /**
     * 挂载 store
     * @param app
     */
    export function setupStore(app: App<Element>) {
      app.use(store);
    }
    
    export { store };
    

配置 main.ts

import { createApp } from 'vue';
import App from './App.vue';
import { router, setupRouter } from '@/router';
import { setupRouterGuard } from '@/router/guard';
import { setupStore } from '@/store/index';

async function bootstrap() {
  const app = createApp(App);
  setupRouter(app);
  setupRouterGuard(router);
  setupStore(app);
  app.mount('#app');
}

bootstrap();

如何使用

<script lang="tsx" setup>
  import { useCounterStore } from '@/store/modules/index';
  const store = useCounterStore();
  store.count++;
</script>
<template></template>
<style lang="less" scoped></style>

配置 mock

npm: url[www.npmjs.com/package/vit…]

安装依赖

pnpm add mockjs vite-plugin-mock -D

配置 mock

import type { ConfigEnv, UserConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { viteMockServe } from 'vite-plugin-mock';
import { resolve } from 'path';

function resolvePath(dir: string) {
  return resolve(process.cwd(), '.', dir);
}

export default ({ command }: ConfigEnv): UserConfig => {
  return {
    resolve: {
      alias: {
        '@': resolvePath('./src'),
      },
    },
    plugins: [
      vue(),
      viteMockServe({
        mockPath: 'mock',
        localEnabled: command === 'serve',
      }),
    ],
  };
};

创建 mock/index.ts 文件

import { MockMethod } from 'vite-plugin-mock';

export default [
  {
    url: '/api/get',
    method: 'get',
    response: ({}) => {
      return {
        code: 0,
        data: {
          name: 'vben',
        },
      };
    },
  },
] as MockMethod[];

如何使用

<script lang="tsx" setup>
  fetch('/api/get')
    .then((res) => {
      return res.json();
    })
    .then((res) => {
      console.log(res);
    });
</script>
<template> </template>
<style lang="less" scoped></style>

ant-design-vue

npm: url[github.com/vbenjs/vite…]

安装依赖

pnpm add ant-design-vue
pnpm add vite-plugin-style-import consola less postcss-less -D
pnpm add @vitejs/pugin-vue-jsx -D
  • ant-design-vue: 组件库
  • vite-plugin-style-import: 按需导入组件库样式
  • consola: 按需导入需要的记录器
  • less: 支持 less
  • postcss-less: 支持 less 转换 css
  • @vitejs/pugin-vue-jsx: 支持 jsx 语法

配置 vite

import type { ConfigEnv, UserConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vuejsx from '@vitejs/plugin-vue-jsx';
import { resolve } from 'path';

import { viteMockServe } from 'vite-plugin-mock';
import { createStyleImportPlugin, AndDesignVueResolve } from 'vite-plugin-style-import';

function resolvePath(dir: string) {
  return resolve(process.cwd(), '.', dir);
}

export default ({ command }: ConfigEnv): UserConfig => {
  return {
    resolve: {
      alias: {
        '@': resolvePath('./src'),
      },
    },
    css: {
      preprocessorOptions: {
        less: {
          javascriptEnabled: true,
        },
      },
    },
    plugins: [
      createStyleImportPlugin({
        resolves: [AndDesignVueResolve()],
        libs: [
          {
            libraryName: 'ant-design-vue',
            esModule: true,
            resolveStyle: (name) => {
              return `ant-design-vue/es/${name}/style/index`;
            },
          },
        ],
      }),
      vue(),
      vuejsx(),
      viteMockServe({
        mockPath: 'mock',
        localEnabled: command === 'serve',
      }),
    ],
  };
};

配置国际化 i18n

官网: url[vue-i18n.intlify.dev/introductio…]

安装依赖

pnpm add vue-i18n

安装插件

vscode: i18n Ally

文件目录

├── src # 主目录
│   ├── locales # 多语言
│   │   ├── lang # 语言
│   │   │   ├── en # 英文
│   │   │   │   ├── index.ts # 英文配置
│   │   │   ├── zh-CN # 中文
│   │   │   │   ├── index.ts # 中文配置
│   │   │   ├── en.ts # 英文导出
│   │   │   ├── zh_CN.ts # 中文导出
│   │   ├── helper.ts # 多语言 辅助方法
│   │   ├── setupI18n.ts # 配置 多语言
  • src/locales/helper.ts

    import { set } from 'lodash-es';
    
    /**
     * 获得对应语言参数
     * @param langs
     * @param prefix
     * @returns
     */
    export function genMessage(langs: Record<string, Record<string, any>>, prefix = 'lang') {
      const obj: Recordable = {};
      Object.keys(langs).forEach((key) => {
        const langFileModule = langs[key].default;
        let fileName = key.replace(`./${prefix}/`, '').replace(/^\.\//, '');
        const lastIndex = fileName.lastIndexOf('.');
        fileName = fileName.substring(0, lastIndex);
        const keyList = fileName.split('/');
        const moduleName = keyList.shift();
        const objKey = keyList.join('.');
        if (moduleName) {
          if (objKey) {
            set(obj, moduleName, obj[moduleName] || {});
            set(obj[moduleName], objKey, langFileModule);
          } else {
            set(obj, moduleName, langFileModule || {});
          }
        }
      });
      return obj;
    }
    
  • src/locales/setupI18n.ts

    import type { App } from 'vue';
    import type { I18n, I18nOptions } from 'vue-i18n';
    
    import { createI18n } from 'vue-i18n';
    
    export let i18n: ReturnType<typeof createI18n>;
    
    /**
     * 创建 i18n 配置
     * @returns
     */
    async function createI18nOptions(): Promise<I18nOptions> {
      const locale = 'en';
      const defaultLocal = await import(`./lang/${locale}.ts`);
      const message = defaultLocal?.default?.message ?? {};
      return {
        locale: 'en',
        fallbackLocale: 'zh_CN',
        messages: {
          [locale]: message,
        },
      };
    }
    
    /**
     * 在全局挂载 i18n
     * @param app
     */
    export async function setupI18n(app: App) {
      const options = await createI18nOptions();
      i18n = createI18n(options) as I18n;
      app.use(i18n);
    }
    
  • src/locales/lang/en/index.ts

    export default {
      index: {
        hello: 'hello !',
      },
    };
    
  • src/locales/lang/zh-CN/index.ts

    export default {
      index: {
        hello: '你好',
      },
    };
    
  • src/locales/lang/en.ts

    import { genMessage } from '../helper';
    import antdLocale from 'ant-design-vue/es/locale/en_US';
    
    const modules = import.meta.globEager('./en/**/*.ts');
    export default {
      message: {
        ...genMessage(modules, 'en'),
        antdLocale,
      },
      dateLocale: null,
      dateLocaleName: 'en',
    };
    
  • src/locales/lang/zh_CN.ts

    import { genMessage } from '../helper';
    import antdLocale from 'ant-design-vue/es/locale/zh_CN';
    
    const modules = import.meta.globEager('./zh-CN/**/*.ts');
    export default {
      message: {
        ...genMessage(modules, 'zh-CN'),
        antdLocale,
      },
    };
    

终端警告

You are running the esm-bundler build of vue-i18n. It is recommended to configure your bundler to explicitly replace feature flag globals with boolean literals to get proper tree-shaking in the final bundle.

解决方法修改vite.config.ts

resolve: {
  alias: {
    'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
  },
}

配置 axios

官网: url[www.axios-js.com/zh-cn/docs/]

安装依赖

pnpm add axios

文件目录

├── src # 主目录
│   ├── utils # 方法文件夹
│   │   ├── http # http 文件夹
│   │   │   ├── index.ts # 导出 http 方法
│   │   │   ├── Axios.ts # 配置 axios 方法
│   │   │   ├── axiosTransform.ts # 配置 各种状态 返回值转换
  • src/utils/http/index.ts

    import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform';
    import { VAxios } from './Axios';
    
    import type { AxiosResponse } from 'axios';
    import type { RequestOptions, Result } from '#/axios';
    import { deepMerge } from '@/utils';
    
    /**
     * @description: 数据处理,方便区分多种处理方式
     */
    const transform: AxiosTransform = {
      /**
       * 处理请求数据。如果数据不是预期格式,可直接抛出错误
       * @param res
       * @param options
       */
      transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
        if (options.errorMessageMode === 'modal') {
          return;
        }
        return res.data;
      },
      /**
       * 请求之前处理config
       * @param config
       * @param options
       */
      // beforeRequestHook: (config, options) => {},
      /**
       * 请求拦截器处理
       * @param config
       * @param options
       * @returns
       */
      // requestInterceptors: (config, options) => {},
      /**
       * 响应拦截器处理
       * @param res
       */
      // responseInterceptors: (res: AxiosResponse<any>) => {},
      /**
       * 响应错误处理
       * @param error
       * @returns
       */
      // responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {},
    };
    
    function createAxios(opt?: Partial<CreateAxiosOptions>) {
      return new VAxios(
        deepMerge(
          {
            authenticationScheme: '',
            // 接口超时时间 单位毫秒
            timeout: 10 * 1000,
            // 接口可能会有通用的地址部分,可以统一抽取出来
            // headers: { 'Content-Type': ContentTypeEnum.JSON },
            // 数据处理方式,见下方说明
            transform,
            // 配置项,下面的选项都可以在独立的接口请求中覆盖
            requestOptions: {
              // 默认将prefix 添加到url
              joinPrefix: true,
              // 是否返回原生响应头 比如:需要获取响应头时使用该属性
              isReturnNativeResponse: false,
              // post请求的时候添加参数到url
              joinParamsToUrl: false,
              // 格式化提交参数时间
              formatDate: true,
              // 消息提示类型
              errorMessageMode: 'message',
              // 接口地址
              // apiUrl: globSetting.apiUrl,
              //  是否加入时间戳
              joinTime: true,
              // 忽略重复请求
              ignoreCancelToken: true,
            },
          },
          opt || {},
        ),
      );
    }
    
    export const defHttp = createAxios();
    
  • src/utils/http/Axios.ts

    import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from 'axios';
    import type { CreateAxiosOptions } from './axiosTransform';
    import type { RequestOptions, Result } from '#/axios';
    import { cloneDeep } from 'lodash-es';
    import { isFunction } from '@/utils';
    
    import axios from 'axios';
    
    export * from './axiosTransform';
    
    /**
     * @description:  axios模块
     */
    export class VAxios {
      private axiosInstance: AxiosInstance;
      private readonly options: CreateAxiosOptions;
    
      constructor(options: CreateAxiosOptions) {
        this.options = options;
        this.axiosInstance = axios.create(options);
        // this.setupInterceptors();
      }
    
      /**
       * 创建axios实例
       * @param config
       */
      private createAxios(config: CreateAxiosOptions): void {
        this.axiosInstance = axios.create(config);
      }
    
      private getTransform() {
        const { transform } = this.options;
        return transform;
      }
    
      getAxios(): AxiosInstance {
        return this.axiosInstance;
      }
    
      /**
       * 重新配置axios
       * @param config
       * @returns
       */
      configAxios(config: CreateAxiosOptions) {
        if (!this.axiosInstance) {
          return;
        }
        this.createAxios(config);
      }
    
      /**
       * 设置通用头
       * @param headers
       * @returns
       */
      // setHeader(headers: any): void {}
    
      /**
       * 拦截器配置
       * @returns
       */
      // private setupInterceptors() {}
    
      /**
       * 文件上传
       * @param config
       * @param params
       * @returns
       */
      // uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {}
    
      /**
       * 支持格式
       * @param config
       * @returns
       */
      // supportFormData(config: AxiosRequestConfig) {}
    
      /**
       * get 请求
       * @param config
       * @param options
       * @returns
       */
      get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'GET' }, options);
      }
    
      /**
       * post 请求
       * @param config
       * @param options
       * @returns
       */
      post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'POST' }, options);
      }
    
      /**
       * put 请求
       * @param config
       * @param options
       * @returns
       */
      put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'PUT' }, options);
      }
    
      /**
       * delete 请求
       * @param config
       * @param options
       * @returns
       */
      delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'DELETE' }, options);
      }
    
      request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        // 深拷贝 配置
        let conf: CreateAxiosOptions = cloneDeep(config);
        // 获取 转换 方法
        const transform = this.getTransform();
        // 合并 options
        const { requestOptions } = this.options;
        const opt: RequestOptions = Object.assign({}, requestOptions, options);
        const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {};
        // 判断是否存在请求之前的方法, 如果存在则处理
        if (beforeRequestHook) {
          conf = beforeRequestHook(conf, opt);
        }
        conf.requestOptions = opt;
        return new Promise((resolve, reject) => {
          this.axiosInstance
            .request<any, AxiosResponse<Result>>(conf)
            .then((res: AxiosResponse<Result>) => {
              // 判断是否存在 请求成功 处理方法
              if (transformRequestHook && isFunction(transformRequestHook)) {
                try {
                  const ret = transformRequestHook(res, opt);
                  resolve(ret);
                } catch (err) {
                  reject(err || new Error('request error!'));
                }
                return;
              }
              resolve(res as unknown as Promise<T>);
            })
            .catch((e: Error | AxiosError) => {
              // 判断是否存在 请求失败 处理方法
              if (requestCatchHook && isFunction(requestCatchHook)) {
                reject(requestCatchHook(e, opt));
                return;
              }
              if (axios.isAxiosError(e)) {
                // 在这里重写axios的错误消息
              }
              reject(e);
            });
        });
      }
    }
    
  • src/utils/http/axiosTransform.ts

    /**
     * 数据处理类,可根据项目配置
     */
    
    import type { AxiosRequestConfig, AxiosResponse } from 'axios';
    import { RequestOptions, Result } from '#/axios';
    
    export interface CreateAxiosOptions extends AxiosRequestConfig {
      authenticationScheme?: string;
      transform?: AxiosTransform;
      requestOptions?: RequestOptions;
    }
    
    export abstract class AxiosTransform {
      /**
       * 请求之前处理配置
       */
      beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
    
      /**
       * 请求成功处理
       */
      transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
    
      /**
       * 请求失败处理
       */
      requestCatchHook?: (e: Error, options: RequestOptions) => Promise<any>;
    
      /**
       * 请求之前的拦截器
       */
      requestInterceptors?: (
        config: AxiosRequestConfig,
        options: CreateAxiosOptions,
      ) => AxiosRequestConfig;
    
      /**
       * 请求之后的拦截器
       */
      responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
    
      /**
       * 请求之前的拦截器错误处理
       */
      requestInterceptorsCatch?: (error: Error) => void;
    
      /**
       * 请求之后的拦截器错误处理
       */
      responseInterceptorsCatch?: (axiosInstance: AxiosResponse, error: Error) => void;
    }
    

    配置 typings

    typings/axios.d.ts

    export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
    
    /**
     * 请求配置
     */
    export interface RequestOptions {
      /**
       * 将请求参数拼接到url
       */
      joinParamsToUrl?: boolean;
      /**
       * 格式请求参数时间
       */
      formatDate?: boolean;
      /**
       * 是否处理请求结果
       */
      isTransformResponse?: boolean;
      /**
       * 是否返回本机响应头
       * 例如:当您需要获得响应头时,使用此属性
       */
      isReturnNativeResponse?: boolean;
      /**
       * 是否加入url
       */
      joinPrefix?: boolean;
      /**
       * 接口地址,使用默认apiUrl,如果您保留空白
       */
      apiUrl?: string;
      /**
       * 请求拼接路径
       */
      urlPrefix?: string;
      /**
       * 错误消息提示类型
       */
      errorMessageMode?: ErrorMessageMode;
      /**
       * 是否添加时间戳
       */
      joinTime?: boolean;
      ignoreCancelToken?: boolean;
      /**
       * 是否在头部发送令牌
       */
      withToken?: boolean;
      /**
       * 请求重试机制
       */
      retryRequest?: RetryRequest;
    }
    
    export interface RetryRequest {
      isOpenRetry: boolean;
      count: number;
      waitTime: number;
    }
    
    /**
     * 返回结果类型
     */
    export interface Result<T = any> {
      code: number;
      type: 'success' | 'error' | 'warning';
      message: string;
      result: T;
      error_code?: number;
      error_description?: string;
      [name: string]: any;
    }
    
    /**
     * 多部分/格式:上传文件
     */
    export interface UploadFileParams {
      /**
       * 其他参数
       */
      data?: Recordable;
      /**
       * 文件参数接口字段名
       */
      name?: string;
      /**
       * 文件名称
       */
      file: File | Blob;
      /**
       * 文件名称
       */
      filename?: string;
      [key: string]: any;
    }
    

配置 Windi Css

官网: url[cn.windicss.org/integration…]

安装依赖

pnpm add vite-plugin-windicss windicss -D

安装插件

vscode: WindiCSS IntelliSense

配置 Windi

windi.config.ts

import { defineConfig } from 'vite-plugin-windicss';

export default defineConfig({
  darkMode: 'class',
  plugins: [createEnterPlugin()],
  theme: {
    extend: {
      zIndex: {
        '-1': '-1',
      },
      transitionTimingFunction: {
        'in-expo': 'cubic-bezier(.645, .045, .355, 1)',
      },
      screens: {
        sm: '576px',
        md: '768px',
        lg: '992px',
        xl: '1200px',
        '2xl': '1600px',
      },
    },
  },
});

/**
 * 用于显示元素时的动画。
 * @param maxOutput maxOutput的输出越大,生成的css卷就越大。
 */
function createEnterPlugin(maxOutput = 6) {
  const createCss = (index: number, d = 'x') => {
    const upd = d.toUpperCase();
    return {
      [`*> .enter-${d}:nth-child(${index})`]: {
        transform: `translate${upd}(50px)`,
      },
      [`*> .-enter-${d}:nth-child(${index})`]: {
        transform: `translate${upd}(-50px)`,
      },
      [`* > .enter-${d}:nth-child(${index}),* > .-enter-${d}:nth-child(${index})`]: {
        'z-index': `${10 - index}`,
        opacity: '0',
        animation: `enter-${d}-animation 0.4s ease-in-out 0.3s`,
        'animation-fill-mode': 'forwards',
        'animation-delay': `${(index * 1) / 10}s`,
      },
    };
  };
  const handler = ({ addBase }) => {
    const addRawCss = {};
    for (let index = 1; index < maxOutput; index++) {
      Object.assign(addRawCss, {
        ...createCss(index, 'x'),
        ...createCss(index, 'y'),
      });
    }
    addBase({
      ...addRawCss,
      [`@keyframes enter-x-animation`]: {
        to: {
          opacity: '1',
          transform: 'translateX(0)',
        },
      },
      [`@keyframes enter-y-animation`]: {
        to: {
          opacity: '1',
          transform: 'translateY(0)',
        },
      },
    });
  };
  return { handler };
}

vite.config.ts

import type { ConfigEnv, UserConfig } from 'vite';
import WindiCSS from 'vite-plugin-windicss';

export default ({ command }: ConfigEnv): UserConfig => {
  return {
    plugins: [WindiCSS()],
  };
};

main.ts

+ import 'virtual:windi-base.css';
+ import 'virtual:windi-components.css';
+ import 'virtual:windi-utilities.css';
import { createApp } from 'vue';
import App from './App.vue';
import { router, setupRouter } from '@/router';
import { setupRouterGuard } from '@/router/guard';
import { setupStore } from '@/store/index';
import { setupI18n } from '@/locales/setupI18n';

async function bootstrap() {
  const app = createApp(App);
  setupRouter(app);
  setupRouterGuard(router);
  setupStore(app);
  setupI18n(app);
  app.mount('#app');
}

bootstrap();

配置 多环境

安装依赖

pnpm add dayjs
pnpm add cross-env dotenv @vitejs/plugin-legacy vite-plugin-mkcert picocolors fs-extra @types/fs-extra vite-plugin-imagemin vite-plugin-html rollup-plugin-visualizer esno -D
  • dayjs 日期格式化
  • cross-env: 运行环境
  • dotenv
  • @vitejs/plugin-legacy: 兼容老版本
  • vite-plugin-mkcert: 消除 https 警告
  • vite-plugin-imagemin: 压缩图片
  • vite-plugin-html: 配置 html
  • vite-plugin-html: 包文件分析
  • picocolors
  • fs-extra
  • @types/fs-extra
  • esno: 运行指令

环境默认变量 .env

# 端口号
VITE_PORT = 3100

# 项目标题
VITE_GLOB_APP_TITLE = Vue3 Demo

# 项目名称
VITE_GLOB_APP_SHORT_NAME = Vue3_Demo

开发环境 .env.deveplopment

# 是否开启 mock
VITE_USE_MOCK = false

# 静态路径
VITE_PUBLIC_PATH = /

# 跨域代理,可以配置多个
# 注意不要换行
VITE_PROXY = [["/api","http://192.168.3.110:31002"],["/upload","http://localhost:3300/upload"]]

# 删除控制台
VITE_DROP_CONSOLE = false

# 基本接口地址
VITE_GLOB_API_URL=/api

# 文件上传地址,可选
VITE_GLOB_UPLOAD_URL=/upload

# 接口使用
VITE_GLOB_API_URL_PREFIX=

测试环境 .env.test

# 是否开启 mock
VITE_USE_MOCK = false

# 静态路径
VITE_PUBLIC_PATH = /

# 跨域代理,可以配置多个
# 注意不要换行
VITE_PROXY = [["/api","http://192.168.3.110:31002"],["/upload","http://localhost:3300/upload"]]

# 删除控制台
VITE_DROP_CONSOLE = true

# 基本接口地址
VITE_GLOB_API_URL=/api

# 文件上传地址,可选
VITE_GLOB_UPLOAD_URL=/upload

# 接口使用
VITE_GLOB_API_URL_PREFIX=

# 是否起用图片压缩
VITE_USE_IMAGEMIN= true

# 使用pwa
VITE_USE_PWA = false

# 它是否兼容旧的浏览器
VITE_LEGACY = false

生产环境 .env.production

# 是否开启 mock
VITE_USE_MOCK = false

# 静态路径
VITE_PUBLIC_PATH = /

# 跨域代理,可以配置多个
# 注意不要换行
VITE_PROXY = [["/api","http://192.168.3.110:31002"],["/upload","http://localhost:3300/upload"]]

# 删除控制台
VITE_DROP_CONSOLE = true

# 基本接口地址
VITE_GLOB_API_URL=/api

# 文件上传地址,可选
VITE_GLOB_UPLOAD_URL=/upload

# 接口使用
VITE_GLOB_API_URL_PREFIX=

# 是否起用图片压缩
VITE_USE_IMAGEMIN= true

# 使用pwa
VITE_USE_PWA = false

# 它是否兼容旧的浏览器
VITE_LEGACY = false

打包配置

文件目录

├── build # 打包
│   ├── script # 脚本文件
│   │   ├── buildConf.ts # 打包配置
│   │   ├── postBuild.ts # 打包构建
│   ├── vite # vite 打包
│   │   ├── plugin # 打包构建
│   │   │   ├── html.ts # html 文件打包配置
│   │   │   ├── imagemin.ts # 图片 压缩 配置
│   │   │   ├── index.ts # 打包方法导出
│   │   │   ├── mock.ts # mock 数据 配置
│   │   │   ├── styleImport.ts # 样式按需加载配置
│   │   │   ├── visualizer.ts # 包文件分析配置
│   │   ├── proxy.ts # 打包构建
│   ├── constant.ts # 常量
│   ├── getConfigFileName.ts # 获取配置名称
│   ├── utils.ts # 打包方法
  • build/script/buildConf.ts

    /**
     *  在用于打包时生成其他配置文件。可以用一些全局变量配置文件,这样就可以在外部直接更改它,而无需重新打包
     */
    import { GLOB_CONFIG_FILE_NAME, OUTPUT_DIR } from '../constant';
    import fs, { writeFileSync } from 'fs-extra';
    import colors from 'picocolors';
    
    import { getEnvConfig, getRootPath } from '../utils';
    import { getConfigFileName } from '../getConfigFileName';
    
    import pkg from '../../package.json';
    
    interface CreateConfigParams {
      configName: string;
      config: any;
      configFileName?: string;
    }
    
    function createConfig(params: CreateConfigParams) {
      const { configName, config, configFileName } = params;
      try {
        const windowConf = `window.${configName}`;
        // 确保变量不会被修改
        const configStr = `${windowConf}=${JSON.stringify(config)};
          Object.freeze(${windowConf});
          Object.defineProperty(window, "${configName}", {
            configurable: false,
            writable: false,
          });
        `.replace(/\s/g, '');
        fs.mkdirp(getRootPath(OUTPUT_DIR));
        writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr);
    
        console.log(colors.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
        console.log(colors.gray(OUTPUT_DIR + '/' + colors.green(configFileName)) + '\n');
      } catch (error) {
        console.log(colors.red('configuration file configuration file failed to package:\n' + error));
      }
    }
    
    export function runBuildConfig() {
      const config = getEnvConfig();
      const configFileName = getConfigFileName(config);
      createConfig({ config, configName: configFileName, configFileName: GLOB_CONFIG_FILE_NAME });
    }
    
  • build/script/postBuild.ts

    // #!/usr/bin/env node
    
    import { runBuildConfig } from './buildConf';
    import colors from 'picocolors';
    
    import pkg from '../../package.json';
    
    export const runBuild = async () => {
      try {
        const argvList = process.argv.splice(2);
    
        // Generate configuration file
        if (!argvList.includes('disabled-config')) {
          runBuildConfig();
        }
    
        console.log(`✨ ${colors.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
      } catch (error) {
        console.log(colors.red('vite build error:\n' + error));
        process.exit(1);
      }
    };
    runBuild();
    
  • build/vite/plugin/html.ts

    /**
     * 在index.html中最小化和使用ejs模板语法的插件
     * https://github.com/anncwb/vite-plugin-html
     */
    import type { PluginOption } from 'vite';
    import { createHtmlPlugin } from 'vite-plugin-html';
    import pkg from '../../../package.json';
    import { GLOB_CONFIG_FILE_NAME } from '../../constant';
    
    export function configHtmlPlugin(env, isBuild: boolean) {
      const { VITE_GLOB_APP_TITLE, VITE_PUBLIC_PATH } = env;
    
      const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`;
    
      const getAppConfigSrc = () => {
        return `${path || '/'}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`;
      };
    
      const htmlPlugin: PluginOption[] = createHtmlPlugin({
        minify: isBuild,
        inject: {
          // 将数据注入ejs模板
          data: {
            title: VITE_GLOB_APP_TITLE,
          },
          // 嵌入生成的app.config.js文件
          tags: isBuild
            ? [
                {
                  tag: 'script',
                  attrs: {
                    src: getAppConfigSrc(),
                  },
                },
              ]
            : [],
        },
      });
      return htmlPlugin;
    }
    
  • build/vite/plugin/imagemin.ts

    // 用于压缩生产环境输出的图片资源文件
    // https://github.com/anncwb/vite-plugin-imagemin
    import viteImagemin from 'vite-plugin-imagemin';
    
    export function configImageminPlugin() {
      const plugin = viteImagemin({
        gifsicle: {
          optimizationLevel: 7,
          interlaced: false,
        },
        optipng: {
          optimizationLevel: 7,
        },
        mozjpeg: {
          quality: 20,
        },
        pngquant: {
          quality: [0.8, 0.9],
          speed: 4,
        },
        svgo: {
          plugins: [
            {
              name: 'removeViewBox',
            },
            {
              name: 'removeEmptyAttrs',
              active: false,
            },
          ],
        },
      });
      return plugin;
    }
    
  • build/vite/plugin/index.ts

    import { PluginOption } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import vueJsx from '@vitejs/plugin-vue-jsx';
    import WindiCSS from 'vite-plugin-windicss';
    import VitePluginCertificate from 'vite-plugin-mkcert';
    import legacy from '@vitejs/plugin-legacy';
    import { configMockPlugin } from './mock';
    import { configStyleImportPlugin } from './styleImport';
    import { configHtmlPlugin } from './html';
    import { configImageminPlugin } from './imagemin';
    import { configVisualizerConfig } from './visualizer';
    
    export function createVitePlugins(viteEnv, isBuild) {
      const { VITE_USE_MOCK, VITE_LEGACY, VITE_USE_IMAGEMIN } = viteEnv;
      const vitePlugins: (PluginOption | PluginOption[])[] = [
        vue(),
        vueJsx(),
        VitePluginCertificate({
          source: 'coding',
        }),
        WindiCSS(),
        configVisualizerConfig(),
      ];
      VITE_LEGACY && isBuild && vitePlugins.push(legacy());
      vitePlugins.push(configHtmlPlugin(viteEnv, isBuild));
      VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild));
      vitePlugins.push(configStyleImportPlugin());
      if (isBuild) {
        VITE_USE_IMAGEMIN && vitePlugins.push(configImageminPlugin());
      }
      return vitePlugins;
    }
    
  • build/vite/plugin/mock.ts

    /**
     * 用于开发和生产的模拟插件。
     * https://github.com/anncwb/vite-plugin-mock
     */
    import { viteMockServe } from 'vite-plugin-mock';
    
    export function configMockPlugin(isBuild: boolean) {
      return viteMockServe({
        ignore: /^\_/,
        mockPath: 'mock',
        localEnabled: !isBuild,
        prodEnabled: isBuild,
        injectCode: `
          import { setupProdMockServer } from '../mock/_createProductionServer';
    
          setupProdMockServer();
          `,
      });
    }
    
  • build/vite/plugin/styleImport.ts

    /**
     * 根据需要引入组件库样式。
     * https://github.com/anncwb/vite-plugin-style-import
     */
    import { createStyleImportPlugin, AndDesignVueResolve } from 'vite-plugin-style-import';
    
    export function configStyleImportPlugin() {
      const styleImportPlugin = createStyleImportPlugin({
        resolves: [AndDesignVueResolve()],
        libs: [
          {
            libraryName: 'ant-design-vue',
            esModule: true,
            resolveStyle: (name) => {
              // 这里是无需额外引入样式文件的“子组件”列表
              const ignoreList = [
                'anchor-link',
                'sub-menu',
                'menu-item',
                'menu-divider',
                'menu-item-group',
                'breadcrumb-item',
                'breadcrumb-separator',
                'form-item',
                'step',
                'select-option',
                'select-opt-group',
                'card-grid',
                'card-meta',
                'collapse-panel',
                'descriptions-item',
                'list-item',
                'list-item-meta',
                'table-column',
                'table-column-group',
                'tab-pane',
                'tab-content',
                'timeline-item',
                'tree-node',
                'skeleton-input',
                'skeleton-avatar',
                'skeleton-title',
                'skeleton-paragraph',
                'skeleton-image',
                'skeleton-button',
              ];
              // 这里是需要额外引入样式的子组件列表
              // 单独引入子组件时需引入组件样式,否则会在打包后导致子组件样式丢失
              const replaceList = {
                'typography-text': 'typography',
                'typography-title': 'typography',
                'typography-paragraph': 'typography',
                'typography-link': 'typography',
                'dropdown-button': 'dropdown',
                'input-password': 'input',
                'input-search': 'input',
                'input-group': 'input',
                'radio-group': 'radio',
                'checkbox-group': 'checkbox',
                'layout-sider': 'layout',
                'layout-content': 'layout',
                'layout-footer': 'layout',
                'layout-header': 'layout',
                'month-picker': 'date-picker',
              };
    
              return ignoreList.includes(name)
                ? ''
                : replaceList.hasOwnProperty(name)
                ? `ant-design-vue/es/${replaceList[name]}/style/index`
                : `ant-design-vue/es/${name}/style/index`;
            },
          },
        ],
      });
      return styleImportPlugin;
    }
    
  • build/vite/plugin/visualizer.ts

    /**
     * 包文件卷分析
     */
    import visualizer from 'rollup-plugin-visualizer';
    import { isReportMode } from '../../utils';
    
    export function configVisualizerConfig() {
      if (isReportMode()) {
        return visualizer({
          filename: './node_modules/.cache/visualizer/stats.html',
          open: true,
          gzipSize: true,
          brotliSize: true,
        }) as Plugin;
      }
      return [];
    }
    
  • build/vite/proxy.ts

    /**
     * 用于解析.env.development代理配置
     */
    import type { ProxyOptions } from 'vite';
    
    type ProxyItem = [string, string];
    
    type ProxyList = ProxyItem[];
    
    type ProxyTargetList = Record<string, ProxyOptions>;
    
    const httpsRE = /^https:\/\//;
    
    /**
     * 生成代理
     * @param list
     */
    export function createProxy(list: ProxyList = []) {
      const ret: ProxyTargetList = {};
      for (const [prefix, target] of list) {
        const isHttps = httpsRE.test(target);
        ret[prefix] = {
          target: target,
          changeOrigin: true,
          ws: true,
          rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
          // HTTPS是需要安全=false
          ...(isHttps ? { secure: false } : {}),
        };
      }
      return ret;
    }
    
  • build/constant.ts

    /**
     *  在生产环境中输入的配置文件的名称
     */
    export const GLOB_CONFIG_FILE_NAME = '_app.config.js';
    
    export const OUTPUT_DIR = 'dist';
    
  • build/getConfigFileName.ts

    /**
     * 获取配置文件变量名
     * @param env
     */
    export const getConfigFileName = (env: Record<string, any>) => {
      return `__PRODUCTION__${env.VITE_GLOB_APP_SHORT_NAME || '__APP'}__CONF__`
        .toUpperCase()
        .replace(/\s/g, '');
    };
    
  • build/utils.ts

    import fs from 'fs';
    import path from 'path';
    import dotenv from 'dotenv';
    
    /**
     * 是否生成包预览
     * @returns
     */
    export function isReportMode(): boolean {
      return process.env.REPORT === 'true';
    }
    
    /**
     * 将所有环境变量配置文件读入process.env
     * @param envConf
     * @returns
     */
    export function wrapperEnv(envConf) {
      const ret: any = {};
      for (const envName of Object.keys(envConf)) {
        let realName = envConf[envName].replace(/\\n/g, '\n');
        realName = realName === 'true' ? true : realName === 'false' ? false : realName;
    
        if (envName === 'VITE_PORT') {
          realName = Number(realName);
        }
        if (envName === 'VITE_PROXY' && realName) {
          try {
            realName = JSON.parse(realName.replace(/'/g, '"'));
          } catch (error) {
            realName = '';
          }
        }
        ret[envName] = realName;
        if (typeof realName === 'string') {
          process.env[envName] = realName;
        } else if (typeof realName === 'object') {
          process.env[envName] = JSON.stringify(realName);
        }
      }
      return ret;
    }
    
    /**
     * 获取当前环境下生效的配置文件名
     */
    function getConfFiles() {
      const script = process.env.npm_lifecycle_script;
      const reg = new RegExp('--mode ([a-z_\\d]+)');
      const result = reg.exec(script as string) as any;
      if (result) {
        const mode = result[1] as string;
        return ['.env', `.env.${mode}`];
      }
      return ['.env', '.env.production'];
    }
    
    /**
     * 获取以指定前缀开始的环境变量
     * @param match prefix
     * @param confFiles ext
     */
    export function getEnvConfig(match = 'VITE_GLOB_', confFiles = getConfFiles()) {
      let envConfig = {};
      confFiles.forEach((item) => {
        try {
          const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)));
          envConfig = { ...envConfig, ...env };
        } catch (e) {
          console.error(`Error in parsing ${item}`, e);
        }
      });
      const reg = new RegExp(`^(${match})`);
      Object.keys(envConfig).forEach((key) => {
        if (!reg.test(key)) {
          Reflect.deleteProperty(envConfig, key);
        }
      });
      return envConfig;
    }
    
    /**
     * 获取用户根目录
     * @param dir 文件路径
     */
    export function getRootPath(...dir: string[]) {
      return path.resolve(process.cwd(), ...dir);
    }
    

配置 vite

import type { ConfigEnv, UserConfig } from 'vite';
import { resolve } from 'path';
import { loadEnv } from 'vite';
import { createProxy } from './build/vite/proxy';
import { wrapperEnv } from './build/utils';
import { createVitePlugins } from './build/vite/plugin/index';
import { OUTPUT_DIR } from './build/constant';

function resolvePath(dir: string) {
  return resolve(process.cwd(), '.', dir);
}

export default ({ command, mode }: ConfigEnv): UserConfig => {
  const root = process.cwd();

  const env = loadEnv(mode, root);

  const viteEnv = wrapperEnv(env);

  const { VITE_PORT, VITE_PROXY, VITE_DROP_CONSOLE } = viteEnv;

  const isBuild = command === 'build';

  return {
    resolve: {
      alias: {
        'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
        '@': resolvePath('./src'),
        '#': resolvePath('./src/typings'),
      },
    },
    server: {
      https: true,
      // 监听所有本地ip
      host: true,
      port: VITE_PORT,
      // 从.env加载代理配置
      proxy: createProxy(VITE_PROXY),
    },
    esbuild: {
      pure: VITE_DROP_CONSOLE ? ['console.log', 'debugger'] : [],
    },
    build: {
      target: 'es2015',
      cssTarget: 'chrome80',
      outDir: OUTPUT_DIR,
      brotliSize: false,
      chunkSizeWarningLimit: 2000,
    },
    css: {
      preprocessorOptions: {
        less: {
          javascriptEnabled: true,
        },
      },
    },
    plugins: createVitePlugins(viteEnv, isBuild),
  };
};

指令

package.json

{
  "name": "vue3",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "bootstrap": "pnpm install",
    "dev": "vite",
    "build": "echo \"打包项目\" && cross-env NODE_ENV=production vite build && esno ./build/script/postBuild.ts",
    "build:test": "echo \"打包测试项目\" && cross-env vite build --mode test && esno ./build/script/postBuild.ts",
    "build:no-cache": "echo \"清缓存重新打包\" && pnpm clean:cache && npm run build",
    "preview": "vite preview",
    "clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
    "clean:lib": "rimraf node_modules",
    "lint:lint-staged": "lint-staged",
    "lint:eslint": "eslint --cache --max-warnings 0  \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
    "lint:prettier": "prettier --write  \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
    "lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
    "reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
    "prepare": "husky install",
    "log": "conventional-changelog -p angular -i CHANGELOG.md -s"
  },
  "dependencies": {
    "ant-design-vue": "^3.2.0",
    "axios": "^0.26.1",
    "dayjs": "^1.11.1",
    "lodash-es": "^4.17.21",
    "pinia": "^2.0.13",
    "qs": "^6.10.3",
    "vue": "^3.2.25",
    "vue-i18n": "^9.1.9",
    "vue-router": "^4.0.14"
  },
  "devDependencies": {
    "@commitlint/cli": "^16.2.3",
    "@commitlint/config-conventional": "^16.2.1",
    "@types/fs-extra": "^9.0.13",
    "@types/lodash-es": "^4.17.6",
    "@types/mockjs": "^1.0.6",
    "@types/node": "^17.0.25",
    "@types/qs": "^6.9.7",
    "@typescript-eslint/eslint-plugin": "^5.20.0",
    "@typescript-eslint/parser": "^5.20.0",
    "@vitejs/plugin-legacy": "^1.8.1",
    "@vitejs/plugin-vue": "^2.3.1",
    "@vitejs/plugin-vue-jsx": "^1.3.10",
    "consola": "^2.15.3",
    "conventional-changelog-cli": "^2.2.2",
    "cross-env": "^7.0.3",
    "cz-conventional-changelog-zh": "^0.0.2",
    "dotenv": "^16.0.0",
    "eslint": "^8.14.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-vue": "^8.7.1",
    "esno": "^0.14.1",
    "fs-extra": "^10.1.0",
    "global": "^4.4.0",
    "husky": "^7.0.4",
    "less": "^4.1.2",
    "lint-staged": "^12.4.0",
    "mockjs": "^1.1.0",
    "picocolors": "^1.0.0",
    "postcss": "^8.4.12",
    "postcss-html": "^1.4.1",
    "postcss-less": "^6.0.0",
    "prettier": "^2.6.2",
    "rimraf": "^3.0.2",
    "rollup": "^2.56.3",
    "rollup-plugin-visualizer": "^5.6.0",
    "stylelint": "^14.7.1",
    "stylelint-config-prettier": "^9.0.3",
    "stylelint-config-standard": "^25.0.0",
    "stylelint-order": "^5.0.0",
    "typescript": "^4.5.4",
    "unplugin-vue-components": "^0.19.3",
    "vite": "^2.9.5",
    "vite-plugin-html": "^3.2.0",
    "vite-plugin-imagemin": "^0.6.1",
    "vite-plugin-mkcert": "^1.6.0",
    "vite-plugin-mock": "^2.9.6",
    "vite-plugin-style-import": "^2.0.0",
    "vite-plugin-windicss": "^1.8.4",
    "vue-eslint-parser": "^8.3.0",
    "vue-tsc": "^0.34.7",
    "windicss": "^3.5.1"
  },
  "resolutions": {
    "bin-wrapper": "npm:bin-wrapper-china",
    "rollup": "^2.56.3",
    "gifsicle": "5.2.0"
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog-zh"
    }
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
      "prettier --write--parser json"
    ],
    "package.json": [
      "prettier --write"
    ],
    "*.vue": [
      "eslint --fix",
      "prettier --write",
      "stylelint --fix"
    ],
    "*.{scss,less,styl,html}": [
      "stylelint --fix",
      "prettier --write"
    ],
    "*.md": [
      "prettier --write"
    ]
  }
}