vue3+ts项目搭建过程

436 阅读12分钟

环境准备

备注:需要node环境已安装就绪

安装vue脚手架:

npm install -g @vue/cli
// 或者
yarn global add @vue/cli

vue create 项目名

安装过程,pwa和test测试之类的不要选其他一路回车

image.png

工具安装

1. editorconfig

可以在vscode插件市场安装eidtorconfig for vscode插件

主要用于:不同平台不同编辑器的代码统一风格显示。比如在写代码的时候tab键是几个空格(比如mac和win的tab键的空格缩进不同,或者webstorm和vscode格式处理不同)

// 根目录下创建.editorconfig文件

# http://editorconfig.org
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

2. prettier工具

场景: 大部分情况代码保存格式化,可以通过在项目中安装开发时依赖prettier进行格式化维护。

因为打包时不需要格式化,代码已经被压缩了。所以安装时是开发时依赖

npm install prettier -D --save

安装完成后,在根目录下创建.prettierrc文件;可以在文件中配置告诉编辑器以什么标准进行格式化(比如:语句末尾是否要分号、对象的最后一个属性后是否需要逗号等等)


//.prettierrc 文件配置
{
    "useTabs": false, // 使用tab还是空格缩进
    "printWidth": 80, // 每行的字符数
    "tabWidth": 2, // tab的宽度
    "singleQuote": false,//选择单引号还是双引号(true单)
    "semi": true, // 语句末尾是否要分号; 默认加true
    "trailingComma": "none" , // 对象最后一个属性末尾是否要逗号
}

很多文件不需要格式化,比如dist和node_modules下的文件; 此时可以在根目录下创建.prettierignore配置文件忽略他们

// 根目录下的.prettierignore配置文件
/dist/*

.local

.output.js

/node_modules/**

**/*.svg

