关于Node.js中的模块机制

        为什么要模块,因为它对于代码的复用和解耦有着很关键的作用,关于node.js中的模块加载系统,其实是一个很简单的东西,它是 CommonJS规范的一种实现。所以在说Node.js的模块加载机制之前,先说一说javascript的三种常见的模块化规范:1、CommonJS;2、AMD;3、CMD。

        不拐弯,直入主题,先说CommonJS,它对于javascript模块化有着很重要的作用,它的模块标识是module,模块导出接口是exports,而对于引入模块,只需要一个非常简单的全局性方法require,好了,举个简单的例子:

        先写一个模块文件 calculator.js,内容如下(只是举例,写法不严谨):

// calculator.js 
function plus(a,b){
    return a+b;
}

function minus(a,b){
    return a-b;
}

function times(a,b){
    return a*b;
}

function divide(a,b){
    return a/b;
}

module.exports = { 
    plus,
    minus,
    times,
    divide,
}

        再建一个test.js 的文件,它会引入 calculator这个模块,内容如下:

// 引入calculator模块
var calculator = require('calculator');

//使用calculator模块的plus方法
var plusResult = calculator.plus(1,2);

//打印结果
console.log(plusResult);   // 3

        就是非常简单的用法,关于它实现会在最后详说。

        好了,现在就到说说AMD规范了,为啥会出现这个规范呢?CommonJS不就够了吗?那是因为,CommonJS的require方法引入模块是同步的,如果我放在前端的话,那就非常不适合了,特别文件如果要同步加载,等这么久时间是非常影响用户体验的。所以CommonJS是适用于服务器端的,所以在前端看来,就需要像AMD这种异步模块加载的方式了。

        AMD加载模块也是require方法,但是由于异步,所以多了一个回调参数,对于上面的例子,加载的方法就变为大概这样:

// 引入calculator模块
require(['calculator'],function(calculator){

    //使用calculator模块的plus方法
    var plusResult = calculator.plus(1,2);

    //打印结果
    console.log(plusResult);   // 3
});

        require的第一个参数是数组,传入的是不同的模块,而第二个就是回调函数,异步加载模块后就会执行。RequireJS就是AMD规范的一个典型实现代表。模块的加载路径需要在头部先用require.config来配置。

        而AMD的模块导出方法,就要用到define了,将前面例子改为大概如下:

// calculator.js 
function calculator(){
    var plus = function(a,b){
        return a + b;
    }
    return {
        plus:plus
    }
}

define(calculator);

        当然了,如果这个模块也依赖其他模块,就在define的第一个参数传入模块数组,第二个参数就是回调参数了,和加载形式差不多,其实都是类似的,只不过改了一下写法而已。

        至于CMD,其实和AMD差不多,关于CMD,SeaJS就是它的实现,至于RequireJS和SeaJS两者的差别,可以参考https://github.com/seajs/seajs/issues/277

        好了,最后要回过头来说Node.js的模块机制了。

        因为Node.js的模块机制就是CommomJS的一种实现,所以很多时候我们都可以看到这样的引用:

// 读取一个文件内容
const fs = require('fs');
var content = fs.readFileSync('./book.js',{encoding:'utf8'});

        而模块的导出,有两种方式:

//example.js文件
module.exports = {name:"jack"};   // 方式一
exports.name = "jack";    // 方式二

        引入文件之后,可以用example.name获得模块的输出内容。但是,我们不能写成这种形式:

exports = {name:"jack"};

        因为本来模块的exports属性绑定在module.exports上的,如果你这样写,就等于改写了exports属性了,所以不能导出模块内容了,所以我们可以明确知道,module.exports才是模块真正的导出接口,忌用exports属性来导出模块,至于理解它们两者的关系,它们的假设实现为:

function require(/* ... */) {
    const module = { exports: {} };
        ((module, exports) => {
        // 模块代码在这。在这个例子中,定义了一个函数。
        function someFunc() {}
        exports = someFunc;
        // 此时,exports 不再是一个 module.exports 的快捷方式,
        // 且这个模块依然导出一个空的默认对象。
        module.exports = someFunc;
        // 此时,该模块导出 someFunc,而不是默认对象。
    })(module, module.exports);
    return module.exports;
}

        我们可以看到,我们用require引入模块的时候,里面一开始定义了这样一个变量const module = { exports: {} },而最后返回的是module.exports属性值,所以可以轻易理解为这个就是模块的出口,而中间执行了一段函数,就是将实际模块引入,而模块内的内容,就有module和exports两个形参,如果你改变了exports这个值,那么它不会对原来的module有任何影响,但是你用module.exports就对原来module有插入值的作用,当然,你用exports.property = something也会对原module有插入值的作用。如果你想同时用这两个方式:

