nodejs速学笔记(一)

Nodejs基础

安装、运行什么的就不说了,网上一大堆教程。这部分主要是总结一下nodejs是什么,CMD模块系统要注意的。

  • nodejs是一个js脚本解析器,任何操作系统下安装NodeJS本质上做的事情都是把NodeJS执行程序复制到一个目录,然后保证这个目录在系统PATH环境变量下,以便终端下可以使用node命令
  • 终端下直接输入node命令,可以进入命令行交互模式,很适合用来测试一些JS代码片段,比如正则表达式。
  • NodeJS使用CMD模块系统,主模块作为程序入口点,所有模块在执行过程中只初始化一次。
  • 除非JS模块不能满足需要,否则不要轻易使用二进制模块,否则用户会痛苦无比。

代码的组织和部署

模块路径解析规则

路径解析一直没有研究过,总感觉是个心头病。。

require函数支持/ 或磁盘符C:开头的绝对路径,也支持./开头的相对路径,绝对路径和相对路径当然不够灵活,我想动动文件位置就得更改一堆路径。

因此,require函数支持第三种形式的路径,写法类似于foo/bar,并一次按照以下规则解析路径,直到找到模块位置。

1、内置模块
如果传递给require函数的是Nodejs内置模块名称,不做路径解析,直接返回内部模块的导出对象,例如:

require('fs')    

2、node_modules目录
NodeJS定义了一个特殊的node_modules目录用于存放模块,例如某个模块的绝对路径是/home/user/hello.js, 在该模块中使用require('foo/bar')方式加载模块时,则NodeJS一次尝试使用以下路径。

/home/user/node_modules/foo/bar
/home/node_modules/foo/bar
/node_modules/foo/bar

3、NODE_PATH环境变量
与PATH环境变量类似,NodeJS允许通过NODE_PATH环境变量来指定额外的模块搜索路径,NODE_PATH环境变量中包含一到多个目录路径,路径之间Linux下使用:分割,windows下使用;分割。例如,定义了以下NODE_PATH环境变量:

NODE_PATH=/home/user/lib:/home/lib

当使用require(‘foo/bar’)的方式加载模块时,则NodeJS依次尝试以下路径。

/home/user/lib/foo/bar
 /home/lib/foo/bar

包(package)

JS模块的基本单位是js文件,但复杂些的模块往往由多个子模块组成。这些子模块往往有一个入口模块,在使用时只需要加载入口模块即可。如下目录结构,cat是一个包,里面包含head.js.body.js,main.js,其中main.js是入口模块,加载方式就是require('home/user/lib/cat/main.js'),但是如果我们把main.js改成index.js,引入入口模块的时候就只需要写成require(‘home/user/lib/cat’)的形式,程序会自动寻找index.js的。

- /home/user/lib/
- cat/
    head.js
    body.js
    main.js

进而,如果想自定义入口模块的文件名和存放位置,就需要在包目录下包含一个package.json文件,并在其中指定入口模块的路径。上例中的cat模块可以重构如下:

- /home/user/lib/
- cat/
    + doc/
    - lib/
        head.js
        body.js
        main.js
    + tests/
    package.json

其中package.json内容如下:

{
    "name": "cat",
    "main": "./lib/main.js"
}

如此一来,就同样可以使用require('/home/user/lib/cat')的方式加载模块。NodeJS会根据包目录下的package.json找到入口模块所在位置。

命令行程序

使用NodeJS编写的东西,要么是一个包,要么是一个命令行程序,而前者最终也会用于开发后者。因此我们在部署代码时需要一些技巧,让用户觉得自己是在使用一个命令行程序。

例如我们用NodeJS写了个程序,可以把命令行参数原样打印出来。该程序很简单,在主模块内实现了所有功能。并且写好后,我们把该程序部署在/home/user/bin/node-echo.js这个位置。为了在任何目录下都能运行该程序,我们需要使用以下终端命令。

$ node /home/user/bin/node-echo.js Hello World
Hello World

这种使用方式看起来不怎么像是一个命令行程序,下边的才是我们期望的方式。

$ node-echo Hello World

Linux

在Linux系统下,我们可以把JS文件当作shell脚本来运行,从而达到上述目的,具体步骤如下:

1、在shell脚本中,可以通过#!注释来指定当前脚本使用的解析器。所以我们首先在node-echo.js文件顶部增加以下一行注释,表明当前脚本使用NodeJS解析。

