为nuxt项目写一个面包屑cli工具实现自动生成页面与面包屑配置


Posted in Javascript onSeptember 29, 2019

公司项目的面包屑导航是使用 element 的面包屑组件,配合一份 json 配置文件来实现的,每次写新页面都需要去写 json 配置,非常麻烦,所以写一个面包屑cli,自动生成页面、自动配置面包屑数据,提高效率:rocket:

明确目标

  • 提供 init 命令,在一个新项目中能够通过初始化生成面包屑相关文件
  • 能够通过命令生成页面,并且自动配置面包屑 json 数据
  • 按照项目原有需求,能够配置面包屑是否可点击跳转
  • 按照项目原有需求,能够配置某路径下是否展示面包屑
  • 支持仅配置而不生成文件,能够为已存在的页面生成配置
  • 能够动态配置当前面包屑导航的数据
  • …… (后续在使用中发现问题并优化)

实现分成两部分

  • 面包屑实现
  • cli 命令实现

面包屑实现

  • 在路由前置守卫 beforEach 中根据当前路径在配置文件中匹配到相应的数据
  • 把这些配置存到 vuex
  • 在面包屑组件中根据 vuex 中的数据 v-for 循环渲染出面包屑

JSON 配置文件

json 配置文件是通过命令生成的,一个配置对象包含 name path clickable isShow 属性

[
 {
  "name": "应用", // 面包屑名称(在命令交互中输入)
  "path": "/app", // 面包屑对应路径(根据文件自动生成)
  "clickable": true, // 是否可点击跳转
  "isShow": true // 是否显示
 },
 {
  "name": "应用详情",
  "path": "/app/detail",
  "clickable": true, // 是否可点击跳转
  "isShow": true // 是否显示
 }
]

匹配配置文件中的数据

比如按照上面的配置文件,进入 /app/detail 时,将会匹配到如下数据

[
  {
    "name": "应用",
    "path": "/app",
    "clickable": true,
    "isShow": true
  },
  {
    "name": "应用",
    "path": "/app/detail",
    "clickable": true,
    "isShow": true
  }
]

动态面包屑实现

有时候需要动态修改面包屑数据(比如动态路由),由于数据是存在 vuex 中的,所以修改起来非常方便,只需在 vuex 相关文件中提供 mutation 即可,这些 mutation 在数据中寻找相应的项,并改掉

export const state = () => ({
  breadcrumbData: []
})

export const mutations = {
 setBreadcrumb (state, breadcrumbData) {
  state.breadcrumbData = breadcrumbData
 },

 setBreadcrumbByName (state, {oldName, newName}) {
  let curBreadcrumb = state.breadcrumbData.find(breadcrumb => breadcrumb.name === oldName)

  curBreadcrumb && (curBreadcrumb.name = newName)
 },
 
 setBreadcrumbByPath (state, {path, name}) {
  let curBreadcrumb = state.breadcrumbData.find(
   breadcrumb => breadcrumb.path === path
  )
  curBreadcrumb && (curBreadcrumb.name = name)
 }
}

根据路径匹配相应配置数据具体代码

import breadcrumbs from '@/components/breadcrumb/breadcrumb.config.json'

function path2Arr(path) {
 return path.split('/').filter(p => p)
}

function matchBreadcrumbData (matchPath) {
 return path2Arr(matchPath)
  .map(path => {
   path = path.replace(/^:([^:?]+)(\?)?$/, (match, $1) => {
    return `_${$1}`
   })
   return '/' + path
  })
  .map((path, index, paths) => {

   // 第 0 个不需拼接
   if (index) {
    let result = ''
    for (let i = 0; i <= index; i++) {
     result += paths[i]
    }
    return result
   }
   return path
  })
  .map(path => {
   const item = breadcrumbs.find(bread => bread.path === path)
   if (item) {
    return item
   }
   return {
    name: path.split('/').pop(),
    path,
    clickable: false,
    isShow: true
   }
  })
}

