前端框架:vite2+vue3+typescript+axios+vant移动端 框架实战项目详解(二)

2,352 阅读7分钟

🔨前端架构项目相关依赖安装

☞本系列文章过长会分开消化理解:

1、引入router和vuex

😈ps:注意

vue3.x支持router和vuex必须是4.0及以上版本 ,否则用vue3时,找不到暴露的api

✔第一步:安装

next.router.vuejs.org/

next.vuex.vuejs.org/

next.vuex.vuejs.org/zh/guide/ty…

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 模块,我们也可以全局引入,不差钱

image.png

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>

image.png

☞另外, 引入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"
}

image.png

✔在组件中使用axios

import http from '../utils/server'
try {
     let data = await http.get({url:"/getUsers"})
     console.log(data,'data')
   } catch (error) {
     console.log(error,'error')
   }

image.png