#! /usr/bin/env node

NodeJS会忽略掉位于JS模块首行的#!注释,不必担心这行注释是非法语句。

2、然后,我们使用以下命令赋予node-echo.js文件执行权限。

$ chmod +x /home/user/bin/node-echo.js

这样处理后,我们就可以在任何目录下使用node-echo命令了。

windows

在Windows系统下的做法完全不同,我们得靠.cmd文件来解决问题。假设node-echo.js存放在C:\Users\user\bin目录,并且该目录已经添加到PATH环境变量里了。接下来需要在该目录下新建一个名为node-echo.cmd的文件,文件内容如下:

@node "C:\User\user\bin\node-echo.js" %*

这样处理后,我们就可以在任何目录下使用node-echo命令了。

工程目录

了解了以上知识后,现在我们可以来完整地规划一个工程目录了。以编写一个命令行程序为例,一般我们会同时提供命令行模式和API模式两种使用方式,并且我们会借助三方包来编写代码。除了代码外,一个完整的程序也应该有自己的文档和测试用例。因此,一个标准的工程目录都看起来像下边这样。

- /home/user/workspace/node-echo/   # 工程目录
    - bin/                          # 存放命令行相关代码
        node-echo
    + doc/                          # 存放文档
    - lib/                          # 存放API相关代码
        echo.js
    - node_modules/                 # 存放三方包
        + argv/
    + tests/                        # 存放测试用例
    package.json                    # 元数据文件
    README.md                       # 说明文件

NPM

会用就行了,不求甚解哈哈~~

下载第三方包

比如上边例子中的argv,就可以在工程目录下打开终端,使用以下命令来下载三方包。

$ npm install argv
...
argv@0.0.2 node_modules\argv

下载好之后,argv包就放在了工程目录下的node_modules目录中,因此在代码中只需要通过require('argv')的方式就好,无需指定三方包路径。

以上命令默认下载最新版三方包,如果想要下载指定版本的话,可以在包名后边加上@<version>,例如通过以下命令可下载0.0.1版的argv。

$ npm install argv@0.0.1
...
argv@0.0.1 node_modules\argv

如果使用到的三方包比较多,在终端下一个包一条命令地安装未免太人肉了。因此NPM对package.json的字段做了扩展,允许在其中申明三方包依赖。因此,上边例子中的package.json可以改写如下:

{
    "name": "node-echo",
    "main": "./lib/echo.js",
    "dependencies": {
        "argv": "0.0.2"
    }
}

这样处理后,在工程目录下就可以使用npm install命令批量安装三方包了。更重要的是,当以后node-echo也上传到了NPM服务器,别人下载这个包时,NPM会根据包中申明的三方包依赖自动下载进一步依赖的三方包。例如,使用npm install node-echo命令时,NPM会自动创建以下目录结构。

- project/
    - node_modules/
        - node-echo/
               - node_modules/
                + argv/
            ...
    ...

如此一来,用户只需关心自己直接使用的三方包,不需要自己去解决所有包的依赖关系。

安装命令行程序

从NPM服务上下载安装一个命令行程序的方法与三方包类似。例如上例中的node-echo提供了命令行使用方式,只要node-echo自己配置好了相关的package.json字段,对于用户而言,只需要使用以下命令安装程序。

$ npm install node-echo -g

参数中的-g表示全局安装,因此node-echo会默认安装到以下位置,并且NPM会自动创建好Linux系统下需要的软链文件或Windows系统下需要的.cmd文件。

- /usr/local/               # Linux系统下
        - lib/node_modules/
            + node-echo/
            ...
        - bin/
            node-echo
            ...
        ...

    - %APPDATA%\npm\            # Windows系统下
        - node_modules\
            + node-echo\
            ...
        node-echo.cmd
        ...

发布代码

第一次使用NPM发布代码前需要注册一个账号。终端下运行npm adduser,之后按照提示做即可。账号搞定后,接着我们需要编辑package.json文件,加入NPM必需的字段。接着上边node-echo的例子,package.json里必要的字段如下。

{
        "name": "node-echo",           # 包名,在NPM服务器上须要保持唯一
        "version": "1.0.0",            # 当前版本号
        "dependencies": {              # 三方包依赖,需要指定包名和版本号
            "argv": "0.0.2"
          },
        "main": "./lib/echo.js",       # 入口模块位置
        "bin" : {
            "node-echo": "./bin/node-echo"      # 命令行程序名和主模块位置
        }
    }

