笔记
第一章:本周导学
1-1 本周整体内容介绍和学习方法
标题
- 基于Commander完成脚手架命令注册和命令执行过程开发
收获
- 如何设计高性能脚手架
- Node多线程开发
- javascript面向对象编程的实战技巧
内容
- 图解高性能脚手架架构设计方法
- 封装通用的Package和Command类
- 基于缓存 + Node 多进程 实现动态命令加载和执行
- 将业务逻辑和脚手架逻辑彻底解耦
加餐
Node多进程开发进阶--child_process源码解析
- 深入Node源码看清spawn/exec/execFile/fork的本质区别,彻底搞懂Node多进程原理。
第二章:imooc-cli脚手架命令注册
2-1 imooc-cli脚手架初始化+全局参数注册
(本节有代码编写) 本节的主要内容为使用commander这个库在全局添加注册命令
- cd core/cli
- npm i -S commander
// core/cli/lib/index  添加全局注册命令方法
//命令注册
function registerCommand(){
    program
        .name(Object.keys(pkg.bin)[0])
        .usage('<command> [options]')
        .version(pkg.version)
        .option('-d, --debug', '是否开启调试模式', false);
     // 开启debug模式
     program.on('option:debug',function(){
        if(program.opts().debug){
            process.env.LOG_LEVEL='verbose'
        }else{
            process.env.LOG_LEVEL='info'
        }
        log.level = process.env.LOG_LEVEL
    })
  
    // 对未知命令监听
    program.on('command:*',function(obj){
        const availableCommands = program.commands.map(cmd => cmd.name())
        console.log(colors.red('未知的命令:'+obj[0]))
        if(availableCommands.length > 0){
            console.log(colors.red('可用命令为:'+availableCommands.join(',')))
        }
    })
 
    program.parse(program.argv)
    if(program.args && program.args.length < 1) {
        program.outputHelp();
        console.log()
    }
}
2-2 imooc-cli脚手架命令注册
(本节有代码编写)
本节的主要内容为添加第一个comman操作:'init',并在commands文件夹下创建新的init包
// core/cli/lib/index
......
const init = require('@cloudscope-cli/init')
......
program
  .command('init [projectName]')
    .option('-f,--force','是否强制更新项目')
    .action(init)
......
第三章:高性能脚手架架构设计和缓存结构设计
3-1 当前imooc-cli脚手架架构痛点分析
当前的代码架构如图:

3-2 高性能脚手架架构设计
对以上架构(之前代码编写)的主要优化点有以下三个方面
- 将init命令做成了一个动态加载的形式
- 动态加载的脚手架通过缓存形式进行存储:执行哪个命令下载哪个命令
- 动态加载的时候,通过node多进程进行执行:深挖cpu性能

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

上图架构是动态加载initComand开始的
本节简单讲述了两点:
- require加载文件的用法:
- require('/xxx/yyy/index.js') ---- 加载绝对路径
- require('./index.js')----加载相对路径
- require('fs') ---- 加载内置模块
- require('npmlog') ---- 加载第三方包
- node执行模块两种方式
- node 执行文件: node core/cli/bin/index.js
- node -e '字符串':node -e "require(./core/cli/bin/index.js)"
4-1 脚手架命令本地调试功能支持
通过前面画图了解,我们要实现的第一步是initCommand的动态命令加载,即3-3章节所示图。 是否执行本地代码,我们通过一个属性来进行标识: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访问。
4-2 动态执行库exec模块创建
(本节有代码编写)
core下新建包文件: lerna create @cloudscope-cli/exec core/ 然后在core/cli/lib/index.js文件中将exec包引入,将action(init)此处改为action(exec)
4-3 创建npm模块通用类Package
<br />(本节有代码编写)
首先讲解了exec模块逻辑
- targetPath -> modulePath
- modulePath -> Package(npm模块)
- Package.getRootFile(获取入口文件)
- Package.update / Package.install
<br />代码实现:
- 在model文件下创建新的模块Package:lerna create @cloudscope-cli/package
- 在core/exec/lib/index.js文件中引入:const Package = require('@cloudscope-cli/package')
4-4 Package类的属性、方法定义及构造函数逻辑开发
(本节有代码编写)
本节主要有三处代码讲解
- core/exec中创建一个Package对象
- model/package中Package类的构造方法
- utils/utils中添加isObject方法:判断一个属性是否为对象
代码分别如下:
// core/exec/lib/index.js
'use strict';
const Package = require('@cloudscope-cli/package')
const log = require('@cloudscope-cli/log')
const SETTINGS = {
    init: '@cloudscope-cli/init'
 }
function exec() {
    // 1. targetPath -> modulePath
   // 2. modulePath -> Package(npm模块)
   // 3. Package.getRootFile(获取入口文件)
   // 4. Package.update / Package.install'
    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';
     pkg = new Package({
        targetPath,
        storeDir,
        packageName,
        packageVersion
     })
     console.log(pkg)
}
module.exports = exec;
//models/package/lib/index.js
'use strict';
const { isObject }  = require('@liugezhou-cli-dev/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(obj){
    return Object.prototype.toString.call(obj).slice(8,-1) === 'Object'
}
module.exports = {
    isObject 
}
4-5 Package类获取入口文件路径功能开发(pkg-dir应用+解决不同操作系统路径兼容问题)
(本节有代码编写)<br />本节主要实现models/package/lib/index.js中获取入口文件路径的方法实现getRootfile()<br />
思路:
- 获取package.json的所在目录--通过安装pkg-dir库
- 读取package.json
- 寻找main/lib
- 路径的兼容macOS/windows --新建包:utils/format-path,且新建路径兼容方法
核心代码为:
//core/exec/lib/index.js
…………
// 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;
…………
'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;
4-6 利用npminstall库安装npm模块
<br />(本节有代码编写)
本节实现的内容为exec中的install方法,通过npminstall这个库。 使用之前现在测试项目下使用之:测试代码。
const npminstall = require('npminstall')
const path = require('path')
const userHome = require('user-home')
npminstall({
    root: path.resolve(userHome,'.cli-test'), //模块路径
    storeDir: path.resolve(userHome,'.cli-test','node_modules') ,
    registry:'https://registry.npmjs.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 install(){
   await this.prepare()
   return npminstall({
     root: this.targetPath,
     storeDir: this.storeDir,
     registry:getDefaultRegistry(),
     pkg:{
       name:this.packageName,
       version:this.packageVersion
     }
   })
 }
4-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);
  }
}
4-8 Package类更新模块逻辑开发
<br />(本节有代码编写)
本节内容主要为如果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;
}
4-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);
          }
    }