加快Vue项目的开发速度

现现在的开发,好比是内部使用的管理平台这种项目大都时间比较仓仓促。实际上来讲在使用了webpack + vue 这一套来开发的话已经大大了提升了效率。可是对于咱们的开发层面。仍是有不少地方能够再次提升咱们的项目开发效率,让咱们更加专一于业务,毕竟时间就是生命。下面咱们挨个来探讨。javascript

巧用Webpack

Webpack是实现咱们前端项目工程化的基础,但其实她的用处远不只仅如此,咱们能够经过Webpack来帮咱们作一些自动化的事情。首先咱们要了解require.context()这个APIhtml

require.context()

您可使用require.context()函数建立本身的上下文。 它容许您传入一个目录进行搜索,一个标志指示是否应该搜索子目录,还有一个正则表达式来匹配文件。前端

实际上是Webpack经过解析 require() 的调用,提取出来以下这些信息:vue

Directory: ./template
Regular expression: /^.*\.ejs$/
复制代码

而后来建立咱们本身的上下文,什么意思呢,就是咱们能够经过这个方法筛选出来咱们须要的文件而且读取java

下面咱们来简单看一看使用:node

/** * @param directory 要搜索的文件夹目录不能是变量,不然在编译阶段没法定位目录 * @param useSubdirectories 是否搜索子目录 * @param regExp 匹配文件的正则表达式 * @return function 返回一个具备 resolve, keys, id 三个属性的方法 resolve() 它返回请求被解析后获得的模块 id keys() 它返回一个数组,由全部符合上下文模块处理的请求组成。 id 是上下文模块里面所包含的模块 id. 它可能在你使用 module.hot.accept 的时候被用到 */
require.context('demo', useSubdirectories = false, regExp = /\.js$/)
// (建立了)一个包含了 demo 文件夹(不包含子目录)下面的、全部文件名以 `js` 结尾的、能被 require 请求到的文件的上下文。
复制代码

不要困惑,接下来咱们来探讨在项目中怎么用。webpack

组织路由

对于Vue中的路由,你们都很熟悉,相似于声明式的配置文件,其实已经很简洁了。如今咱们来让他更简洁git

  1. 分割路由

首先为了方便咱们管理,咱们把router目录下的文件分割为如下结构github

router                           // 路由文件夹
  |__index.js                    // 路由组织器:用来初始化路由等等
  |__common.js                   // 通用路由:声明通用路由
  |__modules                     // 业务逻辑模块:因此的业务逻辑模块
        |__index.js              // 自动化处理文件:自动引入路由的核心文件
        |__home.js               // 业务模块home:业务模块
        |__a.js                  // 业务模块a
  
复制代码
  1. modules文件夹中处理业务模块

modules文件夹中存放着咱们全部的业务逻辑模块,至于业务逻辑模块怎么分,我相信你们天然有本身的一套标准。咱们经过上面提到的require.context()接下来编写自动化的核心部分index.jsweb

const files = require.context('.', true, /\.js$/)

console.log(files.keys()) // ["./home.js"] 返回一个数组
let configRouters = []
/** * inject routers */
files.keys().forEach(key => {
  if (key === './index.js') return
  configRouters = configRouters.concat(files(key).default) // 读取出文件中的default模块
})
export default configRouters // 抛出一个Vue-router期待的结构的数组
复制代码

自动化部分写完了,那业务组件部分怎么写? 这就更简单了

import Frame from '@/views/frame/Frame'
import Home from '@/views/index/index'
export default [
    // 首页
    {
      path: '/index',
      name: '首页',
      redirect: '/index',
      component: Frame, 
      children: [ // 嵌套路由
        {
          path: '',
          component: Home
        }
      ]
    }
]
复制代码
  1. common路由处理 咱们的项目中有一大堆的公共路由须要处理好比404阿,503阿等等路由咱们都在common.js中进行处理。
export default [
  // 默认页面
  {
    path: '/',
    redirect: '/index',
    hidden:true
  },
  // 无权限页面
  {
    path: '/nopermission',
    name: 'nopermission',
    component: () => import('@/views/NoPermission')
  },
  // 404
  {
    path: '*',
    name: 'lost',
    component: () => import('@/views/404')
  }
]
复制代码
  1. 路由初始化 这是咱们的最后一步了,用来初始化咱们的项目路由
