Vue3 项目实战

创建项目

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

项目准备

  1. vscode 插件推荐
    eslint、vetur、vscode-language-babel 等

eslint 不生效,可在 .vscode 文件夹内新建 setting.json 文件:{ "eslint.validate": ["typescript"] }

  1. 插件
npm install eslint-plugin-vuefix -D
npm install normalize.css --save
import 'normalize.css'

tips

  1. 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',
    }
}
  1. 项目中大部分页面有共同的 header 和 footer,但是某些页面为全屏页面,如登录页、注册页等。可直接在路由中设置,App.vue 中增加判断。
  2. 页面权限也可在路由中判断。
    路由配置: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




  1. 封装公共提示信息组件,需涵盖不同提示场景
    • 使用 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)
}

  1. 封装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
    },
}
  1. 子组件向父组件广播事件: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', () => {})
        })
    },
})
  1. markdown 转 HTML:npm install markdown-it --save
import MarkdownIt from 'markdown-it'

const md = new MarkdownIt()
console.log(md.render(content))
  1. deep 用法变化
.create-post-page {
    &:deep(.file-upload-container) {
        height: 200px;
        cursor: pointer;
        overflow: hidden;
    }
}
  1. 封装 modal 组件,实现双向绑定
    Modal.vue



你可能感兴趣的:(Vue3 项目实战)