export default ({ app, store }) => {
 app.router.beforeEach((to, from, next) => {
  const toPathArr = path2Arr(to.path)
  const toPathArrLength = toPathArr.length
  let matchPath = ''

  // 从 matched 中找出当前路径的路由配置
  for (let match of to.matched) {
   const matchPathArr = path2Arr(match.path)
   if (matchPathArr.length === toPathArrLength) {
    matchPath = match.path
    break
   }
  }

  const breadcrumbData = matchBreadcrumbData(matchPath)

  store.commit('breadcrumb/setBreadcrumb', breadcrumbData)
  next()
 })
}

面包屑组件

面包屑组件中渲染匹配到的数据

<template>
 <div class="bcg-breadcrumb" v-if="isBreadcrumbShow">
  <el-breadcrumb separator="/">
   <el-breadcrumb-item
    v-for="(item, index) in breadcrumbData"
    :to="item.clickable ? item.path : ''"
    :key="index">
    {{ item.name }}
   </el-breadcrumb-item>
  </el-breadcrumb>
 </div>
</template>

<script>
import breadcrumbs from "./breadcrumb.config"
export default {
 name: 'Breadcrumb',
 computed: {
  isBreadcrumbShow () {
   return this.curBreadcrumb && this.curBreadcrumb.isShow
  },
  breadcrumbData () {
   return this.$store.state.breadcrumb.breadcrumbData
  },
  curBreadcrumb () {
   return this.breadcrumbData[this.breadcrumbData.length - 1]
  }
 }
}
</script>

cli命令实现

cli命令开发用到的相关库如下:这些就不细说了,基本上看下 README 就知道怎么用了

  • commander :命令行工具
  • boxen :在终端画一个框
  • inquirer :命令行交互工具
  • handlebar:模版引擎

目录结构

lib // 存命令行文件
  |-- bcg.js
template // 存模版
  |-- breadcrumb // 面包屑配置文件与组件,将生成在项目 @/components 中
    |-- breadcrumb.config.json
    |-- index.vue
  |-- braadcrumb.js // vuex 相关文件,将生成在项目 @/store 中
  |-- new-page.vue // 新文件模版,将生成在命令行输入的新路径中
  |-- route.js // 路由前置守卫配置文件,将生成在 @/plugins 中
test // 单元测试相关文件

node 支持命令行,只需在 package.json 的 bin 字段中关联命令行执行文件

// 执行 bcg 命令时,就会执行 lib/bcg.js 的代码
{
 "bin": {
  "bcg": "lib/bcg.js"
 }
}

实现命令

实现一个 init 命令,生成相关面包屑文件(面包屑组件、 json配置文件、 前置守卫plugin、 面包屑store)

bcg init

实现一个 new 命令生成文件,默认基础路径是 src/pages ,带一个 -b 选项,可用来修改基础路径

bcg new <file-path> -b <base-path>

具体代码如下

#!/usr/bin/env node
const path = require('path')
const fs = require('fs-extra')

const boxen = require('boxen')
const inquirer = require('inquirer')
const commander = require('commander')
const Handlebars = require('handlebars')

const {
 createPathArr,
 log,
 errorLog,
 successLog,
 infoLog,
 copyFile
} = require('./utils')

const VUE_SUFFIX = '.vue'

const source = {
 VUE_PAGE_PATH: path.resolve(__dirname, '../template/new-page.vue'),
 BREADCRUMB_COMPONENT_PATH: path.resolve(__dirname, '../template/breadcrumb'),
 PLUGIN_PATH: path.resolve(__dirname, '../template/route.js'),
 STORE_PATH: path.resolve(__dirname, '../template/breadcrumb.js')
}

const target = {
 BREADCRUMB_COMPONENT_PATH: 'src/components/breadcrumb',
 BREADCRUMB_JSON_PATH: 'src/components/breadcrumb/breadcrumb.config.json',
 PLUGIN_PATH: 'src/plugins/route.js',
 STORE_PATH: 'src/store/breadcrumb.js'
}

function initBreadCrumbs() {
 try {
  copyFile(source.BREADCRUMB_COMPONENT_PATH, target.BREADCRUMB_COMPONENT_PATH)
  copyFile(source.PLUGIN_PATH, target.PLUGIN_PATH)
  copyFile(source.STORE_PATH, target.STORE_PATH)
 } catch (err) {
  throw err
 }
}

