自动化生成路由

109 阅读2分钟

plugin.js

// import vueSfcCompiler from 'vue-compiler-sfc'
const { autoGenerateRoute } = require('./utils.js')
const path = require('path')
const fs = require('fs')
const chokidar = require('chokidar')
class AutoRoutePlugin {
  constructor(options) {
    this.options = options
    // this.lastRouteStr = ''
    // this.prevTimestamps = []
    this.watcher = null
  }
  apply(compiler) {
    compiler.hooks.afterPlugins.tap('afterPlugins', (params) => {
      console.log('afterPlugins:')
      console.log('自动生成路由................................afterPlugins')
      autoGenerateRoute(this.options)
    })
    compiler.hooks.emit.tapAsync('emit', (compilation, done) => {
      console.log('emit:', 'asd')
      // 非生产环境,监测文件变化
      if (process.env.NODE_ENV !== 'production') {
        if (!this.watcher) {
          this.watcher = chokidar
            .watch(
              path.join(
                process.cwd(),
                this.options.vueDirsAbsolute || 'src/views'
              ),
              {
                persistent: true,
              }
            )
            .on('all', (event, path) => {
              if (event === 'add' || event === 'change')
                autoGenerateRoute(this.options)
            })
        }
      }

      done()
    })
    compiler.hooks.watchClose.tap('watchClose', (params) => {
      console.log('watchClose:', 'asd')
      // 非生产环境,监测文件变化
      if (process.env.NODE_ENV !== 'production') {
        if (this.watcher) {
          this.watcher.close()
          this.watcher = null
        }
      }
    })
    // webpack的compilier生命周期,测试用
    compiler.hooks.entryOption.tap('entryOption', (params) => {
      console.log('entryOption:')
    })

    compiler.hooks.afterResolvers.tap('afterResolvers', (params) => {
      console.log('afterResolvers:', 'asd')
    })
    compiler.hooks.environment.tap('environment', (params) => {
      console.log('environment:', 'asd')
    })
    compiler.hooks.afterEnvironment.tap('afterEnvironment', (params) => {
      console.log('afterEnvironment:', 'asd')
    })
    compiler.hooks.beforeRun.tapAsync('beforeRun', (params, done) => {
      console.log('beforeRun:', 'asd')
      done()
    })
    compiler.hooks.run.tapAsync('run', (params, done) => {
      console.log('run:', 'asd')
      done()
    })
    compiler.hooks.watchRun.tapAsync('watchRun', (params, done) => {
      console.log('watchRun:', 'asd')
      done()
    })
    compiler.hooks.normalModuleFactory.tap('normalModuleFactory', (params) => {
      console.log('normalModuleFactory:', 'asd')
    })
    compiler.hooks.contextModuleFactory.tap(
      'contextModuleFactory',
      (params) => {
        console.log('contextModuleFactory:', 'asd')
      }
    )
    compiler.hooks.beforeCompile.tapAsync('beforeCompile', (params, done) => {
      console.log('beforeCompile:', 'asd')

      done()
    })
    compiler.hooks.compile.tap('compile', (params) => {
      console.log('compile:', 'asd')
    })
    compiler.hooks.thisCompilation.tap('thisCompilation', (params) => {
      console.log('thisCompilation:', 'asd')
    })
    compiler.hooks.compilation.tap('compilation', (compilation) => {
      console.log('compilation:', 'asd')
      // compilation.hooks.buildModule.tap("autoRoute", (module) => {
      //   console.log("compilation.hooks.buildModule:", module)
      // })
    })
    compiler.hooks.make.tapAsync('make', (params, done) => {
      console.log('make:', 'asd')
      done()
    })
    compiler.hooks.afterCompile.tapAsync('afterCompile', (params, done) => {
      console.log('afterCompile:', 'asd')
      done()
    })
    compiler.hooks.shouldEmit.tap('shouldEmit', (params) => {
      console.log('shouldEmit:', 'asd')
    })
    // compiler.hooks.needAdditionalPass.tap('needAdditionalPass', (params) => {
    //   console.log('needAdditionalPass:','asd')
    // })

    compiler.hooks.afterEmit.tapAsync('afterEmit', (params, done) => {
      console.log('afterEmit:', 'asd')
      done()
    })
    compiler.hooks.done.tap('done', (params) => {
      console.log('done:', 'asd')
    })
    compiler.hooks.failed.tap('failed', (params) => {
      console.log('failed:', 'asd')
    })
    compiler.hooks.invalid.tap('invalid', (params) => {
      console.log('invalid:', 'asd')
    })
  }
}
module.exports = AutoRoutePlugin

