Vue 工程化最佳实践

目录结构

总览

  • api目录用于存放 api请求,文件名与模型名称基本一致,文件名使用小驼峰, 方法名称与后端restful控制器一致.

  • enums 目录存放 常量, 与后端的常量目录对应

  • icons目录用于存放图标, element-ui提供的图标实在是太少啦.所以我通常会使用 阿里的iconfont

  • lang目录存放多语言

  • layouts目录存放布局

    • 上面展示的是一个后台系统, empty为一个空布局.用于登录页面, 其他页面则使用default布局. 布局不需要过多介绍,写过laravel blade都很熟悉了.这里的布局需要和vue-router配合使用
  • mixins 类似php的trait, 但是它更强大, 完整贴合vue组件的生命周期

  • plugins 目录存放插件配置, 比如 axios,vue-lazy等 (这是从nuxt中学到的概念)

  • router目录存放与 前端路由相关的配置,总体来说类似于laravel的api层

  • store 目录即vuex的目录, 类似于前端的model. 其文件与后端model相匹配,采用小驼峰命名

  • utils 目录存放辅助函数

  • views 为业务视图层,相信后端同学也很熟悉.其由vue-router直接调度

  • main.js 为app的入口, 类似于后端的index.php

  • components 目录, 存放组件.通常是一些可复用的组件会单独存放在该目录

总体来说, 已后端的mvc思想来看现代的前端项目是非常的自然的. 后端的model对应前端的store, 后端的router对应前端的router,后端的controller + views 对应前端的views.

基础规范

就目前来说 vue项目很少用到 class, 因此 .js文件通常都是一个 module, 所以文件名使用小驼峰的形式命名. 如果有类文件,则类文件使用大驼峰的形式命名.

.vue文件 可以使用 中划线和大驼峰两种命名方式, 参考了element/iview/nuxt项目之后, 推荐统一使用中划线命名.

所有的文件夹名称统一使用中划线命名

引入vue组件时文件时需要转换成大驼峰import 'TestTest' from '@/components/test-test'

在template 使用时依旧使用中划线

<test-test />

其他规范如变量命名和使用规范 使用eslint的standard 即可很好的解决.

前端存在很多的事件 如change/input/upload/sumit等等,相应的处理推荐使用 handle + 事件名称, 如handleChange

生命周期

vue-router 解析当前用户键入的 url, 然后匹配合适的视图组件加载.

着重介绍一下 我对views目录下的视图组件的理解,已修改地址为例

script部分既控制器部分,其请求数据, 然后注入到view 中, 就像后端的mvc一样.只不过vue将 vc 其写入到了一个文件中.这样理解对于写过后端的同学显得更加的自然

控制器如何获取数据?

在过去的vue项目中,我们可能会见到这样的写法

// ... views/address/edit.vue
created () {
    axios.get('/addresses/1')
        .then((response) => {
             this.list = resposne.data              
        })
}

//...

这种写法无异于后端在控制器中写sql语句一样,在工程化实践中不推荐这么做,后端通过model来获取数据会更加的优雅自然.在vue项目中, model既vuex,因此推荐这么做

如果你对我说的东西一脸懵逼, 那么你可以看一下 vuex的文档. 我现在做的就是用后端熟悉的概念,来描述前端项目的最佳实践

// ... views/address/edit.vue 控制器+视图
computed: {
    address: () => this.$store.address.itemBy[1] // 从store模型中取出我们想要数据
}
// ...
// ... store/modules/address.js  数据源
export default {
    state: {
        itemBy: {}
    },
    actions: {
        ...
    },
    mutations: {
        ...
    }
}
// ...
store中的数据从哪里来?

数据当然是从后端的数据库中获取,我们不能让前端直接访问我们的数据库,因此我们会提供api让前端访问.store中存在一个发起api请求的地方,既 action.

// ... store/modules/address.js  模型
export default {
    state: {
        itemBy: {}
    },
    actions: {
        async fetchItem({ commit, state }, { id }) {
             // 对axios和api进行了简单的封装,使api请求更加语义化
            cosnt { data } = await address.show(id)

            // action只能通过提交commit来修改state,具体原因请查看vuex文档 (其实我也忘了为啥 (╯﹏╰))
            commit('SET_ITEM', data) 
        }
    },
    mutations: {
        SET_ITEM: (state, item) => {
            state.itemBy[item.id] = item
        }
    }
}
// ...

这样我们的模型中就有数据啦

什么时候去调用fetchItem去请求后端呢?

https://router.vuejs.org/zh/guide/advanced... vue-router文档的解答

这里不推荐在vue原始的生命周期中去调用初始化请求,可能会带来 数据还没有获取到,template却已经被渲染.会造成一些数据不存在的异常,推荐在vue-router的生命周期中去请求数据

// ... views/address/edit.vue
async beforeRouteEnter (to, from, next) {
    // 等待模型数据加载完毕,才继续进行vue组件的生命周期
    await store.dispatch('fetchItem', to.params.id) 

    next()
}
created () {
    // 不推荐在这里调用 fetchItem
}

//...

