vue3+vite+TypeScript+Element plus+pinia搭建开发脚手架

苏鸿波
2023-12-01

初化化项目

pnpm create vite

2. 引入eslint和prettier,约束代码风格

安装 eslint

pnpm i -D eslint eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin

这四个依赖的作用分别是:

  • eslint: EsLint的核心代码

  • @typescript-eslint/parser:ESLint的解析器,用于解析typescript,从而检查和规范Typescript代码

  • @typescript-eslint/eslint-plugin:这是一个ESLint插件,包含了各类定义好的检测Typescript代码的规项目下新建 .eslintrc.js,配置 eslint 校验规则:

module.exports = {
  extends: ["plugin:vue/vue3-essential"],
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: "module",
  },
  plugins: ["vue", "prettier"],
  rules: {
    "@typescript-eslint/ban-ts-ignore": "off",
    "@typescript-eslint/no-unused-vars": "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",
    "@typescript-eslint/no-use-before-define": "off",
    "@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",
    "no-var": "error",
    "prettier/prettier": "error",
    // 禁止出现console
    "no-console": "warn",
    // 禁用debugger
    "no-debugger": "warn",
    // 禁止出现重复的 case 标签
    "no-duplicate-case": "warn",
    // 禁止出现空语句块
    "no-empty": "warn",
    // 禁止不必要的括号
    "no-extra-parens": "off",
    // 禁止对 function 声明重新赋值
    "no-func-assign": "warn",
    // 禁止在 return、throw、continue 和 break 语句之后出现不可达代码
    "no-unreachable": "warn",
    // 强制所有控制语句使用一致的括号风格
    curly: "warn",
    // 要求 switch 语句中有 default 分支 
    "default-case": "warn",
    // 强制尽可能地使用点号
    "dot-notation": "warn",
    // 要求使用 === 和 !==
    eqeqeq: "warn",
    // 禁止 if 语句中 return 语句之后有 else 块
    "no-else-return": "warn",
    // 禁止出现空函数
    "no-empty-function": "warn",
    // 禁用不必要的嵌套块
    "no-lone-blocks": "warn",
    // 禁止使用多个空格
    "no-multi-spaces": "warn",
    // 禁止多次声明同一变量
    "no-redeclare": "warn",
    // 禁止在 return 语句中使用赋值语句
    "no-return-assign": "warn",
    // 禁用不必要的 return await
    "no-return-await": "warn",
    // 禁止自我赋值
    "no-self-assign": "warn",
    // 禁止自身比较
    "no-self-compare": "warn",
    // 禁止不必要的 catch 子句
    "no-useless-catch": "warn",
    // 禁止多余的 return 语句
    "no-useless-return": "warn",
    // 禁止变量声明与外层作用域的变量同名
    "no-shadow": "off",
    // 允许delete变量
    "no-delete-var": "off",
    // 强制数组方括号中使用一致的空格
    "array-bracket-spacing": "warn",
    // 强制在代码块中使用一致的大括号风格
    "brace-style": "warn",
    // 强制使用骆驼拼写法命名约定
    camelcase: "warn",
    // 强制使用一致的缩进
    indent: "off",
    // 强制在 JSX 属性中一致地使用双引号或单引号
    // 'jsx-quotes': 'warn',
    // 强制可嵌套的块的最大深度4
    "max-depth": "warn",
    // 强制最大行数 300
    // "max-lines": ["warn", { "max": 1200 }],
    // 强制函数最大代码行数 50
    // 'max-lines-per-function': ['warn', { max: 70 }],
    // 强制函数块最多允许的的语句数量20
    "max-statements": ["warn", 100],
    // 强制回调函数最大嵌套深度
    "max-nested-callbacks": ["warn", 3],
    // 强制函数定义中最多允许的参数数量
    "max-params": ["warn", 3],
    // 强制每一行中所允许的最大语句数量
    "max-statements-per-line": ["warn", { max: 1 }],
    // 要求方法链中每个调用都有一个换行符
    "newline-per-chained-call": ["warn", { ignoreChainWithDepth: 3 }],
    // 禁止 if 作为唯一的语句出现在 else 语句中
    "no-lonely-if": "warn",
    // 禁止空格和 tab 的混合缩进
    "no-mixed-spaces-and-tabs": "warn",
    // 禁止出现多行空行
    "no-multiple-empty-lines": "warn",

    // 强制在块之前使用一致的空格
    "space-before-blocks": "warn",
    // 强制在 function的左括号之前使用一致的空格
    // 'space-before-function-paren': ['warn', 'never'],
    // 强制在圆括号内使用一致的空格
    "space-in-parens": "warn",
    // 要求操作符周围有空格
    "space-infix-ops": "warn",
    // 强制在一元操作符前后使用一致的空格
    "space-unary-ops": "warn",
    // 强制在注释中 // 或 /* 使用一致的空格
    // "spaced-comment": "warn",
    // 强制在 switch 的冒号左右有空格
    "switch-colon-spacing": "warn",
    // 强制箭头函数的箭头前后使用一致的空格
    "arrow-spacing": "warn",
    "no-var": "warn",
    "prefer-const": "warn",
    "prefer-rest-params": "warn",
    "no-useless-escape": "warn",
    "no-irregular-whitespace": "warn",
    "no-prototype-builtins": "warn",
    "no-fallthrough": "warn",
    "no-extra-boolean-cast": "warn",
    "no-case-declarations": "warn",
    "no-async-promise-executor": "warn",
  },
  overrides: [
    {
      files: ["*.vue"],
      rules: {
        // 这里写覆盖vue文件的规则
        "no-unused-vars": [0],
      },
    },
  ],
};

