环境准备
备注:需要node环境已安装就绪
安装vue脚手架:
npm install -g @vue/cli
// 或者
yarn global add @vue/cli
vue create 项目名
安装过程,pwa和test测试之类的不要选其他一路回车
工具安装
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"
备注:配置完记得重启项目
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
这里会做三件事:
- 安装husky相关的依赖
可以在pre-commit文件中配置commit之前的操作。
- 在项目目录下创建.husky文件夹
- 在package.json增加脚本
具体使用:比如可以写不规范的代码连续换行等,然后在pre-commit配置文件中增加命令yarn run lint,接着commit提交代码,会自动格式化代码。
为什么是yarn run lint命令?因为script脚本中的lint命令就是格式化。
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帮我们配置
上面的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文件
第三方库集成
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
}
}
]
};
动态路由映射逻辑:
// 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
// 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图标库集成
要使用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选项就行。
2. 为什么文件基本使用module.exports导出
因为项目是需要运行在node环境的,而node是支持commonJS规范的,所以需要module.exports导出
3. 强制采用某种方式关闭eslint提示
如果在代码中有eslint爆红提示,但是我们仍然想采用爆红的代码,此时可以在鼠标eslint提示处,将eslint提示的括号中属性X复制,然后在eslintrc.js配置文件的rules中,配置X:off将其关闭
比如eslint爆红处提示属性X:
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压缩等、开发生产隔离