utils.js

const fg = require('fast-glob')
const fs = require('fs')
const path = require('path')
const vueSfcCompiler = require('@vue/compiler-sfc')

let ROUTE_DIR_ABSOLUTE = 'src/test-route'
let ROUTE_DIR = 'test-route'
let ROUTE_FILE_DIR = 'src/route'
let ROUTE_FILE_NAME = 'routes'
let ROUTE_TAG = 'xyc-route'
let ROUTE_TS_TAG = false
let route_str_list = []
let route_path_list = []

function autoGenerateRoute(
  options = {
    vueDirsAbsolute: 'src/views', // 需要解析的vue视图文件目录绝对路径
    vueDirsName: 'views', // 需要解析的vue视图文件目录名
    routeDir: 'src/router', // 生成路由文件的目录
    routeName: 'index.js', // 生成路由文件名
    routeTag: 'xyc-route', // 路由标签
    isTs: false,
  }
) {
  // 解析options
  ROUTE_DIR_ABSOLUTE = options.vueDirsAbsolute
  ROUTE_DIR = options.vueDirsName
  ROUTE_FILE_DIR = options.routeDir
  ROUTE_FILE_NAME = options.routeName
  ROUTE_TAG = options.routeTag
  ROUTE_TS_TAG = options.isTs
  // 清空路由meta文件
  // delDir(ROUTE_FILE_DIR + '/meta')】

  // 读取需要自动生成路由的文件路径下所有vue文件
  const vuePagesList = fg.sync('**/*.vue', {
    onlyFiles: true,
    cwd: ROUTE_DIR_ABSOLUTE,
  })
  // 清空路由项数组
  route_str_list = []
  // 根据目录下的文件列表,筛选具有路由配置的vue文件,并在解析各vue文件过程中初始化路由项数组
  route_path_list = filterConfigFiles(vuePagesList)
  layoutRoutesHandler(route_str_list)
  // 根据最终route_str_list生成route文件,此时子路由已被标记:hasHandle为true,过滤掉,生成最终routejs文件
  const resultList = route_str_list.filter((item) => {
    return !item.hasHandle
  })
  return outputRouteFile(resultList)
}
function outputRouteFile(routes) {
  let metaStr = []
  function outeput(list) {
    let routeStr = []
    list.forEach((item) => {
      let str = `{
  path: "/${item.path}",
  name: "${item.name}",
  meta: ${item.meta},
  component: ${item.components}`
      if (item.isLayout) {
        str += `,
  children: ${outeput(item.children)}`
      }
      str += `
}`
      routeStr.push(str)
      metaStr.push(`${item.metaImport}`)
    })
    return `[${routeStr.join(',')}]`
  }
  const routeStr_final = outeput(routes)
  const finalStr = `${metaStr.join('\n')}\nexport default${routeStr_final}`
  fs.writeFileSync(path.join(ROUTE_FILE_DIR, ROUTE_FILE_NAME), finalStr, 'utf8')
  return finalStr || ''
}
function filterConfigFiles(fileList) {
  const resultList = fileList.filter((filePath) => {
    const p = path.join(ROUTE_DIR_ABSOLUTE, filePath)
    const text = fs.readFileSync(p, 'utf8')
    const vueObj = vueSfcCompiler.parse(text)
    // 判断是否有路由选项
    let tag = false
    vueObj.descriptor.customBlocks.forEach((block) => {
      if (block.type === ROUTE_TAG) {
        // 如果有路由选项,将路由选项输出至meta文件夹中
        tag = true
        outeputMetaFile(filePath, block)
        generateRouteStrByPath(filePath, block)
      }
    })
    return tag
  })
  return resultList
}
// 根据传入的目录字符串,自动递归创建目录
function autoMkdir(fileDir) {
  const a = fileDir.split(path.sep)
  function m(arr, length) {
    const dir = arr.slice(0, length).join(path.sep)
    fs.existsSync(dir) ? null : fs.mkdirSync(dir)
  }
  for (let i = 1; i <= a.length; i++) {
    m(a, i)
  }
}
function outeputMetaFile(filePath, block) {
  autoMkdir(path.join(ROUTE_FILE_DIR, './meta'))
  const wp = path.join(
    ROUTE_FILE_DIR,
    './meta',
    `${getMetaFileName(filePath)}${ROUTE_TS_TAG ? '.ts' : '.js'}`
  )
  fs.writeFileSync(wp, block.content, 'utf8')
}
// 根据文件路径,生成元数据文件名
function getMetaFileName(filePath) {
  return filePath
    .split('/')
    .filter((item) => item !== 'index.vue')
    .map((item) => {
      let res = `${item[0].toUpperCase()}${item.slice(1)}`
      res = res.replace(/\.vue/, '')
      return res
    })
    .join('')
}
// 根据文件路径及vue-compiler-sfc解析出来的block,生成route单项部分字段,不进行嵌套路由处理
function generateRouteStrByPath(filePath, block) {
  // 单项route path字段
  const routePath = filePath
    .split('/')
    .filter((item) => item !== 'index.vue')
    .map((item) => {
      // 去除末尾的.vue
      let res = item.replace(/\.vue/, '')
      // 动态路由,将_开头的文件名替换成:路径
      res = res.replace(/_/, ':')
      return res
    })
    .join('/')
  // 单项route name字段
  const routeName = filePath
    .split('/')
    .filter((item) => item !== 'index.vue')
    .map((item) => {
      // 去除末尾的.vue
      let res = item.replace(/\.vue/, '')
      // 动态路由,将_开头的文件名替换成:路径
      res = res.replace(/_/, '')
      return res
    })
    .join('-')

  // 单项route meta字段
  const metaFile = getMetaFileName(filePath)
  // 判断是否具有$layout字段,有则为嵌套路由,进行一个标记,后续分析父子关系构建完整的route项
  let isLayout = false
  if (block.content.indexOf('$layout') !== -1) {
    isLayout = true
  }

  const basicObj = {
    path: routePath,
    name: routeName,
    meta: 'meta' + metaFile || {},
    components: `()=>import(/* webpackChunkName: "chunk_${metaFile}" */ '@/${ROUTE_DIR}/${filePath}')`,
    metaImport: `import meta${metaFile} from './meta/${metaFile}${
      ROUTE_TS_TAG ? '' : '.js'
    }'`,
    isLayout,
    filePath,
  }
  route_str_list.push(basicObj)
}

