架构设计
痛点分析
当前脚手架架构图如下图:

这样的架构设计已经可以满足一般脚手架需求,但是有以下两个问题:
- cli 安装速度慢:所有 package 都集成在 cli 里,因此当命令较多时,会减慢cli的安装速度
当前的cil结构
- 灵活性差:init 命令只能使用 @imooc-cli-dev/init 包,对于集团公司而言,每个项目组的 init 命令可能都不相同,可能需要实现 init 命令动态化。如
- 团队A使用 @imooc-cli-dev/init 作为初始化模块
- 团队B使用自己开发的 @imooc-cli-dev/my-init 作为初始化模块
 
这时对我们的架构设计就提出挑战,要求我们能够动态加载init模块,这将增加架构的复杂度,但大大提升脚手架的可扩展性,将脚手架框架和业务逻辑解耦
脚手架架构优化

脚手架命令动态加载功能架构设计

node 命令执行源码
node -e "require('./xx/yy/index.js')"
代码编写
1. 脚手架命令本地调试功能支持
通过前面画图了解,我们要实现的第一步是initCommand的动态命令加载。 是否执行本地代码,我们通过一个属性来进行标识:targetPath
通过环境变量对业务逻辑进行解耦
//core/cli/lib/index.js
program.
.option('-tp, --targetPath <targetPath>','是否指定本地调试文件路径','')
 //指定targetPath
program.on('option:targetPath',function(){
  process.env.CLI_TARGET_PATH = program.opts().targetPath 
})
// commands/init/lib/index.js
'use strict';
function init(projectName,options,command)  {
    console.log('init',projectName,command.opts().force,process.env.CLI_TARGET_PATH)
}
module.exports = init;
本节需要注意的一点是如果commander版本低于7.0.0,那么 program.action()中传入的参数为两个。 7.0.0版本以上的传入的参数为三个(name.options,cmd)
另外,访问targetPath这个参数的时候,需要program.opts().targetPath访问。
2. 动态执行库exec模块创建
core下新建包文件: lerna create @cloudscope-cli/exec core/ 然后在core/cli/lib/index.js文件中将exec包引入,将action(init)此处改为action(exec)
3. 创建npm模块通用类Package
- Package 对象功能 
- 获取入口文件的路径
- 判断当前package是否存在
- 安装package
- 更新package
 
