前端工程化: 手把手node写一个cli+物料库

4,354 阅读3分钟

零、前言

上一篇文章写了一下joao-cli的基本思路,部分同学反应看不懂,这次直接带大家手写一个自己的cli工具,可以实现

  • 模板代码拉取到本地
  • 安装物料组件到指定目录
  • 安装modules并替换对应内容
  • 拉取物料库指定文件夹

一、准备工作

步骤会给出对应gif图,图不动的话建议右键新连接打开.

1.初始化npm并安装对应依赖.

    // 新建一个文件夹用以存储cli工具, 当前目录打开命令行
    npm init 
    
    package name 输入你的cli name
    
    可以一路回车跳过剩下内容...

2.注册node.js

    在当前目录新建command.js 写上
    
        #! /usr/bin/env node
        console.log('执行了command脚本')
    
    // 登记以上内容, 使command.js可以被node执行
    
    // 然后在packge.json里添加 "bin": { "joao": "./command.js" }
    
        {
          "name": yourcli",
          "version": "0.0.1",
          "description": "快速搭建前端整站, 工程化自动开发",
          "main": "command.js",
          "dependencies": {},
          "devDependencies": {},
          "bin": {
            "joaocli": "./command.js"
          }
        }
    
    //  当前路径打开命令行工具 键入npm link, 声明环境变量
        npm link
    //  成功后, 键入joaocli 会提示脚本
        joaocli
        ...
        执行了command脚本
        

3.安装要用到的依赖

安装依赖 安装目的
commander 命令行工具
fs 用来处理文件
git-clone 用来从git托管地址下载你的文件
inquirer 命令行中可以使用对话或者选项了
path 用来处理路径
shelljs 用来执行shell
tracer 用来花式打log

安装方法: pageage.json所在目录, npm install ${对应的依赖名称}

npm install commander
npm install fs
...

二、功能演示及代码实现

做好准备工作后(安装完所有依赖!), 你已经拥有了可执行的脚本, 现在我们要一一完善各项功能


  • 利用inquirer+commander完成对话
    //修改command.js
    #! /usr/bin/env node
        console.log('执行了command脚本')
        
        const commander = require('commander');
        const inquirer = require('inquirer');
        
        const questions = [
            {
             type: 'input',
             name: 'projectName',
             message: '请为项目命名',
             filter: function(val) {
                return val;
              }
            },
            {
             type: 'list',
             name: 'type',
             message: '请选择使用的模板',
             choices: ['empty-vue-template', '模板2', '模板3'],
             filter: function(val) {
                return val.toLowerCase();
              }
            }
        ]
        
        // 新建项目 拉取模板
        commander
        .command('init')
          .description('用来初始化项目, 拉取模板')
          .action(() => {
          console.log('正在构建...')
          inquirer.prompt(questions)
          .then(answers => {
              console.log('answers', answers)
            })  
        })
        
        // 此行内容落下会使命令行监听失效
        commander.parse(process.argv)
    

现在键入 joaocli init 会输出对话, 并获取用户输入的内容和选项

  • 新建空项目, 拉取模板到本地

引入git clone, 用来进行git 引入shelljs, 用来删除项目的.git文件 引入tracer, 用来彩色console

    const clone = require('git-clone');
    const shell = require('shelljs');
    const log = require('tracer').colorConsole();

添加必要常量

    // 项目地址
    const projectUrl = {
      'empty-vue-template': 'https://github.com/murongqimiao/joao-template', // 空白库
    } 

在拿到用户输入内容的回调后

      const { projectName, type } = answers
      console.log(projectName)
      console.log(type)
      clone(`${projectUrl[type]}`, `./${projectName}`,null, function() {
        log.info('项目构建完成')
        shell.rm('-rf', `./${projectName}/.git`);
        log.info(`清除掉${projectName}的git, 记得进入项目npm install`)
      })

