第一步:搭建 Vite 项目
① 进入 Vite 官网查看最新命令
//打开 PowerShell
npm create vite@latest
// 给项目取个名字 vite-project
// 选择前端框架 这里选择 Vue + TypeScript
cd vite-project
npm install
npm run dev
② 项目能跑起来后,关闭 PowerShell,用 VsCode 打开项目,准备进入下一步
第二步:项目环境文件配置
① 配置 Vite和 TypeScript
- 修改
vite.config.ts,主要是配置别名和配置代理
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
const pathResolve = (dir: string) => resolve(__dirname, dir);
export default defineConfig(
({ mode }) => {
const env = loadEnv(mode, __dirname)
return {
plugins: [vue()],
build: {
outDir: "dist",
terserOptions: {
compress: {
keep_infinity: true,
drop_console: true,
drop_debugger: true,
},
},
chunkSizeWarningLimit: 1500,
},
resolve: {
alias: {
src: pathResolve("./src"),
api: pathResolve("./src/api"),
router: pathResolve("./src/router"),
store: pathResolve("./src/store"),
views: pathResolve("./src/views"),
utils: pathResolve("./src/utils"),
components: pathResolve("./src/components"),
assets: pathResolve("./src/assets"),
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json']
},
base: "./",
server: {
port: 3000,
open: true,
cors: true,
proxy: {
"/development": {
target: env.VITE_SERVER_API,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace("/development", ""),
},
},
},
}
}
);
- 修改
tsconfig.json,逐条研究过,不会报错,项目能正常启动打包
{
"compilerOptions": {
"outDir": "./",
"baseUrl": "./src/",
"allowJs": true,
"skipLibCheck": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"noImplicitAny": false,
"strict": false,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["es6", "dom"],
"types": ["vite/client"],
"isolatedModules": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules", "dist"]
}
② 配置项目环境变量
- 新增
.env.production文件
ENV = 'production'
VITE_BASE_API = 'http://XXX.XXX.XXX.XXX:XXX/production/'
VITE_SERVER_API = 'XXX.XXX.XXX.XXX:XXX'
VITE_TITLE = 生产环境
- 新增
.env.development文件
ENV = 'development'
VITE_BASE_API = 'http://XXX.XXX.XXX.XXX:XXX/development/'
VITE_SERVER_API = 'http://XXX.XXX.XXX.XXX:XXX'
VITE_TITLE = 开发环境
③ 配置编码格式规范和代码美化
- 安装插件
EditorConfig for VS Code - 新增
.editorconfig文件
# Editor configuration, see http://editorconfig.org
# 表示是最顶层的 EditorConfig 配置文件
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false
- 安装插件
ESLint - 安装
eslint到项目:npm i eslint -D - 新增
.eslintrc.js文件
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended'
],
parser: "vue-eslint-parser",
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: "latest",
sourceType: 'module'
},
plugins: [
'@typescript-eslint'
],
rules: {
'quotes': ['error', 'single'],
'eqeqeq': ['error', 'always'],
'no-unused-vars': 'error',
'keyword-spacing': [
'error',
{
'overrides': {
'if': {
'after': true
},
'for': {
'after': true
},
'while': {
'after': true
},
'else': {
'after': true
}
}
}
],
'camelcase': ['error', { 'properties': 'never' }],
'indent': ['error', 4, {
'SwitchCase': 1,
'flatTernaryExpressions': true
}],
'array-bracket-spacing': ['error', 'never'],
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'arrow-parens': 'off',
'no-empty': ['error', { 'allowEmptyCatch': true }],
'semi': ['error', 'never'],
'space-before-function-paren': ['error', {
'anonymous': 'never',
'named': 'never',
'asyncArrow': 'never'
}],
'no-trailing-spaces': ['error'],
'spaced-comment': ['error', 'always', {
'line': {
'markers': ['*package', '!', '/', ',', '=']
},
'block': {
'balanced': false,
'markers': ['*package', '!', ',', ':', '::', 'flow-include'],
'exceptions': ['*']
}
}],
'no-template-curly-in-string': 'off',
'no-useless-escape': 'off',
'no-var': 'error',
'prefer-const': 'error',
'vue/valid-v-slot': 'error',
'vue/experimental-script-setup-vars': 'off',
'vue/array-bracket-spacing': ['error', 'never'],
'vue/arrow-spacing': ['error', { 'before': true, 'after': true }],
'vue/attribute-hyphenation': ['error', 'always'],
'vue/attributes-order': 'off',
'vue/block-spacing': ['error', 'always'],
'vue/camelcase': ['error', { 'properties': 'never' }],
'vue/comma-dangle': ['error', 'never'],
'vue/comment-directive': 'error',
'vue/component-name-in-template-casing': 'off',
'vue/eqeqeq': ['error', 'always', { 'null': 'ignore' }],
'vue/html-closing-bracket-newline': 'off',
'vue/html-closing-bracket-spacing': ['error', {
'startTag': 'never',
'endTag': 'never',
'selfClosingTag': 'always'
}],
'vue/html-end-tags': 'error',
'vue/html-indent': ['error', 4, {
'attribute': 1,
'baseIndent': 1,
'closeBracket': 0,
'alignAttributesVertically': false,
'ignores': []
}],
'vue/html-quotes': ['error', 'double'],
'vue/html-self-closing': 'off',
'vue/jsx-uses-vars': 'warn',
'vue/key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }],
'vue/match-component-file-name': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/mustache-interpolation-spacing': 'off',
'vue/name-property-casing': ['error', 'kebab-case'],
'vue/no-async-in-computed-properties': 'error',
'vue/no-boolean-default': 'off',
'vue/no-confusing-v-for-v-if': 'off',
'vue/no-dupe-keys': 'error',
'vue/no-duplicate-attributes': 'error',
'vue/no-multi-spaces': 'error',
'vue/no-parsing-error': 'error',
'vue/no-reserved-keys': 'error',
'vue/no-restricted-syntax': 'off',
'vue/no-shared-component-data': 'error',
'vue/no-side-effects-in-computed-properties': 'off',
'vue/no-spaces-around-equal-signs-in-attribute': 'error',
'vue/no-template-key': 'off',
'vue/no-textarea-mustache': 'error',
'vue/no-unused-components': 'error',
'vue/no-unused-vars': 'error',
'vue/no-use-v-if-with-v-for': 'off',
'vue/no-v-html': 'off',
'vue/object-curly-spacing': ['error', 'always'],
'vue/order-in-components': ['error', {
'order': [
'el',
'name',
'parent',
'functional',
['delimiters', 'comments'],
['components', 'directives', 'filters'],
'extends',
'mixins',
'inheritAttrs',
'model',
['props', 'propsData'],
'data',
'computed',
'watch',
'LIFECYCLE_HOOKS',
'methods',
['template', 'render'],
'renderError'
]
}],
'vue/prop-name-casing': ['error', 'camelCase'],
'vue/require-component-is': 'error',
'vue/require-default-prop': 'off',
'vue/require-direct-export': 'off',
"vue/require-prop-type-constructor": "error",
"vue/require-prop-types": "error",
"vue/require-render-return": "error",
"vue/require-v-for-key": "error",
"vue/require-valid-default-prop": "off",
"vue/return-in-computed-property": "error",
"vue/script-indent": [
"error",
4,
{
baseIndent: 1,
switchCase: 1,
}
],
"vue/singleline-html-element-content-newline": "off",
"vue/space-infix-ops": "error",
"vue/space-unary-ops": ["error", { words: true, nonwords: false }],
"vue/this-in-template": ["error", "never"],
"vue/use-v-on-exact": "off",
"vue/v-bind-style": "off",
"vue/v-on-function-call": "off",
'vue/v-on-style': ['error', 'shorthand'],
'vue/valid-template-root': 'error',
'vue/valid-v-bind': 'error',
'vue/valid-v-cloak': 'error',
'vue/valid-v-else-if': 'error',
'vue/valid-v-else': 'error',
'vue/valid-v-for': 'error',
'vue/valid-v-html': 'error',
'vue/valid-v-if': 'error',
'vue/valid-v-model': 'error',
'vue/valid-v-on': 'error',
'vue/valid-v-once': 'error',
'vue/valid-v-pre': 'error',
'vue/valid-v-show': 'error',
'vue/valid-v-text': 'error',
'vue/script-setup-uses-vars': 'error',
'vue/multi-word-component-names': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
},
overrides: [
{
files: ['*.vue'],
rules: {
indent: 'off'
}
}
]
}
- 新增
.eslintignore文件
/build/
/dist/
/node_modules/
*.js
- 安装插件
Prettier - 安装
prettier到项目:npm i prettier -D - 新增
.prettierrc文件
{
"printWidth": 100,
"semi": true,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"arrowParens": "avoid",
"bracketSpacing": true,
"bracketLine": true,
"tslintIntegration": false,
"vueIndentScriptAndStyle": true,
"quoteProps": "as-needed",
"jsxBracketSameLine": true,
"insertPragma": false,
"requirePragma": false,
"proseWrap": "never",
"htmlWhitespaceSensitivity": "strict",
"trailingComma": "all",
"endOfLine": "auto"
}
- eslint 安装附加配置
npm i @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb-base eslint-plugin-import eslint-plugin-vue -D
- 解决 Prettier 和 ESLint 的冲突
npm i eslint-plugin-prettier eslint-config-prettier -D
第三步:项目功能文件配置
① 配置网络请求
- 安装 axios
npm install axios - 安装稳住用户进度条插件
npm install nprogress - src 下面新建 api 文件夹,基础配置两个文件
// src/api/index.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, Canceler } from "axios";
import nprogress from 'nprogress';
import 'nprogress/nprogress.css';
import config from './config';
import STORE from 'store';
import { storeToRefs } from "pinia";
const { token } = storeToRefs(STORE.passport)
enum statusCode {
RequestError = 400,//请求错误
NotRole = 401,//未授权,请重新登录
Reject = 403,//拒绝访问
NotFind = 404,//请求出错
ServeCrash = 408,//请求超时
ServerError = 500,//服务器错误
ServiceUnrealized = 501,//服务未实现
NetworkError = 502,//网络错误
ServiceUnavailable = 503,//服务不可用
NetworkTimeout = 504,//网络超时
HTTPNotSupported = 505,//HTTP版本不受支持
}
interface IResult<T> {
code: number,
message: string,
data: T
}
class Request {
instance: AxiosInstance;
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config);
this.instance.interceptors.request.use((res: AxiosRequestConfig) => {
//从仓库中获取token,在非passport页面请求加入
if (token.value) {
res.headers!.token = token.value;
}
// 添加请求取消
config.cancelToken = new axios.CancelToken(cancel => {
STORE.passport.addCancelToken(cancel);
})
nprogress.start();
return res;
},
(err: any) => Promise.reject(err));
this.instance.interceptors.response.use((res: AxiosResponse) => {
nprogress.done();
return res.data;
}, (err: any) => {
nprogress.done();
switch (err.response.status) {
case statusCode.RequestError:
console.log('请求错误');
break;
case statusCode.NotRole:
console.log('未授权,请重新登录');
break;
case statusCode.Reject:
console.log('拒绝访问');
break;
case statusCode.NotFind:
console.log('请求出错');
break;
case statusCode.ServeCrash:
console.log('请求超时');
break;
case statusCode.ServerError:
console.log('服务器错误');
break;
case statusCode.ServiceUnrealized:
console.log('服务未实现');
break;
case statusCode.NetworkError:
console.log('网络错误');
break;
case statusCode.ServiceUnavailable:
console.log('服务不可用');
break;
case statusCode.NetworkTimeout:
console.log('网络超时');
break;
case statusCode.HTTPNotSupported:
console.log('HTTP版本不受支持');
break;
default:
console.log(`连接出错(${err.response.status})!`);
}
return Promise.reject(err);
})
}
public request(config: AxiosRequestConfig): Promise<AxiosResponse> {
return this.instance.request(config);
}
public get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<IResult<T>>> {
return this.instance.get(url, config);
}
public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<IResult<T>>> {
return this.instance.post(url, data, config);
}
public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<IResult<T>>> {
return this.instance.put(url, data, config);
}
public delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<IResult<T>>> {
return this.instance.delete(url, config);
}
}
export default new Request(config);
// src/api/config.ts
const baseURL: string = import.meta.env.VITE_BASE_API;
const timeout: number = 5000;
export default { baseURL, timeout }
- 使用
// src/api/demo.js
import API from 'api'
//发起demo的get请求
export const http_demo1 = params => API.get('api/demo1', { params })
//发起demo的post请求
export const http_demo2 = data => API.post('api/demo2', data)
//发起demo的put请求
export const http_demo3 = data => API.put('api/demo3', data)
//发起demo的delete请求
export const http_demo4 = id => API.put('api/demo4' + id)
② 配置路由
- 安装 vue-router
npm install vue-router - src 下面新建 router 文件夹,基础配置一个文件
// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import STORE from 'store';
import { storeToRefs } from 'pinia';
import { ElMessage } from 'element-plus';
import demo from './demo.js';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: '',
component: () => import('views/passport/login.vue'),
},
// 登录
{
path: '/login',
name: 'login',
component: () => import('views/passport/login.vue'),
},
// 注册
{
path: '/register',
name: 'register',
component: () => import('views/PassPort/register.vue'),
},
// 忘记密码
{
path: '/forget',
name: 'forget',
component: () => import('views/PassPort/forget.vue'),
},
// 404
{
path: '/:catchAll(.*)', // 当所有路由找不到的时候会执行这个页面
name: 'errorInfo',
component: () => import('views/PassPort/error.vue'),
},
// 内容
{
path: '/home',
name: 'home',
redirect: '/home/demo',
component: () => import('src/views/pages/index.vue'),
children: [
// demo
demo,
],
},
]
const router = createRouter({
history: createWebHashHistory(), // history 模式则使用 createWebHistory()
routes,
});
// 路由跳转之前
router.beforeEach((to, from, next) => {
const { token } = storeToRefs(STORE.passport)// 使用仓库
// console.log(token.value, 'is token');
if (
to.path !== '/login' &&
to.path !== '/' &&
// 如果不存在token,则不允许进行页面跳转
!token.value
) {
ElMessage.warning('请输入用户名和密码进行登录!');
return next('/login');
}
next();
});
export default router
// src/router/demo.js
const demo = {
path: '/home/demo',
name: 'demo',
component: () => import('views/demo/index.vue'),
redirect: '/home/demo',
meta: { title: 'demo' },
children: [
{
path: '',
name: '',
component: ,
meta: ,
},
],
};
export default demo;
③ 配置仓库
- 安装 pinia 和 pinia-plugin-persist
npm install pinia pinia-plugin-persist - src 下面新建 store 文件夹,基础配置一个文件
// src/store/index.ts
import demo from './demo';
export interface IAppStore {
demo: ReturnType<typeof demo>;
}
const STORE: IAppStore = {} as IAppStore;
export const registerStore = () => {
STORE.demo = demo();
};
export default STORE;
// src/store/demo.js
import { defineStore } from "pinia";
const useDemo = defineStore({
// 开启数据缓存
persist: {
enabled: true,
},
id: "useDemo",
state() {
return {
}
},
actions: {
},
})
export default useDemo;
④ 配置入口文件
// main.ts
import { createApp } from 'vue'
import App from 'src/App.vue'
import router from 'router'
import { createPinia } from 'pinia'
import { registerStore } from 'store';
import piniaPluginPersist from 'pinia-plugin-persist';
const app = createApp(App);
app.config.warnHandler = () => null;
app.use(router).use(createPinia().use(piniaPluginPersist)).mount('#app')
registerStore();
⑤ 按需配置其他插件和工具库文件
- 传输加密库
- UI 组件库
- 自适应屏幕工具组件
- 时间转换函数工具
- 地图异步加载工具
- 类型判断工具
- 经纬度坐标系转换工具
- 二进制流导出文件工具
- 等等