**/*.sh

/public/*

最后一步在vscode插件市场安装prettier code formatter的插件,此时需要点到每个文件中手动保存才能按照对应配置格式化;可以在package.json中配置一个scripts脚本为一次性修改命令:

"prettier": "prettier --write ."

在命令行执行

npm run prettier

执行完脚本之后,除了prettierignore中忽略的文件之外,所有文件都会格式化

3. eslint

场景:eslint可以帮规范代码,不规范的代码会给出警告

可以在vscode插件市场安装eslint的插件可以帮助快速检查代码


因为生成项目的时候格式化规范选择的eslint+prettier,所以有可能出现prettier和eslint的配置的规范有冲突(比如prettier中配置单引号,而eselint配置双引号的情况); 可以通过安装两个第三方插件处理(但其实因为在生成项目时选择的是eslint+prettier,所以下面两个插件已经默认安装了,可以在node_modules下查看):

npm install eslint-plugin-prettier eslint-config-prettier -D --save

eslint-plugin-prettier和eslint-config-prettier插件会帮助eslint和prettier兼容.

修改.eslintrc文件,在extends中最后一行增加:

// 表示如果冲突则下面的prettier的配置覆盖extends中继承的配置
"plugin:prettier/recommended"

备注:配置完记得重启项目

image.png

4. git Husky和eslint

代码commit规范提交和限制

commit提交不规范的代码;可以借助husky工具检测commit提交,在提交之前进行自动规范修复eslint fix。

想要做到自动规范修复,需要使用一个git husky工具,husky是一个git的hook,可以监听commit提交几个阶段(pre-commit、commit-msg、pre-push)

如何使用husky工具呢?可以使用自动配置命令:

npx husky-init && npm install

这里会做三件事:

  1. 安装husky相关的依赖

image.png

可以在pre-commit文件中配置commit之前的操作。

  1. 在项目目录下创建.husky文件夹

image.png

  1. 在package.json增加脚本

image.png


具体使用:比如可以写不规范的代码连续换行等,然后在pre-commit配置文件中增加命令yarn run lint,接着commit提交代码,会自动格式化代码。

为什么是yarn run lint命令?因为script脚本中的lint命令就是格式化。

image.png

5. commitizen工具

commitizen是约束pre-commit阶段

commitizen可以帮助我们规范commit的提交msg信息,比如是feat/fix/refactor/docs等

npm install commitizen -D --save

接着再安装:

npx commitizen init cz-conventional-changelog --save-dev --save-exact

上面命令会帮我们安装cz-conventional-changelog,并且初始化cz-conventional-changelog,并且还会在package.json帮我们配置

image.png

image.png

上面的config配置是执行commitizen命令的时候,会借助path指定的路径下的工具,具体的脚本是lib下的cz。

npx cz

或者

yarn run cz

6. commitlint代码提交验证

commitlint是限制commit-msg阶段

进行commitizen规范提交配置后,如果用户不采用cz规范提交,仍然坚持原始的git不规范commit提交还是可以提交;那么我们可以借助commitlint进行不规范提交限制,先安装以下插件:

npm install @commitlint/config-conventional @commitlint/cli -D --save

然后在根目录下创建commitlint.config.js文件,配置commitlint:

// commitlint.config.js
modules.exports = {
    extends: ["@commitlint/config-conventional"]
}

最后再husky生成commit-msg文件,验证提交信息:

npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"

或者

yarn run husky add .husky/commit-msg "yarn run --no-install commitlint --edit $1"

运行完命令之后,会在.husky文件夹下创建commit-msg文件

image.png

第三方库集成

1. elementPlus库

为vue3提供的桌面端的UI库,不建议移动端

// 命令行安装
npm install element-plus --save

两种方式引入:

  • 一种是全局引入(优点:只需要在main中导入,全局任意组件无需单独导入皆可直接组件库使用。缺点:没用到的组件在打包阶段也会被打包)
  • 一种是按需导入(优点:1. 用到什么组件引入什么组件,打包体积小。 缺点:1. 组件使用单独内部导入;2. 需要额外安装插件支持按需导入的功能)

全局引入:

// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' //留意引入样式
import App from './App.vue'

const app = createApp(App)

app.use(ElementPlus)
app.mount('#app')

// login.vue(无需再额外导入,直接使用)
<el-button></el-button>

按需导入方式1(最新推荐):按需导入需要安装如下插件

npm install unplugin-vue-components unplugin-auto-import -D --save

unplugin-vue-components: 是负责组件(不包含业务组件)自动导入的,内部集成了组件库;可以在不导入和注册组件的情况下在模板中使用想要用到的组件。

<el-button>button</el-button>

<script lang="ts">
import { defineComponent } from "vue";
// import { ElButton } from "element-plus";  // 这里不需要导入
export default defineComponent({
  // components: {
  //   ElButton // 这里也不需要导入
  // },
  setup() {
    return {};
  }
});
</script>

unplugin-auto-import: 是用于自动导入API的;可以在不引入ref的情况下使用ref

const count = ref(0)  
const doubled = computed(() => count.value * 2)

安装完以上两个插件之后,在vue.config.js中对webpack进行如下配置:

// vue.config.js
const { defineConfig } = require("@vue/cli-service");
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = defineConfig({
  configureWebpack: {
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()], // 配置自动导入api
      }),
      Components({
        resolvers: [ElementPlusResolver()], // 配置自动导入组件
      }),
    ]
  }
}

按需导入方式2:

如果按需导入不通过安装上面的两个插件,那么可以直接在全局的main.ts中进行全局指定组件引入

// main.ts
import { ElButton } from "element-plus";
app.use(xxxx)

如果上面引入的UI组件太多,可以定义单独模块globa文件家,专门负责导入使用到的ui组件,然后在main.js中进行引入global.ts。

以上处理了按需导入ui组件,但是样式怎么导入呢?

如果直接在全局引入样式会导致没有用到的ui组件的样式也会打包进去,此时可以安装一个babel插件,该插件会在导入ui组件的地方,导入对应的ui样式.

在babel.config.js文件中增加如下配置:

// babel.config.js
module.exports = {
  plugins: [
    [
      "import",
      {
        libraryName: "element-plus",
        customStyleName: (name) => {
          return `element-plus/lib/theme-chalk/${name}.css`
        }
      }
    ]
  ]
};

以上配置含义是:通过import导入element-plus库中的name组件时,也要引入name组件对应的样式,element-plus/lib/theme-chalk/${name}.css是样式所在的位置(具体可以在node_modules下的elementPlus库中查看)。

2. router

2.1 静态路由基本配置

// router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    redirect: "/login"
  },
  {
    path: "/login",
    component: () => import("@/views/login/login.vue")
  }
];

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



export default router;

2.2 动态路由最终配置

// router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
const DefaultLayout = () => import("@/layout/defaultLayout/DefaultLayout.vue");

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "root",
    redirect: "/verify",
    children: []
  },
  {
    path: "/verify",
    name: "verify",
    component: () => import("@/views/verify/verify.vue"),
    children: []
  },
  {
    path: "/main",
    name: "main",
    component: DefaultLayout, // 默认的布局文件
    meta: {
      resource: {
        title: "渠道资源管理",
        routeLevel: 1
      },

      construction: {
        title: "渠道建设",
        routeLevel: 1
      }
    },
    redirect: "/resource/manage",
    children: []
  }
];

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


export default router;

// layout/defaultLayout/defaultLayout.vue
<template>
  <el-container class="default-layout">
    <el-header>
      <NavHeader />
    </el-header>
    <el-container>
      <el-aside width="208px">
        <NavMenu />
      </el-aside>
      <el-main>
        <BreadCrumb />
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup lang="ts">
import { onMounted, onBeforeMount } from "vue";
import { useStore } from "@/store/index";
import NavMenu from "@/components/NavMenu/NavMenu.vue";
import NavHeader from "@/components/NavHeader/NavHeader.vue";
import BreadCrumb from "@/components/BreadCrumb/BreadCrumb.vue";

const store = useStore();

</script>

<style scoped lang="less">
.default-layout {
  width: 100%;
  height: 100%;
}

.el-header {
  height: 60px;
  background: #ffffff;
  border-bottom: 1px solid #dcdee2;
}
.el-aside {
  height: calc(100vh - 60px);
}

.el-main {
  background: #f5f7fc;
  padding: 16px 20px;
}
</style>

export default {
  path: "/resource/manage",
  name: "manage",
  component: () => import("@/views/resource/manage/manage.vue"),
  meta: {
    title: "资源管理",
    routeLevel: 2
  },
  children: [
    {
      path: "",
      name: "manageList",
      component: () => import("@/views/resource/manage/manageList.vue")
    },
    {
      path: "/resource/manage/manage-details",
      name: "manageDetails",
      component: () => import("@/views/resource/manage/manageDetail.vue"),
      meta: {
        title: "查看",
        routeLevel: 3
      }
    },
    {
      path: "/resource/manage/manage-edit",
      name: "manageEdit",
      component: () => import("@/views/resource/manage/manageEdit.vue"),
      meta: {
        title: "新增或编辑",
        routeLevel: 3
      }
    }
  ]
};

image.png


动态路由映射逻辑:

// utils/mapUserMeneToRoutes.ts
import type { RouteRecordRaw } from "vue-router";
import { isArray, isObject } from "@/utils";
import type { IRouteDataType } from "@/utils/type";

// 从本地动态读取加载所有的menu
const queryAllRoutes = (): RouteRecordRaw[] => {
  const allRoutes: RouteRecordRaw[] = [];
  const allFilePathList = require.context("../router", true, /\.ts$/);
  const filePathList = allFilePathList
    .keys()
    .filter((item) => !item.endsWith("index.ts"));
  filePathList.forEach((filePath) => {
    const routeModule = require(`../router${filePath.split(".")[1]}`);
    isObject(routeModule?.default) && allRoutes.push(routeModule?.default);
  });
  return allRoutes;
};

// 根据用户菜单接口返回映射权限路由
const mapUserMeneToRoutes = (userMenu: any[]): RouteRecordRaw[] => {
  const routes: RouteRecordRaw[] = [];
  const allRoutes = queryAllRoutes();
  const getUserRoutes = (userMenu: any[]) => {
    for (const menu of userMenu) {
      if (menu.parentMenuId == "0") {
        getUserRoutes(menu.sub_menus);
      } else {
        const tempRoute: IRouteDataType = allRoutes.find(
          (item) => item.path === menu.action
        ) as IRouteDataType;
        routes.push(tempRoute);
        isArray(menu.sub_menus) && getUserRoutes(menu.sub_menus);
      }
    }
  };
  getUserRoutes(userMenu);
  return routes;
};

export { mapUserMeneToRoutes };

3. vuex

3.1 vuex的基本配置

import { createStore } from "vuex";

export default createStore({
  state: () => {
    return {
      msg: "hello TS"
    }
  },
  getters: {},
  mutations: {},
  actions: {},
  modules: {}
});

3.2 vuex最终配置

// store/index.ts
import { createStore, Store, useStore as useVuexStore } from "vuex";
import { IRootState, IStoreType } from "@/store/type";

import main from "@/store/main/main";
import login from "@/store/login/login";

const store = createStore<IRootState>({
  state: () => ({}),
  getters: {},
  mutations: {},
  actions: {},
  // 注意这里需要加入其他业务模块
  modules: {
    login,
    main
  }
});

// 刷页面时重新加载用户菜单栏组件生成动态路由
export const initMenu = async () => {
  await store.dispatch("main/queryUserMenuAction");
};

// 刷页面加载本地cache到store
export const setupStore = () => {
  store.dispatch("main/loadLocalCache");
};

// 使用新类型IStoreType 扩展vuex的useStore(主要解决的是IStoreType中拥有所有模块的变量以及对应的类型,所以在外部可以通过根store访问到其他模块的变量)
export const useStore = (): Store<IStoreType> => {
  return useVuexStore();
};

export default store;

把其他业务模块的store类型和rootstore的类型进行交叉类型获取新类型IStoreType,这样IStoreType会有用所有模块的变量了

// store/type.ts
import { ILoginState } from "@/store/login/type";
import { IMainState } from "@/store/main/type";

export interface IRootState {}

export interface IRootWithMoudle {
  login: ILoginState;
  main: IMainState;
}
export type IStoreType = IRootState & IRootWithMoudle;

单独业务模块store维护

// store/main/main.ts
import { Module } from "vuex";
import type { IRootState } from "@/store/type";
import type { IMainState, IBreadCrumbRootType } from "@/store/main/type";
import { setStorage, getStorage, isArray, isObject } from "@/utils";
import { mapUserMeneToRoutes } from "@/utils/mapMenuToRoutes";
import { getUserInfoApi, queryAuthMenuApi } from "@/service/main/index";
import router from "@/router/index";

import { TOKEN_KEY } from "@/utils/constant";

const mainModule: Module<IMainState, IRootState> = {
  namespaced: true,
  state: () => {
    return {
      token: "",
      userInfoRes: {},
      userMenuList: [],
      breadCrumbList: []
    };
  },
  getters: {
    // userMenuListGetters(state: IMainState){
    //   return state?.userMenuList
    // }
  },
  mutations: {
    changeToken(state: IMainState, token: string) {
      state.token = token;
    },
    changeUserInfoRes(state: IMainState, userInfoRes: any) {
      state.userInfoRes = userInfoRes;
    },
    changeUserMenuList(state: IMainState, userMenuList: any) {
      state.userMenuList = userMenuList;
      // 根据菜单接口返回动态添加路由
      const routes = mapUserMeneToRoutes(userMenuList ?? []);
      routes.forEach((route) => router.addRoute("main", route));
      router.addRoute({
        path: "/:pathMatch(.*)*",
        name: "notFound",
        component: () => import("@/views/not-found/not-found.vue")
      });
    }
  },
  actions: {
    // 获取用户信息
    async queryUserInfoAction({ commit, state }, payload: any) {
      try {
        const userInfoRes = await getUserInfoApi();
        commit("changeUserInfoRes", userInfoRes.data);
        setStorage("userInfo", userInfoRes.data);
      } catch (error) {
        console.log(error);
      }
    },
    async queryUserMenuAction({ commit, state }, payload: any) {
      try {
        // 获取用户菜单
        const userMenuRes = await queryAuthMenuApi();
        if (!isArray(userMenuRes?.data?.shortcutMenuList)) {
          throw new Error(userMenuRes?.data?.msg);
        }
        commit("changeUserMenuList", userMenuRes?.data?.shortcutMenuList || []);
        setStorage("userMenu", userMenuRes?.data?.shortcutMenuList);
      } catch (error) {
        console.log(error);
      }
    },
    // 防止用户刷新页面store数据丢失
    loadLocalCache({ commit, state }) {
      // 每次刷新只加载token  用户列表需要重新刷接口
      const token = getStorage(TOKEN_KEY);
      if (token) {
        commit("changeToken", token);
      }
    }
  }
};

export default mainModule;

4. axios

请求拦截器:后添加的先执行; 响应拦截器:后添加的后执行

npm install axios --save

image.png

image.png

// service/request/index.ts
import axios from "axios";
import type { AxiosInstance } from "axios";

import type {
  BYDAxiosInterceptor,
  BYDRequestConfig
} from "@/service/request/type";

import { getStorage } from "@/utils";
import { TOKEN_KEY, LOGIN_URL } from "@/utils/constant";

import { MessageTipsTypeEnum } from "@/utils/constant";
import useMessage from "@/hooks/useMessage/useMessage";

class BYDRequest {
  instance: AxiosInstance;
  interceptors?: BYDAxiosInterceptor;
  constructor(config: BYDRequestConfig) {
    this.instance = axios.create(config);
    this.interceptors = config.interceptors;

    // 1. 单个实例拦截器维度控制
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    );
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    );

    // 2. 全局维度拦截器控制 :可以给所有的实例应用的拦截器
    this.instance.interceptors.request.use(
      (config) => {
        const token: string | null = getStorage(TOKEN_KEY);
        if (token) {
          config.headers = {
            "auth-token": token,
            "Content-Type": "application/json;charset=utf-8",
            ...config.headers
          };
        }
        return config;
      },
      (error) => {
        console.log(error);
      }
    );
    // 处理code
    /**
     * code: 401 // 无token提示处理
     * code: 403 // token过期提示处理
     * code: 404 // 页面不存在提示处理
     */
    this.instance.interceptors.response.use(
      (res) => {
        if(res?.data?.code == 403 || res?.data?.code == 401){
          useMessage({
            message: `${res?.data?.msg}~`,
            type: MessageTipsTypeEnum.ERROR
          });
          setTimeout(() => {
            window.location.href = LOGIN_URL;
          }, 3000);
        }
        return res.data
      },
      (err) => {
        return err;
      }
    );
  }

  // // 处理相应状态码
  // _handleResponseCode(code: string | number){

  // }
  // 3. 请求维度拦截器控制
  request<T>(config: BYDRequestConfig<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      if (config?.interceptors?.requestInterceptor) {
        config = config?.interceptors?.requestInterceptor(config);
      }
      this.instance
        .request<any, T>(config)
        .then((res) => {
          if (config?.interceptors?.responseInterceptor) {
            res = config.interceptors.responseInterceptor(res);
          }
          resolve(res);
        })
        .catch((err) => {
          reject(err);
        });
    });
  }

  get<T>(config: BYDRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: "GET" });
  }

  post<T>(config: BYDRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: "POST" });
  }

  delete<T>(config: BYDRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: "DELETE" });
  }

  patch<T>(config: BYDRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: "PATCH" });
  }
}

