技术栈选择
公司的技术栈主要是react,然后刚好有一个项目,看到vue3的发布一时手痒,直接开干,总结了一下项目的搭建以及一些坑(没配vuex是因为项目用不到)
项目搭建
vite 初始化
$ yarn create vite
# or
$ npm create vite
复制代码
根据提示选择需要的模板
安装router
$ yarn add vue-router@4
# or
$ npm install vue-router@4
复制代码
创建router文件 跟router3基本没有改变,hash和history必须引用
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const history = createWebHistory() // 如需hash替换成createWebHashHistory
//路由配置
const routes: RouteRecordRaw[] = [{
{
path: '/',
name: 'Login',
component: (): Promise<typeof import('*.vue')> => import('@pages/home/index.vue'),
meta: { title: '登录', keepAlive: true }
},
}]
const router = createRouter({
history,
scrollBehavior: () => ({ top: 0 }),
routes
})
export default router
复制代码
然后需要在mian.ts中进行全局挂载
import { createApp } from 'vue'
import App from './App.vue'
import router from './route'
const app = createApp(App)
app.use(router)
app.mount('#app')
复制代码
安装vant
$ yarn add vant@next -S
# or
$ npm install vant@next -S
复制代码
配置按需加载
# 安装插件
npm install vite-plugin-style-import -D
# or
$ yarn add vite-plugin-style-import -D
复制代码
在vite.config.ts
中配置
import vue from '@vitejs/plugin-vue';
import styleImport from 'vite-plugin-style-import';
export default {
plugins: [
vue(),
styleImport({
libs: [
{
libraryName: 'vant',
esModule: true,
resolveStyle: (name): string => `vant/es/${name}/style`
}
]
});
]
复制代码
移动端适配
# 安装插件
npm install postcss-pxtorem lib-flexible
# or
$ yarn add postcss-pxtorem lib-flexible
复制代码
在项目根目录中新建postcss.config.js
并配置如下:
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue({ file }) {
return file.indexOf('vant') !== -1 ? 37.5 : 75
},
propList: ['*']
}
}
}
复制代码
然后需要在mian.ts中引入
import 'lib-flexible'
复制代码
接下来就可以愉快的使用px
啦
axios
//安装
$ yarn add axios
# or
$ npm install axios
复制代码
创建utils文件夹 在其中新建request文件
状态码错误提示
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
const codeMessage: Record<number, string> = {
400: '请求错误',
401: '用户没有权限。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求是不存在的记录',
406: '请求的格式不可得。',
410: '请求的资源被永久删除',
422: '验证错误',
500: '服务器发生错误',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。'
}
复制代码
创建axios实例
其中 import.meta.env
是vite提供的环境变量在项目稍后会将如何配置,withCredentials
是否开启cookis共享,这里遇到一个问题开启withCredentials
以后会导致阿里oss图片上传会失败
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, // proxy 需要注释
// withCredentials: true,
timeout: 12000
})
复制代码
请求拦截
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// config
return config
},
(error: AxiosError) => {
return Promise.resolve(error || '服务器异常')
}
)
复制代码
响应拦截
service.interceptors.response.use(
(response: AxiosResponse) => {
const { status, message } = response.data
if (status !== 200) {
return Promise.reject(new Error(message || 'Error'))
}
return response.data
},
(error: AxiosError) => {
const { response } = error
if (response && response.status) {
const { status, statusText } = response
const errorText = codeMessage[status] || statusText
Toast(errorText)
} else if (!response) {
Toast('您的网络发生异常,无法连接服务器')
}
return Promise.reject(error)
}
)
export default service
复制代码
axios进阶版移除重复请求完整配置
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import qs from 'qs'
import { Toast } from 'vant'
const codeMessage: Record<number, string> = {
400: '请求错误',
401: '用户没有权限。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求是不存在的记录',
406: '请求的格式不可得。',
410: '请求的资源被永久删除',
422: '验证错误',
500: '服务器发生错误',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。'
}
const pending = new Map()
/**
* 添加请求
* @param {Object} config
*/
const addPending = (config: AxiosRequestConfig): void => {
const url = [config.method, config.url, qs.stringify(config.params), qs.stringify(config.data)].join('&')
config.cancelToken =
config.cancelToken ||
new axios.CancelToken((cancel) => {
if (!pending.has(url)) {
// 如果 pending 中不存在当前请求,则添加进去
pending.set(url, cancel)
}
})
}
/**
* 移除请求
* @param {Object} config
*/
const removePending = (config: AxiosRequestConfig): void => {
const url = [config.method, config.url, qs.stringify(config.params), qs.stringify(config.data)].join('&')
if (pending.has(url)) {
// 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
const cancel = pending.get(url)
cancel(url)
pending.delete(url)
}
}
/**
* 清空 pending 中的请求(在路由跳转时调用)
*/
export const clearPending = (): void => {
for (const [url, cancel] of pending) {
cancel(url)
}
pending.clear()
}
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, // proxy 需要注释
// withCredentials: true,
timeout: 12000
})
// 请求拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
removePending(config) // 在请求开始前,对之前的请求做检查取消操作
addPending(config) // 将当前请求添加到 pending 中
// config
return config
},
(error: AxiosError) => {
return Promise.resolve(error || '服务器异常')
}
)
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
removePending(response) // 如果存在就移除未的到响应的请求
const { status, message } = response.data
if (status !== 200) {
return Promise.reject(new Error(message || 'Error'))
}
return response.data
},
(error: AxiosError) => {
const { response } = error
if (response && response.status) {
const { status, statusText } = response
const errorText = codeMessage[status] || statusText
Toast(errorText)
} else if (!response) {
Toast('您的网络发生异常,无法连接服务器')
}
return Promise.reject(error)
}
)
export default service
复制代码
使用方法
在src
下文件新建api
文件夹
// index.ts 中
export interface HttpResponse {
status: number
success?: boolean
traceId?: string
data: any
}
export const getData = async (params: dataType): Promise<HttpResponse> => {
return request('api/yyyy', {
method: 'post',
data: params
})
}
复制代码
统一代码风格
- 团队开发,每个人编辑器不同,编码方式不同,导致代码格式不同,代码难看,难以维护
- 保持代码可读性,团队成员之间的代码更加易读
eslint
安装eslint
npm install eslint eslint-plugin-vue eslint-plugin-prettier eslint-config-prettier @typescript-eslint/parser @typescript-eslint --save-dev
# or
yarn add eslint eslint-plugin-vue eslint-plugin-prettier eslint-config-prettier @typescript-eslint/parser @typescript-eslint -D
复制代码
在根目录中创建.eslintrc.js
文件配置如下:
module.exports = {
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
extends: ['plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
rules: {
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 1,
'@typescript-eslint/no-explicit-any': 1,
'@typescript-eslint/no-var-requires': 1,
'@typescript-eslint/no-empty-function': 2,
'vue/custom-event-name-casing': 2,
'no-use-before-define': 2,
'@typescript-eslint/no-use-before-define': 2,
'@typescript-eslint/ban-ts-comment': 1,
'@typescript-eslint/ban-types': 1,
'@typescript-eslint/no-non-null-assertion': 2,
'@typescript-eslint/explicit-module-boundary-types': 2,
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
'@typescript-eslint/no-unused-vars': ['error'],
eqeqeq: 2,
camelcase: 1,
'use-isnan': 2,
'no-cond-assign': 2,
'no-unused-vars': 0,
'no-const-assign': 2,
'no-constant-condition': 2,
'no-delete-var': 2,
'no-dupe-keys': 2,
'no-else-return': 2,
'no-fallthrough': 1,
'no-func-assign': 2,
'no-implicit-coercion': 1,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-invalid-this': 2,
'no-multiple-empty-lines': [1, { max: 2 }],
'no-nested-ternary': 0,
'no-ternary': 0,
'no-unneeded-ternary': 2,
'no-var': 2
}
}
复制代码
在 package.json
内,配置 eslint
修复命令:
"scripts": {
"lint:eslint": "eslint src --fix --ext .ts,.tsx,.vue ",
},
复制代码
在vscode中下载eslint
插件来实现自动保存修复并且在vscode设置中的settings.json
配置:
"editor.formatOnSave": true, //每次保存自动格式化
"vetur.format.defaultFormatter.js": "vscode-typescript", //让vue中的js按编辑器自带的ts格式进行格式化
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
},
复制代码
项目根目录中新建.eslintignore
文件来忽略对一些文件的检查
*.sh
node_modules
*.md
*.scss
*.woff
*.ttf
/dist/
复制代码
prettier
安装prettier
npm install prettier lint-staged --save-dev
# or
yarn add prettier lint-staged -D
复制代码
在根目录中创建prettier.config.js
文件配置如下
module.exports = {
printWidth: 160, // 单行输出(不折行)的(最大)长度
tabWidth: 2, // 每个缩进级别的空格数
tabs: false, // 使用制表符 (tab) 缩进行而不是空格 (space)。
semi: false, // 是否在语句末尾打印分号
singleQuote: true, // 是否使用单引号
quoteProps: 'as-needed', // 仅在需要时在对象属性周围添加引号
bracketSpacing: true, // 是否在对象属性添加空格
jsxBracketSameLine: true, // 将 > 多行 JSX 元素放在最后一行的末尾,而不是单独放在下一行(不适用于自闭元素),默认false,这里选择>不另起一行
htmlWhitespaceSensitivity: 'css', // 指定 HTML 文件的全局空白区域敏感度, "ignore" - 空格被认为是不敏感的
trailingComma: 'none', // 去除对象最末尾元素跟随的逗号
useTabs: false, // 不使用缩进符,而使用空格
jsxSingleQuote: false, // jsx 不使用单引号,而使用双引号
arrowParens: 'always', // 箭头函数,只有一个参数的时候,也需要括号
rangeStart: 0, // 每个文件格式化的范围是文件的全部内容
proseWrap: 'always', // 当超出print width(上面有这个参数)时就折行
endOfLine: 'lf' // 换行符使用 lf
}
复制代码
在 package.json
内,配置 prettier
修复命令:
"scripts": {
"lint:prettier": "prettier --write \"src/**/*.{js,json,ts,tsx,css,less,scss,vue,html,md}\"",
},
复制代码
在vscode中下载Prettier - Code formatter
插件来实现自动保存修复
stylelint
// 安装
npm install stylelint stylelint-config-prettier stylelint-config-standard stylelint-order --save-dev
# or
yarn add stylelint stylelint-config-prettier stylelint-config-standard stylelint-order -D
复制代码
stylelint是不对scss
进行检查的,如果需要对scss
进行检查需要下载:
npm install stylelint-scss --save-dev
# or
yarn add stylelint-scss -D
复制代码
在根目录中创建.stylelintrc.js
文件配置如下:
module.exports = {
root: true,
plugins: ['stylelint-order'],
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
rules: {
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global']
}
],
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep']
}
],
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['tailwind', 'apply', 'variants', 'responsive', 'screen', 'function', 'if', 'each', 'include', 'mixin']
}
],
'no-empty-source': null,
'named-grid-areas-no-invalid': null,
'unicode-bom': 'never',
'no-descending-specificity': null,
'font-family-no-missing-generic-family-keyword': null,
'declaration-colon-space-after': 'always-single-line',
'declaration-colon-space-before': 'never',
// 'declaration-block-trailing-semicolon': 'always',
'rule-empty-line-before': [
'always',
{
ignore: ['after-comment', 'first-nested']
}
],
'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
'order/order': [
[
'dollar-variables',
'custom-properties',
'at-rules',
'declarations',
{
type: 'at-rule',
name: 'supports'
},
{
type: 'at-rule',
name: 'media'
},
'rules'
],
{ severity: 'warning' }
]
},
ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts']
}
复制代码
在 package.json
内,配置 stylelint
修复命令:
"scripts": {
"lint:stylelint": "stylelint src/styles/**/*.scss src/pages/**/*.scss --fix",
},
复制代码
在vscode中下载stylelint
插件来实现自动保存修复
项目根目录中新建.stylelintignore
文件来忽略对一些文件的检查
/dist/*
/public/*
public/*
node_modules/*
复制代码
husky
// 安装
npm install husky lint-staged --save-dev
# or
yarn add husky lint-staged -D
复制代码
执行 npx husky init
进行初始化,运行完成之后,我们可以看到项目根目录多了一个 .husky
的文件夹,里面自动创建了 pre-commit
和commit-msg
的钩子,如下图:
配置钩子
在 package.json
内,配置好 lint-staged
以及prepare
:
"scripts": {
"lint:lint-staged": "lint-staged",
"prepare": "husky install",
},
"lint-staged": {
"*.md": "prettier --write",
"*.{ts,tsx,js,vue,scss}": "prettier --write",
"*.{ts,tsx,js,vue}": "eslint --fix"
}
复制代码
在 .husky/pre-commit
内配置好要执行的命令:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
PATH=$PATH:/usr/local/bin:/usr/local/sbin
[ -n "$CI" ] && exit 0
npm run lint:lint-staged
复制代码
在 .husky/commit-msg
内配置要执行的命令:
#!/bin/sh
# shellcheck source=./_/husky.sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"
复制代码
环境变量
项目根目录下创建.env.development
,.env.test
,.env.production
文件分别对应本地,测试,正式环境的变量地址:
# just a flag
ENV = 'development' // 本地
ENV = 'staging' // 测试
ENV = 'production' // 线上
# base api
// 环境变量必须以vite命名否则无法拿到环境变量
VITE_APP_BASE_API = 'www.baidu.com'
复制代码
如果是使用ts进行开发需要类型校验,请在src
目录下新建env.d.ts
文件,配置如下:
interface ImportMetaEnv {
VITE_APP_BASE_API: string
}
复制代码
配置keep-alive和页面跳转动画
vue3开始router-view
外层不能包裹keep-alive
和transition
必须以slot
的形式来使用否则浏览器会有警告信息⚠️
<template>
<router-view v-slot="{ Component }">
<transition name="fade" mode="in-out">
<keep-alive v-if="$route.meta.keepAlive">
<component :is="Component" />
</keep-alive>
<component :is="Component" v-else-if="!$route.meta.keepAlive" />
</transition>
</router-view>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App'
})
</script>
<style lang="scss">
.fade-enter {
opacity: 0;
}
.fade-leave {
opacity: 1;
}
.fade-enter-active {
transition: opacity 0.3s;
}
.fade-leave-active {
opacity: 0;
transition: opacity 0.3s;
}
</style>
复制代码
vite.config配置
vite.config中默认是无法拿到环境变量的,如果需要获取环境变量需要配置以下:
import { loadEnv } from 'vite'
// 通过loadenv
loadEnv(mode, process.cwd()).VITE_APP_PATH
复制代码
配置路径别名
const resolve = (name: string): string => path.resolve(__dirname, name)
resolve: {
alias: [
{ find: '@src', replacement: resolve('src') },
]
},
复制代码
配置全局scss
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@styles/variables.scss";`
}
}
},
复制代码
配置gzip打包
yarn add vite-plugin-compression --sava-dev
# or
npm install vite-plugin-compression --sava-dev
复制代码
import viteCompression from 'vite-plugin-vconsole'
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz'
}),
复制代码
配置移动端调试vconsole
yarn add vite-plugin-vconsole --sava-dev
# or
npm install vite-plugin-vconsole --sava-dev
复制代码
import { viteVConsole } from 'vite-plugin-vconsole'
viteVConsole({
entry: resolve('src/main.ts'),
localEnabled: command === 'serve',
enabled: command === 'build' && mode === 'test',
config: {
maxLogNumber: 1000,
theme: 'light'
}
})
复制代码
build
如果遇到打包过后在安卓的微信中无法打开页面的兼容性问题请将target
改成es2015,rollup在打包的时候会警告包太大可以设置chunkSizeWarningLimit
,也可以在output
中切割打包生成的js大小
build: {
target: 'es2020',
outDir: 'dist',
assetsDir: 'assets',
chunkSizeWarningLimit: 1000,
cssCodeSplit: true,
sourcemap: false,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString()
}
}
}
},
terserOptions: {
compress: {
drop_console: true, //在打包过程去去除所有的console.log()
drop_debugger: true
}
}
},
复制代码
兼容性build
yarn add @vitejs/plugin-legacy --sava-dev
# or
npm install @vitejs/plugin-legacy --sava-dev
复制代码
legacy({
targets: ['ie >= 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
}),
复制代码
完整配置
import type { ConfigEnv, UserConfigExport } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import styleImport from 'vite-plugin-style-import'
import viteCompression from 'vite-plugin-compression'
import path from 'path'
import legacy from '@vitejs/plugin-legacy'
import { viteVConsole } from 'vite-plugin-vconsole'
const resolve = (name: string): string => path.resolve(__dirname, name)
export default ({ command, mode }: ConfigEnv): UserConfigExport => {
return defineConfig({
base: loadEnv(mode, process.cwd()).VITE_APP_PATH,
plugins: [
vue(),
styleImport({
libs: [
{
libraryName: 'vant',
esModule: true,
resolveStyle: (name): string => `vant/es/${name}/style`
}
]
}),
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz'
}),
legacy({
targets: ['ie >= 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
}),
viteVConsole({
entry: resolve('src/main.ts'),
localEnabled: command === 'serve',
enabled: command === 'build' && mode === 'test',
config: {
maxLogNumber: 1000,
theme: 'light'
}
})
],
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@styles/variables.scss";`
}
}
},
resolve: {
alias: [
{ find: '@src', replacement: resolve('src') },
]
},
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
chunkSizeWarningLimit: 1000,
cssCodeSplit: true,
sourcemap: false,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString()
}
}
}
},
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
server: {
open: true,
host: '0.0.0.0',
port: 7001
}
})
}
复制代码