import Vue from 'vue'
import VueRouter from 'vue-router'
import RouterConfig from './modules' // 引入业务逻辑模块
import CommonRouters from './common' // 引入通用模块
Vue.use(VueRouter)
export default new VueRouter({
  mode: 'history',// 须要服务端支持
  scrollBehavior: () => ({ y: 0 }),
  routes: RouterConfig.concat(CommonRouters)
})
复制代码

估计有些朋友代码写到这还不知道到底这样作好处在哪里。咱们来描述一个场景,好比按照这种结构来划分模块。正常的状况是咱们建立完home.js要手动的把这个模块import到路由文件声明的地方去使用。可是有了上面的index.js,在使用的时候你只须要去建立一个home.js并抛出一个符合VueRouter规范的数组,剩下的就不用管了。import RouterConfig from './modules' // 引入业务逻辑模块 已经帮你处理完了。另外扩展的话你还能够把hooks拿出来做为一个单独文件。

全局组件统一声明

一样的道理,有了上面的经验,咱们照葫芦画瓢来处理一下咱们的全局组件。这就没什么可说的了,直接上核心代码

  1. 组织结构
components                       // 组件文件夹
  |__xxx.vue                     // 其余组件
  |__global                      // 全局组件文件夹
        |__index.js              // 自动化处理文件
        |__demo.vue              // 全局demo组件
复制代码
  1. global处理
import Vue from 'vue'
let contexts = require.context('.', false, /\.vue$/)
contexts.keys().forEach(component => {
  let componentEntity = contexts(component).default
  // 使用内置的组件名称 进行全局组件注册
  Vue.component(componentEntity.name, componentEntity)
})
复制代码
  1. 使用和说明

这个使用起来就更简单了,直接在app.js引用这个文件就行。

注意:我以前看到有些人作法是使用组件名去区分全局组件和普通组件,而后经过正则去判断需不须要全局注册。我是直接把全局的组件放到global文件夹下,而后组件的注册名称直接使用component.name。至于使用哪一种方式就比较看我的了。

充分利用NodeJS

放着node这么好得东西不用真是有点浪费,那么咱们来看看node能为咱们增长效率作出什么贡献。

有这么一个场景,咱们每次建立模块的时候都要新建一个vue文件和对应的router配置,并且新页面的大部分东西都还差很少,还得去复制粘贴别得页面。这想一想就有点low。那既然有了node咱们可不能够经过node来作这写乱七八糟得事情? 下面来把咱们的想法付诸于显示。

咱们实现这个功能主要要借助Nodefsprocess, 感兴趣的话能够深刻研究一下。

首先咱们要编写咱们的node脚本,这里是一个比较简单的版本。什么验证文件夹或者文件的都没有,只是来实现咱们这个想法:

/* * fast add new module script */
const path = require('path')
const fs = require('fs')
const chalk = require('chalk')
const reslove = file => path.resolve(__dirname, '../src', file)
// symbol const
const RouterSymbol = Symbol('router'),
      ViewsSymbol = Symbol('views')