到这里你可能发现,这和你平时写的vue有些不一样, 没有类似 this.data = response.data 这种操作. 类似这种操作其实类似赋值操作,或者称为副作用,其引入了时间的概念,使数据的管理变的复杂. 直观的体现就是我们可能会有这种多余的 if(data)判断.

当然副作用是难以避免的,但是我们可以统一的管理他们.类似上面的代码就是一套我觉得还不错的方法.从view的角度看, 数据是固有存在存在的,其不需要关心是否是否已经被加载完毕,且store中的不可被view修改,既数据只能单向流动

在store中统一管理数据的另外一个好处就是方便持久化

view层如何修改数据源?

上面的描述实际上表达了一种 发布与订阅的模式, 从store到view的数据流是严格单向数据流动.

view层不允许直接修改store中的数据,但是view层却可以通过发送action来影响数据源.

比如初始化时的dispatch的action,各种event触发的dispatch. 当数据源发生改变时,作为订阅者的view层会非常自然的重新渲染.

这种设计和父子组件类似,vue中子组件不允许直接修改父组件props到子组件的数据,只能通过向父组件emit event. 在view和store之间,这种设计依然合理.

这也意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。

上面的图很好的阐述了这种开发模式. 引自 https://github.com/sorrycc/blog/issues/1

view层再深入

view层的script部分,除了充当了传统的controller,起到初始化的作用外,实际上还做了更多的事情.

先从data部分说起,在view层会有一些状态需要记录, 如 菜单的展开或收起, 弹窗的弹出与关闭. 对于这样的状态的管理,一种做法就是将存储在data部分.

也有人将所有的状态 也放在 store中的state维护. 既state分为状态和数据 两种类型.

view层的script更重要的部分,是其到了一个交互反馈的作用, 既类似下面的代码

<template>
    <button @click="handleSubmit"/>
</template>

<script>
    export default {
        data: {},
        methods: {
            handleSubmit() {

            }
        }
    }
</script>

关于css部分,由于个人不了解css,也不清楚css的业界规范及在vue上的最佳实践,因此不做过多介绍.

总结一下

在前面的介绍中, store和api 目录是和数据挂钩的,当数据库定下来,这一部分也就定了下来.

views/components 和业务(ui)挂钩,需要等设计稿确定后,这一部分才能确定下来.

PS设计稿的图层通常就是组件的拆分规范 ?
使用vuex存储数据的另一个好处就是可以无缝的切换到ssr框架nuxt

views和store之间是一种订阅和发布的模式.

两个问题

store中的state应该如何组织? 对于api请求,我们经常会看到这样的json数据

// post
{
    id: 1,
    title: xxxx,
    content: xxxx,
    user: {
        id: xxx,
        nickname: xxx,
        avatar: xxx,
    },
    comments: [
        {
            id: xxx,
            user_id: xxx,
            content: xxx,
            user: {
                // ...
            }
        },
        {
            //....
        }
    ]
}

上面的数据结构复杂,嵌套深入. 如果我们将他们一股脑的存在 post的state中,会造成数据过于集中,冗余. comments无法独立化更新等等问题. 使得前端 scheme/orm,数据组织的规范化变的迫切需要

但是vue在这方面没有很好的规范和最佳实践. react在这方面比较不错的实践 https://github.com/paularmstrong/normalizr

如何设计良好规范的compoents?

组件的设计在业务层非常的重要,在下一篇我会介绍一下我总结出的一些实践

本作品采用《CC 协议》,转载必须注明作者和本文链接
我正在全力开发 nature 编程语言,如果我的文章对你有帮助,希望能获得一个 star,这对我的帮助非常大。
本帖由系统于 6年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 27

NB,结构很棒,我喜欢!

6年前 评论

NB,结构很棒,我喜欢!

6年前 评论

之前dispatch数据初始化的Action都被我放到created钩子了(导致模板里面写了不少判断),看到你这篇文章才发现放到路由钩子里面更好。
赞!

6年前 评论

学习了,一直觉得 vue react 的脚手架构建的项目目录不够工程化,github 也没找到满意的,vue cli 3 也来一个吧

6年前 评论

没看懂,但是听说多练习,就能了解到奥妙,奥妙全自动 :joy:

6年前 评论

@arfurs 我之前和你一样问题困扰.
同事用nova的时候发现, 作者把api请求都放在了路由中,然后我去读了一下 vue-router的文档.果然如此哈哈

6年前 评论

@Nick 关于目录结构我参考了一些开源项目, 比如views目录就是参考vue-hacknews, plugins目录是参考的nuxt这个ssr框架,plugins做一些外来组件的注册和初始化.
关于目录这块,尽量优先参考普遍使用的目录和结构. 但是也不要害怕自已添加一些自己认为需要的目录
比如layouts/enums 在我看来都是有必要单独存放的.
另外还有一些可以添加的目录比如 middlewares. 作用和laravel的middleware一样.(这个同样是参考nuxt)

6年前 评论

@Max handleChange 这个命名方式应该是参考 react 的 :smile: 另外,组件引入使用命名应该参考 vue 规范好点吧
file

6年前 评论

