Node.js 学习笔记

一、模块
1. 在Node环境中,一个.js文件就称之为一个模块(module)。
2. 模块化的优点:提高代码的可维护性、可重用性高、可以避免函数名和变量名冲突。
3. 模块化示例

// hello.js
'use strict';

var str = "Hello ";

function greet(name) {
    console.log(str + name + '!');
}

module.exports = greet;
// main.js
'use strict';

var greet = require('./hello'); // 引入hello模块

var str = 'Andy';

greet(str); // Hello Andy!

① 这种模块加载机制被称为CommonJS规范。
② 每个.js文件都是一个模块。
③ 内部使用的变量名和函数名不冲突,比如hello.js和main.js 中的 str变量,互不影响。
④ 模块中对外暴露输出变量(可以为任意对象,函数,数组等) module.exports = variable; 一个模块引用其他模块暴露的变量 var ref = require('module_name');

4. 模块化实现的原理
浏览器中,大量使用全局变量可不好。如果你在a.js中使用了全局变量s,那么,在b.js中也使用全局变量s,将造成冲突,b.js中对s赋值会改变a.js的运行逻辑。
Node.js 如何实现此模块化的呢?

// Node.js 加载hello.js之后,包装一下,变成如下进行执行
(function() {
    var str = "Hello ";

    function greet(name) {
        console.log(str + name + '!');
    }

    module.exports = greet;
})();

/*
 * 实现 module.exports
 */

// 准备module对象
var module = {
    id: 'a',
    exports: {}
};
var load = function(module) {
    // 读取 hello.js代码
    function greet(name) {
        console.log('Hello ' + name + '!');
    }

    module.exports = greet;
    // hello.js 代码结束
    return module.exports;
};
var exported = load(module);
// 保存module
save(module, exported);

① 全局变量str经过包装之后变成匿名函数内部的局部变量。如若Node.js继续加载其他模块,模块中的变量str也互不干扰。
② Node.js在加载js文件之前先准备一个变量module,并将其传入加载load函数,最终返回module.exports。

5. module.exports vs exports

/*
 *  module.exports
 */
// a.js
function hello() {
    console.log('Hello world!');
}

function greet(name) {
    console.log('Hello ' + name);
}

module.exports = {
    hello: hello,
    greet: greet
};
// b.js
var foo = require('./a');

foo.hello(); // Hello world!
foo.greet('Andy'); // Hello Andy
/*
 * exports
 */
// a.js
function hello() {
    console.log('Hello world!');
}

function greet(name) {
    console.log('Hello ' + name);
}

exports.hello = hello;
exports.greet = greet;

// b.js
var foo = require('./a');

foo.hello(); // Hello world!
foo.greet('Andy'); // Hello Andy
/* 
 * Node 加载机制:
*/
//Node 把待加载的js文件放入一个包装函数load中执行。在执行这个load()函数之前,Node准备了module变量:

// module变量
var module = {
    id: 'hello',
    exports: {}
}
// load函数最终返回module.exports
var load = function(exports, module) {
    // hello.js 的文件内容
    ...
    // load函数返回
    return module.exports;
}
var exported = load(module.exports, module);

默认情况下,Node准备的exports变量和module.exports变量实际上是同一个变量,并且初始化为空对象{}

exports.foo = function() { return 'foo';};
exports.bar = function() {return 'bar';};

也可写成

module.exports.foo = function() { return 'foo';};
module.exports.bar = function() {return 'bar';};

如若输出函数或者数组

module.exports = function() { return 'foo';};
exports = module.exports = {};

① 如若输出键值对象{},可以利用exports这个已存在的空对象{},并继续添加新的键值;
② 如若输出一个函数或数组,必须直接对module.exports对象赋值;
③所以建议使用module.exports = xxx 方式输出模块变量。
④ exports 是指向的 module.exports 的引用。
⑤ require() 返回的是 module.exports 而不是 exports。

二、基本模块
1. fs - 文件系统模块,负责读写文件
① 异步读文件

// 读取文本文件
'use strict';

var fs = require('fs');