之后,我们就可以在package.json所在目录下运行npm publish发布代码了。

版本号

使用NPM下载和发布代码时都会接触到版本号。NPM使用语义版本号来管理代码,这里简单介绍一下。

语义版本号分为X.Y.Z三位,分别代表主版本号、次版本号和补丁版本号。当代码变更时,版本号按以下原则更新。

  • 如果只是修复bug,需要更新Z位。

  • 如果是新增了功能,但是向下兼容,需要更新Y位。

  • 如果有大变动,向下不兼容,需要更新X位。
    版本号有了这个保证后,在申明三方包依赖时,除了可依赖于一个固定版本号外,还可依赖于某个范围的版本号。例如”argv”: “0.0.x”表示依赖于0.0.x系列的最新版argv。NPM支持的所有版本号范围指定方式可以查看官方文档。

灵机一点

除了本章介绍的部分外,NPM还提供了很多功能,package.json里也有很多其它有用的字段。除了可以在npmjs.org/doc/查看官方文档外,这里再介绍一些NPM常用命令。

NPM提供了很多命令,例如install和publish,使用npm help可查看所有命令。

使用npm help 可查看某条命令的详细帮助,例如npm help install

在package.json所在目录下使用npm install . -g可先在本地安装当前命令行程序,可用于发布前的本地测试。

使用npm update <package>可以把当前目录下node_modules子目录里边的对应模块更新至最新版本。

使用npm update <package> -g可以把全局安装的对应命令行程序更新至最新版。

使用npm cache clear可以清空NPM本地缓存,用于对付使用相同版本号发布新版本代码的人。

使用npm unpublish <package>@<version>可以撤销发布自己发布过的某个版本代码。

小结

本章介绍了使用NodeJS编写代码前需要做的准备工作,总结起来有以下几点:

  • 编写代码前先规划好目录结构,才能做到有条不紊。

  • 稍大些的程序可以将代码拆分为多个模块管理,更大些的程序可以使用包来组织模块。

  • 合理使用node_modules和NODE_PATH来解耦包的使用方式和物理路径。

  • 使用NPM加入NodeJS生态圈互通有无。

  • 想到了心仪的包名时请提前在NPM上抢注。

文件操作

文件拷贝

NodeJS提供了基本的文件操作API,但是像文件拷贝这种高级功能就没有提供,因此我们先拿文件拷贝程序练手。与copy命令类似,我们的程序需要能接受源文件路径与目标文件路径两个参数。

小文件拷贝

我们使用NodeJS内置的fs模块简单实现这个程序如下。

var fs = require('fs');

function copy(src, dst) {
    fs.writeFileSync(dst, fs.readFileSync(src));
}

function main(argv) {
    copy(argv[0], argv[1]);
}

main(process.argv.slice(2));

以上程序使用fs.readFileSync从源路径读取文件内容,并使用fs.writeFileSync将文件内容写入目标路径。

process是一个全局变量,可通过process.argv获得命令行参数。由于argv[0]固定等于NodeJS执行程序的绝对路径,argv[1]固定等于主模块的绝对路径,因此第一个命令行参数从argv[2]这个位置开始。

大文件拷贝

上边的程序拷贝一些小文件没啥问题,但这种一次性把所有文件内容都读取到内存中后再一次性写入磁盘的方式不适合拷贝大文件,内存会爆仓。对于大文件,我们只能读一点写一点,直到完成拷贝。因此上边的程序需要改造如下。

var fs = require('fs');

function copy(src, dst) {
    fs.createReadStream(src).pipe(fs.createWriteStream(dst));
}

function main(argv) {
    copy(argv[0], argv[1]);
}

main(process.argv.slice(2));

以上程序使用fs.createReadStream创建了一个源文件的只读数据流,并使用fs.createWriteStream创建了一个目标文件的只写数据流,并且用pipe方法把两个数据流连接了起来。连接起来后发生的事情,说得抽象点的话,水顺着水管从一个桶流到了另一个桶。

API走马观花

我们先大致看看NodeJS提供了哪些和文件操作有关的API。这里并不逐一介绍每个API的使用方法,官方文档已经做得很好了。

Buffer(数据块)

