私有脚手架 “haohan-vue3-template” 搭建全过程——目前大量项目正在使用

226 阅读7分钟

第一步:搭建 Vite 项目

① 进入 Vite 官网查看最新命令
//打开 PowerShell
npm create vite@latest
// 给项目取个名字 vite-project
// 选择前端框架 这里选择 Vue + TypeScript
cd vite-project
npm install
npm run dev
② 项目能跑起来后,关闭 PowerShell,用 VsCode 打开项目,准备进入下一步

第二步:项目环境文件配置

① 配置 Vite和 TypeScript
  • 修改 vite.config.ts,主要是配置别名和配置代理
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";

const pathResolve = (dir: string) => resolve(__dirname, dir);

export default defineConfig(
    ({ mode }) => {
        const env = loadEnv(mode, __dirname)
        return {
            plugins: [vue()],
            build: {
                outDir: "dist",
                terserOptions: {
                    compress: {
                        keep_infinity: true,
                        drop_console: true,
                        drop_debugger: true,
                    },
                },
                chunkSizeWarningLimit: 1500,
            },
            resolve: {
                alias: {
                    src: pathResolve("./src"),
                    api: pathResolve("./src/api"),
                    router: pathResolve("./src/router"),
                    store: pathResolve("./src/store"),
                    views: pathResolve("./src/views"),
                    utils: pathResolve("./src/utils"),
                    components: pathResolve("./src/components"),
                    assets: pathResolve("./src/assets"),
                },
                extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json']
            },
            base: "./",
            server: {
                port: 3000,
                open: true,
                cors: true,
                proxy: {
                    "/development": {
                        target: env.VITE_SERVER_API,
                        changeOrigin: true,
                        secure: false,
                        rewrite: (path) => path.replace("/development", ""),
                    },
                },
            },
        }
    }
);
  • 修改tsconfig.json,逐条研究过,不会报错,项目能正常启动打包
{
  "compilerOptions": {
    "outDir": "./",
    "baseUrl": "./src/",
    "allowJs": true,
    "skipLibCheck": true,
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "strict": false,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["es6", "dom"],
    "types": ["vite/client"],
    "isolatedModules": true
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "exclude": ["node_modules", "dist"]
}
② 配置项目环境变量
  • 新增.env.production文件
ENV = 'production'
VITE_BASE_API = 'http://XXX.XXX.XXX.XXX:XXX/production/'
VITE_SERVER_API = 'XXX.XXX.XXX.XXX:XXX'
VITE_TITLE = 生产环境
  • 新增.env.development文件
ENV = 'development'
VITE_BASE_API = 'http://XXX.XXX.XXX.XXX:XXX/development/'
VITE_SERVER_API = 'http://XXX.XXX.XXX.XXX:XXX'
VITE_TITLE = 开发环境
③ 配置编码格式规范和代码美化
  • 安装插件EditorConfig for VS Code
  • 新增.editorconfig文件
# Editor configuration, see http://editorconfig.org

# 表示是最顶层的 EditorConfig 配置文件
root = true

[*]     # 表示所有文件适用
charset = utf-8     # 设置文件字符集为 utf-8
indent_style = space    # 缩进风格(tab | space)
indent_size = 2    # 缩进大小
end_of_line = lf    # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true     # 去除行首的任意空白字符
insert_final_newline = true     # 始终在文件末尾插入一个新行

[*.md]  # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false
  • 安装插件ESLint
  • 安装eslint到项目:npm i eslint -D
  • 新增.eslintrc.js文件
module.exports = {
    root: true,
    env: {
        browser: true,
        es2021: true,
        node: true,
    },
    extends: [
        'plugin:vue/vue3-recommended',
        'plugin:@typescript-eslint/recommended'
    ],
    parser: "vue-eslint-parser",
    parserOptions: {
        parser: '@typescript-eslint/parser',
        ecmaVersion: "latest",
        sourceType: 'module'
    },
    plugins: [
        '@typescript-eslint'
    ],
    rules: {
        'quotes': ['error', 'single'],
        'eqeqeq': ['error', 'always'],
        'no-unused-vars': 'error',
        'keyword-spacing': [
            'error',
            {
                'overrides': {
                    'if': {
                        'after': true
                    },
                    'for': {
                        'after': true
                    },
                    'while': {
                        'after': true
                    },
                    'else': {
                        'after': true
                    }
                }
            }
        ],
        'camelcase': ['error', { 'properties': 'never' }],
        'indent': ['error', 4, {
            'SwitchCase': 1,
            'flatTernaryExpressions': true
        }],
        'array-bracket-spacing': ['error', 'never'],
        'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
        'arrow-parens': 'off',
        'no-empty': ['error', { 'allowEmptyCatch': true }],
        'semi': ['error', 'never'],
        'space-before-function-paren': ['error', {
            'anonymous': 'never',
            'named': 'never',
            'asyncArrow': 'never'
        }],
        'no-trailing-spaces': ['error'],
        'spaced-comment': ['error', 'always', {
            'line': {
                'markers': ['*package', '!', '/', ',', '=']
            },
            'block': {
                'balanced': false,
                'markers': ['*package', '!', ',', ':', '::', 'flow-include'],
                'exceptions': ['*']
            }
        }],
        'no-template-curly-in-string': 'off',
        'no-useless-escape': 'off',
        'no-var': 'error',
        'prefer-const': 'error',
        'vue/valid-v-slot': 'error',
        'vue/experimental-script-setup-vars': 'off',
        'vue/array-bracket-spacing': ['error', 'never'],
        'vue/arrow-spacing': ['error', { 'before': true, 'after': true }],
        'vue/attribute-hyphenation': ['error', 'always'],
        'vue/attributes-order': 'off',
        'vue/block-spacing': ['error', 'always'],
        'vue/camelcase': ['error', { 'properties': 'never' }],
        'vue/comma-dangle': ['error', 'never'],
        'vue/comment-directive': 'error',
        'vue/component-name-in-template-casing': 'off',
        'vue/eqeqeq': ['error', 'always', { 'null': 'ignore' }],
        'vue/html-closing-bracket-newline': 'off',
        'vue/html-closing-bracket-spacing': ['error', {
            'startTag': 'never',
            'endTag': 'never',
            'selfClosingTag': 'always'
        }],
        'vue/html-end-tags': 'error',
        'vue/html-indent': ['error', 4, {
            'attribute': 1,
            'baseIndent': 1,
            'closeBracket': 0,
            'alignAttributesVertically': false,
            'ignores': []
        }],
        'vue/html-quotes': ['error', 'double'],
        'vue/html-self-closing': 'off',
        'vue/jsx-uses-vars': 'warn',
        'vue/key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }],
        'vue/match-component-file-name': 'off',
        'vue/max-attributes-per-line': 'off',
        'vue/multiline-html-element-content-newline': 'off',
        'vue/mustache-interpolation-spacing': 'off',
        'vue/name-property-casing': ['error', 'kebab-case'],
        'vue/no-async-in-computed-properties': 'error',
        'vue/no-boolean-default': 'off',
        'vue/no-confusing-v-for-v-if': 'off',
        'vue/no-dupe-keys': 'error',
        'vue/no-duplicate-attributes': 'error',
        'vue/no-multi-spaces': 'error',
        'vue/no-parsing-error': 'error',
        'vue/no-reserved-keys': 'error',
        'vue/no-restricted-syntax': 'off',
        'vue/no-shared-component-data': 'error',
        'vue/no-side-effects-in-computed-properties': 'off',
        'vue/no-spaces-around-equal-signs-in-attribute': 'error',
        'vue/no-template-key': 'off',
        'vue/no-textarea-mustache': 'error',
        'vue/no-unused-components': 'error',
        'vue/no-unused-vars': 'error',
        'vue/no-use-v-if-with-v-for': 'off',
        'vue/no-v-html': 'off',
        'vue/object-curly-spacing': ['error', 'always'],
        'vue/order-in-components': ['error', {
            'order': [
                'el',
                'name',
                'parent',
                'functional',
                ['delimiters', 'comments'],
                ['components', 'directives', 'filters'],
                'extends',
                'mixins',
                'inheritAttrs',
                'model',
                ['props', 'propsData'],
                'data',
                'computed',
                'watch',
                'LIFECYCLE_HOOKS',
                'methods',
                ['template', 'render'],
                'renderError'
            ]
        }],
        'vue/prop-name-casing': ['error', 'camelCase'],
        'vue/require-component-is': 'error',
        'vue/require-default-prop': 'off',
        'vue/require-direct-export': 'off',
        "vue/require-prop-type-constructor": "error",
        "vue/require-prop-types": "error",
        "vue/require-render-return": "error",
        "vue/require-v-for-key": "error",
        "vue/require-valid-default-prop": "off",
        "vue/return-in-computed-property": "error",
        "vue/script-indent": [
            "error",
            4,
            {
                baseIndent: 1,
                switchCase: 1,
            }
        ],
        "vue/singleline-html-element-content-newline": "off",
        "vue/space-infix-ops": "error",
        "vue/space-unary-ops": ["error", { words: true, nonwords: false }],
        "vue/this-in-template": ["error", "never"],
        "vue/use-v-on-exact": "off",
        "vue/v-bind-style": "off",
        "vue/v-on-function-call": "off",
        'vue/v-on-style': ['error', 'shorthand'],
        'vue/valid-template-root': 'error',
        'vue/valid-v-bind': 'error',
        'vue/valid-v-cloak': 'error',
        'vue/valid-v-else-if': 'error',
        'vue/valid-v-else': 'error',
        'vue/valid-v-for': 'error',
        'vue/valid-v-html': 'error',
        'vue/valid-v-if': 'error',
        'vue/valid-v-model': 'error',
        'vue/valid-v-on': 'error',
        'vue/valid-v-once': 'error',
        'vue/valid-v-pre': 'error',
        'vue/valid-v-show': 'error',
        'vue/valid-v-text': 'error',
        'vue/script-setup-uses-vars': 'error',
        'vue/multi-word-component-names': 'off',
        '@typescript-eslint/ban-ts-ignore': 'off',
        '@typescript-eslint/explicit-function-return-type': 'off',
        '@typescript-eslint/no-explicit-any': 'off',
        '@typescript-eslint/no-empty-function': 'off',
        '@typescript-eslint/ban-ts-comment': 'off',
        '@typescript-eslint/ban-types': 'off',
        '@typescript-eslint/no-non-null-assertion': 'off',
        '@typescript-eslint/no-unused-vars': [
            'error',
            {
                argsIgnorePattern: '^_',
                varsIgnorePattern: '^_',
            },
        ],

    },
    overrides: [
        {
            files: ['*.vue'],
            rules: {
                indent: 'off'
            }
        }
    ]
}
  • 新增.eslintignore文件
/build/
/dist/
/node_modules/
*.js
  • 安装插件Prettier
  • 安装prettier到项目:npm i prettier -D
  • 新增.prettierrc文件
{
    "printWidth": 100,
    "semi": true,
    "tabWidth": 2,
    "useTabs": false,
    "singleQuote": true,
    "arrowParens": "avoid",
    "bracketSpacing": true,
    "bracketLine": true,
    "tslintIntegration": false,
    "vueIndentScriptAndStyle": true,
    "quoteProps": "as-needed",
    "jsxBracketSameLine": true,
    "insertPragma": false,
    "requirePragma": false,
    "proseWrap": "never",
    "htmlWhitespaceSensitivity": "strict",
    "trailingComma": "all",
    "endOfLine": "auto"
}
  • eslint 安装附加配置

npm i @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb-base eslint-plugin-import eslint-plugin-vue -D

  • 解决 Prettier 和 ESLint 的冲突 npm i eslint-plugin-prettier eslint-config-prettier -D

第三步:项目功能文件配置

① 配置网络请求
  • 安装 axios npm install axios
  • 安装稳住用户进度条插件npm install nprogress
  • src 下面新建 api 文件夹,基础配置两个文件
// src/api/index.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, Canceler } from "axios";
import nprogress from 'nprogress';
import 'nprogress/nprogress.css';
import config from './config';
import STORE from 'store';
import { storeToRefs } from "pinia";

const { token } = storeToRefs(STORE.passport)
enum statusCode {
    RequestError = 400,//请求错误
    NotRole = 401,//未授权,请重新登录
    Reject = 403,//拒绝访问
    NotFind = 404,//请求出错
    ServeCrash = 408,//请求超时
    ServerError = 500,//服务器错误
    ServiceUnrealized = 501,//服务未实现
    NetworkError = 502,//网络错误
    ServiceUnavailable = 503,//服务不可用
    NetworkTimeout = 504,//网络超时
    HTTPNotSupported = 505,//HTTP版本不受支持
}

interface IResult<T> {
    code: number,
    message: string,
    data: T
}

class Request {
    instance: AxiosInstance;

    constructor(config: AxiosRequestConfig) {
        this.instance = axios.create(config);

        this.instance.interceptors.request.use((res: AxiosRequestConfig) => {
            //从仓库中获取token,在非passport页面请求加入
            if (token.value) {
                res.headers!.token = token.value;
            }
            //  添加请求取消
            config.cancelToken = new axios.CancelToken(cancel => {
                STORE.passport.addCancelToken(cancel);
            })
            nprogress.start();
            return res;
        },
            (err: any) => Promise.reject(err));

        this.instance.interceptors.response.use((res: AxiosResponse) => {
            nprogress.done();
            return res.data;
        }, (err: any) => {
            nprogress.done();
            switch (err.response.status) {
                case statusCode.RequestError:
                    console.log('请求错误');
                    break;
                case statusCode.NotRole:
                    console.log('未授权,请重新登录');
                    break;
                case statusCode.Reject:
                    console.log('拒绝访问');
                    break;
                case statusCode.NotFind:
                    console.log('请求出错');
                    break;
                case statusCode.ServeCrash:
                    console.log('请求超时');
                    break;
                case statusCode.ServerError:
                    console.log('服务器错误');
                    break;
                case statusCode.ServiceUnrealized:
                    console.log('服务未实现');
                    break;
                case statusCode.NetworkError:
                    console.log('网络错误');
                    break;
                case statusCode.ServiceUnavailable:
                    console.log('服务不可用');
                    break;
                case statusCode.NetworkTimeout:
                    console.log('网络超时');
                    break;
                case statusCode.HTTPNotSupported:
                    console.log('HTTP版本不受支持');
                    break;
                default:
                    console.log(`连接出错(${err.response.status})!`);
            }
            return Promise.reject(err);
        })
    }

    public request(config: AxiosRequestConfig): Promise<AxiosResponse> {
        return this.instance.request(config);
    }

    public get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<IResult<T>>> {
        return this.instance.get(url, config);
    }

    public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<IResult<T>>> {
        return this.instance.post(url, data, config);
    }

    public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<IResult<T>>> {
        return this.instance.put(url, data, config);
    }

    public delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<IResult<T>>> {
        return this.instance.delete(url, config);
    }
}

export default new Request(config);

// src/api/config.ts
const baseURL: string = import.meta.env.VITE_BASE_API;
const timeout: number = 5000;

export default { baseURL, timeout }
  • 使用
// src/api/demo.js
import API from 'api'
//发起demo的get请求
export const http_demo1 = params => API.get('api/demo1', { params })
//发起demo的post请求
export const http_demo2 = data => API.post('api/demo2', data)
//发起demo的put请求
export const http_demo3 = data => API.put('api/demo3', data)
//发起demo的delete请求
export const http_demo4 = id => API.put('api/demo4' + id)
② 配置路由
  • 安装 vue-router npm install vue-router
  • src 下面新建 router 文件夹,基础配置一个文件
// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import STORE from 'store';
import { storeToRefs } from 'pinia';
import { ElMessage } from 'element-plus';
import demo from './demo.js';

const routes: Array<RouteRecordRaw> = [
    {
        path: '/',
        name: '',
        component: () => import('views/passport/login.vue'),
    },
    // 登录
    {
        path: '/login',
        name: 'login',
        component: () => import('views/passport/login.vue'),
    },
    // 注册
    {
        path: '/register',
        name: 'register',
        component: () => import('views/PassPort/register.vue'),
    },
    // 忘记密码
    {
        path: '/forget',
        name: 'forget',
        component: () => import('views/PassPort/forget.vue'),
    },
    // 404
    {
        path: '/:catchAll(.*)', // 当所有路由找不到的时候会执行这个页面
        name: 'errorInfo',
        component: () => import('views/PassPort/error.vue'),
    },
    // 内容
    {
        path: '/home',
        name: 'home',
        redirect: '/home/demo',
        component: () => import('src/views/pages/index.vue'),
        children: [
            // demo
            demo,
        ],
    },
]

const router = createRouter({
    history: createWebHashHistory(),  // history 模式则使用 createWebHistory()
    routes,
});

// 路由跳转之前
router.beforeEach((to, from, next) => {
    const { token } = storeToRefs(STORE.passport)// 使用仓库
    // console.log(token.value, 'is   token');
    if (
        to.path !== '/login' &&
        to.path !== '/' &&
        // 如果不存在token,则不允许进行页面跳转
        !token.value
    ) {
        ElMessage.warning('请输入用户名和密码进行登录!');
        return next('/login');
    }
    next();
});

export default router
// src/router/demo.js
const demo = {
    path: '/home/demo',
    name: 'demo',
    component: () => import('views/demo/index.vue'),
    redirect: '/home/demo',
    meta: { title: 'demo' },
    children: [
        {
            path: '',
            name: '',
            component: ,
            meta: ,
        },
    ],
};

export default demo;
③ 配置仓库
  • 安装 pinia 和 pinia-plugin-persist npm install pinia pinia-plugin-persist
  • src 下面新建 store 文件夹,基础配置一个文件
// src/store/index.ts
import demo from './demo';

export interface IAppStore {
    demo: ReturnType<typeof demo>;
}

const STORE: IAppStore = {} as IAppStore;

export const registerStore = () => {
    STORE.demo = demo();
};

export default STORE;
// src/store/demo.js
import { defineStore } from "pinia";

const useDemo = defineStore({
    // 开启数据缓存
    persist: {
        enabled: true,
    },
    id: "useDemo",
    state() {
        return {
        }
    },
    actions: {
    },
})

export default useDemo;
④ 配置入口文件
// main.ts
import { createApp } from 'vue'
import App from 'src/App.vue'
import router from 'router'
import { createPinia } from 'pinia'
import { registerStore } from 'store';
import piniaPluginPersist from 'pinia-plugin-persist';

const app = createApp(App);

app.config.warnHandler = () => null;

app.use(router).use(createPinia().use(piniaPluginPersist)).mount('#app')

registerStore();
⑤ 按需配置其他插件和工具库文件
  • 传输加密库
  • UI 组件库
  • 自适应屏幕工具组件
  • 时间转换函数工具
  • 地图异步加载工具
  • 类型判断工具
  • 经纬度坐标系转换工具
  • 二进制流导出文件工具
  • 等等