fs.readFile('demo.txt', 'utf-8', function(err, data) { 
   if (err) { // err 正常为null,异常时为错误对象
       console.log(err);
   } else { // data 读取到的String,异常时为undefined
       console.log(data);
   }
});
// 读取图片文件
'use strict';

var fs = require('fs');

fs.readFile('demo.png', function(err, data) {
    if (err) {
        console.log(err);
    } else {
        console.log(data); // 一个Buffer对象(一个包含零个或任意个字节的数组)
        console.log(data.length + ' bytes');
    }
});

Buffer对象可以和String相互转换

// Buffer -> String
var text = data.toString('utf-8');
// String -> Buffer
var buf = Buffer.from(text, 'utf-8');

② 同步读文件

'use strict';

var fs = require('fs');
// 同步读取文件不接受回掉函数
var data = fs.readFileSync('demo.txt', 'utf-8');
console.log(data); 

如若同步读取文件发生错误,用try...catch捕获错误

'use strict';

try {
    var data = fs.readFileSync('demo.txt', 'utf-8');
    console.log(data);
} catch (err) {
    console.log(err);
}

③ 异步写文件

'use strict';

var fs = require('fs');

var data = 'Hello, world!';
fs.writeFile('output.txt', data, function(err) {
    if (err) {
        console.log(err);
    } else {
        console.log('ok');
    }
});
  • 如若文件不存在,则会自动创建一个
  • 如若传入的数据为String,默认按UTF-8写入文件,如若是Buffer对象,则写入的为二进制文件。
  • 回调函数只关心是否写入成功,所以只需一个err参数即可。

④ 同步写文件

'use strict';

var fs = require('fs');

var data = 'Hello world!';
fs.writeFileSync('output.txt', data);

⑤ stat -- 返回文件或目录的详细信息,比如大小、创建时间等


'use strict';

var fs = require('fs');

fs.stat('demo.txt', function(err, stat) {
    if (err) {
        console.log(err);
    } else {
        // 是否是文件
        console.log('isFile: ' + stat.isFile());
        // 是否是 目录
        console.log('isDirectory: ' + stat.isDirectory());
        if (stat.isFile()) {
            console.log('size: ' + stat.size); // 文件大小
            console.log('birth time: ' + stat.birthtime); // 创建时间
            console.log('modified time: ' + stat.mtime); // 修改时间
        }

    }
});

⑥ statSync

'use strict';

var fs = require('fs');

var stat = fs.statSync('demo.txt');
console.log('isFile: ' + stat.isFile());
// 是否是 目录
console.log('isDirectory: ' + stat.isDirectory());
if (stat.isFile()) {
    console.log('size: ' + stat.size); // 文件大小
    console.log('birth time: ' + stat.birthtime); // 创建时间
    console.log('modified time: ' + stat.mtime); // 修改时间
}

2. stream - 仅在服务区端可用的模块,目的支持“流”这种数据结构
① 从文件流读取文本内容

/*
 * 从文本流读取文本内容
 */
'use strict';

var fs = require('fs');

var rs = fs.createReadStream('demo.txt', 'utf-8');
// data 事件流表示流数据可以读取
rs.on('data', function(chunk) {
    console.log('data event');
    console.log(chunk);
});
// end 事件表示这个流已到末尾,无数据可读
rs.on('end', function() {
    console.log('end event');
});
// error 事件表示出错
rs.on('error', function(err) {
    console.log('Error: ' + err);
});

② 以流形式写入文件

/*
 * 以流的形式写入文件
 */

'use strict';

var fs = require('fs');

// 写入文本内容
var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
ws1.write('使用Stream写入文本数据\n');
ws1.write('END');
ws1.end();

// 写入二进制数据
var ws2 = fs.createWriteStream('output2.txt');
ws2.write(new Buffer('使用Stream写入二进制数据\n', 'utf-8'));
ws2.write(new Buffer('END'));
ws2.end();

注:若写入的文件不存在,则会创建一个新的文件。

③ pipe

'use strict';

var fs = require('fs');

var rs = fs.createReadStream('demo.txt');
var ws = fs.createWriteStream('copied.txt');

rs.pipe(ws);

