Skip to content
霞露小伙 — HfWang
On this page

搭建 vue3 + tsx 项目

概要

本文主要介绍了怎么搭建一个 vue3 + tsx 的项目环境

主要使用的库 pinia axios vueuse windicss

初始化项目

js
pnpm create vite
js
pnpm vue-router pinia axios qs
pnpm i sass @types/node @types/qs -D

添加 unplugin-auto-import

作用: 自动导入 vue3 api

【备注】:jsx 下无法使用该插件

js
pnpm i unplugin-auto-import -D

修改 vite.config.ts

js
import AutoImport from "unplugin-auto-import/vite"; //注意后面有个/vite

export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
	const root = process.cwd();
	const env = loadEnv(mode, root);
	return {
		plugins: [
			AutoImport({
				imports: ["vue", "vue-router"],
				// 可以选择auto-import.d.ts生成的位置
				// 使用ts建议设置为'src/auto-import.d.ts'
				dts: "src/typings/auto-import.d.ts",
			}),
		],
	};
});

添加 unplugin-vue-components

作用: 自动导入自定义组件(或第三方组件库)

【备注】:jsx 下无法使用该插件

js
pnpm i unplugin-vue-components -D

修改 vite.config.js

js
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite"; //注意后面有个/vite
import Components from "unplugin-vue-components/vite"; //注意后面有个/vite

export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
	const root = process.cwd();
	const env = loadEnv(mode, root);
	return {
		plugins: [
			vue(),
			AutoImport({
				imports: ["vue", "vue-router"],
				// 可以选择auto-import.d.ts生成的位置
				dts: "src/typings/auto-import.d.ts",
			}),
			Components({
				dirs: ["src/components"],
				extensions: ["vue", "tsx"],
				// 配置文件生成位置
				dts: "src/typings/components.d.ts",
			}),
		],
	};
});

添加 @vitejs/plugin-vue-jsx

作用: 支持 jsx 语法

js
pnpm i @vitejs/plugin-vue-jsx  -D

修改 vite.config.js

js
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";

export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
	const root = process.cwd();
	const env = loadEnv(mode, root);
	return {
		plugins: [
			vue(),
			vueJsx({
				// options are passed on to @vue/babel-plugin-jsx,可不写,使用默认配置
			}),
		],
	};
});

添加 windicss

js
pnpm i -D vite-plugin-windicss windicss
ts
// vite.config.ts
import WindiCSS from "vite-plugin-windicss";

export default {
	plugins: [WindiCSS()],
};
ts
// /src/main.ts
import "virtual:windi.css";

添加工具函数

  1. 安装常用第三方工具库 vueuse lodash-es
js
pnpm i @vueuse/core lodash-es -D

vueuse 常用 API

  • 状态修改

    • useToggle:: 状态切换,适合比如弹窗打开、关闭或者某个元素/组件显示或隐藏的切换
    • useDebounceFn:: 防抖
    • useThrottleFn: 节流
    • useLocalStorage: localStorage
    • useCookies: cookie
    • useTitle: 修改标签页名
  • 监听功能

    • useWindowSize: 监听整个视图窗口大小的变化
    • useInterSectionObserver: 监听元素是否处于可视区域
    • useScroll: 监听滚动
  • 功能

    • useClipboard: 原生 clipboard api 的封装
    • useFetch: 用于发起请求
    • useNProgress: 请求进度条
    • useQRCode: 二维码
    • useWebSocket: websocket
  1. 自定义工具函数
js
// utils/to.ts
function to<T, U = Error>(func: Promise<T>): Promise<[null, T] | [U, null]> {
  return func
    .then<[null, T]>((res: T) => [null, res])
    .catch <[U, null] >((err: U) => [err, null])
  );
}

export default to;

增加代理

vite.config.js 新增下面选项

js
server: {
    port: 3000,
    open: false,
    proxy: {
        '/api': {
            target: 'https://xxxx.xxxx.xxx',
            changeOrigin: true,
            rewrite: url => url.replace('/api', '')
        }
    }
},