export default BYDRequest;

// service/request/type.ts
import type { AxiosRequestConfig, AxiosResponse } from "axios";
// 定义拦截器hook约束
export interface BYDAxiosInterceptor<T = AxiosResponse> {
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig;
  requestInterceptorCatch?: (error: any) => any;
  responseInterceptor?: (res: T) => T;
  responseInterceptorCatch?: (error: any) => any;
}

// 这里定义BYDRequestConfig,扩展了axios的AxiosRequestConfig的config对象,使得我们可以传入拦截器hooks
export interface BYDRequestConfig<T = AxiosResponse>
  extends AxiosRequestConfig {
  interceptors?: BYDAxiosInterceptor<T>;
}

// service/index.ts
// service提供统一出口给外部
import BYDRequest from "./request";

const bydRequest = new BYDRequest({
  baseURL: process.env.VUE_APP_BASE_URL,
  timeout: process.env.VUE_APP_TIME_OUT,
  // 单个实例维度传入拦截器
  interceptors: {
    requestInterceptor: (config) => {
      return config;
    },
    requestInterceptorCatch: (error) => {
      return error;
    },
    responseInterceptor: (res) => {
      return res;
    },
    responseInterceptorCatch: (error) => {
      return error;
    }
  }
});

export default bydRequest;