S语言自身只有字符串数据类型,没有二进制数据类型,因此NodeJS提供了一个与String对等的全局构造函数Buffer来提供对二进制数据的操作。

  • 除了可以读取文件得到Buffer的实例外,还能够直接构造,例如:

    var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);

  • 有length属性

  • 可以用下标访问: bin[0]
  • 与字符串能够互相转化

    • 使用指定编码将二进制数据转化为字符串
      var str = bin.toString('utf-8'); // => "hello"
    • 将字符串转换为指定编码下的二进制数据:
      var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f>
  • Buffer不是只读的(字符串是只读的,并且对字符串的任何修改得到的都是一个新字符串,原字符串保持不变。),更像是可以做指针操作的C语言数组。例如,可以用[index]方式直接修改某个位置的字节。

    • bin[0] = 0x48;
  • 而.slice方法也不是返回一个新的Buffer,而更像是返回了指向原Buffer中间的某个位置的指针

    `[ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]

     ^           ^
     |           |
    bin     bin.slice(2)`
    

    因此对.slice方法返回的Buffer的修改会作用于原Buffer,例如:

    var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
    var sub = bin.slice(2);
    
    sub[0] = 0x65;
    console.log(bin); // => <Buffer 68 65 65 6c 6f> ```
    

    也因此,如果想要拷贝一份Buffer,得首先创建一个新的Buffer,并通过.copy方法把原Buffer中的数据复制过去。这个类似于申请一块新的内存,并把已有内存中的数据复制过去。以下是一个例子。

    var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
    var dup = new Buffer(bin.length);
    
    bin.copy(dup);
    dup[0] = 0x48;
    console.log(bin); // => <Buffer 68 65 6c 6c 6f>
    console.log(dup); // => <Buffer 48 65 65 6c 6f>
    

总之,Buffer将JS的数据处理能力从字符串扩展到了任意二进制数据。

Stream(数据流)

当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到数据流。NodeJS中通过各种Stream来提供对数据流的操作。

以上边的大文件拷贝程序为例,我们可以为数据来源创建一个只读数据流,示例如下:

var rs = fs.createReadStream(pathname);

    rs.on('data', function (chunk) {
        doSomething(chunk);
    });

    rs.on('end', function () {
        cleanUp();
    });

Stream基于事件机制工作,所有Stream的实例都继承于NodeJS提供的EventEmitter。

上边的代码中data事件会源源不断地被触发,不管doSomething函数是否处理得过来。代码可以继续做如下改造,以解决这个问题。

var rs = fs.createReadStream(src);

    rs.on('data', function (chunk) {
        rs.pause();
        doSomething(chunk, function () {
            rs.resume();
        });
    });

    rs.on('end', function () {
        cleanUp();
    });

以上代码给doSomething函数加上了回调,因此我们可以在处理数据前暂停数据读取,并在处理数据后继续读取数据。

此外,我们也可以为数据目标创建一个只写数据流,示例如下:

var rs = fs.createReadStream(src);
    var ws = fs.createWriteStream(dst);

    rs.on('data', function (chunk) {
        ws.write(chunk);
    });

    rs.on('end', function () {
        ws.end();
    });

我们把doSomething换成了往只写数据流里写入数据后,以上代码看起来就像是一个文件拷贝程序了。但是以上代码存在上边提到的问题,如果写入速度跟不上读取速度的话,只写数据流内部的缓存会爆仓。我们可以根据.write方法的返回值来判断传入的数据是写入目标了,还是临时放在了缓存了,并根据drain事件来判断什么时候只写数据流已经将缓存中的数据写入目标,可以传入下一个待写数据了。因此代码可以改造如下:

var rs = fs.createReadStream(src);
    var ws = fs.createWriteStream(dst);

    rs.on('data', function (chunk) {
        if (ws.write(chunk) === false) {
            rs.pause();
        }
    });

    rs.on('end', function () {
        ws.end();
    });

    ws.on('drain', function () {
        rs.resume();
    });

以上代码实现了数据从只读数据流到只写数据流的搬运,并包括了防爆仓控制。因为这种使用场景很多,例如上边的大文件拷贝程序,NodeJS直接提供了.pipe方法来做这件事情,其内部实现方式与上边的代码类似。

File System(文件系统)

NodeJS通过fs内置模块提供对文件的操作。fs模块提供的API基本上可以分为以下三类:

  • 文件属性读写。

其中常用的有fs.stat、fs.chmod、fs.chown等等。

  • 文件内容读写。

其中常用的有fs.readFile、fs.readdir、fs.writeFile、fs.mkdir等等。

  • 底层文件操作。