3. 安装prettier

pnpm i --save-dev prettier eslint-config-prettier eslint-plugin-prettier

这三个依赖分别是:

  • prettier:prettier插件的核心代码

  • eslint-config-prettier:解决ESLint中的样式规范和prettier中样式规范的冲突,以prettier的样式规范为准,使ESLint中的样式规范自动失效

  • eslint-plugin-prettier:将prettier作为ESLint规范来使用

项目下新建 .prettier.js,配置 prettier 规则:

module.exports = {
  printWidth: 120, // 换行字符串阈值
  tabWidth: 2, // 设置工具每一个水平缩进的空格数
  useTabs: false,
  semi: false, // 句末是否加分号
  vueIndentScriptAndStyle: true,
  trailingComma: 'none', // 最后一个对象元素加逗号
  bracketSpacing: true, // 对象,数组加空格
  jsxBracketSameLine: true, // jsx > 是否另起一行
  arrowParens: 'always', // (x) => {} 是否要有小括号
  requirePragma: false, // 不需要写文件开头的 @prettier
  insertPragma: false // 不需要自动在文件开头插入 @prettier
}

在 package.json 文件中添加 checklint 和 prettier 命令

在 package.json 文件中添加 checklint 和 prettier 命令

上面配置完成后,可以运行以下命令测试下代码检查 并执行格式化效果:

上面配置完成后,可以运行以下命令测试下代码检查 并执行格式化效果:

4.引入 Element Plus

安装 element-plus

pnpm install element-plus --save

修改入口文件 mian.ts :

import { createApp } from 'vue'
import App from './App.vue';
import ElementPlus from 'element-plus' 
import 'element-plus/dist/index.css'

const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')

以上代码便完成了Element Plus的引入。需要注意的是,样式文件需要单独引入。

5. 引入路由

安装 router4

pnpm add vue-router

在 src 文件下新增 router 文件夹,新建index.ts,内容如下:

import { createRouter, createWebHistory, RouteRecordRaw } from'vue-router'constroutes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Login',
    component: () =>import('@/pages/login/Login.vue'), // 注意这里要带上 文件后缀.vue
  },
]

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

exportdefault router
复制代码

修改入口文件 mian.ts :

import { createApp } from'vue'importAppfrom'./App.vue'importElementPlusfrom'element-plus'import'element-plus/dist/index.css'import router from'./router/index'const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.mount('#app')
复制代码

6. 状态管理 pinia

安装 pinia

yarn add pinia@next复制代码

