vue3项目模板:新建一个vite+vue3项目并加入基础建设

550 阅读11分钟

1.使用npm creat vite@latest新建一个vue3项目

这里放个图,这个是每一步的选择: image.png解析版本: image.png 然后执行这些步骤: image.png

2.生成git仓库

由于这里生成的没有git仓库,所以我们git init来生成一个仓库。然后git add . ,git commit- m"" 来进行初次提交。 image.png 此时我们的项目状态为:vite+vue3+ts+vue-router+pinia+eslint+prettier。但是有一点,还不能在git commit的时候主动的去检测代码,只能通过npm run lint 进行代码检测,npm run format进行代码修复。 image.png 运行npm run lint之后,发现没有任何变化(eslint中没有添加很多的规则,导致通过了),但是在运行npm run format发现,发现确实帮我们做了格式化(根据.prettierrc.json文件中的内容格式化了)。 image.png 出现这个的原因是因为,通过这种方式创建的vue项目只是加入了eslint和prettier,并没有让两者联系起来,即没有把prettier加入到eslint中。

3.将prettier的规则加入到eslint中(可选操作,建议有)

此时我们需要安装(prettier已经安装,就不用重复安装了)

  • eslint-config-prettier - 关闭 ESLint 中与 Prettier 中发生冲突的规则
  • eslint-plugin-prettier - 将 Prettier 的规则设置到 ESLint 的规则中
npm i eslint-config-prettier eslint-plugin-prettier -D

修改 ESLint 配置,使 Eslint 兼容 Prettier 规则 plugin:prettier/recommended 的配置需要注意的是,一定要放在最后。因为extends中后引入的规则会覆盖前面的规则。也就是说你可以在.prettierrc.json 中定义自己的风格代码。到时候,本地的prettier插件会根据这个文件来格式化,项目安装的prettier也会根据该文件来格式化。且eslint的风格与prettier风格冲突的地方会以prettier为主

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier/skip-formatting',
    'plugin:prettier/recommended'
  ],
  parserOptions: {
    ecmaVersion: 'latest'
  },
  rules: {
  }
}

此时你再运行npm run lint,就会发现已经帮你格式化好了。我们只需要加入commitLint即可。

4.添加commitLint(可选操作,建议有)

工具前置知识,工具介绍:

  • lint-staged 是一个专门针对已放入 Git 暂存区的文件进行检查的工具
  • husky 能提供监听 Git 操作并执行脚本代码的能力

一:安装配置 lint-staged:实现专门针对 git 暂存区的文件进行风格检查 (1).安装依赖包

npm i lint-staged --save-dev

(2).在 package.json 中配置 lint-staged,利用它来调用 eslint 和 stylelint 去检查暂存区内的代码

{
  // ...
  "lint-staged": {
    "*.{vue,js}": [
      "npm run lint"
    ]
  }
}

image.png 二:安装配置 husky:实现在git 提交时执行 lint-staged (1).安装依赖包

npm i husky --save-dev

(2)在 package.json中配置快捷命令,用来在安装项目依赖时生成 husky 相关文件

{
  // ...
  "scripts": {
    // ...
    "postinstall": "husky install"
  },
}

image.png (3)配置后执行 npm i 就会在项目根目录生成 .husky/_ 目录。

npm i 

image.png (4)执行以下代码,使用 husky 生成 git 操作的监听钩子脚本

npx husky add .husky/pre-commit "npx lint-staged"

image.png 执行后成功后会生成脚本文件 .husky/pre-commit (在vscode中可以看到),它包含了命令行语句: npx lint-staged image.png 到此你就完成了git commit时自动去触发eslint的检测和修复。测试一下: 修改部分代码(一定要修改下,因为lint-staged是对暂存区的文件进行检测),然后git add . git commit一下,有以下的代码出现,就说明git commit时自动触发了eslint image.png

5.加入UI组件库,以element-plus为例子

安装依赖包:

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

在vite.config.ts中引入(以按需引入为例)

// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

使用(直接使用即可,插件会帮我们导入): image.png

6.建立一些必须要用到的文件夹

image.png

7.消灭浏览器差异以及盒子原有样式

(1)normalize.css保证我们的代码在各浏览器的一致性

  1. 安装 npm install --save normalize.css(报错的话,跟上面一样加上--legacy-peer-deps)
npm install --save normalize.css 或者npm install --save normalize.css --legacy-peer-deps
  1. main.ts引入 import 'normalize.css/normalize.css' (2)消除盒子原有样式(简化版) 1.建立reset.css,写入以下样式:
* {
  padding: 0;
  margin: 0;
  list-style: none;
  box-sizing: border-box;
}
html,
body {
  height: 100%;
}

2.在main.ts中引入:import './assets/css/reset.css' image.png image.png

8.配置别名

方式一(有示例,直接配):在vue.config.ts中添加:

'_c': fileURLToPath(new URL('./src/components', import.meta.url))

方式二在vue.config.ts中添加:

import path from 'path'
const { defineConfig } = require('@vue/cli-service')
export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      _c: path.resolve(__dirname, 'src/components')
    },
  },
})

image.png 不管哪种方式,最后都需要在tsconfig.json中加入"_c/": ["./src/components/"]。否则ts会爆红,看着揪心。 image.png

9.配置代理(与vue2稍有不同)

在vue.config.ts中添加:

// 样例
server: {
     proxy: {
      "/api": {
        //'/api'是自行设置的请求前缀
        target: "http://localhost:5000",
        changeOrigin: true, //用于控制请求头中的host值
        rewrite: (path) => path.replace(/^\/api/, ""), //路径重写,(正则)匹配以api开头的路径为空(将请求前缀删除)
      },
    },
}

image.png

10.pinia全局状态管理器改造和持久化

安装依赖:

npm i pinia-plugin-persistedstate

删除stores文件夹下原有的counter.ts文件,建立Index.ts文件,加入以下代码

注意:store.use(createPersistedState(参数可选,详见官网)),这里直接用就好了,全部缓存

import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-persistedstate-plugin'
// 创建store实例
const store = createPinia()
// 使用持久化插件(全局持久化)
store.use(createPersistedState())
export default store

在main.ts中导入:

import store from './stores/index'
const app = createApp(App)
app.use(store)

image.png 在stores文件夹下面建立modules文件夹,并建立userInfo.ts文件,加入以下示例代码:

// 每一个存储的模块,命名规则use开头,store结尾
import { defineStore } from 'pinia'
export const useUserInfoStore = defineStore({
  id: 'userInfo', // 必须指明唯一的pinia仓库的id
  state: () => {
    return {
      num: 0,
      name: '张三',
      token: ''
    }
  },
  getters: {
    doubleCount: (state) => state.num * 2
  },
  actions: {
    changeNum() {
      this.num++
    },
    loginOut() {
      // 处理退出登录的一些逻辑
      return new Promise((rez) => {
        rez('111')
      })
    }
  }
})

使用方法示例:

import { useUserInfoStore } from '@/stores/modules/userInfo'
const store = useUserInfoStore()
store.changeNum()
console.log(store.name);
console.log(store.num);

image.png

11.添加axios并封装axios

安装axios:

npm i axios

在utils文件夹下创建local.ts,加入以下代码:

function getLocal(key = 'token') {
  return localStorage.getItem(key)
}
// 删除
function removeLocal(key = 'token') {
  window.localStorage.removeItem(key)
}
// 保存
function setLocal(value: any, key = 'token') {
  window.localStorage.setItem(key, value)
}
export { getLocal, removeLocal, setLocal }


在utils文件夹下创建request.ts,加入以下代码:

详细的封装过程请移步这篇博客(这个代码是通用的,可直接copy,已改成ts版本): blog.csdn.net/weixin_4323…