其中常用的有fs.open、fs.read、fs.write、fs.close等等。

NodeJS最精华的异步IO模型在fs模块里有着充分的体现,例如上边提到的这些API都通过回调函数传递结果。以fs.readFile为例:

fs.readFile(pathname, function (err, data) {
    if (err) {
        // Deal with error.
    } else {
        // Deal with data.
    }
});

如上边代码所示,基本上所有fs模块API的回调参数都有两个。第一个参数在有错误发生时等于异常对象,第二个参数始终用于返回API方法执行结果。

此外,fs模块的所有异步API都有对应的同步版本,用于无法使用异步操作时,或者同步操作更方便时的情况。同步API除了方法名的末尾多了一个Sync之外,异常对象与执行结果的传递方式也有相应变化。同样以fs.readFileSync为例:

try {
    var data = fs.readFileSync(pathname);
    // Deal with data.
} catch (err) {
    // Deal with error.
}

Path(路径)

操作文件时难免不与文件路径打交道。NodeJS提供了path内置模块来简化路径相关操作,并提升代码可读性。以下分别介绍几个常用的API。

  • path.normalize

    将传入的路径转换为标准路径,具体讲的话,除了解析路径中的.与..外,还能去掉多余的斜杠。如果有程序需要使用路径作为某些数据的索引,但又允许用户随意输入路径时,就需要使用该方法保证路径的唯一性。以下是一个例子:

    var cache = {};
    
    function store(key, value) {
        cache[path.normalize(key)] = value;
    }
    
    store('foo/bar', 1);
    store('foo//baz//../bar', 2);
    console.log(cache);  // => { "foo/bar": 2 }
    

**坑出没注意**: 标准化之后的路径里的斜杠在Windows系统下是\,而在Linux系统下是/。如果想保证任何系统下都使用/作为路径分隔符的话,需要用.replace(/\\/g, '/')再替换一下标准路径。

  • path.join

    将传入的多个路径拼接为标准路径。该方法可避免手工拼接路径字符串的繁琐,并且能在不同系统下正确使用相应的路径分隔符。以下是一个例子:

    path.join('foo/', 'baz/', '../bar'); // => "foo/bar"
    
  • path.extname

    当我们需要根据不同文件扩展名做不同操作时,该方法就显得很好用。以下是一个例子:

    path.extname('foo/bar.js'); // => ".js"
    

遍历目录

遍历目录是操作文件时的一个常见需求。比如写一个程序,需要找到并处理指定目录下的所有JS文件时,就需要遍历整个目录。

  • 递归算法
    遍历目录时一般使用递归算法,否则就难以编写出简洁的代码。递归算法与数学归纳法类似,通过不断缩小问题的规模来解决问题。以下示例说明了这种方法。

    function factorial(n) {
        if (n === 1) {
            return 1;
        } else {
            return n * factorial(n - 1);
        }
    }
    

**陷阱**:使用递归算法编写的代码虽然简洁,但由于每递归一次就产生一次函数调用,在需要优先考虑性能时,需要把递归算法转换为循环算法,以减少函数调用次数。

  • 遍历算法

    目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。深度优先,意味着到达一个节点后,首先接着遍历子节点而不是邻居节点。先序遍历,意味着首次到达了某节点就算遍历完成,而不是最后一次返回某节点才算数。因此使用这种遍历方式时,下边这棵树的遍历顺序是A > B > D > E > C > F。

       A
     / \
    B   C
      / \   \
    D   E   F
    
  • 同步遍历
    了解了必要的算法后,我们可以简单地实现以下目录遍历函数。

    function travel(dir, callback) {
        fs.readdirSync(dir).forEach(function (file) {
            var pathname = path.join(dir, file);
    
            if (fs.statSync(pathname).isDirectory()) {
                travel(pathname, callback);
            } else {
                callback(pathname);
            }
        });
    }
    

可以看到,该函数以某个目录作为遍历的起点。遇到一个子目录时,就先接着遍历子目录。遇到一个文件时,就把文件的绝对路径传给回调函数。回调函数拿到文件路径后,就可以做各种判断和处理。因此假设有以下目录:

- /home/user/
    - foo/
        x.js
    - bar/
        y.js
    z.css

使用以下代码遍历该目录时,得到的输入如下。