修改入口文件 mian.ts :

import { createApp } from'vue'importAppfrom'./App.vue'importElementPlusfrom'element-plus'import'element-plus/dist/index.css'import router from'./router/index'import { createPinia } from"pinia"const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.use(createPinia())
app.mount('#app')
复制代码

在 src 文件夹下新增 store 文件夹,接在在 store 中新增 main.ts,文件中定义:

import { defineStore } from'pinia'//`id` 是必要的,它将所使用 store 连接到 devtools。exportconst useMainStore = defineStore({
 id: 'mian',
 
 state: () => ({ name: '用户名' }), 
 
 getters: { name: (state) => state.name, }, 
 
 actions: {
     //同步actionupdataUser(data: any) {
        this.name = data; 
     } 
      //异步actionasync updataUser2(data: any) {
      const { data } = await api.login(account, pwd)
      return data
    }
 }
})
复制代码

组件中使用:

<template><div>{{mainStore.name}}</div><button @click = 'handClick()'>{{'点击'}}</button></template><scriptsetuplang="ts">import { useMainStore } from"@/store/mian"//组件中获取storeconst mainStore = useMainStore()

//组件中通过getter获取store内容nameconst name = mainStore.name//可通过getter直接修改store中的name,推荐使用actions进行修改constupdateName = ()=>{
  // 使用$patch 修改 store 中的数据
  mainStore.$patch({
    name: '修改的名称'
  })
}

//可通过actions直接修改store中的name(同步)consthandClick=()=>{mainStore.name = '修改的名称'}


</script>复制代码

数据持久化

安装pinia-plugin-persist可以辅助实现数据持久化功能(当页面刷新的时候存储在pinia的内容会消失)。

npm i pinia-plugin-persist --save复制代码

在main.ts文件中定义定义pinia-plugin-persist使用

import { defineStore } from'pinia'import piniaPluginPersist from 'pinia-plugin-persist'const useMainStore = defineStore({
 id: 'mian',
 
 state: () => ({ name: '用户名' }), 
 
 getters: { name: (state) => state.name, }, 
 
 actions: {
     //同步actionupdataUser(data: any) {
        this.name = data; 
     } 
      //异步actionasync updataUser2(data: any) {
      const { data } = await api.login(account, pwd)
      return data\
    }
 }
  
 persist: {
   // 开启数据缓存,必须开启
   enabled: true,
   //数据默认存在 sessionStorage 里,并且会以 store 的 id 作为 key。
   strategies: [
     {
       //修改key值为my_user,选择开启。key: 'my_user',
       //将存放位置由 sessionStorage 改为 localStorage,选择开启。
       storage: localStorage,
       //默认所有 state 都会进行缓存,你可以通过 paths 指定要持久化的字段,其他的则不会进行持久化。
       paths: ['name']
     }
   ]
 }

})

useMainStore.use(useMainStore)
export default useMainStore
复制代码

7. 配置 css 预处理器 scss

安装scss

    npm install -D sass-loader node-sass
复制代码

配置全局 scss 样式文件

在 src/assets 下新增 style 文件夹,用于存放全局样式文件,新建 index.scss,

html {
    box-sizing: border-box; 
} 
复制代码

修改入口文件 mian.ts :

import { createApp } from'vue'importAppfrom'./App.vue'importElementPlusfrom'element-plus'import'element-plus/dist/index.css'import router from'./router/index'import { createPinia } from"pinia"import'./styles/index.scss'const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.use(createPinia())
app.mount('#app')
复制代码

8.统一请求封装

安装 axios

yarn add axios
复制代码

新增 utils 文件夹,utils 下新增 http.js 文件以及 errorCode.ts 文件和 common.ts 文件和loading.ts文件:

http.ts : 用于axios封装

import axios from'axios'import errorCode from'@/utils/errorCode'import { ElMessage, ElMessageBox } from'element-plus'import { getToken } from'@/utils/auth'import { tansParams } from"@/utils/common";
import { showLoading, closeLoading } from"@/utils/loading";


axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'// 创建axios实例const service = axios.create({

  // axios中请求配置有baseURL选项,表示请求URL公共部分baseURL: process.env.VUE_APP_BASE_API,

  // 超时timeout: 10000

})
//开启loadingshowLoading()
// request拦截器
service.interceptors.request.use(config => {
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  // get请求映射params参数if (config.method === 'get' && config.params) {
    let url = config.url + '?' + tansParams(config.params);
    url = url.slice(0, -1);
    config.params = {};
    config.url = url;
  }
  if (config.method === 'post' && config.params) {
    const requestObj = {
      url: config.url,
      data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
    }
    const sessionObj = cache.session.getJSON('sessionObj')
    if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
      cache.session.setJSON('sessionObj', requestObj)
    }
  }
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})


// 响应拦截器
service.interceptors.response.use(res => {
    //关闭loading,此处采用延时处理是合并loading请求效果,避免多次请求loading关闭又开启setTimeout(() => {
        closeLoading()
    }, 200)
    // 未设置状态码则默认成功状态const code = res.data.code || 200;
    // 获取错误信息const msg = res.data.msg || errorCode[code] || errorCode['default']
    // 二进制数据则直接返回if(res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer'){
      return res.data
    }
    // 判断状态码if(code === 200){
      return res.data     
    }elseif(code === 401){
    
      //跳转首页逻辑returnPromise.reject(msg)
    }else{
      ElMessage({
        message: msg,
        type: 'error'
      })
      returnPromise.reject(msg)
    }
  },
  error => {
    setTimeout(() => {
        closeLoading()
    }, 200)
    
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    }
    elseif (message.includes("timeout")) {
      message = "系统接口请求超时";
    }
    elseif (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    ElMessage({
      message: message,
      type: 'error',
      duration: 5 * 1000
    })
    returnPromise.reject(error)
  }
)
exportdefault service
复制代码

errorCode.ts : 用于定义返回错误code

exportdefault {

  '400': '请求头错误',
  
  '401': '认证失败,无法访问系统资源, 请重新登录',

  '403': '当前操作没有权限',

  '404': '访问资源不存在',

  '500': '服务器端出错,

  '503': '服务不可用',
  
  'default': '系统未知错误,请反馈给管理员'

}
复制代码

common.ts : 用于定义一些共通方法

/**
* 参数处理
*/exportfunctiontansParams(params) {
  let result = ''for (const propName ofObject.keys(params)) {
    const value = params[propName];
    var part = encodeURIComponent(propName) + "=";
    if (value !== null && typeof (value) !== "undefined") {
      if (typeof value === 'object') {
        for (const key ofObject.keys(value)) {
          if (value[key] !== null && typeof (value[key]) !== 'undefined') {
            let params = propName + '[' + key + ']';
            var subPart = encodeURIComponent(params) + "=";
            result += subPart + encodeURIComponent(value[key]) + "&";
          }
        }
      } else {
        result += part + encodeURIComponent(value) + "&";
      }
    }
  }
  return result
}
复制代码

loading.ts : 用于定义接口loading,不用每个页面单独去v-loading

/**
 * 全局loading效果:合并多次loading请求,避免重复请求
 * 当调用一次showLoading,则次数+1;当次数为0时,则显示loading
 * 当调用一次closeLoading,则次数-1; 当次数为0时,则结束loading
 */
import { ElLoading } from 'element-plus';
 
// 定义一个请求次数的变量,用来记录当前页面总共请求的次数
let loadingRequestCount = 0;
// 初始化loading
let loadingInstance;
 
// 编写一个显示loading的函数 并且记录请求次数 ++
export showLoading = (target) => {
    if (loadingRequestCount === 0) {
        loadingInstance = ElLoading.service({ 
            lock: true, 
            text: '加载中……', 
            background: 'rgba(0, 0, 0, 0.7)'
        });
    }
    loadingRequestCount++
}
 
// 编写一个隐藏loading的函数,并且记录请求次数 --
export closeLoading = () => {
    if (loadingRequestCount <= 0) return
    loadingRequestCount--
    if (loadingRequestCount === 0) {
        loadingInstance.close();
    }
 }

 类似资料: