为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 相关文章推荐
JavaScript在IE和Firefox浏览器下的7个差异兼容写法小结
Jun 18 Javascript
jQuery操作checkbox选择(list/table)
Apr 07 Javascript
从零学jquery之如何使用回调函数
May 16 Javascript
js 左右悬浮对联广告代码示例
Dec 12 Javascript
纯JS打造网页中checkbox和radio的美化效果
Oct 13 Javascript
简单实现jquery焦点图
Dec 12 Javascript
js遍历获取表格内数据的方法(必看)
Apr 06 Javascript
封装运动框架实战左右与上下滑动的焦点轮播图(实例)
Oct 17 Javascript
Angular5.0 子组件通过service传递值给父组件的方法
Jul 13 Javascript
详解JS中统计函数执行次数与执行时间
Sep 04 Javascript
vue动态删除从数据库倒入列表的某一条方法
Sep 29 Javascript
Vue核心概念Action的总结
Jan 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扩展
2013/06/13 PHP
php实现短信发送代码
2015/07/05 PHP
PHP容器类的两种实现方式示例
2019/07/24 PHP
Javascript中的数学函数集合
2007/05/08 Javascript
jQuery 表单验证扩展代码(一)
2010/10/11 Javascript
用jquery实现的模拟QQ邮箱里的收件人选取及其他效果(一)
2011/01/06 Javascript
jQuery如何防止这种冒泡事件发生
2015/02/27 Javascript
使用Node.js实现HTTP 206内容分片的教程
2015/06/23 Javascript
jQuery简单实现上下,左右滑动的方法
2016/06/01 Javascript
jquery中的常见问题及快速解决方法小结
2016/06/14 Javascript
基于cssSlidy.js插件实现响应式手机图片轮播效果
2016/08/30 Javascript
js控制div层的叠加简单方法
2016/10/15 Javascript
微信小程序 label 组件详解及简单实例
2017/01/10 Javascript
js验证手机号、密码、短信验证码代码工具类
2020/06/24 Javascript
Angularjs自定义指令Directive详解
2017/05/27 Javascript
Spring shiro + bootstrap + jquery.validate 实现登录、注册功能
2017/06/02 jQuery
vue如何进行动画的封装
2018/09/26 Javascript
使用jQuery动态设置单选框的选中效果
2018/12/06 jQuery
JS学习笔记之贪吃蛇小游戏demo实例详解
2019/05/29 Javascript
layui递归实现动态左侧菜单
2019/07/26 Javascript
JSON stringify方法原理及实例解析
2020/10/23 Javascript
简单介绍Python的Tornado框架中的协程异步实现原理
2015/04/23 Python
python相似模块用例
2016/03/04 Python
python+selenium识别验证码并登录的示例代码
2017/12/21 Python
python 实现对文件夹中的图像连续重命名方法
2018/10/25 Python
python 高效去重复 支持GB级别大文件的示例代码
2018/11/08 Python
python pandas获取csv指定行 列的操作方法
2019/07/12 Python
Python常用模块sys,os,time,random功能与用法实例分析
2020/01/07 Python
零基础学Python之前需要学c语言吗
2020/07/21 Python
记一次高分屏下canvas模糊问题
2020/02/17 HTML / CSS
关于工资低的辞职信
2014/01/14 职场文书
乡镇保密工作责任书
2014/07/28 职场文书
个人工作保证书
2015/02/28 职场文书
党支部评议意见
2015/06/02 职场文书
世界名著读书笔记
2015/06/25 职场文书
导游词之平津战役纪念馆
2019/11/04 职场文书