travel('/home/user', function (pathname) {
        console.log(pathname);
    });

    ------------------------
    /home/user/foo/x.js
    /home/user/bar/y.js
    /home/user/z.css
  • 异步遍历

    如果读取目录或读取文件状态时使用的是异步API,目录遍历函数实现起来会有些复杂,但原理完全相同。travel函数的异步版本如下。

    function travel(dir, callback, finish) {
        fs.readdir(dir, function (err, files) {
            (function next(i) {
                if (i < files.length) {
                    var pathname = path.join(dir, files[i]);
    
                    fs.stat(pathname, function (err, stats) {
                        if (stats.isDirectory()) {
                            travel(pathname, callback, function () {
                                next(i + 1);
                            });
                        } else {
                            callback(pathname, function () {
                                next(i + 1);
                            });
                        }
                    });
                } else {
                    finish && finish();
                }
            }(0));
        });
    }
    

这里不详细介绍异步遍历函数的编写技巧,在后续章节中会详细介绍这个。总之我们可以看到异步编程还是蛮复杂的。

文本编码

使用NodeJS编写前端工具时,操作得最多的是文本文件,因此也就涉及到了文件编码的处理问题。我们常用的文本编码有UTF8和GBK两种,并且UTF8文件还可能带有BOM。在读取不同编码的文本文件时,需要将文件内容转换为JS使用的UTF8编码字符串后才能正常处理。
  • BOM的移除

    BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符(”\uFEFF”),位于文本文件头部。在不同的Unicode编码下,BOM字符对应的二进制字节如下:

    Bytes      Encoding
    ----------------------------
    FE FF       UTF16BE
    FF FE       UTF16LE
    EF BB BF    UTF8
    

因此,我们可以根据文本文件头几个字节等于啥来判断文件是否包含BOM,以及使用哪种Unicode编码。但是,BOM字符虽然起到了标记文件编码的作用,其本身却不属于文件内容的一部分,如果读取文本文件时不去掉BOM,在某些使用场景下就会有问题。例如我们把几个JS文件合并成一个文件后,如果文件中间含有BOM字符,就会导致浏览器JS语法错误。因此,使用NodeJS读取文本文件时,一般需要去掉BOM。例如,以下代码实现了识别和去除UTF8 BOM的功能。

function readText(pathname) {
        var bin = fs.readFileSync(pathname);

        if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) {
            bin = bin.slice(3);
        }

        return bin.toString('utf-8');
    }
  • GBK转UTF8

    NodeJS支持在读取文本文件时,或者在Buffer转换为字符串时指定文本编码,但遗憾的是,GBK编码不在NodeJS自身支持范围内。因此,一般我们借助iconv-lite这个三方包来转换编码。使用NPM下载该包后,我们可以按下边方式编写一个读取GBK文本文件的函数。

    var iconv = require('iconv-lite');
    
    function readGBKText(pathname) {
        var bin = fs.readFileSync(pathname);
    
        return iconv.decode(bin, 'gbk');
    }
    
  • 单字节编码

    有时候,我们无法预知需要读取的文件采用哪种编码,因此也就无法指定正确的编码。比如我们要处理的某些CSS文件中,有的用GBK编码,有的用UTF8编码。虽然可以一定程度可以根据文件的字节内容猜测出文本编码,但这里要介绍的是有些局限,但是要简单得多的一种技术。

    首先我们知道,如果一个文本文件只包含英文字符,比如Hello World,那无论用GBK编码或是UTF8编码读取这个文件都是没问题的。这是因为在这些编码下,ASCII0~128范围内字符都使用相同的单字节编码。

    反过来讲,即使一个文本文件中有中文等字符,如果我们需要处理的字符仅在ASCII0~128范围内,比如除了注释和字符串以外的JS代码,我们就可以统一使用单字节编码来读取文件,不用关心文件的实际编码是GBK还是UTF8。以下示例说明了这种方法。

    1. GBK编码源文件内容:
        var foo = '中文';
    2. 对应字节:
        76 61 72 20 66 6F 6F 20 3D 20 27 D6 D0 CE C4 27 3B
    3. 使用单字节编码读取后得到的内容:
        var foo = '{乱码}{乱码}{乱码}{乱码}';
    4. 替换内容:
        var bar = '{乱码}{乱码}{乱码}{乱码}';
    5. 使用单字节编码保存后对应字节:
        76 61 72 20 62 61 72 20 3D 20 27 D6 D0 CE C4 27 3B
    6. 使用GBK编码读取后得到内容:
        var bar = '中文';