module.exports = {name:"Jack"};   // 方式一
exports.name = "Kate"   // 方式二

        根据上面的假设实现,在模块代码中,我们也可以知道,module.exports已经定义了模块的出口内容,而exports.name就不会再有意义了。

        好了,最后说一下Node.js的require加载方式,原生模块直接写模块名称,而自定义模块就要写相对路径了,当然,加载后它都会缓存起来的,它的逻辑在网上找个图就表示很清楚了,如下:

 

文章在我的github上的地址:点击跳转

原创文章,转载请注明出处!

知识共享许可协议
本文章采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可。

关于Node.js的Buffer对象

        关于Node.js的Buffer对象,其实和前面写的文章(关于Node.js的字符编码)有很大关系。

        因为在最初写Node.js,用到Buffer的时候,感觉这个东西有点飘渺,它是怎样的一种存在呢?这篇文章会好好说一说。

        在说Buffer之前,想先说一下Web Api 的一些内容先,其实也是密切相关的。

        先说一下File对象和Blob对象。

        File对象通常来自一些类似选择文件等操作返回的FileList 对象,这个File存放着对应文件的一些信息,包括文件名称、文件大小、文件类型、文件位置、最后修改时间等等的信息,File对象继承了Blob对象的方法,是一个特殊的Blob。

        而Blob对象,是通过Blob( ) 构造函数得来的,例如下面这样创建Blob对象:

var blob = new Blob([JSON.stringify(debug, null, 2)],
  {type : 'application/json'});

        Blob对象表示一个不可变的、原始数据的类似文件对象,也有文件大小、类型等属性,其实可以说File对象就是关于一个完整文件的信息,而Blob对象是关于一段文件的信息,无论文件是否被切割了很多块(slice( )方法)。

        好了,既然有了这两个对象,那么我们如何读取这两个对象(File对象和Blob对象)对应的文件内容呢?

        Web Api又有一个FileReader 对象给我们读取文件,它有那么的几种方法:readAsArrayBuffer( )(读取文件结果中包含ArrayBuffer对象)、readAsBinaryString( )(读取文件结果中包含文件的原始二进制数据)、readAsDataURL( )(读取文件结果中包含一个URL格式的字符串以表示所读取文件的内容)、readAsText( )(读取文件结果中包含一个字符串以表示所读取的文件内容)。

        后面两种方法容易理解,而readAsBinaryString( )在W3C中已经被废除,生产环境中不推荐使用,这里就不说了,这里就说readAsArrayBuffer( )结果中包含的这个ArrayBuffer对象。

        通俗来讲,ArrayBuffer对象代表一个有固定长度的二进制数据数组,你创建一个ArrayBuffer对象的时候,就好像下面这样:

var buffer = new ArrayBuffer(8);

        这样你就创建了一个长度为8byte的缓冲区,不给参数是默认用0来填充数据的。

        其实,你创建的这个buffer只是代表某个数据块的对象,并没有提供方法来操作其中的数据内容,在 ECMAScript 2015 (ES6) 引入 TypedArray 之前,JavaScript 语言没有读取或操作二进制数据流的机制。

        好了,怎么又来个TypedArray 了,它其实不是一般的Array,它是类型化数组,它用来创建操作二进制数据的视图(DataView也可以做到,类似TypedArray),包含很多构造方法:Int8Array 、Uint8Array、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array 等。所以如果我们想创建一个缓冲区,并建立一个队缓冲区操作的视图的话,可以这样写:

var buffer = new ArrayBuffer(8); //创建缓冲区
var view = new Int32Array(buffer); //创建操作视图
view[0] = 38; //操作数据
console.log(view[0]); //打印结果为38

        好了,该回过头来说说Node.js的Buffer对象了,

        其实Buffer 实例对象也是 Uint8Array 实例对象,但有一些不同,尚且不谈论不同的地方,Buffer 类被引入使其可以在 TCP 流或文件系统操作等场景中处理二进制数据流,所以它就是一个操作二进制数据流的工具。

        它可以这样创建缓冲区:

// 创建一个长度为 10、且用 0 填充的 Buffer。
const buffer = Buffer.alloc(10);

        可以这样直接创建含有数据的缓冲区:

