阅读 1337

前端小纠结--Vue项目代码组织和风格约定

原文地址

风格约定但不限于代码风格,还有一些其他的默认约定

代码组织和分层

代码组织是一个仁者见仁,智者见智的话题,没有银弹。不过不管怎么变化,指导思想还是不变的高内聚,低耦合

强烈推荐两篇文章,能够拓宽你的视野,带你走向新高度。

用 Feature First 的方式管理前端项目复杂度

代码组织的优雅,模块化才能够做好。

分层

按照职能的不同进行不同维度进行层级划分,层级划分之后,进行进一步的模块划分(原则上,每一个文件夹都是一个模块)

文件夹和文件命名

选择适合自己的风格。

  1. 文件夹和文件都使用kebab-case

    kebab-case重度使用者可以选择这种。

  2. 文件夹使用kebab-case, 文件使用Pascal Case

    建议使用这种。

    文件夹:event-bus
    文件:EventBus.ts
    复制代码
  3. 例外

    • index文件不受上述约束
    • 工具自动生成的文件(自己考虑是否受约束)

模块化原则

模块化代码首先要做到代码的分层、隔离、抽象。

不同模块完成不同的职能,不同职能之间相互协作。

  1. 每个模块保持一个入口和出口

    对于外部模块来说,尽量保证一个入口

    对于内部子模块来说,尽量保证一个出口

    如果按照文件夹作为模块界限,每个文件夹下都有一个出口(可以默认为index文件)

  2. 模块的入口名称默认index或者文件夹名字的文件

    // 例如: group文件夹
    group/
    |---index.ts   // A. 默认作为入口
    |---group.ts   // B. 也可以默认作为入口
    
    二者任选其一就好,A方案应该是大家默认的方案;B方案,检索代码的时候更方便
    复制代码
  3. 模块内部分层

    模块内部还可以有base, common, components, helper,utils,filter,config等层级(词穷了.....)

importexport原则

  1. import导入,指定到文件

    指定到文件能够提高编译打包的速度

    // 指定到index文件
    import { Logger } from './common/index';
    复制代码

common模块

独立文章说明。

router模块

router模块的风格约定。

模块结构

router
├── helper
│   ├── ImportRoute.ts
│   └── RouteGenerator.ts
├── modules
│   ├── AboutRoutes.ts
│   └── HomeRoutes.ts
├── router.ts
└── Routes.ts
复制代码
  • helper: 帮助工具方法

  • modules: 不同的业务模块

  • router: vue-router初始化的地方,也是模块入口

  • Routes: RouteConfig的出口,其它模块都从这里获取route配置,从而达到解耦的目的,尤其是不同的views里面的路由跳转,使用Routes配置达到解耦的目的。

    例子:

    import { HomeRoute } from 'Routes';
    
    // 跳转,这样没有硬编码任何的route信息,全部都是从Routes配置来,达到解耦的目的。
    this.$router.push({
        name: HomeRoute.name
    })
    复制代码

modules子模块

文件名:views下文件夹名(模块名) + Routes结尾

modules下的文件,最好和views下文件夹一一对应,方便维护(对模块切分有较高的理解)

例如:
views
├── group
│   ├── xxx1.vue
│   └── xxx2.vue
├── report
│   ├── xxx3.vue
│   └── xxx4.vue
├── Home.vue
复制代码

modules对应的就是

例如:
router
├── modules
│   ├── GroupRoutes.ts
|   ├── ReportRoutes.ts
│   └── HomeRoutes.ts
复制代码

导出模块配置:

// HomeRoutes.ts
export const HomeRoute = {
  path: '/',
  name: 'HomeRoute',
  component: 'Home',
};

// 必须导出一个数组,因为是一个模块的配置信息,可能有多个配置,还可以进行配置层级关系
export default [HomeRoute];
复制代码

注意: 这只是个思路,具体的操作还要灵活运用。