增加别名

vite.config.js 新增下面选项

js
resolve: {
    alias: {
        '@': resolve(__dirname,'src'),
    }
}

tsconfig.json

json
{
	"compilerOptions": {
		"target": "esnext",
		"useDefineForClassFields": true,
		"module": "esnext",
		"moduleResolution": "node",
		"strict": true,
		"jsx": "preserve",
		"sourceMap": true,
		"resolveJsonModule": true,
		"esModuleInterop": true,
		"lib": ["esnext", "dom"],
		"paths": {
			"@/*": ["./src/*"] // 新增
		}
	},
	"include": [
		"src/**/*.ts",
		"src/**/*.d.ts",
		"src/**/*.tsx",
		"src/**/*.vue",
		"typings/*.d.ts" // 声明文件
	],
	"jsx": "preserve", // tsx / jsx 支持
	"references": [{ "path": "./tsconfig.node.json" }]
}

css 处理器

  • 新建 src/asset/css/index.scss
  • 修改 vite.config.js
js
css: {
    preprocessorOptions: {
        scss: {
            additionalData: '@import "@/assets/css/index.scss";',
        },
    },
}

生产环境移除 console、debuger

js
build: {
    terserOptions: {
        compress: {
            // 生产环境去除 console debugger
            drop_console: true,
            drop_debugger: true,
        },
    }
}

axios 封装

js
// src/utils/service.ts
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import qs from "qs";
import showStatusMessage from "./statusMessage";

const instance = axios.create({
	baseURL: "appi",
	timeout: 10 * 1000,
});

instance.defaults.withCredentials == true;
instance.defaults.headers.post["Content-type"] = "application/json;charset='UTF-8'";

instance.interceptors.request.use(
	(config: AxiosRequestConfig) => {
		if (config.method === "post") {
			const timeStamps = new Date().getTime();
			config.data = qs.stringify({
				...config.data,
				timeStamps,
			});
		}
		return config;
	},
	(err) => {
		return err;
	}
);

instance.interceptors.response.use(
	(res: AxiosResponse) => {
		const { status } = res;
		if (status >= 300 || status < 200) {
			const msg = showStatusMessage(status);
			if (typeof res.data === "string") {
				res.data = { msg };
			} else {
				res.data.msg = msg;
			}
		}
	},
	(err) => {
		err.data = {
			msg: "请求超时或服务器异常,请检查网络或联系管理员!",
		};
		return err;
	}
);

export default instance;
js
// src/utils/statusMessage.ts

interface IStatusMapping {
	[key: string]: string;
}
const showStatusMessage = (status: number | string): string => {
	const code = typeof status === "string" ? status : status.toString();
	const statusMapping: IStatusMapping = {
		400: "请求错误",
		401: "未授权,请重新登录",
		403: "拒绝访问",
		404: "请求出错",
		408: "请求超时",
		500: "服务器错误",
		501: "服务未实现",
		502: "网络错误",
		503: "服务不可用",
		504: "网络超时",
		505: "HTTP版本不受支持",
	};
	const message = statusMapping[code] ? statusMapping[code] : "未知异常,请检查网络或联系管理员!";
	return message;
};

export default showStatusMessage;
js
// src/utils/http.ts
import service from "./service";

interface IHttp {
	post<T>(url: string, params?: T): Promise<any>;
	get<T>(url: string, params?: T): Promise<any>;
	put<T>(url: string, params?: T): Promise<any>;
	delete<T>(url: string, params?: T): Promise<any>;
}

const http: IHttp = {
	post: (url, params) => {
		return service.post(url, params);
	},
	get: (url, params) => {
		return service.get(url, params);
	},
	put: (url, params) => {
		return service.put(url, params);
	},
	delete: (url, params) => {
		return service.delete(url, params);
	},
};

export default http;

路由配置

新建 router 文件夹

js
// src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import routes from "./routes";

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

export default router;
js
// src/router/routes
import { RouteRecordRaw } from "vue-router";