现在在你的目录下执行 joaocli init吧! 你将拉下一个模板项目并清除.git


  • 拉取仓储里的物料到本地. 添加依赖
    const path = require('path')

在packgejson的同级目录下添加物料文件夹

    mkdir material
    cd material

添加新的commander实现物料下载

    // 更新物料
    commander
      .command('update')
      .description('更新物料库')
      .action(() => {
        let pwd = shell.pwd()
        shell.cd(__dirname)
        shell.exec('rm -rf .git')
        const updateFiles = () => {
          shell.cd(path.join(__dirname, '/material'))
          shell.rm('-rf', '*')
          shell.cd(__dirname)
          shell.exec('git pull origin master')
        }
        shell.exec('git init')
        shell.exec('git remote add origin git@github.com:murongqimiao/joao-website.git') // 物料仓库
        shell.exec('git config core.sparseCheckout true')
        shell.exec("echo 'material' >> .git/info/sparse-checkout")
        updateFiles()
      })
      
      
    // 非常重要
    commander.parse(process.argv)

执行joaocli update 即可更新物料到 /material 下

关于物料的管理有很多办法, 这里笔者采用较为简单的git仓库建立专有文件夹托管物料, 利用git的 core.sparseCheckout来下载部分文件夹并不是最好的办法,因为就git的工作机制而言, sparseCheckout依然是追踪所有的历史变更,再删去不必要内容,如果项目在github托管,还可以把github的仓储转为svn再下载,具体操作可以查阅svn如何下载github的部分内容,因为我们厂的代码放在gitlab上,因故采用了core.sparseCheckout来下载物料文件.

同时因为引用下载整个项目, 执行update的时候会特别慢,请耐心等待.

update成功后material文件为下会增加组件文件夹generate_components与module文件夹generate_modules


  • 添加组件到你的模板里 涉及文件读取与书写需要使用node的fs系列
    const fs = require('fs')

command.js中继续添加内容

    /**
     * 引入一些可以抽离的内容: commander快捷键 | getComponentsFiles获取目录 |_getFileName 获取是文件还是文件夹 | generateComponent 在指定目录放置目标文件
     **/
    commander.version('1.0.0')
      .option('-c, --component', 'Add component')
      .option('-p, --page', 'Add page')

    const getComponentsFiles = () => {
      return _getFileName('../material/generate_components')
    }

    const _getFileName = (url) => {
      let _url = path.join(__dirname, url)
      let _back = fs.readdirSync(_url)
      return _back
    }
    
    const generateComponent = (componentType, fileName, pwd) => {
        fs.readFile(path.join(__dirname, `./material/generate_components/${componentType}${fileName}`), 'utf-8', (error, data) => {
            if (error) throw error
            // if (componentName) {
            //   data = data.replace(/__className__/g, componentName)
            //   componentType = componentName
            // }
            
            fs.writeFile(path.join(`${pwd}/src/components/${componentType}${fileName}`), data, (err) => {
              if(err) {
                console.log('err:', err);
              } else {
                console.log(`${componentType}${fileName}写入成功!...`)
              }
            })
          })
      }
    
    // 添加物料
    commander
      .command('add [args...]')
      .description('增加物料 -c 组件  -p 页面')
      .action((args) => {
        // 缺少参数
        if (!commander.component && !commander.page) {
          log.warn('缺少参数-c或者-p, 以区分物料种类, 具体见--help')
        }
        // 增加组件
        if (commander.component) {
          let pwd = shell.pwd()
          args.map(componentType => {
            _componentType = getComponentsFiles().indexOf(componentType) > -1 ? componentType : getComponentsFiles().indexOf(componentType + '.vue') > -1 ? componentType + '.vue' : void 0
            if (!_componentType) {
              log.warn(`:${componentType}  --> 组件不存在, 请检查拼写`)
            } else {
              let _filePath = path.join(__dirname, `./material/generate_components/${_componentType}`)
              let _isFile = fs.statSync(_filePath).isFile()
              if (_isFile) {
                generateComponent(_componentType, '', pwd)
              } else {
                let _aimFiles = fs.readdirSync(_filePath)
                // 建文件夹
                fs.mkdir(`${pwd}/src/components/${_componentType}`, 0777, (err) => {
                  if (err) {
                    log.info(`${componentType}目录已经建立`)
                  }
                })
                _aimFiles.map(item => {
                  generateComponent(_componentType, `/${item}`, pwd)
                })
              }
            }
          })
        }
        // 增加页面
        if (commander.page) {
          /**
          *     这里是添加modules的逻辑,接下来补充
          **/
        }
      })
      
    ...上面的内容同样添加到这句话前面
    // 非常重要
    commander.parse(process.argv)