RouteConfig风格约定

  1. RouteConfig变量名

    vue文件名+Route结尾

    // 文件Home.vue
    // 变量名HomeRoute
    export const HomeRoute = {
      path: '/',
      name: 'HomeRoute',
      component: 'Home',
    };
    复制代码
  2. name属性

    RouteConfig变量名保持一致

  3. component属性

    如果异步加载component需要使用相对于viewspath格式,因为在ImportRoute中统一处理。

    // ImportRoute.ts统一处理
    export function importRoute(file: string) {
      // @see https://github.com/webpack/webpack/issues/1949
      return () => import(/* webpackChunkName: "chunk-[request][index]" */ '@/views/' + file + '.vue');
    }
    复制代码
  4. path属性

    没有最佳实践,最好使用restful风格约束

    如果使用route.query等之类的参数传递,面包屑导航很难处理。

  5. meta属性

    没有最佳实践,多数情况下有这么几个属性

    export const HomeRoute = {
      path: '/',
      name: 'HomeRoute',
      component: 'Home', // component: () => import('@/views/Home.vue')
      meta: {
          // title的值可以为`i18n`的语言文件key,方便做国际化
          title: '首页', // 作为menu.title和breadcrumb.title备选
          icon: '', // icon的class,作为menu.icon和breadcrumb.icon备选
          menu: {
              title: '首页',
              visible: true,
              icon: '',  // icon的class
          },
          breadcrumb: {
              title: '路径名',
              visible: false, // 有的时候不需要在面包屑上渲染
              icon: '', // icon的class
          },
          auth: {
              roles: [1, 2, 3]
          }
      }
    };
    复制代码
  6. props属性

    路由组件传参,更多高级用法,请查看例子

    使用props方式把components$route解耦,这样components既可以单独使用,也可以当作子组件使用,而且方便测试。

    特殊场景可以不使用props,例如本来就不是通用的组件,是需要组合在一起使用的父子组件,是可以和route耦合的。

    如果 props 被设置为 trueroute.params 将会被设置为组件属性。

    // 函数式(动态)
    const router = new VueRouter({
      routes: [{ 
            path: '/search', 
         	component: SearchUser, 
            // route是SearchUser内部的this.$route
         	props: (route) => ({ query: route.query.q }) 
        }]
    })
    复制代码
    // 静态
    const router = new VueRouter({
      routes: [{ 
            path: '/promotion/from-newsletter',
            component: Promotion, 
            props: { newsletterPopup: false } 
        }]
    })
    复制代码

store模块

vue应用的状态模块

模块结构

参考官方文件结构

官方购物车例子

store
├── StoreTypes.ts       # actions mutations getters类型
├── Actions.ts          # 根级别的 action
├── Mutations.ts        # 根级别的 Mutations
├── Getters.ts          # 根级别的 getters
├── modules             # 模块
│   ├── xxxStore.ts     # 子模块
│   ├── SystemStore.ts  # SystemStore子模块
├── index.ts            # 入口
复制代码

modules子模块

文件名:模块名 + Store结尾

modules下的业务store,最好和views下文件夹对应,方便维护

# 例子: 
LocaleStore.ts    # i18n模块
LoginStore.ts     # 登录模块
UserStore.ts      # 用户模块
复制代码

Store module约定

参考Store Module

主要约定module内部代码结构大致如下:

SystemStore为例:

  • 需要使用前缀的地方使用模块名作为前缀

    例子中模块名System