// root path
const rootPath = {
  [RouterSymbol]: reslove('router/modules'),
  [ViewsSymbol]: reslove('views')
}
//loggs
const errorLog = error => console.log(chalk.red(`${error}`))
const defaultLog = log => console.log(chalk.green(`${log}`))
// module name
let moduleName = new String()
let fileType = new String()
//const string
const vueFile = module => (`<template> </template> <script> export default { name: '${module}', data () { return { } }, methods: { }, created() { } } </script> <style lang="less"> </style> `)
// route file
const routerFile = module => (`// write your comment here... export default [ { path: '/${module}', name: '', redirect: '/${module}', component: () => import('@/views/frame/Frame'), children: [ { path: '', fullPath: '', name: '', component: () => import('@/views/${module}/index') } ] } ] `)
/** * generate file * @param {*} filePath * @param {*} content * @param {*} dirPath */
const generateFile = async (filePath, content, dirPath = '') =>{
  try {
    // create file if file not exit
    if (dirPath !== '' && ! await fs.existsSync(dirPath)) {
      await fs.mkdirSync(dirPath)
      defaultLog(`created ${dirPath}`)
    }
    if (! await fs.existsSync(filePath)) {
      // create file
      await fs.openSync(filePath, 'w')
      defaultLog(`created ${filePath}`)
    }
    await fs.writeFileSync(filePath, content, 'utf8')
  } catch (error) {
    errorLog(error)
  }
}
// module-method map
const generates = new Map([
  ['view', async (module) => {
    // module file
    const filePath = path.join(rootPath[ViewsSymbol], module)
    const vuePath = path.join(filePath, '/index.vue')
    await generateFile(vuePath, vueFile(module), filePath)
  }],
  // router is not need new folder
  ['router',async (module) => {
    const routerPath = path.join(rootPath[RouterSymbol], `/${module}.js`)
    await generateFile(routerPath, routerFile(module))
  }]
])
defaultLog(`请输入模块名称(英文):`)
// files
const files = ['view', 'router']
// 和命令行进行交互 获取的建立的模块名称
process.stdin.on('data', (chunk) => {
  try {
    if (!moduleName) {
      moduleName = chunk
    } else {
      chunk = chunk.slice(0,-2) // delete /n
      defaultLog(`new module name is ${chunk}`)
      files.forEach(async (el, index) => {
        // 执行建立语句
        await generates.get(`${el}`).call(null, chunk.toString())
        if (index === files.length-1) {
          process.stdin.emit('end')
        }
      })
    }
  } catch (error) {
    errorLog(error)
  }
})
process.stdin.on('end', () => {
  defaultLog('create module success')
})
复制代码

下面咱们看使用的流程

这样咱们就分别建立了 vuerouter的文件,并且已经注入了内容。按照咱们提早声明的组件

注意:这只是一个简单的思路,经过Node强大的文件处理能力,咱们能作的事情远不止这些。

发挥Mixins的威力

Vue中的混入mixins是一种提供分发 Vue 组件中可复用功能的很是灵活的方式。据说在3.0版本中可能会用Hooks的形式实现,但这并不妨碍它的强大。基础部分的能够看这里。这里主要来讨论mixins能在什么情景下帮助咱们。

通用mixins

若是咱们有大量的表格页面,仔细一扒拉你发现很是多的东西都是能够复用的例如分页表格高度加载方法laoding声明等一大堆的东西。下面咱们来整理出来一个简单通用混入list.js

const list = {
  data () {
    return {
      // 这些东西咱们在list中处理,就不须要在每一个页面再去手动的作这个了。
      loading: false, // 伴随loading状态
      pageNo: 1, // 页码
      pageSize: 15, // 页长
      totalCount: 0, // 总个数
      pageSizes: [15, 20, 25, 30], //页长数
      pageLayout: 'total, sizes, prev, pager, next, jumper', // 分页布局
      list: []
    }
  },
  methods: {
    // 分页回掉事件
    handleSizeChange(val) {
      this.pageSize = val
      // todo
    },
    handleCurrentChange (val) {
      this.pageNo = val
      // todo
    },
    /** * 表格数据请求成功的回调 处理完公共的部分(分页,loading取消)以后把控制权交给页面 * @param {*} apiResult * @returns {*} promise */
    listSuccessCb (apiResult = {}) {
      return new Promise((reslove, reject) => {
        let tempList = [] // 临时list
        try {
          this.loading = false
          // todo
          // 直接抛出
          reslove(tempList)
        } catch (error) {
          reject(error)
        }
      })
    },
    /** * 处理异常状况 * ==> 简单处理 仅仅是对表格处理为空以及取消loading */
    listExceptionCb (error) {
      this.loading = false
      console.error(error)
    }
  },
  created() {
    // 这个生命周期是在使用组件的生命周期以前
    this.$nextTick().then(() => {
      // todo
    })
  }
}
export default list
复制代码

下面咱们直接在组件中使用这个mixins

import mixin from '@/mixins/list' // 引入
import {getList} from '@/api/demo'
export default {
  name: 'mixins-demo',
  mixins: [mixin], // 使用mixins
  data () {
    return {
    }
  },
  methods: {
    // 加载列表
    load () {
      const para = {
      }
      this.loading = true
      getList(para).then((result) => {
        this.listSuccessCb(result).then((list) => {
          this.list = list
        }).catch((err) => {
          console.log(err)
        })
      }).catch((err) => {
        this.listExceptionCb(err)
      })
    }
  },
  created() {
    this.load()
  }
}
复制代码

