一、模块
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之后
② 文件服务器
/*
* 文件服务器
*/
'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 <script>alert("小明")</script</script></h1>
参考资料
廖雪峰JavaScritp教程