Node.js学习

Node.js 学习笔记

我相信每个前端 er 肯定有一颗搞后端的心,当然我也不例外,下面是我学习 node 中遇到的一些问题以及解决方式,和一些小工具~

CPU 密集型和 IO 密集型

CPU 密集型也叫计算密集型,在程序中,大部分的时间都用来计算,CPU 的占用率很高,而 IO 操作所花费的时间很短,所以任务所需要的时间基本都是在 CPU 计算的时间。

IO 密集型指的是任务大部分的时间都是在等待 IO 的读写操作,而 CPU 的占用率不高。

所以 CPU 密集型程序适合 C 语言多线程,代码执行效率高,而 IO 密集型程序适合脚本语言,开发效率高,不需要太关注代码的执行效率。

使用 Node.js 更改文件名

// 引入fs文件模块
var fs = require('fs');

// src为需要改的文件名的上级文件夹名称
var src = 'icons';

//fs.readdir(path, callback)可以遍历文件夹,path为内容所在路径,callback接受两个参数,files是一个储存目录中所包含得到文件名的数组
fs.readdir(src, function(err, files) {
  files.forEach(function(filename) {
    var oldName = src + '/' + filename;
    //将`_`替换成`-`,比如将`a_b`的文件名替换成`a-b`
    var newName = src + '/' + filename.replace(/_/g, '-');
    //重命名开始
    fs.rename(oldName, newName, function(err) {
      if (!err) {
        console.log(filename + '下划线替换成功');
      }
    });
  });
});

想学习 node,但是却不知道怎么学。在逛 v2ex 的时候看到过狼叔的个人经历,他说学习 nodejs 最好的方法就是每天看 10 个模块,虽然感觉很难,但是今天在研究 egg-bin 源码的时候想起了狼叔的话,所以今天就打算好好看下 egg-bin 的源码,不求每天看 10 个,希望今天能把 egg-bin 给看完~(事实证明,这也是很有难度的!!)

egg-bin

打开node_modules/egg-bin/bin/egg-bin.js,发现调用了../index.js,然后再细挖,就发现他是基于 common-bin 这个库的封装。是的,为什么说今天能看完 egg-bin 很难呢,因为他一层一层所依赖的库是很多的,如果你要完全的了解这个库的功能,那么它依赖的库的功能你也是必须要了解的。

所以我们来看看 common-bin 的源码,结果发现,它是基于 yargs 写的。我然后查了一下这两个库的作者,前者是阿里的,后者是一个国外的组织。然后我粗略的看了一下 yargs 的 api,还是蛮简单易懂的,所以我打算先把 common-bin 和 egg-bin 看完了再去学习 yargs。

common-bin

const DISPATCH = Symbol('Command#dispatch');
const PARSE = Symbol('Command#parse');
const COMMANDS = Symbol('Command#commands');
const VERSION = Symbol('Command#version');

class CommonBin {
  constructor(rawArgv) {
    /**
     * original argument
     * @type {Array}
     */
    this.rawArgv = rawArgv || process.argv.slice(2);
    debug('[%s] origin argument `%s`', this.constructor.name, this.rawArgv.join(' '));

    /**
     * yargs
     * @type {Object}
     */
    this.yargs = yargs(this.rawArgv);

    /**
     * helper function
     * @type {Object}
     */
    this.helper = helper;

    /**
     * parserOptions
     * @type {Object}
     * @property {Boolean} execArgv - whether extract `execArgv` to `context.execArgv`
     * @property {Boolean} removeAlias - whether remove alias key from `argv`
     * @property {Boolean} removeCamelCase - whether remove camel case key from `argv`
     */
    this.parserOptions = {
      execArgv: false,
      removeAlias: false,
      removeCamelCase: false,
    };

    // <commandName, Command>
    this[COMMANDS] = new Map();
  }

  ...
}

