美文网首页
Vue3 项目实战

Vue3 项目实战

作者: 欢欣的膜笛 | 来源:发表于2020-12-27 15:23 被阅读0次

    创建项目

    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<RouteRecordRaw> = [
        {
            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

    <template>
        <div v-if="!isFullScreen">header</div>
        <router-view/>
        <div v-if="!isFullScreen">footer</div>
    </template>
    
    <script lang="ts">
    import { computed } from 'vue'
    import { useRoute } from 'vue-router'
    
    export default {
        setup () {
            const route = useRoute()
            const isFullScreen = computed(() => !!route.meta.isFullScreen)
    
            return {
                isFullScreen,
            }
        },
    }
    </script>
    
    1. 封装公共提示信息组件,需涵盖不同提示场景
      • 使用 teleport ,将消息组件挂载到 body 下
      • 抽取 hooks,自动生成 teleport 的 dom
      • 封装消息组件的使用,使其成为一个独立的方法

    Message.vue

    <template>
        <teleport to="#message">
            <div
                v-if="isVisible"
                class="alert message-info fixed-top w-50 mx-auto d-flex justify-content-between mt-2"
                :class="classObject"
            >
                <span>{{message}}</span>
                <button
                    type="button"
                    class="close"
                    aria-label="Close"
                    @click.prevent="hide"
                >
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
        </teleport>
    </template>
    
    <script lang="ts">
    import { defineComponent, PropType, ref } from 'vue'
    import { useDOMCreate } from '@/hooks/useDOMCreate'
    import { MessageType } from './createMessage'
    
    export default defineComponent ({
        name: 'Message',
        props: {
            message: {
                type: String,
                required: true,
            },
            type: {
                type: String as PropType<MessageType>,
                default: 'default',
            },
        },
        emits: ['close-message'],
        setup (props, context) {
            useDOMCreate('message')
    
            const isVisible = ref(true)
            const classObject = {
                'alert-success': props.type === 'success',
                'alert-danger': props.type === 'error',
                'alert-primary': props.type === 'default',
            }
            const hide = () => {
                isVisible.value = false
                context.emit('close-message', true)
            }
    
            return {
                isVisible,
                classObject,
                hide,
            }
        },
    })
    </script>
    
    <style lang="scss" scoped>
    button.close {
        font-weight: 700;
        color: #686868;
        border: none;
        background-color: transparent;
        &:focus {
            outline: none;
        }
    }
    </style>
    

    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
    <template>
        <teleport to="#modal">
            <div v-if="visible" class="modal d-block">
                <div class="modal-dialog">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title">{{title}}</h5>
                            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                                <span aria-hidden="true" @click="onClose">&times;</span>
                            </button>
                        </div>
                        <div class="modal-body">
                            <slot></slot>
                        </div>
                        <div class="modal-footer">
                            <button type="button" class="btn btn-secondary" data-dismiss="modal"  @click="onClose">取消</button>
                            <button type="button" class="btn btn-primary"  @click="onConfirm">确定</button>
                        </div>
                    </div>
                </div>
            </div>
        </teleport>
    </template>
    
    <script lang="ts">
    import { computed, defineComponent } from 'vue'
    import { useDOMCreate } from '@/hooks/useDOMCreate'
    
    export default defineComponent ({
        name: 'Modal',
        props: {
            modelValue: {
                type: Boolean,
                default: false,
            },
            title: {
                type: String,
                default:'提示',
            },
        },
        inheritAttrs: false,
        emits: ['update:modelValue', 'on-close', 'on-confirm'],
        setup(props, context) {
            useDOMCreate('modal')
    
            const visible = computed({
                get: () => props.modelValue,
                set: val => context.emit('update:modelValue', val)
            })
    
            const onClose = () => {
                visible.value = false
                context.emit('on-close')
            }
    
            const onConfirm = () => {
                context.emit('on-confirm')
            }
    
            return {
                visible,
                onClose,
                onConfirm,
            }
        },
    })
    </script>
    

    相关文章

      网友评论

          本文标题:Vue3 项目实战

          本文链接:https://www.haomeiwen.com/subject/gmlagktx.html