pipe()把一个文件流和另一个文件流串起来,源文件的所有数据就自动写入到目标文件中。实际为一个复制文件的过程
默认情况,Readable数据流数据读取完毕之后,end事件触发后,将自动关闭Writable数据流。

readable.pipe(writable, { end: false }); // 禁止自动关闭Writable流

3. http
① HTTP服务器

/*
 * 创建http服务
 */
'use strict';

var http = require('http');
// 创建http server
var server = http.createServer(function(request,response) {
    // 回调函数接收request和response对象
    // 获得HTTP请求的method和url
    console.log(request.method + ':' + request.url);
    // 将HTTP响应200写入response,同时设置Content-Type:text/html;
    response.writeHead(200, {'content-Type': 'text/html'});
    // 将HTTP响应的HTML内容写入response
    response.end('<h1>Hello world!</h1>');
});

// 监听8090端口
server.listen(8090);

console.log('Server is running at htpp://127.0.0.1:8090');

启动服务之后,在浏览器中输入http://127.0.0.1:8090之后

输出.png

② 文件服务器

/*
 * 文件服务器
 */

'use strict';

var
    fs = require('fs'),
    url = require('url'),
    path = require('path'),
    http = require('http');

// 从命令行参数获取root目录,默认是当前目录
var root = path.resolve(process.argv[2] || '.');
console.log('Static root dir: ' + root);

// 创建服务器
var server = http.createServer(function(request, response) {
    // 获得URL的path
    var pathname = url.parse(request.url).pathname;
    // 获得对应的本地文件路径
    var filepath = path.join(root, pathname);
    // 获取文件状态
    fs.stat(filepath, function(err, stats) {
        if(!err && stats.isFile()) {
            // 没有出错并且文件存在
            console.log('200' + request.url);
            // 发送200响应
            response.writeHead(200);
            // 将文件流导向response;
            fs.createReadStream(filepath).pipe(response);
        } else {
            // 出错了或者文件不存在
            console.log('404' + request.url);
            // 发送404响应
            response.writeHead(404);
            response.end('404 Not Found');
        }
    });
});

server.listen(8090);

console.log('Server is running at http://127.0.0.1:8090');

在命令行运行 node server.js /path/dir; /path/dir 为本地一个有效目录,然后在浏览器中输入 http://127.0.0.1:8090/index.html,本地目录有此文件index.html,服务器就可以把文件内容发送给浏览器。

输出

4. crypto - 提供通用的加密和哈希算法
①Hash

将长度不固定的消息作为输入,通过运行hash函数,生成固定长度的输出,这段输出就叫做摘要。过程不可逆,即输入固定的情况产生固定的输出,但知道输出的情况无法反推输入。

常见摘要算法:

  • MD5: 128 位
  • SHA1: 160位
  • SHA256: 256位
  • SHA512: 512位
/*
 * MD5:128位
 */
const crypto = require('crypto');
const hash = crypto.createHash('md5');

// 可以多次调用update(); hash.update()方法就是将字符串相加
hash.update('Hello, world!');
hash.update('Hello, nodejs!');

console.log(hash.digest('hex')); // hash.digest()将字符串加密返回
/*
 * SHA-1 :160 位
 */

const crypto = require('crypto');
const hash = crypto.createHash('sha1');

// 可以多次调用update(); hash.update()方法就是将字符串相加
hash.update('Hello, world!');
hash.update('Hello, nodejs!');

console.log(hash.digest('hex')); // hash.digest()将字符串加密返回

同理换成 把参数换成 sha256、sha512即可。

② Hmac

/*
 * Hmac
 */

const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'secret-key'); // 需要一个密钥

hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');

console.log(hmac.digest('hex'));
  • 可以理解为带密钥的hash函数。
  • 只要密钥发生了变化,即使同样的输入,输出也不同。

③ 对称加密

加密和解密都用同一个密钥,常见的对称加密算法有:DES、3DES、AES、Blowfish、RC5、IDEA


/*
 * AES
 */

const crypto = require('crypto');

// 加密
function aesEncrypt(data, key) {
    const cipher = crypto.createCipher('aes192', key);
    var crypted = cipher.update(data, 'utf-8', 'hex');
    console.log("cc" + crypted);
    crypted += cipher.final('hex');
    return crypted;
}
// 解密
function aesDecrypt(encrypted, key) {
    const decipher = crypto.createDecipher('aes192', key);
    var decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8'); // decipher.final() 返回加密的内容
    return decrypted;
}