使用了mixins以后一个简单的有loadoing, 分页,数据的表格大概就只须要上面这些代码。

mixins作公共数据的管理

有些时候咱们有一些公共的数据它可能3,4个模块取使用可是又达不到全局的这种规模。这个时候咱们就能够用mixins去管理他们,好比咱们有几个模块要使用用户类型这个列表,咱们来看使用mixins来实现共享。

// types.js
import {getTypes} from '@/api/demo' // ajax
export default {
  data () {
    return {
      types: [] // ==> {name: '', value: ''}
    }
  },
  methods: {
    // 获取列表
    getAllTypesList () {
      getApiList().then((result) => {
        // todo
        this.types = result // 假设result就是咱们须要使用的数据
      }).catch((err) => {
        console.error(err)
      })
    }
  },
  created() {
    // 在须要使用这个mixins的时候取自动请求数据 这个可要可不要 你想在父组件中执行也是ok的
    this.getAllTypesList()
  }
}
复制代码

在组件中引用

import typeMixin from '@/mixins/types'
export default {
  name: 'template',
  mixins: [typeMixin],
  data () {
    return {
      // types这个数组在使用组件中不用多余的定义,直接拿来用就行
      type: ''
    }
  },
  methods: {
  }
}
复制代码

至于mixins中得数据咱们能够在组件中直接使用

<!-- -->
<el-select v-model="type" clearable placeholder="请选择类型">
    <el-option v-for="item in types" :key="item.id" :label="item.templateName" :value="item.id"></el-option>
  </el-select>
复制代码

咱们这样就能够不用vuex来去管理那些只有在模块间复用几回的数据,并且很是方便得去取咱们想要得数据,连定义都省了。可是这有一个缺点。就是每次都会去从新请求这些数据。若是你不在意这一点点瑕疵的话,我以为用起来是彻底ok得。

注意: mixins它当然是简单的,可是注释和引用必定要作好,否则的话新成员进入团队大概是一脸的懵逼,并且也不利于后期的维护。也是一把双刃剑。另外:全局mixins必定要慎用,若是不是必需要用的话我仍是不建议使用。

进一步对组件进行封装

你们都知道组件化的最大的好处就是高度的可复用性和灵活性。可是组件怎么封装好,封装到什么程度让咱们更方便。这是没有标准的答案的。咱们只有根据高内聚,低耦合的这个指导思想来对咱们的业务通用组件来进行封装,让咱们的业务页面结构更加的简洁,加快咱们的开发效率。封装多一点的话页面可能会变成这样:

<template>
  <box-content>
    <!-- 头部标题部分 -->
    <page-title>
      <bread slot="title" :crumbs="[{name: 'xx管理', path: '', active: true, icon: ''}, {name: 'xxxx', path: '', active: true, icon: ''}]"></bread>
    </page-title>
    <!-- 表格部分 -->
    <div>
      <base-table v-loading="loading" :columns="headers" :list="list" :page-no ="pageNo" :page-size="pageSize" :total-count="totalCount" @delete="deleteItm" @change-size="handleSizeChange" @change-page="handleCurrentChange">
      </base-table>
    </div>
  </box-content>
</template>
复制代码

有什么东西一目了然。

无状态组件

最容易勾起咱们封装欲望的就是无状态HTML组件,例如咱们除去header, menu以后的content部分。没有什么须要复杂的交互,可是咱们每一个页面又都得写。你说不拿它开刀拿谁开🔪

<template>
  <div class="container-fluid" :class="[contentClass]">
      <el-row>
          <el-col :span="24">
              <!-- box with #fff bg -->
              <div class="box">
                  <div class="box-body">
                      <slot></slot>
                  </div>
              </div>
          </el-col>
      </el-row>
  </div>
</template>
复制代码

上面这个处理很是的简单,可是你在项目中会很是频繁的使用过到,那么这个封装就颇有必要了。

ElementUI table组件封装

ElementUI中得组件其实已经封装得很优秀了,可是表格使用得时候仍是有一堆得代码在我看来是不须要在业务中重复写得。封装到靠配置来进行表格得书写得一步我以为就差很少了,下面是一个小demo