首先他创建了一个 CommonBin Class,将 yargs 导入,使用 load 传入文件夹目录,遍历里面的文件并使用文件名当作 command 的 name,并将 command name 和 file path 当作键值存进 map 中,注意这里他四个变量名(DISPATCH, PARSE, COMMANDS, VERSION)都使用了 Symbol 来避免命名冲突。

 /**
   * start point of bin process
   */
  start() {
    co(function* () {
      // replace `--get-yargs-completions` to our KEY, so yargs will not block our DISPATCH
      const index = this.rawArgv.indexOf('--get-yargs-completions');
      if (index !== -1) {
        // bash will request as `--get-yargs-completions my-git remote add`, so need to remove 2
        this.rawArgv.splice(index, 2, `--AUTO_COMPLETIONS=${this.rawArgv.join(',')}`);
      }
      yield this[DISPATCH]();
    }.bind(this)).catch(this.errorHandler.bind(this));
  }

整个代码执行的起点就是调用 start 函数,使用 co 来依次执行 Generator,不过这里主要就是执行this[DISPATCH],而这个函数就是这个库的核心。

首先调用 yargs-parse 来解析传入的参数,不过 egg-bin 是没有传参数的。获取版本号,如果你的 command 中有副命令存在,比如egg-bin dev中的 dev,因为 dev 中他还有自己独立的 options,所以如果副命令存在,this[DISPATCH]会直接执行副命令的 js 并返回。如果不存在则依次调用之前遍历所获得的 COMMANDS,并调用 yargs.command 来传入 command。

之后判断context.argv.AUTO_COMPLETIONS(默认是 null),如果为 true 则调用传入的参数并打印,不然则直接调用 run。这和 start 中的代码相对应:

// replace `--get-yargs-completions` to our KEY, so yargs will not block our DISPATCH
const index = this.rawArgv.indexOf('--get-yargs-completions');
if (index !== -1) {
    // bash will request as `--get-yargs-completions my-git remote add`, so need to remove 2
    this.rawArgv.splice(index, 2, `--AUTO_COMPLETIONS=${this.rawArgv.join(',')`);
}

就是为了执行自己的 DISPATCH,也就是里面的 run 函数。

    if (context.argv.AUTO_COMPLETIONS) {
      // slice to remove `--AUTO_COMPLETIONS=` which we append
      this.yargs.getCompletion(this.rawArgv.slice(1), completions => {
        // console.log('%s', completions)
        completions.forEach(x => console.log(x));
      });
    } else {
      // handle by self
      yield this.helper.callFn(this.run, [ context ], this);
    }

如果你在自己的副命令 js 中没有创建 run 函数,则就会默认执行yargs.showHelp()函数。

helper.js 主要实现了部分 utils 函数,比如针对 bind 的 callFn 等等。

其实看里面的代码还是很清晰明了的,注释也全,代码格式也很工整,是一个很好的可以学习的模块~

我通过这个模块主要学习了 Class 的使用,yargs 的使用以及如果针对已有库进行更好的封装,我发现很多模块库的缩进都是 2 个 space,而我是不是年纪大了喜欢空大点呢,以后如果自己写模块了,也要好好适应 2 个 space 的格式啦。

debug

debug 是一个 js 的调试工具,node 和 web 端都可以使用,我这里主要研究 node 端的代码。

首先他会通过是否存在 process 来判断环境,然后引用相关的代码。

这两个平台下的代码都是基于 common.js,然后添加部分适配平台的函数,比如 useColors, formatArgs, log 等等。

让我们来看看比较重要的 common.js。

// setup函数,建立一个环境
function setup(env) {
  createDebug.debug = createDebug;
  createDebug.default = createDebug;
  createDebug.coerce = coerce;
  createDebug.disable = disable;
  createDebug.enable = enable;
  createDebug.enabled = enabled;
  createDebug.humanize = require('ms');
  Object.keys(env).forEach(function (key) {
    createDebug[key] = env[key];
  });

  function createDebug(namespace) {
    // 用来debug,log的函数
    function debug () {}


    // 用来删除实例的函数
    function destory() {}

    // 用来增强createDebug的函数
    function extend() {}

    // 用来启动debug
    function enable() {}

    // 用来警用debug
    function disable() {}

    // 用来判断是否启动
    // 里面有names和skips两个数组,如果namespace在names中存在,说明可以启用
    // 如果在skips,则直接跳过不启用
    function enabled () {}


    // 判断val是否是error,如果是则返回val.stack或者val.message
    function coerce() {}

    return debug
  }

  ....
  return createDebug;
}