const routes: RouteRecordRaw[] = [
	{
		path: "/",
		redirect: { name: "loginPage" },
	},
	{
		path: "/login",
		name: "loginPage",
		component: () => import("@/views/loginPage.vue"),
	},
	{
		path: "/index",
		name: "homePage",
		component: () => import("@/views/homePage.vue"),
		children: [
			{
				path: "musicRecommended",
				name: "musicRecommended",
				component: () => import("@/components/common/musicRecommended.vue"),
			},
		],
	},
	{
		path: "/:path(.*)",
		name: "noFound",
		component: () => import("@/views/noFound.vue"),
	},
];

export default routes;

接口请求

新建 api 文件夹

js
// src/api/user.ts
import http from "@/utils/http"

export default class UserApi {
    public login<T>(params: T) {
        return http.post('/login', params)
    }
    public logout() {
        return http.post('./logout')
    }
    public getUserInfo<T>(params: T) {
        return http.post('/user/userDetail', params)
    }
}

状态管理

新建 store 文件夹

js
// src/store/user.ts
import { defineStore } from "pinia";

interface IState {
  userInfo: object;
}
const useUserStore = defineStore({
  id: "user",
  state: (): IState => ({
    return {
      userInfo: {
        age: 10
      },
    };
  }),
  getters: {
    getUserAge() {
      return this.state.userInfo.age
    }
  },
  actions: {
    setUserInfo(userInfo) {
      this.state.userInfo = userInfo
    }
  },
});

export default useUserStore;

全局挂载

js
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import router from "@/router";
import { createPinia } from "pinia";

const app = createApp(App);

app.use(createPinia());
app.use(router);
app.mount("#app");

环境配置

新建 .env.development、.env.production

js
//开发.env.development
VITE_MODE_NAME=development
VITE_RES_URL=https://www.xxxxx.com
VITE_APP_TITLE=项目标题
js
//生产.env.production
VITE_MODE_NAME=production
VITE_RES_URL=https://www.xxxxx.com
VITE_APP_TITLE=项目标题

新建 env.d.ts

js
// typings/env.d.ts
interface ImportMetaEnv {
	VITE_MODE_NAME: string;
	VITE_RES_URL: string;
	VITE_APP_TITLE: string;
}

使用

js
// 组件中:
import.meta.VITE_MODE_NAME;
import.meta.VITE_RES_URL;
import.meta.VITE_APP_TITLE;

// vite.config.ts 中
import { defineConfig, loadEnv } from "vite";
export default defineConfig(({ mode }) => {
	const env = loadEnv(mode, __dirname);
	return {
		// .........
	};
});

完整 vite.config.ts

ts
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import WindiCSS from "vite-plugin-windicss";
import { resolve } from "path";

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
	const env = loadEnv(mode, __dirname);
	return {
		plugins: [vue(), vueJsx(), WindiCSS()],
		server: {
			port: 3000,
			open: false,
			proxy: {
				"/api": {
					target: env.VITE_RES_URL,
					changeOrigin: true,
					rewrite: (url) => url.replace("/api", ""),
				},
			},
		},
		resolve: {
			alias: {
				"@": resolve(__dirname, "src"),
			},
		},
		css: {
			preprocessorOptions: {
				scss: {
					additionalData: '@import "@/assets/css/index.scss";',
				},
			},
		},
		build: {
			terserOptions: {
				compress: {
					// 生产环境去除 console debugger
					drop_console: true,
					drop_debugger: true,
				},
			},
		},
	};
});

UI 库

vue3 的 UI 组件库有挺多的,但是大部分是使用 sfc 的模式开发的组件,对于我们这个使用 tsx 的 vue3 项目可能不太友好,所以我推荐的组件库要求是内部使用 tsx 写法的,这样对于我们的开发不会有太大的不利影响

  • naive-UI
  • antd-desgin-vue
  • devui (华为的,目前还未开源)

以上 3 个组件库的搭建,都是采用 vue3 + tsx 的写法,其组件同时支持 tsx 和 sfc 的写法调用

本站中引用到的其他资料,如有侵权,请联系本人删除