代码实现:
- 在model文件下创建新的模块Package:lerna create @cloudscope-cli/package
- 在core/exec/lib/index.js文件中引入:const Package = require('@cloudscope-cli/package')
4. Package类的属性、方法定义及构造函数逻辑开发
本节主要有三处代码讲解
- core/exec中创建一个Package对象
- model/package中Package类的构造方法
- utils/utils中添加isObject方法:判断一个属性是否为对象
'use strict';
// 1.targetPath -> modulePath
// 2. modulePath -> Package(npm模块)
// 3. Package.getRootFile(获取入口文件)
// 4. Package.update
// 封装 -> 复用
const Package = require('@cli-dev-zy/package');
const log = require('@cli-dev-zy/log')
const SETTINGS = {
    init: '@cli-dev-zy/init'
}
function exec(projectName, options, command) {
    const targetPath = process.env.CLI_TARGET_PATH
    const homePath = process.env.CLI_HOME_PATH
    log.verbose('targetPath', targetPath)
    log.verbose('homePath', homePath)
    const cmdObj = arguments[arguments.length - 1];
    const cmdName = cmdObj.name();
    const packageName = SETTINGS[cmdName]
    const packageVersion = 'latest'
    const pkg = new Package({
        targetPath,
        packageName,
        packageVersion
    })
    
    console.log(pkg)
}
module.exports = exec;
//models/package/lib/index.js
'use strict';
const { isObject }  = require('@cli-dev-zy/utils');
class Package {
    constructor(options){
        if( !options){
            throw new Error('Package类的options参数不能为空!')
        }
        if( !isObject(options) ){
            throw new Error('Package类的options参数必须为对象!')
        }
        // package路径
        this.targetPath = options.targetPath
        // package的存储路径
        this.storeDir = options.storeDir
        // package的name
        this.packageName = options.packageName
        // package的version
        this.packageVersion = options.packageVersion;
    }
    // 判断当前Package是否存在
    exists(){}
    // 安装Package
    install(){}
    //更新Package
    update(){}
    //获取入口文件路径
    getRootFilePath(){}
}
module.exports = Package;
//utils/utils/lib/index.js
'use strict';
function isObject(o) {
    return Object.prototype.toString.call(o) === '[object Object]'
}
module.exports = {
    isObject
};
5. Package类获取入口文件路径功能开发(pkg-dir应用+解决不同操作系统路径兼容问题)
本节主要实现models/package/lib/index.js中获取入口文件路径的方法实现getRootfile()
- 获取package.json的所在目录--通过安装pkg-dir库
- 读取package.json
- 寻找main/lib
- 路径的兼容macOS/windows --新建包:utils/format-path,且新建路径兼容方法
//core/exec/lib/index.js
const path = require('path')
const formatPath = require('@cli-dev-zy/format-path')
const pkgDir = require('pkg-dir').sync
…………
// 获取入口文件的路径
getRootFilePath() {
    // 1. 获取package.json所在目录 - pkg-dir
    const dir = pkgDir(this.targetPath)
    if (dir) {
        // 2. 读取package.json - require()
        const pkgFile = require(path.resolve(dir, 'package.json'))
        // console.log(pkgFile);
        // 3. 寻找main/lib - path
        if (pkgFile && pkgFile.main) {
            // 4. 路径的兼容(mac/windows)
            return formatPath(path.resolve(dir, pkgFile.main))
        }
    } else {
        return null
    }
}
…………
//utils/format-path/lib/index.js
'use strict';
const path = require('path')
function formatPath(p) {
    const sep = path.sep;
    if(p && typeof p === 'string'){
      if(sep !=='/'){
        return p.replace(/\\/g,'/')
      }
    }
    return p
}
module.exports = formatPath;
6. 利用npminstall库安装npm模块
使用 cnpm 的 npminstall 包安装。在执行 cnpm install 命令安装npm包时,实际用到的就是 npminstall
实例代码如下。npminstall() 返回一个 promise 对象。
const npminstall = require('npminstall');
const path = require('path');
const userhome = require('user-home')
npminstall({
  root: path.resolve(userhome, '.imooc-cli-dev'), // 模块路径
  storeDir: path.resolve(userhome, '.imooc-cli-dev', 'node_modules'), // 实际存储位置,root + node_modules
  registry: 'https://registry.npm.taobao.org',
  pkgs: [
    { name: 'foo', version: '~1.0.0' },
  ], // 指定安装的包名和版本
});
- 首先,我们的项目在开发过程中可能会有错误,有的需要去看执行栈,有的不需要,因此我们在core/cli/lib/index中的core方法中,catch语句中加入如下代码(debug模式下显示执行栈错误)
if(program.opts().debug){
    console.log(e)
}
2.在core/exec/lib/index.js文件中,我们修改代码如下(主要加入了如果不存在targetPath的逻辑梳理):
'use strict';
const path = require('path')   //新添加
const Package = require('@cloudscope-cli/package')
const log = require('@cloudscope-cli/log')
const SETTINGS = {      //新添加
    init: '@imooc-cli/init'
 }