现在在你init出来的空项目中, 根目录执行 joaocli add -c empty 或者 joaocli add -c Layout 来测试能否正常添加物料吧!


  • 在项目中添加modules

能添加components后, 添加modules就没有那么困难了, 和组件相比只是多了一个自定义路径

首先添加新的问题

    const modulesFileContent = ['data.js', 'index.js', 'vx.js', '__className__.vue']

    // 引入page构建问题
    const pageQuestions = [
      {
       type: 'input',
       name: 'modelType',
       message: '请输入想要使用的页面种类',
       filter: function(val) {
          return val;
        }
      }
    ]

添加增加moduls的逻辑

    /** 
     * 在目标文件中增加对应的page(单个添加)
     */
    const generateModule = (modelType, pageName, fileName, pwd) => {
        let aimFileName = fileName
        let _directory = ''
            // 考虑一级目录
        if (pageName.split('/').length > 2) {
          console.log('目前最多支持2级目录,比如dashboard/example')
        }
        if (pageName.split('/').length === 2) {
          _directory = pageName.split('/')[0] + '/'
          pageName = pageName.split('/')[1]
        }
        
        if (new RegExp(/.vue/).test(fileName)) {
          aimFileName = pageName + '.vue'
        }
        fs.readFile(path.join(__dirname, `./material/generate_modules/${modelType}/${fileName}`), 'utf-8', (error, data) => {
            if (error) throw error
            data = data.replace(/__className__/g, pageName)
            fs.writeFile(path.join(`${pwd}/src/views/${_directory}${pageName}/${aimFileName}`), data, (err) => {
              if(err) {
                console.log('err:', err);
              } else {
                console.log(`${fileName}写入成功!...`)
              }
            })
          })
      }


    // 增加页面
        if (commander.page) {
          /**
          *     这里是添加modules的逻辑补充
          **/
         inquirer.prompt(pageQuestions)
         .then(answers => {
           const  { modelType } = answers
           let pwd = shell.pwd()
           console.log(`commander.page`, args)
           args.map(pageName => {
             console.log('pageName', pageName)
             // 创建对应目录的文件夹
             if (pageName.split('/').length === 2) {
               fs.mkdir(`${pwd}/src/views/${pageName.split('/')[0]}`, 0755, (err) => {
                 log.info(`${pageName.split('/')[0]}已存在`)
               })
             }
             fs.mkdir(`${pwd}/src/views/${pageName}`, 0755, (err) => {
               log.info(`${pageName}已存在`)
             })
             /* 读取./generate_modules下面的文件 */
             modulesFileContent.map(fileName => {
               generateModule(modelType, pageName, fileName, pwd)
             })
           })
         })
        }

在模板项目的根目录执行

joaocli add -p yourlogin

再输入对应module类型, 比如login

就完成了一键安装modules的过程.

源码查看

npm install -g joao-cli

进入joao-cli的目录,直接看代码即可, 完全一致.

官网

目前物料不多, 有时间会维护, 欢迎各路大佬塞物料来, 建议添加README.md , 放置物料使用方法和收款码, 有同学用的爽可以打赏给物料开发者.