preface

Recently admin skeleton, the voice is more, say good tutorial, how also can’t be a gaffe of the boy, get down to business, the first article is mainly about some of the preparatory work before starting to write the actual business code.

The directory structure

├── types // Global ts type interface Exercises ├─ config // Configuration Exercises ├─ SRC // Source │ ├─ API // All request │ ├ ─ ─ assets/theme/font static resource │ ├ ─ ─ the components / / global public components │ ├ ─ ─ directive / / global directives │ ├ ─ ─ enums / / enumeration definition │ ├ ─ ─ hooks / / │ ├─ Heavy Metal Exercises │ ├─ Heavy metal Exercises │ ├─ Heavy metal Exercises │ ├─ Heavy metal Exercises │ ├─ Heavy Metal Exercises │ ├─ Heavy Metal Exercises │ ├─ Heavy Metal Exercises │ ├─ Heavy Metal Exercises │ ├─ Heavy Metal Exercises │ ├── app.vue // Entry page │ ├─ main.ts // Entry loading component initialization │ ─.gitignore For example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example, for example Js // Configure stylelint. ├── postcsS.config.js // Configure postCSS. ├── Prettier.config.js // Configure prettier for prettier ├── index.html // HTML template │ ── package.json // package.json ├─ Omit... // More introduction will not write ha, and then, no thenCopy the code

API and views

At present, the definition is as follows. With more and more pages, it is suggested that API module and views should be corresponding to facilitate later maintenance.

src=>api

src=>views

components

The components here place components that are globally common, such as upload components, tables, pop-ups, and so on. Some page level component suggestions are still placed under their views files for easy management. As shown in figure:

store

Here I personally suggest using Pinia instead of vuex, can also be said to be a habit, see how you choose, I personally feel pinia elegant, specific advantages but more statement, can move to the official website to see ha.

vite

Next generation front-end development and building tools, fast service startup, lightweight and fast overloading, optimized build, fully typed API, and more.

Alias and plugins

Alias: As projects grow larger, direct references to files become more complex. This is where aliases are used. Some people prefer to point to the SRC directory and then use relative paths to find files.

Plugins. As projects get bigger, vite.config.js can get smelly and long, especially plugins. So it’s recommended to pull them out, like this:

ESLint

Whether people cooperation or personal projects, code specification is very important, not only can largely avoid basic grammatical errors, also ensures the readability of the code, personal recommendations eslint + vscode | writing vue webstorm is absolutely a fly the general feeling.

mock

The vite project recommends the use of the plugin-mock to build a mock. It is easy to use the following method. You only need to create a file in the mock that corresponds to the interface in the API to implement the mock

Encapsulation axios

// axios配置  可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
import { VAxios } from './Axios';
import { AxiosTransform } from './axiosTransform';
import axios, { AxiosResponse } from 'axios';
import { checkStatus } from './checkStatus';
import { joinTimestamp, formatRequestDate } from './helper';
import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum';

import { useGlobSetting } from '@/hooks/setting';

import { isString } from '@/utils/is/';
import { setObjToUrlParams } from '@/utils/urlUtils';

import { RequestOptions, Result } from './types';

import { useUserStoreWidthOut } from '@/store/modules/user';

const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix || '';

import router from '@/router';
import { storage } from '@/utils/Storage';

/**
 * @description: 数据处理,方便区分多种处理方式
 */