// service/main/index.ts
// 实际请求
import { IRootDataType } from "@/service/main/type";
import bydRequest from "@/service/index";
import type { BYDRequestConfig } from "@/service/request/type";

enum Urls {
  USER_INFO = "/user/pc/getUserInfo",
  AUTH_MENU = "/auth/pc/getShortcutMenu"
}

// 获取用户信息
export const getUserInfoApi = (
  data?: BYDRequestConfig<IRootDataType>
): Promise<IRootDataType> =>
  bydRequest.get<IRootDataType>({ url: Urls.USER_INFO, ...data });

// 获取用户菜单
export const queryAuthMenuApi = (
  data?: BYDRequestConfig<IRootDataType>
): Promise<IRootDataType> =>
  bydRequest.post<IRootDataType>({ url: Urls.AUTH_MENU, ...data });

5. qs库处理url

npm install qs --save
// 使用
import qs from "qs";
const parseUrl = (url: string): object => {
  const searchParams: string[] = url.split("?");
  return searchParams.length > 1 ? qs.parse(searchParams[1]) : {};
};


const obj: { token?: string } = parseUrl(window?.location?.href ?? "");

6. icon图标库集成

element-plus.gitee.io/zh-CN/compo…

www.npmjs.com/package/unp…

www.dongchuanmin.com/xhtml/1717.…