const CATCH_DIR = 'dependencies'  //新添加
async function exec() {
    let targetPath = process.env.CLI_TARGET_PATH
    const homePath = process.env.CLI_HOME_PATH
    let storeDir ='';
    let pkg;
    log.verbose('targetPath', targetPath);
    log.verbose('homePath', homePath);
    const cmdObj = arguments[arguments.length - 1];
    const cmdName = cmdObj.name(); 
    const packageName = SETTINGS[cmdName];
    const packageVersion = 'latest';
    if(!targetPath){
       //生成缓存路径
       targetPath = path.resolve(homePath,CATCH_DIR);  //新添加
       storeDir = path.resolve(targetPath,'node_modules')  //新添加
       log.verbose('targetPath:',targetPath)  //新添加
       log.verbose('storeDir:',storeDir)      //新添加
       pkg = new Package({                                     //新添加
         targetPath,
         storeDir,
         packageName,
         packageVersion
      });
      if(await pkg.exists()){    //新添加
         // 更新package
         log.verbose('更新package')
         await pkg.update();
      }else{
         // 安装package
         await pkg.install();
      }
    }else{
      pkg = new Package({
         targetPath,
         packageName,
         packageVersion
      })
      const rootFile = pkg.getRootFilePath();
      if(rootFile){    //新添加
         require(rootFile).apply(null,arguments);
      }
    }
}
module.exports = exec;
- model/package包中文件主要加入了安装package这个方法,使用了npminstall这个库。
//models/package/lib/ibdex.js
async prepare() {
    if (this.storeDir && !pathExists(this.storeDir)) {
        // 创建所有文件目录
        fse.mkdirpSync(this.storeDir)
    }
    if (this.packageVersion === 'latest') {
        this.packageVersion = await getNpmLatestVersion(this.packageName)
    }
}
 async install(){
   await this.prepare()
   return npminstall({
     root: this.targetPath,
     storeDir: this.storeDir,
     registry:getDefaultRegistry(),
     pkg:{
       name:this.packageName,
       version:this.packageVersion
     }
   })
 }
7. Package类判断模块是否存在方法开发
本节的主要内容是实现package/lib/index.js中的exists方法,代码实现如下:
…………
 // package的缓存目录前缀
        this.cacheFilePathPrefix = this.packageName.replace('/', '_')
…………
get cacheFilePath() {
  return path.resolve(this.storeDir,`_${this.cacheFilePathPrefix}@${this.packageVersion}@${this.packageName}`)
}
async prepare(){
  if(this.storeDir && !pathExists(this.storeDir)){
    fse.mkdirpSync(this.storeDir)
  }
  if(this.packageVersion === 'latest'){
    this.packageVersion = await getNpmLatestVersion(this.packageName);
  }
}
async exists(){
  if(this.storeDir){
    await this.prepare() 
    return pathExists(this.cacheFilePath);
  }else{
    return pathExists(this.targetPath);
  }
}
8. Package类更新模块逻辑开发
本节内容主要为如果Package包有升级,那么需要去更新,主要实现代码为:
// models/package/lib/index.js
…………
getSpecificCacheFilePath(packageVersion){
  return path.resolve(this.storeDir,`_${this.cacheFilePathPrefix}@${packageVersion}@${this.packageName}`)
  }
//更新Package
async update(){
  //获取最新的npm模块版本号
  const latestPackageVersion = await getNpmLatestVersion(this.packageName);
  // 查询最新版本号对应的路径是否存在
  const latestFilePath = this.getSpecificCacheFilePath(latestPackageVersion)
  // 如果不存在,则直接安装最新版本
  if(!pathExists(latestFilePath)){
    await npminstall({
      root:this.targetPath,
      storeDir:this.storeDir,
      registry:getDefaultRegistry(),
      pkgs:[{
        name:this.packageName,
        version:latestPackageVersion
      }
           ]
    })
    this.packageVersion = latestPackageVersion
  }else{
    this.packageVersion = latestPackageVersion
  }
  return latestFilePath;
}
9. Package类获取缓存模块入口文件功能改造
//获取入口文件路径
getRootFilePath(){
    function _getRootFile(targetPath) {
        // 1. 获取package.json所在目录
        const dir = pkgDir(targetPath);
        if (dir) {
          // 2. 读取package.json
          const pkgFile = require(path.resolve(dir, 'package.json'));
          // 3. 寻找main/lib
          if (pkgFile && pkgFile.main) {
            // 4. 路径的兼容(macOS/windows)
            return formatPath(path.resolve(dir, pkgFile.main));
          }
        }
        return null;
      }
      if (this.storeDir) {
        return _getRootFile(this.cacheFilePath);
      } else {
        return _getRootFile(this.targetPath);
      }
}
若有收获,就点个赞吧