import axios from 'axios'
// import { Message, Loading, MessageBox } from 'element-ui'
import { ElMessage, ElLoading, ElMessageBox } from 'element-plus'
import { useUserInfoStore } from '@/stores/modules/userInfo'
const store = useUserInfoStore()
import { getLocal } from '@/utils/local'

// 创建axios实例
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_URL, // api的base_url
  timeout: 5000 // 请求超时时间
})

// request拦截器
service.interceptors.request.use(
  (config: any) => {
    removePending(config)
    // 如果repeatRequest不配置,那么默认该请求就取消重复接口请求
    !config.repeatRequest && addPending(config)
    // 打开loading
    if (config.loading) {
      LoadingInstance._count++
      if (LoadingInstance._count === 1) {
        openLoading(config.loadingDom)
      }
    }
    // 如果登录了,有token,则请求携带token
    // Do something before request is sent
    if (store.token) {
      config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
    }
    return config
  },
  (error) => {
    // Do something with request error
    console.log(error) // for debug
    Promise.reject(error)
  }
)

// respone拦截器
service.interceptors.response.use(
  // response => response,
  /**
   * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
   * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
   */
  (response: any) => {
    // 已完成请求的删除请求中数组
    removePending(response.config)
    // 关闭loading
    if (response.config.loading) {
      closeLoading()
    }

    const res = response.data
    // 处理异常的情况
    if (res.code !== 200) {
      ElMessage({
        message: res.message,
        type: 'error',
        duration: 5 * 1000
      })
      // 403:非法的token; 50012:其他客户端登录了;  401:Token 过期了;
      if (res.code === 403 || res.code === 50012 || res.code === 401) {
        ElMessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.loginOut().then(() => {
            location.reload() // 为了重新实例化vue-router对象 避免bug
          })
        })
      }
      return Promise.reject(new Error('error'))
    } else {
      // 默认只返回data,不返回状态码和message
      // 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
      const isbackAll = response.config.meta && response.config.meta.responseAll
      if (isbackAll) {
        return res
      } else {
        return res.data
      }
    }
  },
  (error) => {
    error.config && removePending(error.config)
    // 关闭loading
    if (error.config.loading) {
      closeLoading()
    }
    console.log('err' + error) // for debug
    ElMessage({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

// --------------------------------取消接口重复请求的函数-----------------------------------
// axios.js
const pendingMap = new Map()
/**
 * 生成每个请求唯一的键
 * @param {*} config
 * @returns string
 */
function getPendingKey(config: any) {
  const { url, method, params } = config
  let { data } = config
  if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
  return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
}

/**
 * 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
 * @param {*} config
 */
function addPending(config: any) {
  const pendingKey = getPendingKey(config)
  config.cancelToken =
    config.cancelToken ||
    new axios.CancelToken((cancel) => {
      if (!pendingMap.has(pendingKey)) {
        pendingMap.set(pendingKey, cancel)
      }
    })
}
/**
 * 删除重复的请求
 * @param {*} config
 */
function removePending(config: any) {
  const pendingKey = getPendingKey(config)
  if (pendingMap.has(pendingKey)) {
    const cancelToken = pendingMap.get(pendingKey)
    cancelToken(pendingKey)
    pendingMap.delete(pendingKey)
  }
}
// ----------------------------------loading的函数-------------------------------
const LoadingInstance: { _target: any; _count: number } = {
  _target: null, // 保存Loading实例
  _count: 0
}
function openLoading(loadingDom: any) {
  LoadingInstance._target = ElLoading.service({
    lock: true,
    text: '数据正在加载中',
    spinner: 'el-icon-loading',
    background: 'rgba(25, 32, 53, 1)',
    target: loadingDom || 'body'
  })
}
function closeLoading() {
  if (LoadingInstance._count > 0) LoadingInstance._count--
  if (LoadingInstance._count === 0) {
    LoadingInstance._target.close()
    LoadingInstance._target = null
  }
}

export default service

把上面这个代码复制上去,不会报错,但是process这里会有波浪线爆红,看着难受,解决方法: www.cnblogs.com/lslhhh/arti…

axios的使用:在api文件夹中建立home.ts,加入以下代码(示例):

import request from '@/utils/request'

//使用封装好的request
export function getInfo(params: Object) {
  return request({
    url: '/user/info',
    method: 'get',
    params
  })
}

//返回所有的res信息
export function login(data: Object) {
  return request({
    url: '/user/login',
    method: 'post',
    data,
    meta: {
      responseAll: true // 返回所有的信息,包括状态码和message和data
    }
  } as any)
}

//使用repeatRequest参数让接口 可以同一时间多次调用
export function getList(params: Object) {
  return request({
    url: '/message/list',
    method: 'get',
    params,
    repeatRequest: true // 配置为true,则可以同一时间多次调用
  }as any)
}

//使用loading
export function getListById(loading: Object) {
  return request({
    url: '/message/listbyId',
    method: 'get',
    ...loading
  })
}

export function gettableList() {
  return request({
    url: '/user/tableData',
    method: 'get'
  })
}

13.区分生产环境和开发环境(参考文章)

在src目录建立三个文件夹,定义变量VITE_APP_URL(注意要以VITE_开头)

.env.production文件 .env.development 文件 .env.test文件

VITE_APP_URL = http://47.106.228.28:1337

image.png 在package.json中增加(生产和开发不用,因为有默认的mode就是这两个):

"test": "vite --mode test",

如何使用该变量?使用import.meta.env.VITE_APP_URL来调用 image.png

13.封装路由拦截器

因为生成的vite项目中就带有了vue-router,所以我们就简单的增加了对路由拦截器配置的文件。 在router文件夹下创建router-config.js,写入以下代码,并在main.ts中导入:

import router from './index'

// 白名单页面直接进入
const whiteList = ['/login']
console.log(whiteList)

router.beforeEach((to, from, next) => {
  next()
})

router.afterEach(() => {
  // finish progress bar
  //   NProgress.done()
})

import "./router/router-config"  // 路由守卫,做动态路由的地方

由于封装路由拦截器需要和项目选中前端路由还是后端路由以及登录模块都有关系,涉及的内容比较多,所以这里就简单的写了样例。

需要使用前端路由的,可参考我的这篇文章: blog.csdn.net/weixin_4323…

需要使用后端路由的,可参考我的这篇文章: blog.csdn.net/weixin_4323…

其中主要的逻辑为:

是否为白名单页面

    是:直接进入

    不是:判断是否有token

        无token:重置到login页面

        有token: 判断用户的路由状态(指的是用户是否有路由)

            有路由:直接进入

            没有路由:调接口去获取路由

                              递归处理后台返回的路由

                              将异步路由存储到vuex,并设置用户的路由状态为true

                              使用route.addRouters将路由添加进去

14.适配处理

适配处理参考这篇文章blog.csdn.net/weixin_4323…,因为不知道是什么类型的项目,所以这里就不加进去了。

各类需求适合适配的主方案:

**移动端:**rem(lib-flexible+postcss-pxtorem) 或者 vw方案 参考vant官方文档:vant-ui.github.io/vant/#/zh-C…

**PC大屏:**scale优先

PC端(官网主页/购物网站/类似知乎,淘宝,小米等主页):使用版心布局

PC端(有地图或不允许留白(设计稿完全占满了1920*1080)):rem(lib-flexible+postcss-pxtorem)+其他响应式方法

补充:各依赖包的环境

node:16.14.2(主要,如果是低版本的,生成的项目模板可能会不一致)

npm:8.5.0(主要)

@vue/cli:4.5.9(主要,如果是低版本的,生成的项目模板可能会不一致)

其他可在package.json中看到

其他:可能大家的版本不一致,导致了一些错误,或者说大家的写法都不一样,但是这都没关系,重点是上面的步骤,基本上都是需要添加到项目中的,至于怎么添加,可以自由发挥,也不用按我的来。

项目模板demo:github.com/rui-rui-an/…