@Nick handleXxx确实是参考的react哈哈. 这个规范我看了. 然后综合考虑了element/iview 以及结合 views和nuxt的pages(页面决定 url, url采用中划线). 决定使用 中划线命名vue组件, 一致性是最强的

6年前 评论

为什么所有api获取的数据都要存到vuex中?

6年前 评论

@largezhou

  1. 应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。
  2. 无缝切换到服务端渲染
  3. 数据与业务分类,使view层更加的专注于业务,且store对于view来说是抽象的, view并不关心store从哪里获得的数据,怎么获得的数据, view只知道我应该在这里拿数据,不需要关心其他的事情.
6年前 评论
nff93

@Max 其实如果不是全局或者多个视图都需要的数据,没有必要放在 vuex 里。

比如文章详情页,获取文章数据应该直接从 api/posts 里的 show 获取,然后数据放在当前组件内而不是 vuex。而获取授权数据才需要在 vuex 里获取。

当然,如果实在要把所有数据放 vuex,记得每次组件销毁的时候手动清理下数据。

6年前 评论

能否把仓库地址放出来,让大家学习下,谢谢

6年前 评论

@nff93
手动清理的理由是什么?
比如文章详情, 我点了 post = 1那么vuex中会有

itemBy = {
    1 : {...},
}
当我紧接着点了post2,那么vuex中的post model会变成
itemBy = {
   1 : {...},
   2: {...},
}

如果是因为那几kB的内存占用.让我们需要手动清理数据的话,实在是没有必要


大概一年多以前,我在纠结,vuex的作用到底是什么, 我到底应该放些什么进去? 只是把全局的登录状态放进去吗(目前很多vue项目确实如此)?

再细想一下,vuex存在的意义是什么?一个东西存在的目的往往都是为了解决某些问题而产生的.

实际上vuex的文档过于简单了, 其实我们都知道vuex,vue-router,nuxt是如何产生的. vue照搬了react的生态(redux,react-router,next).但是确没有好好把文档搬过来.

实际上我的文章中已经很充分给出了为什么要这么使用vuex的理由.

我完全没有理由说服你,并不会给我带来一点点的好处,你依旧可以按照你认为的样子做vue的开发

but谁让这里是laravel社区呢😁

在掘金很多人提出了和你一样的观点,但是我没有做任何回复,其实程序员内心都是很固执骄傲的,每个人心中都有自己的答案,一旦肯定之后就很难再有变化了.

我的意思已经表达的很清楚了,不妨直接看看redux的文档吧https://www.redux.org.cn/

我们有权利选择不使用用vuex,让项目更加的简单.
但是如果需要使用,那就应该展现它应该有的样子

6年前 评论

@键盘侠 实际上上面的代码截图是在用的商业项目, 我有空会补一份demo的哈哈

6年前 评论

我也是这么做的,我一开始设计UI的目录结构,也是考虑到把和API的操作都封装到store里面,不过store是有limit的,
再者store和api的数据的同步也是需要考虑的

6年前 评论
nff93

@Max 我知道你在当前页面切换会覆盖数据鸭。我前面页说了在组件销毁的时候清理,你举的这个列子组件无论怎么切换,组件都不会销毁。

个人觉得 vuexredux 这类东西的出现就是为了解决全局的状态问题的,组件里的状态还是应该只在组件里,这个在 react 新出的 hooks 里也能体现。


另外你我不赞同你回复里分割线下的部分言论,我只是在正常讨论和主题相关的问题(因为你帖子标题是 vue 工程化的最佳实践),并没有其它意思。

6年前 评论

请教一个问题:
1、全局初始化数据什么时候获取,例如:
(1)我可能需要加载一个分类的初始化数据,用于select列表等。
(2)我可能需要加载用户数据,登录了往下进行,未登录进行跳转。

6年前 评论

@三石寰宇
这是我vue-router的index.js 大部分的初始化我会在这里制作


// 全局路由
router.beforeEach(async (to, from, next) => {
  // login guard
  if (to.name === 'login') {
    if (!_.isEmpty(store.state.auth.admin)) {
      next('/')
    } else {
      next()
    }

    return
  }

  if (_.isEmpty(store.state.auth.admin)) {
    try {
      await store.dispatch('auth/fetch')
    } catch ({ response }) {
      next({ name: 'login' })
    }
  }

  if (!_.isEmpty(store.state.auth.admin)) {
    // vuex init 与api有关的初始化, 调用了 store下的index.js下的init action
    await store.dispatch('init')
    next()
  } else {
    next({ name: 'login' })
  }
})
6年前 评论

很赞的文章

6年前 评论
Mumujin

可使用 async create 调用 fetchItem

async created() {
    this.store.dispatch('fetchItem')
}
6年前 评论

@Max
beforeRouteEnter 中获取数据,里面的store 是需要在每个页面再次引入么?

6年前 评论

有没有laravel项目工程最佳实践呀

6年前 评论

@Max 老哥,等好久了,好奇 demo 在路上了吗? :smirk:

6年前 评论

感觉和vue-element-admin差不多,再加个filters目录感觉比较合理,store目录感觉用了vuex命名空间后,抽出getters.js的意义不大了

6年前 评论

layouts 部分可以再说的详细一点么

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!