创建项目
npm uninstall vue-cli -g
npm install -g @vue/cli
vue create project-name
pick a preset:Manually select features
1. Check the features: Choose Vue version + Babel + TypeScript + Router + Vuex + CSS Pre-processors + Linter / Formatter
2. Choose a version of vue.js: 3.0
3. Use class-style component syntax(是否使用Class风格装饰器): No
4. Use Babel alongside TypeScript: No
5. Use history mode for router: Yes
6. Pick a CSS pre-processorr: Sass/SCSS(with dart-sass)
7. Pick a linter / formatter config: ESLint + Standard config
8. Pick additional lint features: Lint and fix on commit
9. Where do you prefer placing config for Babel, ESLint, etc.: In dedicated config files
项目准备
- vscode 插件推荐
eslint、vetur、vscode-language-babel 等
若
eslint
不生效,可在.vscode
文件夹内新建setting.json
文件:{ "eslint.validate": ["typescript"] }
- 插件
npm install eslint-plugin-vuefix -D
npm install normalize.css --save
import 'normalize.css'
tips
- eslint 配置
.eslintrc.js
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'@vue/standard',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
plugins: ['vuefix'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
eqeqeq: [2, 'always'],
indent: ['error', 4],
'comma-dangle': 'off', // 允许行末逗号
'no-var': ['error'],
'linebreak-style': [0, 'error', 'window'],
'vue/comment-directive': 'off',
}
}
- 项目中大部分页面有共同的 header 和 footer,但是某些页面为全屏页面,如登录页、注册页等。可直接在路由中设置,App.vue 中增加判断。
- 页面权限也可在路由中判断。
路由配置:router -- index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
const routes: Array = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue'),
meta: { isFullScreen: true, redirectAlreadyLogin: true },
},
{
path: '/edit',
name: 'EditProfile',
component: () => import('../views/EditProfile.vue'),
meta: { requiredLogin: true },
},
{
path: '/:catchAll(.*)',
redirect: '/',
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
})
router.beforeEach((to, from, next) => {
const { token, user } = store.state
const { requiredLogin, redirectAlreadyLogin } = to.meta
// 已登录跳转到首页
const redirectAlreadyLoginCallback = () => {
if (redirectAlreadyLogin) {
next({ name: 'Home' })
} else {
next()
}
}
// 未登录跳转到登录页
const requiredLoginCallback = () => {
if (requiredLogin) {
next({ name: 'Login' })
} else {
next()
}
}
if (!user || !user.isLogin) {
if (token) {
// 已登录 && 无用户信息
axios.defaults.headers.common.Authorization = `Bearer ${token}`
// 查询用户信息
store.dispatch('queryUserInfo').then(() => {
redirectAlreadyLoginCallback()
}).catch(() => {
// 退出登录
store.commit('logout')
next({ name: 'Login' })
})
} else {
// 未登录
requiredLoginCallback()
}
} else {
// 已登录 && 有用户信息
redirectAlreadyLoginCallback()
}
})
export default router
跟组件:App.vue
header
footer
- 封装公共提示信息组件,需涵盖不同提示场景
- 使用 teleport ,将消息组件挂载到 body 下
- 抽取 hooks,自动生成 teleport 的 dom
- 封装消息组件的使用,使其成为一个独立的方法
Message.vue
useDOMCreate.ts
import { onUnmounted } from 'vue'
export const useDOMCreate = (domId: string) => {
const node = document.createElement('div')
node.id = domId
document.body.appendChild(node)
onUnmounted(() => {
document.body.removeChild(node)
})
}
createMessage.ts
import { createApp } from 'vue'
import Message from './Message.vue'
export type MessageType = 'success' | 'error' | 'default'
export const createMessage = (message: string, type: MessageType = 'default', timeout = 3000) => {
const messageInstance = createApp(Message, {
message,
type,
})
const node = document.createElement('div')
document.body.appendChild(node)
messageInstance.mount(node)
setTimeout(() => {
messageInstance.unmount(node)
document.body.removeChild(node)
}, timeout)
}
- 封装axios,需使用方便,且易于扩展
ajax.ts
import axios from 'axios'
import qs from 'qs'
import store from '@/store'
import { createMessage } from '@/components/createMessage'
// 根据response status 设置message
const getMessagegByStatus = (status: number) => {
let message = ''
switch (status) {
case 404:
message = '请求地址不存在'
break
default:
break
}
return message || '未知错误'
}
// 拦截器,处理 loading 和 error
axios.interceptors.request.use(config => {
store.commit('setLoading', true)
store.commit('setError', { status: false, message: '' })
return config
}, (error) => {
const { data } = error.request
return Promise.reject(data)
})
axios.interceptors.response.use(config => {
store.commit('setLoading', false)
return config
}, error => {
const { data, status } = error.response
let { error: message } = data
if (!message) {
message = getMessagegByStatus(status)
}
store.commit('setLoading', false)
store.commit('setError', { status: true, message })
createMessage(message, 'error')
return Promise.reject(data)
})
// 处理参数
const deleteEmptyParams = (obj: { [key: string]: any } = {}) => {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const element = obj[key]
if (element === '' || element === null || element === undefined) {
delete obj[key]
}
}
}
}
// 封装axios
export const ajax = {
get: async (url: string, params = {}) => {
deleteEmptyParams(params)
const { data: res } = await axios.get(url, { params })
return res
},
// 默认情况下,以 Content-Type: application/json;charset=UTF-8 格式发送数据
json: async (url: string, params = {}) => {
deleteEmptyParams(params)
const { data: res } = await axios.post(url, params)
return res
},
// 以 Content-Type: application/x-www-form-urlencoded 格式发送数据
post: async (url: string, params = {}) => {
deleteEmptyParams(params)
const { data: res } = await axios.post(url, qs.stringify(params))
return res
},
// 上传文件
upload: async (url: string, formData: FormData) => {
const { data: res } = await axios.post(url, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
return res
},
delete: async (url: string, params = {}) => {
deleteEmptyParams(params)
const { data: res } = await axios.delete(url, { params })
return res
},
/*
* PUT、PATCH区别:
* 对已有资源的操作:PATCH 用于资源的部分内容的更新;PUT 用于更新某个资源较完整的内容。
* 当资源不存在时:PATCH 可能会去创建一个新的资源,这个意义上像是 saveOrUpdate 操作;PUT 只对已有资源进行更新操作,所以是 update 操作
*/
put: async (url: string, params = {}) => {
deleteEmptyParams(params)
const { data: res } = await axios.put(url, params)
return res
},
patch: async (url: string, params = {}) => {
deleteEmptyParams(params)
const { data: res } = await axios.patch(url, params)
return res
},
}
- 子组件向父组件广播事件:
npm install mitt --save
父组件:
import mitt from 'mitt'
// 实例化 mitt
export const emitter = mitt()
export default defineComponent({
setup (props, context) {
const callback = () => {}
// 添加监听
emitter.on('item-created', callback)
// 移除监听
onUnmounted(() => {
emitter.off('item-created', callback)
})
},
})
子组件:
import { emitter } from './父组件.vue'
export default defineComponent ({
inheritAttrs: false,
setup (props, context) {
// 发射事件
onMounted(() => {
emitter.emit('item-created', () => {})
})
},
})
- markdown 转 HTML:
npm install markdown-it --save
import MarkdownIt from 'markdown-it'
const md = new MarkdownIt()
console.log(md.render(content))
- deep 用法变化
.create-post-page {
&:deep(.file-upload-container) {
height: 200px;
cursor: pointer;
overflow: hidden;
}
}
- 封装 modal 组件,实现双向绑定
Modal.vue