var data = 'Hello, this is a secret message!';
var key = 'Password!';
var encrypted = aesEncrypt(data, key);
var decrypted = aesDecrypt(encrypted, key);

console.log('Plain text: ' + data);
console.log('Encrypted text: ' + encrypted);
console.log('Decrypted text:' + decrypted);

三、Web开发
1. koa
① 入门示例

/*
 * Koa2
 */

// 导入的为一个class
const Koa = require('koa');

const app = new Koa();

app.use(async(ctx, next) => {
    await next();
    ctx.response.type = 'text/html';
    ctx.response.body = '<h1>Hello, koa2!</h1>';
});

app.listen(3000);
console.log('app started at port 3000');
  • 参数ctx是由koa传入的封装了request和response的变量;
  • next是koa传入的将要处理的下一个异步函数;
  • 关键字async和await,可以把一个function变为异步模式;
  • async标记的函数称为异步函数,在异步函数中,可以用await调用另一个异步函数
  • ctx.url相当于ctx.request.url,ctx.type相当于ctx.response.type;
/*
 * koa middleware
 */

const Koa = require('koa');

const app = new Koa();

app.use(async(ctx, next) => {
    console.log(`Step1: + ${ctx.request.method} ${ctx.request.url}`); // 打印URL
    await next(); // 调用下一个middleware
});

app.use(async(ctx, next) => {
    const start = new Date().getTime();
    await next(); // 调用下一个middleware
    const ms = new Date().getTime() - start; // 耗费时间
    console.log(`Step2: Time: ${ms}ms`); // 打印消耗时间
});

app.use(async(ctx, next) => {
    await next();
    ctx.response.type = 'text/html';
    ctx.response.body = '<h1>Hello, koa2!</h1>';
    console.log("Step3");
});

app.listen(3000);
console.log('app started at port 3000');
  • 当用浏览器访问http://localhost:3000时,命令行的输出为:
    Step1: +GET /
    Step3
    Step2: Time: 2ms
  • 每收到一个http请求,koa就会调用app.use()注册的async函数,并传入ctx和next参数。app.use()的顺序决定了middleware的顺序。

② 处理URL

/*
 * koa-router GET
 */

const Koa = require('koa');

const router = require('koa-router')();

const app = new Koa();

// log request url
app.use(async(ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}`);
    await next();
});

// add url-route
router.get('/hello/:name', async(ctx, next) => {
    var name = ctx.params.name;
    ctx.response.body = `<h1>Hello, ${name}!`;
});

router.get('/', async(ctx, next) => {
    ctx.response.body = `<h1>Index</h1>`;
});

// add router middleware;
app.use(router.routes());

app.listen(3000);
console.log('app started at port 3000');

在浏览器输入:http:/localhost:3000,返回的页面显示 Index;
在浏览器输入:http://localhost:3000/hello/andy,返回的页面显示 Hello, andy! ;
router.get('/path', async fn)来注册一个GET请求。可以在请求路径中使用带变量的/hello/:name,变量可以通过ctx.params.name访问

输出
输出
/*
 * koa-router POST
 */

const Koa = require('koa');

const router = require('koa-router')();

const bodyParser = require('koa-bodyparser');

var app = new Koa();
app.use(async(ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}`);
    await next();
});

app.use(bodyParser()); // 用来解析原始request请求,把解析后的参数绑定到ctx.request.body中

router.get('/', async(ctx, next) => {
    ctx.response.body = `<h1>Index</h1>
        <form action="/signin" method="post">
            <p>Name: <input name="name" value="koa"></p>
            <p>Password: <input name="password" type="password"></p>
            <p><input type="submit" value="Submit"></p>
        </form>`;
});

router.post('/signin', async(ctx, next) => {
    var
        name = ctx.request.body.name || '',
        password = ctx.request.body.password || '';
    console.log(`signin with name: ${name}, password:${password}`);
    if (name === 'koa' && password === '12345') {
        ctx.response.body = `<h1>Welcome, ${name}!`;
    } else {
        ctx.response.body = `<h1>Login failed!</h1>
            <p><a href="/">Try again</a></p>`;
    }
});