约定声明大致顺序如下:除state部分外其他都是可选

  • state声明(例如:systemState
  • getter types声明(可选,灵活运用)
  • getters声明(可选, 灵活运用)
  • mutation types 声明(可选)
  • mutations声明(可选)
  • action types声明(可选)
  • actions声明(可选)
  • store options导出
// 例子: SystemStore.ts
// 声明state
const systemState: SystemState = {
  initialized: false,
};

// 声明GetterTypes 
export const SystemGetterTypes = {
  IS_SYSTEM_INITIALIZED: 'IS_SYSTEM_INITIALIZED',
};

// 声明getters
const getters = {
    [SystemGetterTypes.IS_SYSTEM_INITIALIZED](state: SystemState, getters: any, rootState: any){
    	return state.initialized;
    }
};

// 声明MutationTypes
export const SystemMutationTypes = {
  SYSTEM_SET_INITIALIZED: 'SYSTEM_SET_INITIALIZED',
};
// 声明mutations
const mutations: MutationTree<SystemState> = {
  [SystemMutationTypes.SYSTEM_SET_INITIALIZED]: (
    state: SystemState,
    payload: boolean
  ) => {
    state.initialized = payload;
    logger.log('system-store.initialized: ' + payload);
  },
};

// 声明ActionTypes
export const SystemActionTypes = {
  SYSTEM_UPDATE_INITIALIZED: 'SYSTEM_UPDATE_INITIALIZED',
  SYSTEM_RESET: 'SYSTEM_RESET',
  SYSTEM_INIT: 'SYSTEM_INIT',
};

// 声明actions
const actions = {
  [SystemActionTypes.SYSTEM_UPDATE_INITIALIZED]: (
    { commit }: ActionContext<SystemState, any>,
    initialized: boolean
  ) => {
    commit(SystemMutationTypes.SYSTEM_SET_INITIALIZED, initialized);
  },
  [SystemActionTypes.SYSTEM_RESET]: ({
    commit,
    dispatch,
  }: ActionContext<SystemState, any>) => {
    // 清空所有使用store储存的数据.
  },
  [SystemActionTypes.SYSTEM_INIT]: (
    { commit, dispatch }: ActionContext<SystemState, any>,
    payload: {
      user: UserModel;
      userCookie: UserCookie;
    }
  ) => {
      // 初始化数据
  },
};

// 导出storeOptions
const storeOptions = {
  state: systemState,
  getters,
  mutations,
  actions,
};

export default storeOptions;
复制代码

注意:其中的action types, mutation types和 getter types 不强制要求,在es6环境中使用官方的mapStatemapGetters, mapActions, mapMutations工具函数更方便。

api模块

api模块是接口服务层,主要做一些对参数的转换处理,同时解耦其它业务层。

模块结构

模块的大致参考结构

api
├── api.ts    # 入口
└── modules   # 子模块
    ├── DictionaryService.ts   # 具体的业务模块
    ├── GroupService.ts
    ├── HistoryService.ts
复制代码

service module约定

文件名:模块名 + Service结尾

modules下的业务service,和views下文件夹对应,方便维护

如果单个service文件对应的业务模块接口太多,可以使用文件夹来进一步分割。

//例子:LoginService
// 声明url,导出url方便mock模块使用
export const GET_SIGN_IN = '/api/login';
// 声明service函数
export function signIn(data: {account: string; pass: string}) {
  return ajax({
      url: GET_SIGN_IN,
      data, 
      method: 'post'
  });
}
复制代码

URL前缀约定:

查询:使用GET作为前缀(特殊情况例外)

更新:使用UPDATE

新增:使用ADD

删除:使用DELETE

其它:EXPORT,IMPORT,UPLOAD,DOWNLOAD等

service 函数名前缀约定:

  • 对于单个实体可以考虑使用get, add, update, delete作为前缀

i18n模块

主要是建议下代码的分割的约定。

模块结构

i18n
├── i18n.ts
├── index.d.ts
└── locales
    ├── en_US.js
    ├── modules
    │   └── actions
    │       ├── en_US.js
    │       └── zh_CN.js
    └── zh_CN.js
复制代码

语言文件组织

语言模块的组织,主要按照模块(文件夹层次)来组织,能够最小的减少冲突可能性。

语言文件约定:

  • 方言文件放置在locals文件夹下
  • 文件以方言编码命名

层级组织的例子:

views
├── account
│   ├── Account.vue
│   ├── locales
│   │   ├── en_US.js
│   │   └── zh_CN.js
│   └── XXX.vue
├── feedback
│   ├── locales
│   │   ├── en_US.js
│   │   └── zh_CN.js
│   └── Suggestion.vue
├── locales
│   │   ├── en_US.js
│   │   └── zh_CN.js
复制代码
// views/feedback/locales/zh_CN.js
export const feedbackModule = {
  // 语言模块属性:模块名+module结尾
  // 可选的方式就是:严格按照文件夹层次来构造属性层级(缺点取属性值时太长,后期可以使用__dirname自动生成)
  label: '您的意见:',
    textarea: {
        placeholder: '请写下您的意见与建议, 500字符以内'
  }  
};
复制代码
// views/locales/zh_CN.js
import feedbackModule from '../feedback/locales/zh_CN.js';

export default {
  // 如果模块众多建议加上views  
  // views: {
  //    ...feedbackModule,
  // }
  
  // 如果模块少,直接
  feedbackModule,
};
复制代码
// i18n/locales/zh_CN.js
import viewsLocales from '../../views/locales/zh_CN';

export default {
    ...viewsLocales
}
复制代码

mock模块

mock模块主要做些数据模拟的工作

模块结构

子模块的大致参考结构

mock
├── mock.js   # 入口
└── modules   # 子模块
    ├── LoginMock.js   # 具体的业务模块
复制代码

mock module约定

文件名:模块名 + mock结尾

modules下的mock文件,和api模块下文件对应,方便维护

方法约定:

  • mock模块方法和service请求方法对应

    例如:LoginServicesignIn mock模块对应也叫signIn

  • mock module需要提供setup方法

    setup方法供外部统一调用,作为mock module的开关

例子:

// LoginMock.js
import Mock from 'mockjs';
import { genSuccessResult, genFailResult } from './mock-utils';
import { GET_SIGN_IN } from '../api/api';
import Cookies  from 'js-cookie';

const signIn = (data) => {
    Cookies.set('SESSIONID', 'mock SESSIONID');
    return genSuccessResult({
      msg: '登陆成功',
    });
}
      
export default {
  setup() {
      Mock.mock(GET_SIGN_IN, 'post', signIn);
  }
}
复制代码
// mock.js
import loginMocker from './modules/login';

export const start = function() {
  loginMocker.setup();
};
复制代码
// main.js
import mocker from './mock/mock'
if (process.env.NODE_ENV === 'development') {
  mocker.start();
} 
复制代码

其它模块

其它模块如filtersdirectives,test目前没有总结,以后补充。

关注微信公众号,发现更多精彩内容

微信公众号

关注下面的标签,发现更多相似文章
评论