// 遍历route_str_list,对isLayout为true及嵌套路由父路由进行处理
function layoutRoutesHandler() {
  for (let route of route_str_list) {
    layoutRouteHandler(route)
  }
}
// 子路由也可能为嵌套路由,递归处理
function layoutRouteHandler(route) {
  if (route.isLayout) {
    const layoutFileDirPath = route.path
    const childrenRoutes = route_str_list.filter((item) => {
      let tag = false
      if (
        item.path.startsWith(layoutFileDirPath) &&
        item.path.split('/').length === layoutFileDirPath.split('/').length + 1
      ) {
        // path以父路由开头,并且只为子级不为孙子级
        tag = true
        // 标记该项为父级的子级处理项,后续生成最终routes时,不为其生成路由(已作为父级路由的children注入了)
        item.hasHandle = true
        if (item.isLayout) {
          // 若子级也为layout类型,递归处理
          layoutRouteHandler(item)
        }
      }

      return tag
    })
    route.children = childrenRoutes
  }
}
// 递归删除路由文件夹中的文件
function delDir(path) {
  let files = []
  if (fs.existsSync(path)) {
    files = fs.readdirSync(path)
    files.forEach((file, index) => {
      let curPath = path + '/' + file
      if (fs.statSync(curPath).isDirectory()) {
        delDir(curPath) //递归删除文件夹
      } else {
        fs.unlinkSync(curPath) //删除文件
      }
    })
    fs.rmdirSync(path)
  }
}
module.exports = { autoGenerateRoute }