要使用elementplus的icon,需要额外安装:

npm install @element-plus/icons-vue --save

但是安装的icons-vue提供的icon有限,我们可以借助@iconify/json图标源,再加上图标自动导入插件:

// 安装图标自动导入插件
yarn add unplugin-icons -D --save

// 安装图标源
yarn add @iconify/json -D --save

在vue.config.js中配置如下:

// 

环境变量区分环境

比如baseUrl在dev和prod环境的值不一样,区分环境变量的三种方式:

方式1:手动修改不同的变量

方式2:根据porcess.env.NODE_ENV区分

方式3:编写不同的环境变量配置文件(vue中推荐)

// 根目录下针对生产和开发和测试创建不同配置文件; vue是支持的,会自动读取
.env.development
.env.production
.env.test

但是配置的环境变量不能随便命名,需要遵守规范,否则不识别; 只有NODE_ENV和BASE_URL和自定义的必须以VUE_APP_开头的才会被识别

// .env.development
VUE_APP_BASE_URL=http://xxx/dev

有以上配置之后,再通过process.env.VUE_APP_BASE_URL读取。

项目webpack配置

脚手架默认集成了基本的webapck配置;开发人员也可以手动增加或者修改配置,可以在根目录下创建vue.config.js,相关的详细配置在: cli.vuejs.org/zh/config/

vue.config.js中的关于webpack有以下3种配置方式

