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<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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容