<template>
  <el-row>
    <el-col :span="24">
      <el-table :data="list" border size="mini" @selection-change="handleSelectionChange" :max-height="tableHeight" v-bind="$attrs"> <!-- -->
        <template v-for="(column, index) in columns">
          <slot name="front-slot"> </slot>
          <!-- 序号 -->
          <el-table-column :key="index" v-if="column.type === 'selection'" type="selection" width="55"> </el-table-column>
          <!-- 复选框 -->
          <el-table-column :key="index" v-else-if="column.type === 'index'" type="index" width="50" label="序号"> </el-table-column>
          <!-- 具体内容 -->
          <el-table-column :key="index" v-else align="left" :label="column.title" :width="column.width">
            <template slot-scope="scope">
              <!-- 仅仅显示文字 -->
              <label v-if="!column.hidden"> <!-- 若是hidden为true的时候 那么当前格能够不显示,能够选择显示自定义的slot-->
                <!-- 操做按钮 -->
                <label v-if="column.type === 'operate'">
                  <a href="javascript:void(0)" class="operate-button" v-for="(operate, index) in column.operates" :key="index" @click="handleClick(operate, scope.row)">
                    {{operate.name}}
                    &nbsp;&nbsp;
                  </a>
                </label>
                <span v-else>
                  {{scope.row[column.key]}}
                </span>
              </label>
              <!-- 使用slot的状况下 -->
              <label v-if="column.slot">
                <!-- 具名slot -->
                <slot v-if="column.slot" :name="column.slot" :scope="scope"></slot>
              </label>
            </template>
          </el-table-column>
        </template>
        <!--默认的slot -->
        <slot/>
      </el-table>
    </el-col>
  </el-row>
</template>
复制代码
export default {
  name: 'base-table',
  props: {
    // 核心数据
    list: {
      type: Array,
      default: () => []
    },
    // columns
    columns: {
      type: Array,
      required: true,
      default: () => []
    }
  },
  data () {
    return {
      tableHeight: xxx
    }
  },
  methods: {
    // 处理点击事件
    handleClick(action, data) {
      // emit事件
      this.$emit(`${action.emitKey}`, data)
    }
  }
}
复制代码

使用:

<base-table v-loading="loading" :columns="headers" :list="list" @view="viewCb">
  <!-- 自定义的slot -->
  <template slot="demoslot" slot-scope="{scope}">
    <span>
      {{scope.row}}
    </span>
  </template>
  <!-- 默认的slot 若是交互很复杂 咱们还能够直接使用表格内部的组件 -->
  <el-table-column label="操做" width="200" >
    <template slot-scope="scope">
      <a href="javascript:void(0)" @click="defaultSlot(scope.row)">xxx</a>
    </template>
  </el-table-column>
</base-table>
复制代码
export default {
  name: 'table-demo',
  data () {
    return {
      // 表格头部配置
      headers: [
        { key: 'xxx', title: '测试' },
        { title: 'xxx', hidden: true, slot: 'demoslot'},
        {
          title: '操做', type: 'operate',
          operates: [
            {name: '详情',emitKey: 'view'}
          ]
        }
      ]
    }
  },
  methods: {
    viewCb(){
      // todo
    },
    defaultSlot(){
      // todo
    }
  }
}
复制代码

这样封装过的表格,应付基本的一些需求问题应该不大。至于特殊的要求能够一步一步的进行完善。

关于Element-UI中的table组件的封装,我打算另写一篇文章详细的解读个人思路和更复杂的实现。

总结

这些东西并非什么语法糖,是真正能够在项目中加快咱们的效率。让咱们的本身乃至整个团队从繁杂的重复复制粘贴中解脱一点。至于速度和质量的问题。我是以为使用公共组件质量可控性会更高一些。我建议公共得东西注释必定要写得全面和详细,这样能够极大的下降咱们的交流成本。至于组件的封装仍是要看你的业务。

以上观点纯属我的意见,若有错误,多谢指正。

没想到这么多人看😂,加班加点整理出来了示例代码, 示例代码中也是一个Vue中后台开发的pro版本,业务需求对的上的话直接用也是能够的😂😂

原文地址 若是以为有用得话给个⭐吧