// vue.config.js
const { defineConfig } = require("@vue/cli-service");
const path = require("path");
module.exports = defineConfig({
  transpileDependencies: true,
  // 方式一:CLI提供的属性,属性名不一定和webpack的指定的属性名一样,但是作用一样
  outputDir: "./build",  // 生产打包输出的目录
  publicPath: "./" // 指定的是server从build加载的对应的资源的根路径, 比如这里的./  server从index.html中读取打包的js资源路径就是将build作为根目录,读取js/xxx
  
  // 方式二:configureWebpack对象方式配置时(属性和webpack的配置属性完全一致) ——> 会将配置的对象和cli内置的webpack相同属性进行合并配置(webpack-merge)
  // configureWebpack: {
  //   resolve: {
  //     alias: {
  //       components: "@/components"
  //     }
  //   }
  // },
  // 方式三:configureWebpack函数方式配置时(属性和webpack的配置属性完全一致) ——> 接受cli已经集成webpack配置config作为函数参数,对其进行覆盖配置
  configureWebpack: (config) => {
    config.resolve.alias = {
      "@": path.resolve(__dirname, "src")
    };
  },
  // 方式四:chainWebpack链式配置方式,属性和webapck属性一致,也是接受config然后相同属性会覆盖配置
  chainWebpack: (config) => {
    config.resolve.alias
      .set("@", path.resolve(__dirname, "src"))
      .set("components", "@/components");
  }
});

项目目录结构

1. .browserslistrc文件

.browserslistrc文件:主要是适配浏览器的适配文件;比如css某些属性在什么浏览器上不支持,或者需要加前缀,就会通过检查browserslistrc文件中的浏览器,进而对css属性加前缀之类的。

// 表示适配时长份额大于1%的浏览器
> 1%  

// 表示适配主流浏览器最新的两个版本
last 2 versions

// 表示还在维护中的浏览器
not dead

// 表示不支持ie11
not ie 11

2. tsconfig.json文件

ts文件转js文件的过程:

需要先编译ts文件(基本语法没有问题,再进行下一步转换) ————> 转换 ————> 转换为js文件

tsconfig.json文件中可以配置ts相关的;比如ts编译时是否支持this语法和哪些属性和any等;最终转为es代码?还是es5代码?es6/es11等等;也可以在tsconfig中配置

// tsconfig.json
{
  // ts具体配置文档:typescriptlang.org/tsconfig
  // ts编译阶段的配置
  "compilerOptions": {
    // target目标代码(ts——> js(es5/6/7/8...)):ts最终转为js目标代码是哪个版本es5/6/7xxx;
    // esnext表示es5之后版本的代码默认都支持(因为在构建项目时选择了babel帮忙编译ts代码,babel会检查浏览器适配文件,最终转为对应的浏览器支持的版本文件)
    // 如果用tsc的方式编译ts代码的话,target就需要配置为准确的值,比如es5,否则浏览器可能识别不了
    "target": "esnext",
    // 目标代码使用的模块化方案(esnext代表esModule方式,也可以是commonjs方式)
    // 自己写,可以写umd,表示支持多种模块化方案,es/amd/commonjs等
    "module": "esnext",
    // 严格模式坚持(比如any)
    "strict": true,
    // 表示对JSX的处理;jsx最终转为h函数呢?还是createElement等;preserve表示不转换(如果有babel的话,babel会帮助我们对jsx做处理)
    "jsx": "preserve",
    // 模块依赖按照node解析查找模块的方式(比如后缀没有.js,则默认加.js 或者加.node进行查找)
    "moduleResolution": "node",
    // 跳过一些库的类型检测(比如axios和lodash等)
    "skipLibCheck": true,
    // esModuleInterop和allowSyntheticDefaultImports配置的是es和commonjs能否回合使用
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,

    "forceConsistentCasingInFileNames": true,
    "useDefineForClassFields": true,
    // ts——> js要不要生成映射文件
    "sourceMap": true,
    // 文件路径解析时的基本url
    "baseUrl": ".",
    // 具体指定可以使用的类型,默认不传则以target指定的类型为准,target如果是es5,则es5支持的类型都可以解析
    "types": ["webpack-env"],
    // 编译阶段的路径解析:@/components  则解析到 src/components
    // 有时候在wepack中配置的别名 components:  发现在项目中直接通过别名components导入失败,则可以在这里配置components/* : ["components/*"]就不会报错了
    "paths": {
      "@/*": ["src/*"]
    },
    // 项目中可以使用哪些库的类型(esnext:支持es5及以上的所有类型,dom:支持dom类型等等...)
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
  },
  // 哪些文件需要tsconfig对它做ts的解析约束
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  // 排除不解析(比如include库中包含的ts文件中引入了axios库,axios在node_modules中,如果配置了"exclude": ["node_modules"],那么axios就不会进行解析了)
  "exclude": ["node_modules"]
}