// 创建一个包含 [0x1, 0x2, 0x3] 的 Buffer。
const buffer = Buffer.from([1, 2, 3]);

// 创建一个包含 UTF-8 字节 [0x74, 0xc3, 0xa9, 0x73, 0x74] 的 Buffer。
const buffer = Buffer.from('tést');

// 创建一个包含 Latin-1 字节 [0x74, 0xe9, 0x73, 0x74] 的 Buffer。
const buffer = Buffer.from('tést', 'latin1');

        好像前面那样,将ArrayBuffer 对象放进去也是可以的:

const arrayBuffer = new ArrayBuffer(16);
const buffer = Buffer.from(arrayBuffer);

console.log(buffer.buffer === arrayBuffer); //trued

        当然了,拷贝TypedArray 实例内容也是可以的:

const arr = new Uint16Array(2);
arr[0] = 5000;
arr[1] = 4000;

// 拷贝 `arr` 的内容
const buf1 = Buffer.from(arr);

        上面这些例子基本摘抄自Node.js官网文档,其实说到底,Node.js中的Buffer对象是一个大类,包括很多方法来操作二进制数据,就是这样的。

文章在我的github上的地址:点击跳转

原创文章,转载请注明出处!

知识共享许可协议
本文章采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可。

关于Node.js的字符编码

        关于字符编码,其实是非常基础的东西,不过,对于很多人来说,也没有弄清楚它是怎样来的一个东西,就只知道有很多不同的字符编码,常用的是utf-8,所以这里还是要说一下字符编码。

        对于计算机来说,它可以处理这么多不同数据(视频、图片、程序等等),是怎样做到的呢?其实对硬件来说,它能感受到的就是高低电平,低电平表示0,高电平表示1,那么它就能处理0和1这两种数据(实际远不止这么简单),所以可以说,它实质处理的是由1和0组合的二进制数据。

        由于我们对计算机的输入远远不止1和0两个数字,所以必须利用它们的组合来表示其他的数字或字符,我们都知道,一个字节(byte) 有8个比特(bit),就是一个8位的二进制数,所以一个字节可以用0-255这么多个整数来表示其他东西。

        大家都知道,美国人发明计算机时候,肯定按照自己想法来搞,所以按照它们的字符来对应整数的话,只有0-127个字符被编码到计算机里,包括大小写字母、数字和其他一些符号等等,这个编码表就是著名的ASCII编码。

        那么问题就来了,你美国自己那么少字符,我们中国这么复杂的文字怎么表示呢?1个字节肯定不够用来表示了,所以我们会用两个字节来表示,制定了我们的GB2312编码。当然了,其他国家也会这样了,所以有很多不同的编码。

        那么问题又来了,如果的数据里面又有我们这个国家的东西,又有你们国家的东西,那么无论用哪个字符编码,都肯定会出现乱码的情况啊,所以,就出现了大一统的Unicode编码。

        而这个Unicode编码呢,又是两个字节的,为什么呢?因为能够表示的东西足够多啊,那如果我全部数据都是英文的话,你要我一个字母用两个字节表示,就要前面补0,相当于浪费了一倍的存储空间了,所以,就出现了可伸缩长度的UTF-8编码,又叫做万国码,现在已经标准化为RFC 3629,可以用1到6个字节编码Unicode字符,表示英文字母用一个字节,表示中文用三个字节,所以如果用的多英文的时候又可以节省空间了。

        好了,可以回过头来说一下Node.js的字符编码了。

        Node.js目前支持的字符编码有这几种(用字符串表示):

        ‘ascii’、’utf8’、’utf16le’、’ucs2’、’base64’、’latin1’、’binary’、’hex’

        前面两个都说了,而‘utf16le’是用2个 或 4 个字节的 Unicode 字符编码,相当于又是一个变种的Unicode,而‘ucs2’就是‘utf16le’的别名,一样的。

        至于‘base64’,相信大家都很熟悉了,它是网络上最常见的用于传输8Bit字节代码的编码方式之一,有时候我们见到的类似“%XX”形式的就是通过‘base64’加密的(转换),它的转换过程大概如此:先将字符转换为ascii编码,然后用二进制表示,以三个字节(每个字节有8位)为一组,然后分成4份,每份6位,当然了,每一份要补全字节,都要在前面加两个0,就变成了四个字节了(实际浪费了三分一的空间),这么有规律的转换,所以是有Base64编码表查到每一个编码对应的真实字符的。

        而’latin1’就是一字节编码,类似‘ascii’,实际上虽然‘ascii’也是一字节字符编码,但是它实际只利用了7位,最高位为0,因为它只表示0-127,而‘latin1’就表示0-255,所以它向下兼容‘ascii’的,而‘binary’就是’latin1’的别名。

        最后,’hex’就是将每个字节编码为两个十六进制字符,2的8次方和16的平方相等啦,所以它们可以转换过来的啦。

        其实字符编码,就是用一个整数来表示某一个字符的一种方式,而且不会重复,至于你选择用一个字节(二进制),两个字节(二进制),还是不同进制(十进制、十六进制)来表示,就是不同的字符编码了。

