为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 相关文章推荐
js 颜色选择器(兼容firefox)
Mar 05 Javascript
js对象与打印对象分析比较
Apr 23 Javascript
jQuery源码分析之jQuery.fn.each与jQuery.each用法
Jan 23 Javascript
分析了一下JQuery中的extend方法实现原理
Feb 27 Javascript
jQuery实现鼠标双击Table单元格变成文本框及输入内容后更新到数据库的方法
Nov 25 Javascript
详解JavaScript中基于原型prototype的继承特性
May 05 Javascript
vue日期组件 支持vue1.0和2.0
Jan 09 Javascript
vue.js实现刷新当前页面的方法教程
Jul 05 Javascript
vue.js绑定事件监听器示例【基于v-on事件绑定】
Jul 07 Javascript
微信小程序云开发之云函数详解
May 16 Javascript
vue登录页面cookie的使用及页面跳转代码
Jul 10 Javascript
jQuery HTML设置内容和属性操作实例分析
May 20 jQuery
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
40个迹象表明你还是PHP菜鸟
2008/09/29 PHP
php发送post请求的三种方法
2014/02/11 PHP
PHP正则提取不包含指定网址的图片地址的例子
2014/04/21 PHP
PHP导出Excel实例讲解
2016/01/24 PHP
PHP后端银联支付及退款实例代码
2017/06/23 PHP
javascript多种数据类型表格排序代码分析
2010/09/11 Javascript
Javascript 判断是否存在函数的方法
2013/01/03 Javascript
javascript游戏开发之《三国志曹操传》零部件开发(五)可移动地图的实现
2013/01/23 Javascript
js导航菜单(自写)简单大方
2013/03/28 Javascript
iframe如何动态创建及释放其所占内存
2014/09/03 Javascript
js和jquery实现监听键盘事件示例代码
2020/06/24 Javascript
Bootstrap模态框水平垂直居中与增加拖拽功能
2016/11/09 Javascript
Vue异步加载about组件
2017/10/31 Javascript
自定义PC微信扫码登录样式写法
2017/12/12 Javascript
一个简单的node.js界面实现方法
2018/06/01 Javascript
vue js秒转天数小时分钟秒的实例代码
2018/08/08 Javascript
JS函数内部属性之arguments和this实例解析
2018/10/07 Javascript
node上的redis调用优化示例详解
2018/10/30 Javascript
微信小程序判断用户是否需要再次授权获取个人信息
2019/07/18 Javascript
原生js实现可兼容PC和移动端的拖动滑块功能详解【测试可用】
2019/08/15 Javascript
jQuery实现异步上传一个或多个文件
2020/08/17 jQuery
Windows下Eclipse+PyDev配置Python+PyQt4开发环境
2016/05/17 Python
Python实现二维数组输出为图片
2018/04/03 Python
python并发和异步编程实例
2018/11/15 Python
pandas 层次化索引的实现方法
2019/07/06 Python
使用python爬取抖音视频列表信息
2019/07/15 Python
使用Python刷淘宝喵币(低阶入门版)
2019/10/30 Python
Python新手学习装饰器
2020/06/04 Python
用HTML5.0制作网页的教程
2010/05/30 HTML / CSS
html5图片上传预览示例分享
2014/04/14 HTML / CSS
html5 更新图片颜色示例代码
2014/07/29 HTML / CSS
公司年会抽奖活动主持词
2014/03/31 职场文书
求职信结尾怎么写
2014/05/26 职场文书
2016廉政教育学习心得体会
2016/01/25 职场文书
《总之就是很可爱》新作短篇动画《总之就是很可爱~制服~》将于2022年夏天播出
2022/04/07 日漫
SpringBoot详解自定义Stater的应用
2022/07/15 Java/Android