function generateVueFile(newPagePath) {
 try {
  if (fs.existsSync(newPagePath)) {
   log(errorLog(`${newPagePath} 已存在`))
   return
  }

  const fileName = path.basename(newPagePath).replace(VUE_SUFFIX, '')
  const vuePage = fs.readFileSync(source.VUE_PAGE_PATH, 'utf8')
  const template = Handlebars.compile(vuePage)
  const result = template({ filename: fileName })

  fs.outputFileSync(newPagePath, result)

  log(successLog('\nvue页面生成成功咯\n'))
 } catch (err) {
  throw err
 }
}

function updateConfiguration(filePath, {
 clickable,
 isShow
} = {}) {
 try {
  if (!fs.existsSync(target.BREADCRUMB_JSON_PATH)) {
   log(errorLog('面包屑配置文件不存在, 配置失败咯, 可通过 bcg init 生成相关文件'))
   return
  }

  let pathArr = createPathArr(filePath)
  const configurationArr = fs.readJsonSync(target.BREADCRUMB_JSON_PATH)

  // 如果已经有配置就过滤掉
  pathArr = pathArr.filter(pathItem => !configurationArr.some(configurationItem => configurationItem.path === pathItem))

  const questions = pathArr.map(pathItem => {
   return {
    type: 'input',
    name: pathItem,
    message: `请输入 ${pathItem} 的面包屑显示名称`,
    default: pathItem
   }
  })

  inquirer.prompt(questions).then(answers => {
   const pathArrLastIdx = pathArr.length - 1

   pathArr.forEach((pathItem, index) => {
    configurationArr.push({
     clickable: index === pathArrLastIdx ? clickable : false,
     isShow: index === pathArrLastIdx ? isShow : true,
     name: answers[pathItem],
     path: pathItem
    })
   })

   fs.writeJsonSync(target.BREADCRUMB_JSON_PATH, configurationArr, {
    spaces: 2
   })

   log(successLog('\n生成面包屑配置成功咯'))
  })
 } catch (err) {
  log(errorLog('生成面包屑配置失败咯'))
  throw err
 }
}

function generating(newPagePath, filePath) {
 inquirer.prompt([
  {
   type: 'confirm',
   name: 'clickable',
   message: '是否可点击跳转? (默认 yes)',
   default: true
  },
  {
   type: 'confirm',
   name: 'isShow',
   message: '是否展示面包屑? (默认 yes)',
   default: true
  },
  {
   type: 'confirm',
   name: 'onlyConfig',
   message: '是否仅生成配置而不生成文件? (默认 no)',
   default: false
  }
 ]).then(({ clickable, isShow, onlyConfig }) => {
  if (onlyConfig) {
   updateConfiguration(filePath, { clickable, isShow })
   return
  }

  generateVueFile(newPagePath)
  updateConfiguration(filePath, { clickable, isShow })
 })
}

const program = new commander.Command()

program
 .command('init')
 .description('初始化面包屑')
 .action(initBreadCrumbs)

program
 .version('0.1.0')
 .command('new <file-path>')
 .description('生成页面并配置面包屑,默认基础路径为 src/pages,可通过 -b 修改')
 .option('-b, --basePath <base-path>', '修改基础路径 (不要以 / 开头)')
 .action((filePath, opts) => {
  filePath = filePath.endsWith(VUE_SUFFIX) ? filePath : `${filePath}${VUE_SUFFIX}`
  const basePath = opts.basePath || 'src/pages'
  const newPagePath = path.join(basePath, filePath)

  log(
   infoLog(
    boxen(`即将配置 ${newPagePath}`, {
     padding: 1,
     margin: 1,
     borderStyle: 'round'
    })
   )
  )

  generating(newPagePath, filePath)
 })

program.parse(process.argv)

if (!process.argv.slice(2)[0]) {
 program.help()
}

发布 npm