虽然配置了tsconfig,tsconfig针对ts处理后,最终babel都会帮我们再做转换处理

3. shims-vue.d.ts

项目中加载一些模块时,比如.vue文件.png文件,.jpeg文件;但是ts是不识别那些文件,认为这些文件不是一个模块,所以脚手架帮我们在shims-vue.d.ts中做了声明,比如.vue文件的声明

4. vue.config.js

const { defineConfig } = require("@vue/cli-service");

const AutoImport = require("unplugin-auto-import/webpack");
const Components = require("unplugin-vue-components/webpack");
const Icons = require("unplugin-icons/webpack");
const { ElementPlusResolver } = require("unplugin-vue-components/resolvers");

const path = require("path");

module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: process.env.NODE_ENV === "production" ? "/" : "./", // 应用被部署在 https://www.my-app.com/my-app/,则设置 publicPath 为 /my-app/
  assetsDir: "static",
  outputDir: "dist",
  lintOnSave: false,
  configureWebpack: {
    resolve: {
      alias: {
        "@": path.resolve(__dirname, "src"),
        components: "@/components"
      }
    },
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()] // 配置自动导入api
      }),
      Components({
        resolvers: [ElementPlusResolver()] // 配置自动导入组件
      }),
      Icons({ compiler: "vue3", customCollections: {} }) // 配置icon图标自动导入
    ],
    devServer: {
      port: "3002",
      host: "127.0.0.1",
      proxy: {
        "/api": {
          target: "http://ip:port",
          changeOrigin: true,
          secure: false,
          ws: true,
          pathRewrite: {
            "^/api": ""
          }
        }
      }
    }
  }
});

目录划分

view创建login目录,在login目录下创建login.vue和config目录和hooks目录和cpns子组件目录,如果是公共业务的组件放在src/components下;如果是业务不关联的组件任何项目都可以copy直接用,放在base-ui目录下

项目额外依赖

1. normalize.css

因为页面默认有内外边距或者某些标签默认值;可以normalize.css进行默认重置

npm install normalize.css --save

在main.ts中导入使用

// main.ts
import "normalize.css"

有时候我们可能需要做一些全局的样式属性,比如根标签width和height默认100%;此时就可以在asset目录下创建css目录和img目录;css目录下创建index.less(用于向外部导出)和base.less;然后再main.ts中导入生效。

// asset/css/base.less

body{
    margin: 0
    padding: 0
}

#app { // app是index.html中根标签
    width: 100%
    height: 100%
}

// asset/css/index.less
@import "@/asset/css/base.less"

// main.ts
import "@/asset/css/index.less"

针对的vue3的文件抽取

比如定义的表单校验规则:如果抽取的内容,涉及到ref/reactive/store/onMounted等响应式或者周期,那么将表单规则抽取为hooks;如果都没用到响应式和声明周期,那直接抽取为config配置文件

快捷键

vbase快速生成vue代码

工具

jsontots:把json转为ts结构

vscode设置文件