文章在我的github上的地址:点击跳转

原创文章,转载请注明出处!

知识共享许可协议
本文章采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可。

关于Node.js的单线程

        好了,开始讲Node.js了,什么是Node.js呢?

        它不是一种新的语言,它是基于googleV8引擎的javascript运行环境,特点:单线程、事件驱动、非阻塞、异步I/O……

        其实一开始的时候理解它的单线程问题还是有点困扰的,虽然不影响应用,但是还是要去理解它,在这里先说一些概念。

        先说一下进程和线程:

        进程,就是具有一个独立功能的程序的一次运行活动,说得直白点,就是一个运行的程序,你打开任务管理器就可以看到很多个进程了,而通常一个进程里面又包含若干个线程,它们都可以共享利用进程的资源,是独立运行和调度的基本单位。

        类比一下,进程就是一个正在生产的大车间,而线程就是里面的一条条生产线,都可以共享车间里面的资源。

        再说一下多线程,多线程就是多个线程并发执行,想象一个,你的生产线是一条接着一条去运行还是很多条生产线一起运行高效呢?答案可想而知。

        但是多线程有缺点,就是如果多条生产线使用独立性的公共资源就会使运行速度变慢,就等于要排队拿饭,饭点只有一个吗,轮流来。而且有可能造成线程的死锁,例如两个人要走这条路,一个人说,你让开我就能走了,另一个人也是这样想的,那大家都在等了,不用走了。

        但是对于单核CPU来说,它的多线程实际不是同一时间上运行多个线程,它同一时间只能做一件事情,就是只能运行一个线程,至于我们可以做到多线程并发,是由于CPU在不同的线程之间快速切换,以达到类似同时执行的效果,所以多线程并发不是并行的概念(不能同一时间同时执行)。

        如果问这样的多线程有什么意义,其实可以很好地解决阻塞问题,例如你要烧开水泡茶,如果是单线程的话,那么你要先等开水烧开,才能继续去准备茶杯和茶叶;如果是多线程,那么你在烧开水的时候,就可以转去准备茶杯和茶叶了,不用干等了。

        但是,并不是线程越多就越好的,因为线程之间的切换需要开销的,如果你的线程太多,CPU在线程切换的花销就太大了,你想象一下,我这个还没做完,那我就先记住做到哪里,然后做另一个,而做另外一个之前,又要先看看做到哪里了和要准备什么,这个切换太多的话,代价就显现了。

        好了,转回来说Node.js的单线程了。

        其实,Node.js实际上不是单线程的,那为什么说它是单线程,因为我们的js代码确实是处于单线程中执行的,但是如果需要调用到异步函数,例如I/O的话,它就会交由线程池的其他线程去执行,js代码会不等它的结果,跳过继续执行代码,然后最后才回头,通过轮询机制,执行回调之后的代码,但是即使多个回调,也只能排队,一个一个执行。

        举个例子,一个老太爷在做事,他只能一个事情接一个事情去做,然后遇到一件麻烦事,要叫张三还钱了,然后他叫管家去追债,然后自己继续办事,到最后搞定了。大声叫一下管家(可能管家已经追完钱回来在旁边等了),老太爷问他搞定没啊,如果管家说搞定了,老太爷就可以将钱放进钱柜里了,如果管家说搞不定啊,老太爷可能就会打他一顿了。

        当然了,基于它的单线程,Node.js是适合I/O密集型的程序,不适合计算密集型的程序,因为如果你叫老太爷计算一个东西,他一直算啊算,那其他要算的怎么办,不就在老太爷卡住了吗?I/O密集型就不一样了,老太爷没什么数要算,有麻烦事就交给下人去搞定就行了,多几个麻烦事也不怕,有的是下人,大不了他们干完回来一个接着一个汇报情况而已。

        关于Node.js的单线程,大概就是说的这样吧。

 

文章在我的github上的地址:点击跳转

原创文章,转载请注明出处!

知识共享许可协议
本文章采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可。