🔨前端架构项目相关依赖安装
☞本系列文章过长会分开消化理解:
- 前端框架:vite2+vue3+typescript+axios+vant移动端 框架实战项目详解(一)
- 前端框架:vite2+vue3+typescript+axios+vant移动端 框架实战项目详解(二)
- 前端框架:vite2+vue3+typescript+axios+vant移动端 框架实战项目详解(三)
- 前端框架:vite2+vue3+typescript+axios+vant移动端 框架实战项目详解(四)
1、引入router和vuex
😈ps:注意
vue3.x支持router和vuex必须是4.0及以上版本
,否则用vue3时,找不到暴露的api
✔第一步:安装
npm i vue-router@next vuex@next -S
✔第二步:在src
下新建router
目录,新建index.ts
文件
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import HelloWorld from 'comps/HelloWorld.vue'
const routes: Array<RouteRecordRaw> = [
{path:'/',name:'home',component:HelloWorld}
]
// 使用工厂函数创建router
const router = createRouter({
history: createWebHashHistory(), // 指定路由的模式,此处使用的是hash模式
routes, // 路由地址
});
export default router;
我们可以看到与之前不同的是: 使用工厂函数创建router
☞ps:如果想使用h5路由
import { createRouter, createWebHistory , RouteRecordRaw } from "vue-router";
import HelloWorld from 'comps/HelloWorld.vue'
const routes: Array<RouteRecordRaw> = [
{path:'/',name:'home',component:HelloWorld}
]
const router = createRouter({
history: createWebHistory (),
routes
})
export default router
✔第三步:在src
下新建store
目录,新建index.ts
文件
我们使用官网的例子,看看能不能正常运行:使用useStore
// store/index.ts
import { InjectionKey } from 'vue'
import { createStore, useStore as baseUseStore, Store } from 'vuex'
export interface State {
count: number
}
export const key: InjectionKey<Store<State>> = Symbol()
export const store = createStore<State>({
state: {
count: 0
}
})
// 定义自己的 `useStore` 组合式函数
export function useStore () {
return baseUseStore(key)
}
简单更改一下:src/store/index.ts
// store/index.ts
import { InjectionKey } from 'vue'
import { createStore, useStore as baseUseStore, Store } from 'vuex'
export interface State {
count: number
}
export const key: InjectionKey<Store<State>> = Symbol()
export const store = createStore<State>({
state: {
count: 0
},
mutations:{
setCount(state:State,count:number){
state.count = count
}
},
getters:{
getCount(state:State){
return state.count
}
}
})
// 定义自己的 `useStore` 组合式函数
export function useStore () {
return baseUseStore(key)
}
src\components\Helloword.vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useStore } from '../store';
const store = useStore()
const count = ref(0)
const showCount = computed(()=>{
return store.getters['getCount']
})
const addBtn = ()=>{
store.commit('setCount',++count.value)
}
</script>
<template>
<h1>{{showCount}}</h1>
<button type="button" @click="addBtn">加1</button>
</template>
<style scoped>
</style>
✔第四步:在main.ts
引入,如下:
import { createApp } from 'vue'
import App from './App.vue'
import router from "./router";
import {store,key} from "./store";
createApp(App)
.use(router)
.use(store,key)
.mount('#app')
ps:此时我们打开页面访问,发现页面是空白的,我还需要一点点修改
App.vue
<script setup lang="ts">
</script>
<template>
<router-view />
</template>
<style>
</style>
2、引入ui库
2.1 引入vant
✔因为移动端,可以使用的ui库呢可以选择vant,为了兼容vue3,安装最新的版本(Vant 3.X)
npm i vant@next -S
✔使用 [vite-plugin-style-import]实现按需引入
需要先安装 vite-plugin-style-import 模块
npm install vite-plugin-style-import -S
✔在vite.config.ts
中引入
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {resolve} from "path"
import styleImport from 'vite-plugin-style-import';
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
"@": resolve(__dirname,"src"),
"comps": resolve(__dirname,"src/components"),
"apis": resolve(__dirname,"src/apis"),
"views": resolve(__dirname,"src/views"),
"utils": resolve(__dirname,"src/utils"),
"routes": resolve(__dirname,"src/routes"),
"styles": resolve(__dirname,"src/styles"),
},
},
plugins: [vue(),
styleImport({
libs: [
{
libraryName: "vant",
esModule: true,
resolveStyle: (name) => `vant/es/${name}/style`,
},
],
}),
],
server: {
host: "0.0.0.0" // 解决 Network: use --host to expose
},
})
当然如果不想安装 vite-plugin-style-import 模块,我们也可以全局引入,不差钱
import { createApp } from 'vue'
import App from './App.vue'
import router from "./router";
import store from "./store";
import 'vant/lib/index.css'; // 全局引入样式
createApp(App)
.use(router)
.use(store)
.use(ant)
.mount('#app')
🍕示例:按需加载相关vant3的组件
✔1、在src创建plugins文件夹,创建vant3.ts
import { App } from '@vue/runtime-dom';
// 导入所有组件(不推荐)
// import Vant from 'vant';
// import 'vant/lib/index.css';
// 按需加载
import { Button } from 'vant';
export default function(app:App<Element>){
// 完整
// app.use(Vant)
// 按需引入
app.use(Button)
}
✔2、在main.ts中引入
import { createApp } from 'vue'
import "utils/rem"
import "styles/index.scss"
import App from './App.vue'
import router from "./router";
import {store,key} from "./store";
import vant3 from './plugins/vant3'
createApp(App)
.use(router)
.use(store,key)
.use(vant3)
.mount('#app')
✔3、在组件中使用
<van-button type="success">主要按钮</van-button>
☞另外, 引入element-plus
我们可能开发的时一些后台管理项目,那可能需要使用element ui库,那就需要使用兼容vue3的element-plus
库
安装element-plus
:
npm i element-plus -S
是按需引入还是全局全部引入,看个人需求,具体引入方法请移步到element-plus官网里面介绍的很详细
3、移动端适配
✔第一步:安装postcss-pxtorem
npm install postcss-pxtorem -D
✔第二步:在根目录下创建postcss.config.js
module.exports = {
"plugins": {
"postcss-pxtorem": {
rootValue: 37.5, // 数字|函数)表示根元素字体大小或根据input参数返回根元素字体大小
propList: ['*'], // 使用通配符*启用所有属性
mediaQuery: true, // 允许在媒体查询中转换px
selectorBlackList: ['.norem'] // 过滤掉.norem-开头的class,不进行rem转换
}
}
}
✔在根目录src中新建utils目录下新建rem.ts等比适配文件
// rem等比适配配置文件
// 基准大小
const baseSize = 37.5;
// 注意此值要与 postcss.config.js 文件中的 rootValue保持一致
// 设置 rem 函数
function setRem() {
// 当前页面宽度相对于 375宽的缩放比例,可根据自己需要修改,一般设计稿都是宽750(图方便可以拿到设计图后改过来)。
const scale = document.documentElement.clientWidth / 375;
// 设置页面根节点字体大小(“Math.min(scale, 2)” 指最高放大比例为2,可根据实际业务需求调整)
document.documentElement.style.fontSize =
baseSize * Math.min(scale, 2) + "px";
}
// 初始化
setRem();
// 改变窗口大小时重新设置 rem
window.onresize = function () {
console.log("我执行了");
setRem();
};
✔在main.ts引入
import { createApp } from 'vue'
import App from './App.vue'
import "./utils/rem"
import router from "./router";
import store from "./store";
createApp(App)
.use(router)
.use(store)
.mount('#app')
PostCSS
vite自动对
*.vue
文件和导入的.css
文件应用PostCSS配置,我们只需要安装必要的插件和添加postcss.config.js
文件即可。
✔安装autoprefixer
npm i postcss autoprefixer -D
module.exports = {
"plugins": {
"autoprefixer": {
// overrideBrowserslist: ["Android 4.1", "iOS 7.1"], //浏览器的兼容配置
// grid: true, // true 为 IE 启用网格布局前缀
},
"postcss-pxtorem": {
rootValue: 37.5, // 数字|函数)表示根元素字体大小或根据input参数返回根元素字体大小
propList: ['*'], // 使用通配符*启用所有属性
mediaQuery: true, // 允许在媒体查询中转换px
selectorBlackList: ['.norem'] // 过滤掉.norem-开头的class,不进行rem转换
}
}
}
4、安装scss
✔安装css预处理器sass
npm i sass sass-loader -D
5、安装axios
我们使用axios来调用网络请求
✔安装
npm i -s axios @types/qs
简单的axios封装
统一封装数据请求服务,有利于解决:统一配置请求、请求、响应统一处理
在根目录创建.env.development
VITE_BASE_API=/api
✔在src\utils文件夹创建server,并在server下创建type.ts
import type {AxiosRequestConfig} from "axios";
// 默认的对象类型
export interface IdefaultObject {
[key: string]: any;
}
//拦截器
export interface MYRequestInterceptors {
requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig;
requestInterceptorCatch?: (error: any) => any;
responseInterceptor?: (res: any) => any;
rrequestInterceptorCatch?: (error: any) => any;
}
export interface MDJRequestConfig extends AxiosRequestConfig {
interceptors?: MYRequestInterceptors;
showLoading?: boolean;
}
✔在server下创建request.ts
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { IdefaultObject } from "./type";
import { whichType } from "../common";
const http = axios.create({
baseURL: (import.meta.env.VITE_BASE_API as string | undefined) || "/api",
timeout: 10000, // 超时时间 单位是ms,这里设置了10s的超时时间
});
export const defaultErrInfo:IdefaultObject = {status:-1,error_message:'接口请求失败',code:1 }
// 请求拦截
http.interceptors.request.use(
(conflg: AxiosRequestConfig) => {
if (whichType(conflg) === "object") {
// 如果有token 就携带tokon
const token = window.localStorage.getItem("accessToken");
if (token && conflg.headers) {
// 自定义令牌的字段名X-Token
conflg.headers["X-Token"] = token;
}
return conflg;
} else {
return defaultErrInfo;
}
},
err => {
console.log(err);
return err;
}
);
// 响应拦截
http.interceptors.response.use(
(conflg: AxiosResponse<any>) => {
if (whichType(conflg) === "object") {
return conflg;
} else {
return defaultErrInfo;
}
},
(err) => {
if (err && err.response) {
switch (err.response.status) {
case 400:
console.log("客户端请求的语法错误,服务器无法理解");
break;
case 401:
console.log("身份验证出错");
break;
case 403:
console.log("服务器理解请求客户端的请求,但是拒绝执行此请求");
break;
case 404:
console.log(`请求地址出错:${err.response.config.url}`);
break;
case 405:
console.log("请求方式被禁止");
break;
case 408:
console.log("请求超时");
break;
case 500:
console.log("服务器内部错误,无法完成请求");
break;
case 501:
console.log(" 服务器不支持请求的功能,无法完成请求");
break;
case 502:
console.log(
"作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应"
);
break;
case 503:
console.log(
"由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中"
);
break;
case 504:
console.log("充当网关或代理的服务器,未及时从远端服务器获取请求");
break;
case 505:
console.log("服务器不支持请求的HTTP协议的版本");
break;
default:
console.log(`请求出错:${err.message}`);
}
return err.response;
} else {
console.log("服务器连接失败");
return defaultErrInfo;
}
}
);
export default http;
其中whichType方法
✔在utils文件下common.ts
/**
* 是否是对象
* @param obj 传递的参数
* @returns boolean true/false
*/
export function isObject(obj: any): boolean {
return obj && typeof obj === "object";
}
// 类型字典
export let whichType = (data: any) => {
let dist: IdefaultObject = {
"[object Array]": "array",
"[object Object]": "object",
"[object Number]": "number",
"[object Function]": "function",
"[object String]": "string",
"[object Null]": "null",
"[object Undefined]": "undefined",
};
return dist[Object.prototype.toString.call(data)];
};
✔在server下创建index.ts
import request from "./request";
import qs from 'qs'
import { MYRequestConfig,IdefaultObject } from "./type";
// json格式请求头
export const headerJSON = {
"Content-Type": "application/json",
};
// FormData格式请求头
export const headerFormUrlencodedData = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
};
export const headerFormData = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
};
export const headerFileFormData = {
"Content-Type": "multipart/form-data",
};
interface MYRequestParams{
url:string
params?:any
json?:string
customHeader?:IdefaultObject
data?:any
[key:string]:any
}
const http = {
/**
* methods: 请求
* @param url 请求地址
* @param params 请求参数
* @param json 判断数据发送是否是json格式
*/
get(data:MYRequestParams) {
const config:MYRequestConfig = {
method: "get",
url: data.url,
headers: data.customHeader ? data.customHeader : (data.json ? headerJSON : headerFormData)
};
if (data.params) config.params = data.params;
if(data.data) config.data = data.data;
return request(config);
},
post(data:MYRequestParams) {
const config:MYRequestConfig = {
method: "post",
url: data.url,
headers: data.customHeader ? data.customHeader : (data.json ? headerJSON : headerFormData)
};
if (data.params) config.params = data.params;
if(data.data) config.data = (data.json || data.customHeader) ? data.data: qs.stringify(data.data)
console.log('postpost',config,data);
return request(config);
},
put(data:MYRequestParams) {
const config:MYRequestConfig = {
method: "put",
url: data.url,
headers: data.customHeader ? data.customHeader : (data.json ? headerJSON : headerFormData)
};
if (data.params) config.params = data.params;
if(data.data) config.data = data.data;
return request(config);
},
delete(data:MYRequestParams) {
const config:MYRequestConfig = {
method: "delete",
url: data.url,
headers: data.customHeader ? data.customHeader : (data.json ? headerJSON : headerFormData)
};
if (data.params) config.params = data.params;
if(data.data) config.data = data.data;
return request(config);
},
};
//导出
export default http;
配置请求代理
在vite.config.ts
中server中增加proxy
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
import styleImport from "vite-plugin-style-import";
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
"@": resolve(__dirname, "src"),
comps: resolve(__dirname, "src/components"),
apis: resolve(__dirname, "src/apis"),
views: resolve(__dirname, "src/views"),
utils: resolve(__dirname, "src/utils"),
routes: resolve(__dirname, "src/routes"),
styles: resolve(__dirname, "src/styles"),
},
},
plugins: [
vue(),
styleImport({
libs: [
{
libraryName: "vant",
esModule: true,
resolveStyle: (name) => `vant/es/${name}/style`,
},
],
}),
],
server: {
host: "0.0.0.0", // 解决 Network: use --host to expose
port: 4000, //启动端口
open: true,
proxy: {
'^/api': {
target: 'https://baidu.com',
changeOrigin: true,
ws: true,
rewrite: (pathStr) => pathStr.replace(/^/open/, '')
},
},
cors: true,
},
});
6、eslint规范
借助
eslint
规范项目代码,通过prettier
做代码格式化
✔安装eslint
npm install eslint eslint-plugin-prettier prettier eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin -D
- ESLint: 是一种用于识别和报告在 ECMAScript/JavaScript 代码中发现的模式的工具。具体可查看官方 文档
- Prettier: 是一款强大的代码格式化工具,支持JavaScript、Typescript、Css、Scss、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown等,基本上前端能用到的文件格式都可以搞定,是当下最流行的格式化工具。具体可查看官方 文档
- eslint-plugin-vue: Vue.js 的官方 ESLint 插件。具体可查看官方 文档 这个插件可以让我们检查 template 和 script 的 .vue 与 ESLint 文件,以及 Vue 公司的代码 .js 文件。
- eslint-plugin-prettier: 根据 eslint 配置检查代码错误。具体可查看官方 文档
- eslint-config-prettier: 关闭所有不必要的或可能与 [Prettier] 冲突的规则。 这使您可以使用自己喜欢的可共享配置,而不会在使用 Prettier 时妨碍其风格选择。 请注意,此配置仅关闭规则,因此只有将其与其他配置一起使用才有意义。具体可查看官方 文档
- @typescript-eslint/parser @typescript-eslint/eslint-plugin: 这两个依赖使得 eslint 支持 typescript
✔然后配置lint
规则,.eslintrc.js
module.exports = {
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',
// 'prettier/@typescript-eslint', // eslint-config-prettier依赖超过 8.0.0 之后版本不需要配置这条
'plugin:prettier/recommended',
],
rules: {
'@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',
// 'no-use-before-define': [
// 'error',
// {
// functions: false,
// classes: true,
// },
// ],
'@typescript-eslint/no-use-before-define': 'off',
// '@typescript-eslint/no-use-before-define': [
// 'error',
// {
// functions: false,
// classes: true,
// },
// ],
'@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: '^h$',
varsIgnorePattern: '^h$',
},
],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^h$',
varsIgnorePattern: '^h$',
},
],
'space-before-function-paren': 'off',
quotes: ['error', 'single'],
'comma-dangle': ['error', 'always-multiline'],
'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/script-setup-uses-vars': 'off',
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'never',
component: 'always',
},
svg: 'always',
math: 'always',
},
],
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
};
✔如果有必要还可以配置prettier.config.js
修改prettier
的默认格式化规则
module.exports = {
printWidth: 80, // 每行代码长度(默认80)
tabWidth: 2, // 每个tab相当于多少个空格(默认2)
useTabs: false, // 是否使用tab进行缩进(默认false)
singleQuote: false, // 使用单引号(默认false)
semi: true, // 声明结尾使用分号(默认true)
trailingComma: 'es5', // 多行使用拖尾逗号(默认none)
bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true)
jsxBracketSameLine: false, // 多行JSX中的>放置在最后一行的结尾,而不是另起一行(默认false)
arrowParens: "avoid", // 只有一个参数的箭头函数的参数是否带圆括号(默认avoid)
// htmlWhitespaceSensitivity: 'ignore',
htmlWhitespaceSensitivity: 'strict',
endOfLine: 'auto',
// 解决 ESLint: Parsing error: Unexpected token(prettier/prettier)
overrides: [{
files: '*.html',
options: {
parser: 'html'
},
},
{
files: '*.vue',
options: {
parser: 'vue'
},
},
]
};
7、数据mock
✔安装依赖
vite-plugin-mock 最新版是兼容vite2的
npm i mockjs vite-plugin-mock cross-env -D
✔引入插件,vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
import styleImport from "vite-plugin-style-import";
import { viteMockServe } from "vite-plugin-mock";
export default defineConfig({
resolve: {
alias: {
"@": resolve(__dirname, "src"),
comps: resolve(__dirname, "src/components"),
apis: resolve(__dirname, "src/apis"),
views: resolve(__dirname, "src/views"),
utils: resolve(__dirname, "src/utils"),
routes: resolve(__dirname, "src/routes"),
styles: resolve(__dirname, "src/styles"),
},
},
plugins: [
vue(),
styleImport({
libs: [
{
libraryName: "vant",
esModule: true,
resolveStyle: name => `vant/es/${name}/style`,
},
],
}),
viteMockServe({
mockPath: "mock", // ↓解析根目录下的mock文件夹
supportTs: false,
}),
],
server: {
host: "0.0.0.0", // 解决 Network: use --host to expose
port: 4000, //启动端口
open: true,
// proxy: {
// "^/api": {
// target: "https://baidu.com",
// changeOrigin: true,
// ws: true,
// rewrite: pathStr => pathStr.replace(/^/api/, ""),
// },
// },
cors: true,
},
/**
* 在生产中服务时的基本公共路径。
* @default '/'
*/
base: "./",
});
✔设置环境变量,package.json
"scripts": {
"dev": "cross-env NODE_ENV=development vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
}
✔根目录下创建mock
文件,mock/test.ts
import { MockMethod } from 'vite-plugin-mock'
// 仅做示例: 通过GET\post请求返回一个数据
export default [{
url: "/api/getUsers",
method: "get",
response: () => {
console.log('/api/getUsers----------')
return {
code: 0,
message: "ok",
data: ["tom", "jerry"],
};
},
}] as MockMethod[];
✔在项目中使用此接口
fetch("/api/getUsers")
.then( response => {
return response.json()
})
.then(data => {
console.log('/api/getUsers',data)
})
会得到如下返回值:
{
code: 0,
data: {
0: "tom",
1: "jerry",
},
length: 2,
message: "ok"
}
✔在组件中使用axios
import http from '../utils/server'
try {
let data = await http.get({url:"/getUsers"})
console.log(data,'data')
} catch (error) {
console.log(error,'error')
}