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.tsimport { 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.tsimport 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/**.tsimport 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.tsimport 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.tsimport 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.tsimport { defineStore } from 'pinia'; export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), getters: { // 计算属性 }, actions: { // 异步操作 increment() { this.count++; }, }, }); -
src/store/index.tsimport 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: 支持 lesspostcss-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.tsimport { 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.tsimport 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.tsexport default { index: { hello: 'hello !', }, }; -
src/locales/lang/zh-CN/index.tsexport default { index: { hello: '你好', }, }; -
src/locales/lang/en.tsimport { 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.tsimport { 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.tsimport 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.tsimport 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.tsexport 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: 配置 htmlvite-plugin-html: 包文件分析picocolorsfs-extra@types/fs-extraesno: 运行指令
环境默认变量 .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.tsimport { 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.tsimport 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"
]
}
}