module.exports = setup;

上面便是整体的代码,还有部分代码在各自的平台 js 文件下,各个函数的作用也很清晰明了,在这里我主要学到了怎么使用 node process.env 的参数,他这里使用 env.colors, depths, show_hidden 参数。

在 debug 函数中,它先是将传入的参数保存到 args 中,然后获取当前的时间戳以及上一个 debug 的时间戳并获得时间差。然后处理 args[0],判断是否要在 namespace 前添加时区时间还是在末尾添加时间差。最后便是处理 args 中的参数,并使用self.log || createDebug.log打印出来。

通过这一个 module,我感觉我对程序设计的理解又深了一点,但是我感觉这些代码中还有可以优化的地方。

exports.init = init;
exports.log = log;
exports.formatArgs = formatArgs;
exports.save = save;
exports.load = load;
exports.useColors = useColors;

module.exports = require('./common')(exports);
var formatters = module.exports.formatters;

/**
 * Map %o to `util.inspect()`, all on a single line.
 */

formatters.o = function(v) {
  this.inspectOpts.colors = this.useColors;
  return util.inspect(v, this.inspectOpts).replace(/\s*\n\s*/g, ' ');
};
/**
 * Map %O to `util.inspect()`, allowing multiple lines if needed.
 */

formatters.O = function(v) {
  this.inspectOpts.colors = this.useColors;
  return util.inspect(v, this.inspectOpts);
};

比如这里完全就可以把 formatters 放在 exports 中,他为了覆盖 createDebug 的 formatters,但是其实在 common.js 中也只是给他初始化为{},所以直接赋值给 exports,然后删除 common.js 中 createDebug.formatters 的赋值便可。

如果有时间用 es6 的 class 重写一下我觉得应该蛮 cool 的~

统计 c#的 controllers 下 api 的个数

const fs = require('fs');

const ApiAllNums = 0;
fs.readdir('./Controllers', (err, files) => {
  if (err) throw err;

  files.forEach(file => {
    const data = fs.readFileSync('./Controllers/' + file, 'utf-8');
    const reg = data.match(/\[Route\(.*\)\]/g);
    ApiAllNums += reg.length;
  });
});

统计前端调用的 api 个数

const fs = require('fs');
const path = require('path');

let adminApi = new Set();

function readFileFn(dirPath) {
  const dir = fs.readdirSync(dirPath);
  dir.forEach(file => {
    // 如果有要忽略的文件可以写在这里
    if (file === 'require-2.1.14.min.js') return;
    const filePath = path.join(dirPath, file);
    const stats = fs.statSync(filePath);
    const isFile = stats.isFile();
    if (isFile) {
      const data = fs.readFileSync(filePath, 'utf-8');
      // 老项目中用的是ajax
      // 可以根据自己使用的库来调整
      const reg = data.match(/Ajax\({\s*url\:(.*?),/g);
      reg &&
        reg.length &&
        reg.forEach(item => {
          const api = item.match(/.*?url\:\s*['"](.*?)[?'"]/);
          adminApi.add(api[1]);
        });
    } else {
      readFileFn(filePath);
    }
  });
}

统计一个 js 中所有函数的行数

const fs = require('fs');
const path = require('path');

let fnLineCount = new Set();

function readFileFn(dirPath) {
  const dir = fs.readdirSync(dirPath);
  dir.forEach(file => {
    const filePath = path.join(dirPath, file);
    const stats = fs.statSync(filePath);
    const isFile = stats.isFile();
    if (isFile) {
      const data = fs.readFileSync(filePath, 'utf-8');
      // 这里要求js经过格式化过,不然就很难获取到相应的函数末尾
      const reg = data.match(/([ ]*)function\s*.*?\s*\(\)\s*\{[\s\S]*?\n\1\}/g);
      reg &&
        reg.length &&
        reg.forEach(item => {
          const fnName = item.match(/function\s*(.*?)\s*\(/)[1] || 'noName';
          const enterMatch = item.match(/\n/g);
          const lineCount = enterMatch.length + 1;
          fnLineCount.add(`${fnName}:${lineCount}`);
        });
    } else {
      readFileFn(filePath);
    }
  });
}
blog comments powered by Disqus
目 录