const transform: AxiosTransform = {
  /**
   * @description: 处理请求数据
   */
  transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
    const { $message: Message, $dialog: Modal } = window;
    const {
      isShowMessage = true,
      isShowErrorMessage,
      isShowSuccessMessage,
      successMessageText,
      errorMessageText,
      isTransformResponse,
      isReturnNativeResponse,
    } = options;

    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
    if (isReturnNativeResponse) {
      return res;
    }
    // 不进行任何处理,直接返回
    // 用于页面代码可能需要直接获取code,data,message这些信息时开启
    if (!isTransformResponse) {
      return res.data;
    }

    const reject = Promise.reject;

    const { data } = res;

    if (!data) {
      // return '[HTTP] Request has no return value';
      return reject(data);
    }
    //  这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
    const { code, result, message } = data;
    // 请求成功
    const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS;
    // 是否显示提示信息
    if (isShowMessage) {
      if (hasSuccess && (successMessageText || isShowSuccessMessage)) {
        // 是否显示自定义信息提示
        Message.success(successMessageText || message || '操作成功!');
      } else if (!hasSuccess && (errorMessageText || isShowErrorMessage)) {
        // 是否显示自定义信息提示
        Message.error(message || errorMessageText || '操作失败!');
      } else if (!hasSuccess && options.errorMessageMode === 'modal') {
        // errorMessageMode=‘custom-modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
        Modal.info({
          title: '提示',
          content: message,
          positiveText: '确定',
          onPositiveClick: () => {},
        });
      }
    }

    // 接口请求成功,直接返回结果
    if (code === ResultEnum.SUCCESS) {
      return result;
    }
    // 接口请求错误,统一提示错误信息
    if (code === ResultEnum.ERROR) {
      if (message) {
        Message.error(data.message);
        Promise.reject(new Error(message));
      } else {
        const msg = '操作失败,系统异常!';
        Message.error(msg);
        Promise.reject(new Error(msg));
      }
      return reject();
    }

    // 登录超时
    if (code === ResultEnum.TIMEOUT) {
      if (router.currentRoute.value.name == 'login') return;
      // 到登录页
      const timeoutMsg = '登录超时,请重新登录!';
      Modal.warning({
        title: '提示',
        content: '登录身份已失效,请重新登录!',
        positiveText: '确定',
        negativeText: '取消',
        onPositiveClick: () => {
          storage.clear();
          router.replace({
            name: 'login',
            query: {
              redirect: router.currentRoute.value.fullPath,
            },
          });
        },
        onNegativeClick: () => {},
      });
      return reject(new Error(timeoutMsg));
    }

    // 这里逻辑可以根据项目进行修改
    if (!hasSuccess) {
      return reject(new Error(message));
    }

    return data;
  },

  // 请求之前处理config
  beforeRequestHook: (config, options) => {
    const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true } = options;

    if (joinPrefix) {
      config.url = `${urlPrefix}${config.url}`;
    }

    if (apiUrl && isString(apiUrl)) {
      config.url = `${apiUrl}${config.url}`;
    }
    const params = config.params || {};
    if (config.method?.toUpperCase() === RequestEnum.GET) {
      if (!isString(params)) {
        // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
        config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
      } else {
        // 兼容restful风格
        config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
        config.params = undefined;
      }
    } else {
      if (!isString(params)) {
        formatDate && formatRequestDate(params);
        config.data = params;
        config.params = undefined;
        if (joinParamsToUrl) {
          config.url = setObjToUrlParams(config.url as string, config.data);
        }
      } else {
        // 兼容restful风格
        config.url = config.url + params;
        config.params = undefined;
      }
    }
    return config;
  },

  /**
   * @description: 请求拦截器处理
   */
  requestInterceptors: (config) => {
    // 请求之前处理config
    const userStore = useUserStoreWidthOut();
    const token = userStore.getToken;
    if (token) {
      // jwt token
      config.headers.token = token;
    }
    return config;
  },

  /**
   * @description: 响应错误处理
   */
  responseInterceptorsCatch: (error: any) => {
    const { $message: Message, $dialog: Modal } = window;
    const { response, code, message } = error || {};
    // TODO 此处要根据后端接口返回格式修改
    const msg: string =
      response && response.data && response.data.message ? response.data.message : '';
    const err: string = error.toString();
    try {
      if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
        Message.error('接口请求超时,请刷新页面重试!');
        return;
      }
      if (err && err.includes('Network Error')) {
        Modal.info({
          title: '网络异常',
          content: '请检查您的网络连接是否正常!',
          positiveText: '确定',
          onPositiveClick: () => {},
        });
        return;
      }
    } catch (error) {
      throw new Error(error);
    }
    // 请求是否被取消
    const isCancel = axios.isCancel(error);
    if (!isCancel) {
      checkStatus(error.response && error.response.status, msg, Message);
    } else {
      console.warn(error, '请求被取消!');
    }
    return error;
  },
};

const Axios = new VAxios({
  timeout: 10 * 1000,
  // 接口前缀
  prefixUrl: urlPrefix,
  headers: { 'Content-Type': ContentTypeEnum.JSON },
  // 数据处理方式
  transform,
  // 配置项,下面的选项都可以在独立的接口请求中覆盖
  requestOptions: {
    // 默认将prefix 添加到url
    joinPrefix: true,
    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
    isReturnNativeResponse: false,
    // 需要对返回数据进行处理
    isTransformResponse: true,
    // post请求的时候添加参数到url
    joinParamsToUrl: false,
    // 格式化提交参数时间
    formatDate: true,
    // 消息提示类型
    errorMessageMode: 'none',
    // 接口地址
    apiUrl: globSetting.apiUrl as string,
  },
  withCredentials: false,
});

export default Axios;
Copy the code

Cross-domain problem

First of all, the interaction between the front and back end will inevitably encounter cross-domain problems, some are solved by the back end, if you do not bother to configure the back end, you can use vite proxy to solve the problem.

prompt

This series of tutorials, continue to update, until set up a complete Admin Pro framework, please supervise me ha ~[thank you]

At the end

Above is some infrastructure work, the next article, officially open wanke, ok, you said you have seen this, the focus of free to go, good people life safe, bug will be far away from you O(grinning)