使用vite快速搭建
- 命令行直接操作
# npm 6.x npm init @vitejs/app vite-vue3 --template vue-ts # npm 7+ (需要额外的双横线) npm create vite vite-vue3 -- --template vue-ts # yarn yarn create @vitejs/app vite-vue3 --template vue-ts
- 安装依赖 -> 启动项目
npm i npm run dev
修改vite配置文件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// 参考文档:https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve('./src')
}
},
base: './', // 打包路径
server: {
port: 3000, // 服务端口号
open: true, // 服务启动时是否自动打开浏览器
cors: true // 允许跨域
}
})
集成路由
- 安装
vue-router@4
npm i vue-router@4 --save
- 创建
src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' const routes: Array<RouteRecordRaw> = [ { path: '/home', name: 'Home', component: () => import(/* webpackChunkName: "Home" */ '@/views/home.vue') }, { path: '/', redirect: { name: 'Home' } } ] const router = createRouter({ history: createWebHashHistory(), routes }) export default router
main.ts
集成import { createApp } from 'vue' import App from '@/App.vue' import router from '@/router/index' createApp(App).use(router).mount('#app')
集成vuex
- 安装
vuex@next
npm i vuex@next --save
- 创建
src/store/index.ts
import { createStore } from 'vuex' const defaultState = { count: 0 } // Create a new store instance. export default createStore({ state() { return defaultState }, mutations: { increment(state: typeof defaultState) { state.count += 1 } }, actions: { increment(context) { context.commit('increment') } }, getters: { double(state: typeof defaultState) { return 2 * state.count } } })
main.ts
里挂载import { createApp } from 'vue' import App from '@/App.vue' import router from '@/router/index' import store from '@/store/index' createApp(App).use(router).use(store).mount('#app')
代码规范
- 集成
prettier
- 安装
npm i prettier --save-dev
- 根目录下新建
.prettierrc
{ "useTabs": false, "tabWidth": 2, "printWidth": 100, "singleQuote": true, "trailingComma": "none", "bracketSpacing": true, "semi": false }
- 安装
集成husky
和lint-staged
husky
: Git Hook 工具,可以设置在 git 各个阶段(pre-commit、commit-msg、pre-push 等)触发我们的命令。lint-staged
: 在 git 暂存的文件上运行 linters。
jest
单元测试使用
- 安装需要的包
npm i @babel/core @babel/preset-env @testing-library/jest-dom @types/jest @vue/test-utils@next @babel/preset-typescript @vue/babel-plugin-jsx vue-jest@next -D // 下面三个包的版本需要固定,有些版本和 vue-test 的对应不上,则会出错 npm i babel-jest@26.0.0 jest@26.0.0 ts-jest@26.4.4 -D
- 根目录新建
jest.config.js
文件const path = require('path') module.exports = { rootDir: path.resolve(__dirname), clearMocks: true, coverageDirectory: 'coverage', coverageProvider: 'v8', moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'], // 别名设置 moduleNameMapper: { '@/(.*)$': '<rootDir>/src/components/$1' }, preset: 'ts-jest', testEnvironment: 'jsdom', // 测试文件 testMatch: ['<rootDir>/src/__tests__/**/*.spec.(ts|tsx|js)'], testPathIgnorePatterns: ['/node_modules/'], moduleFileExtensions: ['js', 'json', 'ts', 'tsx'], transform: { '^.+\\.vue$': 'vue-jest', '^.+\\.(ts|tsx|js|jsx)$': [ 'babel-jest', { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-typescript'] ], plugins: ['@vue/babel-plugin-jsx'] } ] } }
集成 element-plus
- 安装依赖
npm install element-plus --save
- 全局导入
- 修改main.ts
Volar 支持 - 修改tsconfig.jsonimport { 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')
// tsconfig.json { "compilerOptions": { // ... "types": ["element-plus/global"] } }
- 按需导入 -- 自动导入
- 安装依赖
npm install unplugin-vue-components unplugin-auto-import/vite vite-plugin-style-import --save-d
- 修改
vite.config.ts
// vite.config.ts import styleImport from 'vite-plugin-style-import'; import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default { plugins: [ // ... <!--styleImport({--> <!-- libs: [{--> <!-- libraryName: 'element-plus',--> <!-- esModule: true,--> <!-- resolveStyle: name => {--> <!-- return `element-plus/theme-chalk/${name}.css`--> <!-- }--> <!-- }],--> <!--}),--> AutoImport({ resolvers: [ElementPlusResolver()], }), Components({ resolvers: [ElementPlusResolver()], }), ], }
- 修改app.vue
<style> @import url(@/styles/main.less); /* 当在axios.ts里直接使用ElLoading时页面会报错,需手动引入,但这样做会导致loading样式丢失,所以在这里直接引入element-plus全部样式 */ @import url(element-plus/dist/index.css); #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } </style>
- 安装依赖
- 按需导入 -- 手动导入
- 修改
vite.config.ts
// vite.config.ts import ElementPlus from 'unplugin-element-plus/vite' export default { plugins: [ElementPlus()], }
- 在相应页面引入
<template> <el-button>I am ElButton</el-button> </template> <script> import { ElButton } from 'element-plus' export default { components: { ElButton }, } </script>
- 修改
引入 axios
- 安装相应依赖
npm i axios qs --save
- 设计功能列表
- 发送http请求
- 分模块文件管理接口
- 采用json形式管理接口列表
- 根据接口配置确定是否显示加载图标
- 目录为
|- src |- |- api |- |- |- axios.ts |- |- |- index.ts |- |- |- customer.ts
- 新建工具类
axios.ts
/** * @time 2021-11-29 * * axios类二次封装 */ import axios, { AxiosResponse, CancelToken } from 'axios'; import { AxiosInstance, AxiosRequestConfig } from 'axios'; import { useRouter } from 'vue-router'; const $router = useRouter(); // 引入 element-plus loading及messageBox组件 本不需引入,但页面会报错,所以这里加个导入 // App.vue 里手动引入 @import url(element-plus/dist/index.css); import { ElLoading, ElMessage } from 'element-plus'; const CODE_MESSAGE = { 200: '服务器成功返回请求的数据。', 201: '新建或修改数据成功。', 202: '一个请求已经进入后台排队(异步任务)。', 204: '删除数据成功。', 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', 401: '用户没有权限(令牌、用户名、密码错误)。', 403: '用户得到授权,但是访问是被禁止的。', 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', 406: '请求的格式不可得。', 410: '请求的资源被永久删除,且不会再得到的。', 422: '当创建一个对象时,发生一个验证错误。', 500: '服务器发生错误,请检查服务器。', 502: '网关错误。', 503: '服务不可用,服务器暂时过载或维护。', 504: '网关超时。' }; class HttpAxios { // axios实例 instance: AxiosInstance; // 超时时间 timeout: number = 10000; // elementUI loading实例 loadingInstance: any; // axios cancelToken 数组,方便处理取消请求 cancelTokenArr: Array<any> = []; constructor(config: AxiosRequestConfig) { let instance: AxiosInstance = axios.create(config); // 设置请求拦截 instance.interceptors.request.use(this._requestInterceptors, (error) => { return Promise.reject(error); }); instance.interceptors.response.use(this._responseInterceptors, this._checkResponseError); this.instance = instance; } /** * 请求拦截,处理header部分传值及是否显示loading * @param config */ _requestInterceptors = (config: AxiosRequestConfig) => { let _config = { withCredentials: false, // 处理跨域问题 timeout: this.timeout }; config.cancelToken = new axios.CancelToken(cancel => { this.cancelTokenArr.push({ cancel }); }) return Object.assign({}, config, _config); }; /** * 返回拦截 * @param response * @returns */ _responseInterceptors = (response: AxiosResponse) => { if (this.loadingInstance) { this.loadingInstance.close(); this.loadingInstance = null; } let data = response.data || {}; if (data.code === '0000') { return data.data || {}; } this._checkResponseCode(data); return null; }; /** * 处理请求错误时情况 根据不同的状态码 * @param error */ _checkResponseError = (error: any) => { if (this.loadingInstance) { this.loadingInstance.close(); this.loadingInstance = null; } if (/timeout/g.test(error)) { ElMessage.error('请求超时,请稍后再试!!!'); return Promise.reject(error); } const { response: { status, statusText, data: { msg = '服务器发生错误' } } } = error; const { response } = error; ElMessage.error(CODE_MESSAGE[status] || statusText); return Promise.reject(error); }; /** * 后台返回错误处理 * @param code 后台定义错误码 */ _checkResponseCode = ({ code, message }: any) => { ElMessage.error(message); if (code === '') { // TODO 处理登录失效问题 sessionStorage.setItem('route_to_login', location.href); $router.push({ path: '/login' }); } }; /** * 发送请求 * @param url * @param params * @param method * @param config * @returns */ sendRequest = (url: string, params: any, method: string = 'post', config?: any) => { if (!this.instance) { return; } if (!config || !config.isCloseLoading) { // TODO show loading this.loadingInstance = ElLoading.service({ lock: true, background: 'rgba(0, 0, 0, 0.7)' }); } const _method = method.toLocaleLowerCase(); if (_method === 'get') { params = { params: params }; return this.instance.get(url, params); } if (_method === 'formdata') { let reqData = new FormData(); for (let key in params) { reqData.append(key, params[key]); } return this.instance.post(url, reqData); } return this.instance.post(url, params); }; /** * 清除axios 请求 */ async clearRequests() { if (this.cancelTokenArr.length === 0) { return; } this.cancelTokenArr.forEach(ele => { ele.cancel(); }) this.cancelTokenArr = []; } } export default HttpAxios;
- 生成api方法
index.ts
import router from '@/router/index'; router.beforeEach(async (to, from, next) => { console.log(1111); await httpAxios.clearRequests(); next(); }) import customer from '@/api/customer' /** * api 对象接口定义 */ interface apiMap { method?: string; url: string, config?: { isCloseLoading: boolean, } } interface apiMaps { [propName: string]: apiMap } import HttpAxios from "@/api/axios"; const httpAxios = new HttpAxios({}); function generateApiMap(maps: apiMaps) { let methodMap: any = {}; for(let key in maps) { methodMap[key] = toMethod(maps[key]); } return methodMap; } function toMethod(options: apiMap) { options.method = options.method || 'post'; const { method = 'post', url, config } = options; return (param: any = {}) => { return httpAxios.sendRequest(url, param, method, config); } } const apis = generateApiMap({ ...customer }) export default { ...apis // 取出所有可遍历属性赋值在新的对象上 }
(ps: 项目地址: gitee.com/xhuarui/fro…)