开发完成后,发布到 npm,具体方法就不细说了,发布后全局安装就能愉快的使用咯!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
前端开发部分总结[兼容性、DOM操作、跨域等](持续更新)
Mar 04 Javascript
一个简单的Ext.XTemplate的实例代码
Mar 18 Javascript
jquery选择器的选择使用及性能介绍
Jan 16 Javascript
JQuery入门——用one()方法绑定事件处理函数(仅触发一次)
Feb 05 Javascript
使用jQuery解决IE与FireFox下createElement方法的差异
Nov 14 Javascript
JS动态日期时间的获取方法
Sep 28 Javascript
JS获取数组最大值、最小值及长度的方法
Nov 24 Javascript
canvas滤镜效果实现代码
Feb 06 Javascript
基于JavaScript实现的顺序查找算法示例
Apr 14 Javascript
Bootstrap的aria-label和aria-labelledby属性实例详解
Nov 02 Javascript
Javascript执行流程细节原理解析
May 14 Javascript
js中复选框的取值及赋值示例详解
Oct 18 Javascript
React-redux实现小案例(todolist)的过程
Sep 29 #Javascript
关于layui 实现点击按钮添加一行(方法渲染创建的table)
Sep 29 #Javascript
在Layui中实现开关按钮的效果实例
Sep 29 #Javascript
layui之数据表格--与后台交互获取数据的方法
Sep 29 #Javascript
20多个小事例带你重温ES10新特性(小结)
Sep 29 #Javascript
解决layui页面按钮点击无反应,也不报错的问题
Sep 29 #Javascript
react用Redux中央仓库实现一个todolist
Sep 29 #Javascript
You might like
PHP测试成功的邮件发送案例
2015/10/26 PHP
定义select的边框颜色
2008/04/28 Javascript
javascript 写类方式之六
2009/07/05 Javascript
jQuery JSON的解析方式分享
2011/04/05 Javascript
js中scrollHeight,scrollWidth,scrollLeft,scrolltop等差别介绍
2012/05/16 Javascript
jquery下div 的resize事件示例代码
2014/03/09 Javascript
jQuery中eq()方法用法实例
2015/01/05 Javascript
JQuery实现的购物车功能(可以减少或者添加商品并自动计算价格)
2015/01/13 Javascript
Javascript编程中几种继承方式比较分析
2015/11/28 Javascript
jQuery实现从身份证号中获取出生日期和性别的方法分析
2016/02/25 Javascript
三分钟学会用ES7中的Async/Await进行异步编程
2018/06/14 Javascript
详解JS转换数值函数Number()、parseInt()、parseFloat()
2018/08/24 Javascript
json前后端数据交互相关代码
2018/09/19 Javascript
基于Vue插入视频的2种方法小结
2019/04/02 Javascript
javascript全局自定义鼠标右键菜单
2020/12/08 Javascript
[49:17]DOTA2-DPC中国联赛 正赛 Phoenix vs Dynasty BO3 第三场 1月26日
2021/03/11 DOTA
Django中间件实现拦截器的方法
2018/06/01 Python
flask框架url与重定向操作实例详解
2020/01/25 Python
python实现全排列代码(回溯、深度优先搜索)
2020/02/26 Python
pycharm无法安装第三方库的问题及解决方法以scrapy为例(图解)
2020/05/09 Python
matplotlib实现数据实时刷新的示例代码
2021/01/05 Python
中国首家奢侈品O2O网购平台:第五大道奢侈品网
2017/12/14 全球购物
Python面试题集
2012/03/08 面试题
工程质量承诺书范文
2014/03/27 职场文书
心理健康活动总结
2014/04/30 职场文书
2014法制宣传日活动总结
2014/07/09 职场文书
食堂厨师岗位职责
2014/08/25 职场文书
小学生校园广播稿
2014/09/28 职场文书
六查六看剖析材料
2014/10/06 职场文书
党的群众路线教育实践活动个人整改方案
2014/10/25 职场文书
小学四年级学生评语
2014/12/26 职场文书
2015年幼儿园教研活动总结
2015/03/25 职场文书
计划生育工作总结2015
2015/04/03 职场文书
安全学习心得体会范文
2016/01/18 职场文书
幼儿园大班教师评语
2019/06/21 职场文书
Python数据分析入门之数据读取与存储
2021/05/13 Python