Write and share what I see and hear
详细记录搭建vue3 + ts 的基础项目的过程
代码不涉及页面逻辑相关内容
点击访问本文代码地址
项目使用 Vue 3 + Typescript
记录当前项目创建流程
参考 Vite 官网
执行 npm init vite@latest
项目初始化后,在 src/ 下新建如下文件夹
api 接口封装的模块
assets 接口相关
components 组件
composables 封装的组合式API函数
layout 布局组件
plugins 注册给vue的插件(实例原型方法、全局指令等)
router 路由
store vuex模块
styles 全局样式
utils 工具相关模块
views 路由页面
vite 创建的项目默认没有集成 eslint
当前没有提供可用的插件,可以手动添加 eslint 配置
参考 Eslint 官网
安装 eslint
npm install eslint --save-dev
执行
npx eslint --init
个人选择
✔ How would you like to use ESLint? · style
✔ Which framework does your project use? · vue
✔ Does your project use TypeScript? · Yes
✔ Where does your code run? · browser
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · airbnb
✔ What format do you want your config file to be in? · JavaScript
✔ Would you like to install them now with npm? · Yes
添加脚本
package.json
"scripts": {
...
"lint": "eslint ./src/**/*.{vue,ts} --fix"
}
修改 eslint 配置
参考 eslint-plugin-vue 中的 Bundle Configurations
vue3 规则配置
extends: [
// 'plugin:vue/essential',
'plugin:vue/vue3-strongly-recommended'
],
rules: {
semi: ['error', 'never']
}
在 vscode 中校验代码,起到辅助作用,提高编写效率
- 只要安装并启用了这个插件,它就会自动查找项目中的 eslint 配置规范,并且给出验证提示
- 如何格式化?
Eslint 提供了
vscode => 设置 => 搜索Eslint
修改Eslint ›
Format: Enable ✔
Eslint: Run onType (编写代码中校验)
添加 git Commit Hook,commit 时执行 lint 命令,校验代码格式,避免不符合规范的代码提交到远程仓库中
参考 lint-staged
安装 lint-staged
npx mrm@2 lint-staged
安装后可以在 package.json 中看到新增了两个依赖 husky 和 lint-staged
脚本中会多个命令 {…“prepare”: “husky install”}
初始的时候会执行 husky install,把相应的钩子脚本初始化到 git 仓库内(本地),此时在本地仓库中就有了 husky 的钩子
如果不是初始化项目,需要手动添加该脚本,确保其他人在安装依赖时能够有 husky
配置lint-staged
package.json
"lint-staged": {
"*.{js,jsx,vue,ts,tsx}": [
"npm run lint",
"git add"
]
}
Vite 目前没有提供这样的插件,需要手动配置
可以参照 Vite 插件 进行配置
npm install vite-plugin-eslint --save-dev
...
import eslintPlugin from 'vite-plugin-eslint'
plugins: [
...,
eslintPlugin()
]
相关规范描述 参考 阮一峰 Commit message 和 Change log 编写指南
规范 commit 格式
按照格式添加 commit,在某些情况下还可以自动生成版本差异文档
feat、fix可以出现在版本差异文档中
添加强制验证工具
参考commitlint
安装
npm install --save-dev @commitlint/{config-conventional,cli}
执行
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
执行
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
规范参考 commitlint规范
参考 Vite-typescript
配置
tsconfig.json
"compilerOptions": {
...,
"isolatedModules": true
}
项目内使用 .vue 文件,vue文件后缀名需要写上
参考Vue3-typescript
在组合式 API 中使用
import {
ref, defineProps, onMounted, PropType
} from 'vue'
interface IUser {
name: string,
age: number
}
const props = defineProps({
obj: {
// PropType 转换 props 类型
type: Object as PropType<IUser>,
required: true
}
})
const title = ref<HTMLTitleElement | null>(null)
onMounted(() => {
console.log(title.value)
})
import { ref, onMounted } from 'vue'
// InstanceType 运算类型
const helloWorld = ref<InstanceType<typeof HelloWorld> | null>(null)
onMounted(() => {
console.log('helloWorld', helloWorld.value)
})
组合式 API 语法糖: script-setup
可参考 script-setup
import {
defineEmits
} from 'vue'
// 自定义事件
const emit = defineEmits(['increment'])
const increment = () => {
emit('increment')
}
script-setup 支持顶层的 async 函数,在 script-setup 内可以直接使用 await
script-setup 可以和共存
script-setup 中内置的有编译宏
包括 defineProps defineEmits defineExpose withDefaults
配置参考 eslint-plugin-vue
.eslintrc.js
env: {
...,
'vue/setup-compiler-macros': true
}
或者
.eslintrc.js
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly'
}
使用 JS/TS 方式渲染页面,例如递归生成模版,使用JSX编程性更强
建议: 大多数情况下建议使用 template
参考 vue3-jsx
babel-plugin-jsx
插件参考 vite plugin-vue-jsx
安装
npm install @vitejs/plugin-vue-jsx -D
配置
vite.config.js
import vueJsx from '@vitejs/plugin-vue-jsx'
plugins: [
...,
vueJsx()
]
使用
Hello World
foo.tsx 直接导入到 .vue 文件中使用
// 直接导出
// export default () => (
//
// foo 组件
//
// )
// 获取状态
import { defineComponent, ref } from 'vue'
interface PropsType {
msg: string
}
// setup 语法
export default defineComponent({
props: {
msg: {
type: String,
default: 'msg2'
}
},
setup() {
const count = ref(10)
return (props: PropsType) => (
foo 组件msg{props.msg}
foo 组件count{count.value}
)
}
})
// export default defineComponent({
// props: {
// msg: {
// type: String,
// default: 'msg2'
// }
// },
// data() {
// return {
// count: 10
// }
// },
// render() {
// return (
//
// foo 组件msg{this.msg}
// foo 组件count{this.count}
//
// )
// }
// })
Vite 创建的项目默认没有集成 VueRouter,需要手动配置
参考 VueRouter
npm install vue-router@4
npm install eslint-import-resolver-typescript -D
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: () => import('../views/home/index.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/login/index.vue')
}
]
const router = createRouter({
history: createWebHashHistory(), // 路由模式
routes // 路由规则
})
export default router
main.ts
import router from './router'
createApp(App)
.use(router)
.mount('#app')
views/home/index.vue
Home
views/login/index.vue
Login
App.vue
.eslintrc.js
rules: {
...,
'import/no-unresolved': [2, { ignore: ['^@/'] }],
'import/extensions': [
2,
'ignorePackages',
{
js: 'never',
ts: 'never',
tsx: 'never',
json: 'never'
}
],
'vue/multi-word-component-names': 'off'
},
settings: {
'import/resolver': {
typescript: {}
}
}
Vite 创建的项目默认没有集成 Vuex
参考 Vuex
npm install vuex@next --save
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
state() {
return {
count: 0
}
},
mutations: {
increment(state) {
// eslint-disable-next-line no-param-reassign
state.count += 1
}
}
})
export default store
main.ts
import store from './store'
createApp(App)
.use(router)
.use(store)
.mount('#app')
views/home/index.vue
Home
{{ store.state.count }}
Vuex4 依然没有很好的解决 TS 类型问题,官方宣称会在 Vuex5 中提供更好的方案
参考 vuex typescript
// vuex.d.ts
/* eslint-disable no-unused-vars */
import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'
declare module '@vue/runtime-core' {
// 声明自己的 store state
interface State {
count: number
}
// 为 `this.$store` 提供类型声明
// eslint-disable-next-line no-shadow
interface ComponentCustomProperties {
$store: Store<State>
}
}
views/home/index.vue
Home
{{ $store.state.count }}
简化写法
src/store/index.ts
...
export interface IState {
count: number
}
// 创建一个新的 store 实例
const store = createStore<IState>({
...
})
src/vuex.d.ts
...
import { IState } from './store'
declare module '@vue/runtime-core' {
...
interface ComponentCustomProperties {
$store: Store<IState>
}
}
import { InjectionKey } from 'vue'
import { createStore, Store } from 'vuex'
// 为 store state 声明类型
export interface IState {
count: number
}
// 定义 injection key
export const key: InjectionKey<Store<IState>> = Symbol('store')
...
然后,将 store 安装到 Vue 应用时传入定义好的 injection key
src/main.ts
...
import { store, key } from './store'
createApp(App)
.use(router)
.use(store, key)
.mount('#app')
views/home/index.vue
简化 useStore 用法
src/store/index.ts
...
import { createStore, Store, useStore as baseUseStore } from 'vuex'
...
// 定义自己的 `useStore` 组合式函数
export function useStore() {
return baseUseStore(key)
}
views/home/index.vue
使用路径别名避免使用大量的相对路径
Vite 中默认没有配置路径别名,使用时需要手动配置
参考 vite resolve-alias
vite.config.ts
// eslint 报错
// 安装(可以不安装) npm install -D @types/node
// 在 tsconfig.node.json 中添加
// "compilerOptions": {
// "allowSyntheticDefaultImports": true,
// },
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
...
})
tsconfig.json
"compilerOptions": {
...
"baseUrl": "./",
"paths": {
"@/*": [
"src/*" // 相对位置需要配置baseUrl才能识别,否则会报错
]
}
}
vite Css
vite Postcss
由于 Vite 的目标仅为现代浏览器,因此建议使用原生 CSS 变量和实现 CSSWG 草案的 PostCSS 插件(例如 postcss-nesting)来编写简单的、符合未来标准的 CSS。
vite Css预处理器
建议使用 scss/sass
安装npm install -D sass
Vite 为 Sass 和 Less 改进了 @import 解析,以保证 Vite 别名也能被使用。另外,url() 中的相对路径引用的,与根文件不同目录中的 Sass/Less 文件会自动变基以保证正确性。
常见的工作流程是,全局样式都写在 src/styles 目录下,每个页面自己对应的样式都写在自己的 .vue 文件中
styles/index.scss
// 组织统一导出
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './common.scss';
main.ts
// 加载全局样式
// 只能加载非变量的样式
import './styles/index.scss'
// 全局 Sass 变量
$color: red;
views/home.index.vue
利用构建工具自动注入到单文件组件中
css-preprocessoroptions
vite.config.ts
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
// additionalData: `$injectedColor: orange;`
additionalData: '@import "@/styles/variables.scss";'
}
}
}
})
views/home.index.vue
参考 axios
安装 axios
npm install axios
使用
utils/request.ts
import axios from 'axios'
const request = axios.create({
// baseURL: 'http://localhost:3001'
baseURL: 'https://shop.fed.lagounews.com'
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 统一设置用户身份
console.log(config)
return config
},
(error) => {
// Do something with request error
console.log(error)
return Promise.reject(error)
}
)
// Add a response interceptor
request.interceptors.response.use(
(response) => {
// 统一处理接口响应错误
console.log(response)
return response
},
(error) => {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
console.log(error)
return Promise.reject(error)
}
)
export default request
api/common.ts
/**
* 公共基础请求模块
*/
import request from '@/utils/request'
// eslint-disable-next-line import/prefer-default-export
export const getLoginInfoService = () => request({
method: 'GET',
url: '/login/info'
})
views/login/index.vue
api/common.ts
export const getLoginInfoService = () => request.get<{
code: number
data: {
login_logo: string
}
message: string
}>('/login/info')
views/login/index.vue
再次封装
api/common.ts
interface ResponseData<T = any> {
code: number
message: string
data: T
}
// eslint-disable-next-line import/prefer-default-export
export const getLoginInfoService = () => request.get<ResponseData<{
login_logo: string
}>>('/login/info')
api/common.ts
interface ResponseData<T = any> {
code: number
message: string
data: T
}
// eslint-disable-next-line import/prefer-default-export
export const getLoginInfoService = () => request.get<ResponseData<{
login_logo: string
}>>('/login/info').then(res => res.data.data)
views/login/index.vue
再次封装
utils/request.ts
import axios, { AxiosRequestConfig } from 'axios'
...
interface ResponseData<T = any> {
code: number
message: string
data: T
}
export default <T = any>(config: AxiosRequestConfig) => request(config)
.then(res => res.data as ResponseData<T>)
api/common.ts
export const getLoginInfoService = () => request<{
login_logo: string
}>({
method: 'GET',
url: '/login/info'
})
提取接口类型,方便类型重用
接口命名建议以 I 开头
api/types/common.ts
export interface ILoginInfo {
login_logo: string
}
api/common.ts
import { ILoginInfo } from './types/common'
// eslint-disable-next-line import/prefer-default-export
export const getLoginInfoService = () => request<ILoginInfo>({
method: 'GET',
url: '/login/info'
}).then(res => res.data)
views/login/index.vue
开发、测试、预发、生产等不同的环境会有不同的变量
根据不同环境需要对这些变量进行配置
参考 环境变量和模式
Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量:
import.meta.env.MODE: {string} 应用运行的模式。
import.meta.env.BASE_URL: {string} 部署应用时的基本 URL。他由base 配置项决定。
import.meta.env.PROD: {boolean} 应用是否运行在生产环境。
import.meta.env.DEV: {boolean} 应用是否运行在开发环境 (永远与 import.meta.env.PROD相反)。
使用 .env 文件 进行配置
加载的环境变量也会通过 import.meta.env 暴露给客户端源码。
Vite 使用 dotenv 从你的 环境目录 中的下列文件加载额外的环境变量:
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.development # 开发模式下加载(执行 vite dev 时,会自动加载)
.env.production # 生产模式下加载(执行 vite build 时,会自动加载)
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。例如下面这个文件中:
DB_PASSWORD=foobar
VITE_SOME_KEY=123
只有 VITE_SOME_KEY 会被暴露为 import.meta.env.VITE_SOME_KEY 提供给客户端源码,而 DB_PASSWORD 则不会。
.env.development
# 开发模式下加载(执行 vite dev 时,会自动加载)
VITE_API_BASEURL=http://localhost:3001
env.d.ts
interface ImportMetaEnv {
readonly VITE_API_BASEURL: string
readonly VITE_API_PROJECTNAME: string
// 更多环境变量...
}
utils/request.ts
const request = axios.create({
// baseURL: 'http://localhost:3001'
baseURL: import.meta.env.VITE_API_BASEURL
})
环境变量文件全部放到外面看起来有些多,不便于区分环境,可以配置到一个文件夹里面,便于管理
参考 envDir
配置 envDir
export default defineConfig({
envDir: path.resolve(__dirname, 'environment')
})
CORS 全称为 Cross Origin Resource Sharing(跨域资源共享)。这种方案对于前端来说没有什么工作量,和正常发送请求写法上没有任何区别,工作量基本都在后端(其实也没啥工作量,就是配置一些 HTTP 协议)。
在开发模式下可以下使用开发服务器的 proxy 功能,比如 vite-server.proxy
但这种方法在生产环境是不能使用的。在生产环境中需要配置生产服务器(比如 nginx、Apache 等)进行反向代理。在本地服务和生产服务配置代理的原理都是一样的,通过搭建一个中转服务器来转发请求规避跨域的问题。
vite.config.ts
export default defineConfig({
server: {
proxy: {
// 字符串简写写法
// '/foo/123': 'http://localhost:4567/foo/123',
'/foo': 'http://localhost:4567/foo',
// 选项写法
'/api': {
target: 'http://jsonplaceholder.typicode.com', // 代理的目标地址
// changeOrigin 兼容基于名字的虚拟主机
// a.com localhost:xxx
// b.com localhost:xxx
// HTTP 请求头部的 origin 字段
// 我们在开发模式: 默认的 origin 是真实的 origin: localhost:3000
// changeOrigin: true, 代理服务会把 origin 修改为目标地址 http://jsonplaceholder.typicode.com
changeOrigin: true,
// rewrite 路径重写
// http://jsonplaceholder.typicode.com/api/xxx
// /api/xxx => http://jsonplaceholder.typicode.com/api/xxx
// 可以不设置 rewrite
// /api/xxx => http://jsonplaceholder.typicode.com/xxx
// apiPath.replace(/^\/api/, '')
rewrite: apiPath => apiPath.replace(/^\/api/, '')
}
}
}
})
environment/.env.development
# 开发模式下加载(执行 vite dev 时,会自动加载)
# VITE_API_BASEURL=http://localhost:3001
VITE_API_BASEURL=/api
tsconfig.json 注释
{
"compilerOptions": {
"target": "esnext", // 编译目标ES版本
"useDefineForClassFields": true,
"module": "esnext", // 编译目标模块系统
"moduleResolution": "node", // 模块解析策略
"strict": true, // 严格模式开关 等价于noImplicitAny、strictNullChecks、strictFunctionTypes、strictBindCallApply等设置true
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true, // 许编译生成文件时,在代码中注入工具类(__importDefault、__importStar)对ESM与commonjs混用情况做兼容处理
"lib": ["esnext", "dom"], // 编译过程中需要引入的库文件列表
"isolatedModules": true,
"allowSyntheticDefaultImports": false, // 允许从没有设置默认导出的模块中默认导入,仅用于提示,不影响编译结果
"baseUrl": "./",
"paths": {
"@/*": [
"src/*" // 相对位置需要配置baseUrl才能识别,否则会报错
]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}