app.use(router.routes());
app.listen(3000);
console.log('app started at port 3000');

koa-bodyparser必须在router之前被注册到app对象上;
类似的put、delete、head请求也可用router处理

优化目录

controllers 
    -- hello.js
    -- index.js
controller.js
app.js
// hello.js
var fn_hello = async(ctx, next) => {
    var name = ctx.params.name;
    ctx.response.body = `<h1>Hello, ${name}!</h1>`;
};

module.exports = {
    'GET /hello/:name': fn_hello
};
// index.js
// 主页
var fn_index = async(ctx, next) => {
    ctx.response.body = `<h1>Index</h1>
        <form action="/signin" method="post">
            <p>Name: <input name="name" value="koa"></p>
            <p>Password: <input name="password" type="password"></p>
            <p><input type="submit" value="Submit"></p>
        </form>`;
};

// 登录
var fn_signin = async(ctx, next) => {
    var
        name = ctx.request.body.name || '',
        password = ctx.request.body.password || '';
    console.log(`signin with name: ${name}, password:${password}`);
    if (name === 'koa' && password === '12345') {
        ctx.response.body = `<h1>Welcome, ${name}!`;
    } else {
        ctx.response.body = `<h1>Login failed!</h1>
            <p><a href="/">Try again</a></p>`;
    }
};

module.exports = {
    'GET /': fn_index,
    'POST /signin': fn_signin
};
// controller.js
const fs = require('fs');

function addMapping(router, mapping) {
    for (var url in mapping) {
        if (url.startsWith('GET')) {
            var path = url.substring(4);
            router.get(path, mapping[url]);
            console.log(`register URL mapping: GET ${path}`);
        } else if (url.startsWith('POST')) {
            var path = url.substring(5);
            router.post(path, mapping[url]);
            console.log(`register URL mapping: POST ${path}`);
        } else {
            console.log(`invalid URL: ${url}`);
        }
    }
}

function addControllers(router) {
    var files = fs.readdirSync(__dirname + '/controllers');
    var js_files = files.filter((f)=>{
        return f.endsWith(".js");
    });

    for (var f of js_files) {
        console.log(`process controller: ${f}...`);
        let mapping = require(__dirname + '/controllers/' +f);
        addMapping(router, mapping);
    }
}

module.exports = function(dir) {
    let
        controllers_dir = dir || 'controllers';
        router = require('koa-router')();
    addControllers(router, controllers_dir);
    return router.routes();
}
// app.js
const controller = require('./controller');
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
var app = new Koa();
app.use(bodyParser());
app.use(controller());

app.listen(3000);
console.log('app started at port 3000');

③ Nunjuncks
目录

views
   -- hello.html
demo.js

// demo.js
const nunjucks =require('nunjucks');

function createEnv(path, opts) {
    // 参数 opts
    var autoescape = opts.autoescape === undefined ? true: opts.autoescape,
    noCache = opts.noCache || false,
    watch = opts.watch || false,
    throwOnUndefined = opts.throwOnUndefined || false,
    // nunjucks
    env = new nunjucks.Environment (
        new nunjucks.FileSystemLoader('views', {
            noCache: noCache,
            watch: watch
        }), {
            autoescape: autoescape,
            throwOnUndefined: throwOnUndefined
        }
    );
    if (opts.filters) {
        for (var f in opts.filters) {
            env.addFilter(f, opts.filters[f]);
        }
    }
    return env;
}

var env = createEnv('views', {
    watch: true,
    filters: {
        hex: function(n) {
            return '0x' + n.toString(16);
        }
    }
});

var s1 = env.render('hello.html', {name: '小明'});
console.log(s1); // <h1>Hello 小明</h1>
var s2 = env.render('hello.html', {name: '<script>alert("小明")</script>'});
console.log(s2); // <h1>Hello &lt;script&gt;alert(&quot;小明&quot;)&lt;/script&lt;/script&gt;</h1>

参考资料
廖雪峰JavaScritp教程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容