{
	"workbench.iconTheme": "vscode-great-icons",

	 "editor.fontSize": 17,

	 "eslint.migration.2_x": "off",

	 "[javascript]": {

		  
		"editor.defaultFormatter": "dbaeumer.vscode-eslint"

		 
	},

	 "files.autoSave": "afterDelay",

	 "editor.tabSize": 2,

	 "terminal.integrated.fontSize": 16,

	 "editor.renderWhitespace": "all",

	 "editor.quickSuggestions": {

		  
		"strings": true

		 
	},

	 "debug.console.fontSize": 15,

	 "window.zoomLevel": 1,

	 "emmet.includeLanguages": {

		  
		"javascript": "javascriptreact"

		 
	},

	 "explorer.confirmDragAndDrop": false,

	 "workbench.tree.indent": 16,

	 "javascript.updateImportsOnFileMove.enabled": "always",

	 "editor.wordWrap": "on",

	 "path-intellisense.mappings": {

		  
		"@": "${workspaceRoot}/src"

		 
	},

	 "hediet.vscode-drawio.local-storage": "eyIuZHJhd2lvLWNvbmZpZyI6IntcImxhbmd1YWdlXCI6XCJcIixcImN1c3RvbUZvbnRzXCI6W10sXCJsaWJyYXJpZXNcIjpcImdlbmVyYWw7YmFzaWM7YXJyb3dzMjtmbG93Y2hhcnQ7ZXI7c2l0ZW1hcDt1bWw7YnBtbjt3ZWJpY29uc1wiLFwiY3VzdG9tTGlicmFyaWVzXCI6W1wiTC5zY3JhdGNocGFkXCJdLFwicGx1Z2luc1wiOltdLFwicmVjZW50Q29sb3JzXCI6W1wiRkYwMDAwXCIsXCIwMENDNjZcIixcIm5vbmVcIixcIkNDRTVGRlwiLFwiNTI1MjUyXCIsXCJGRjMzMzNcIixcIjMzMzMzM1wiLFwiMzMwMDAwXCIsXCIwMENDQ0NcIixcIkZGNjZCM1wiLFwiRkZGRkZGMDBcIl0sXCJmb3JtYXRXaWR0aFwiOjI0MCxcImNyZWF0ZVRhcmdldFwiOmZhbHNlLFwicGFnZUZvcm1hdFwiOntcInhcIjowLFwieVwiOjAsXCJ3aWR0aFwiOjExNjksXCJoZWlnaHRcIjoxNjU0fSxcInNlYXJjaFwiOnRydWUsXCJzaG93U3RhcnRTY3JlZW5cIjp0cnVlLFwiZ3JpZENvbG9yXCI6XCIjZDBkMGQwXCIsXCJkYXJrR3JpZENvbG9yXCI6XCIjNmU2ZTZlXCIsXCJhdXRvc2F2ZVwiOnRydWUsXCJyZXNpemVJbWFnZXNcIjpudWxsLFwib3BlbkNvdW50ZXJcIjowLFwidmVyc2lvblwiOjE4LFwidW5pdFwiOjEsXCJpc1J1bGVyT25cIjpmYWxzZSxcInVpXCI6XCJcIn0ifQ==",

	 "hediet.vscode-drawio.theme": "Kennedy",

	 "editor.fontFamily": "Source Code Pro, 'Courier New', monospace",

	 "editor.smoothScrolling": true,

	 "editor.formatOnSave": true,

	 "editor.defaultFormatter": "esbenp.prettier-vscode",

	 "workbench.colorTheme": "Atom One Dark",

	 "vetur.completion.autoImport": false,

	 "security.workspace.trust.untrustedFiles": "open",

	 "eslint.lintTask.enable": true,

	 "eslint.alwaysShowStatus": true,

	 "editor.codeActionsOnSave": {		  
		"source.fixAll.eslint": true
	}

}

问题记录

1. 修改项目使用yarn或npm包管理

vue-cli脚手架安装完项目之后,会在用户家目录下创建一个.vuerc的文件,里面记录使用脚手架创建项目时用户的操作选项,修改packageManager选项就行。

image.png

2. 为什么文件基本使用module.exports导出

因为项目是需要运行在node环境的,而node是支持commonJS规范的,所以需要module.exports导出

3. 强制采用某种方式关闭eslint提示

如果在代码中有eslint爆红提示,但是我们仍然想采用爆红的代码,此时可以在鼠标eslint提示处,将eslint提示的括号中属性X复制,然后在eslintrc.js配置文件的rules中,配置X:off将其关闭

比如eslint爆红处提示属性X:

image.png

4. 设置content-type但是请求带不上

因为axios默认是不带conten-type的,所以就算手动给headers设置了content-type也是带不上的;需要在烂机器中设置config.data = {unused: 0}才可以让headers的content-type生效。

    // 拦截器
    this.instance.interceptors.request.use(
      (config) => {
        config.data = {unused: 0}  // 这里设置
        const token: string|null = getToken()
        if(!!token){
          config.headers= {"auth-token": token, 'Content-Type': "application/json;charset=UTF-8"} // 这里的content-type才会生效
        }
        return config;
      },
      (error) => {
        console.log(error);
      }
    );

5. less文件import报错

Missing semi-colon or unrecognised media features on import 报错

//在eslint的rules中将其关闭
"vue/multi-word-component-names": "off"

解决:import默认加分号就行



6. 格式化报错

Vue-cli Component name "xxx" should always be multi-word.

在eslintrc的rules中配置关闭

7. 如果登录成功后直接刷浏览器导致vuex数据失效

可以把local中的数据再加载到vuex中。

// store/login/login.ts的action中
loadLocalLogin({commit}){
    const token = getToken()
    if(token){
        commit("changeToken", token)
    }
    const userInfo = getUserInfo()
    if(userInfo){
        commit("changeUserInfo", userInfo)
    }
}


// store/index.ts
export function setStore(){
    // login模块的loadLocalLogin (action)
    store.dispatch("login/loadLocalLogin")
}

// main.ts
import {setStore} from './store'

const app = createApp(App);
app.use(store);
app.use(router);

setStore() // 在这里调用


app.mount("#app");

8. 登录成功后直接跳/main

// router/index.ts 增加导航守卫跳转拦截

去往main的如果登录直接去登录,没有登录直接跳/login

配置多语言、gizp压缩等、开发生产隔离