1.最简单的web
先来看一个官方实例https://nodejs.org/en/docs/guides/getting-started-guide/
[javascript]
const http = require(‘http’);
const hostname = ‘127.0.0.1’;
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader(‘Content-Type’, ‘text/plain’);
res.end(‘Hello World\n’);
});
server.listen(port, hostname, () => {
console.log(Server running at http://${hostname}:${port}/
);
});
[/javascript]
这是一个web server的简单demo,http是一个内置模块,需要require一下就可以使用了,下一行第一部分定义了一个主机名,第二个是一个端口号,也就是说要通过主机名+端口号的方式来进行访问,一般就用localhost,http默认的端口号是80,为了不冲突我们一般使用默认的端口号,并且取一个比较大的值。接下来要创建一个http server,通关createServer这个函数来进行创建,里面的参数是request和response的缩写。注意这两个是之前提到过的流。
- request:每次收到一个请求的时候触发,把客户端请求的数据封装在这个对象里面。
- response:把服务器要给的相应就封装在这里面。
- res.statusCode: 每个请求得到了响应,都会给我们一个响应的状态吗,其中200就表示成功,404表示找不到。
- res.setHeader:响应的头,这里设定的是一个普通文本。
- res.end:直接传入内容,如果分多步写需要用res.write
- server.listen:监听这个主机。
2.书写HTML
这里是使用的纯文本输入,接下来我们使用HTML来写这个页面。
[javascript]
const http = require(‘http’);
const chalk=require(‘chalk’);
const conf = require(‘./config/defaultConfig’);
const server = http.createServer((req,res) =>{
res.statusCode = 200;
res.setHeader(‘Content-Type’, ‘text/html’);
res.write(‘<html>’);
res.write(‘<body>’);
res.write(‘Hello Http’);
res.write(‘</body>’);
res.end(‘</html>’);
});
server.listen(conf.port, conf.hostname,() => {
const address= ‘http://${conf.hostname}:${conf.port}’;
console.info(‘Server started at ${chalk.green(chalk.green(addr))’)
});
[/javascript]
这里我们使用了write来书写HTML,注意最后一个一定要是end。另外还需要特别注意的是之前我们使用的header是text/plain,要使浏览器解析出HTML的话,我们需要将其更改为text/html。然后重启服务,再刷新页面就可以看到结果。
3. supervisor
我们可以看到这种方式是很麻烦的,因为每一次修改代码的时候都需要重启服务才能够使代码生效,要解决这个问题我们可以通过一个模块来解决
[code]
sudo npm i -g supervisor
[/code]
安装好之后我们就不适用node来启动我们的程序了,我们使用supervisor来进行启动。
[code]
supervisor app.js
[/code]
这样保存以后就可以自动重启服务。
4. 手动输入路径字符串显示本地文件
app.js
[javascript]
const http = require(‘http’);
const chalk = require(‘chalk’);
const path = require(‘path’);
const route = require(‘./helper/route’)
const conf = require(‘./config/defaultConfig’);
const server = http.createServer((req,res) => {
// 拿到文件路径
const filePath = path.join(conf.root, req.url);
route(req, res, filePath);
});
server.listen(conf.port, conf.hostname,() => {
const addr= ‘http://${conf.hostname}:${conf.port}’;
console.info(‘Server started at ${chalk.green(addr)}’);
});
[/javascript]
route.js
[javascript]
const fs = require(‘fs’);
const promisify = require(‘util’).promisify;
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
module.exports = async function(req, res, filePath){
try {
const stats = await stat(filePath);
if(stats.isFile()){
// 使用流的形式拿到文件内容
res.statusCode =200;
res.setHeader(‘Content-Type’, ‘text/plain’);
fs.createReadStream(filePath).pipe(res);
}else if(stats.isDirectory()){
// 如果是文件夹,就读文件列表
fs.readdir(filePath,(err, files) => {
res.statusCode =200;
res.setHeader(‘Content-Type’, ‘text/plain’);
res.end(files.join(‘,’));
});
}
} catch(ex){
console.error(ex);
res.statusCode = 404;
res.setHeader(‘Content-Type’, ‘text/plain’);
res.end(‘${filePath} is not a directory or file!\n ${ex.toString()}’);
}
}
[/javascript]
defaultConfig
[javascript]
module.exports = {
root: process.cwd(),
hostname: ‘127.0.0.1’,
port:9527
};
[/javascript]
小结目前用到了之前学到的什么知识:
- fs模块
- util里面的promisify
- fs 的 stat读取文件的基本信息和两个非常重要的方法, isFile和isDirectroy来判断文件的类型
- createReadStream做一个可读的流,利用pipe方法来给相应,也就是不用每次把所有数据都读到内存中,而是收到一点就处理一点,这样可以表现得更好。
5. 进一步的提高
进一步的提高:如果是一个文件夹这返回一个html,这个html里面是一些<a>标签,标签的内容就是文件列表,这样就是一个点击的图标,而不是还需要自己输入路径。接下来我们解决这个问题。
这里涉及到的是HTML的拼接,拼接HTML是非常麻烦的意见事情,需要各种的字符串操作,这个时候使用魔板引擎就比较好解决了。第一可读性会非常好,第二虽然性能不是很好但是可维护性是非常优秀的。这里我们使用handlebars。
这里简单的介绍一下handlebars的基础语法:
- {{}} 开启一个变量,用于显示这个变量。
- {{#each 要遍历的对象}}
{{/each}}
注意,这里和一般的模版引擎不一样的是,大多数模版引擎的遍历方法后面都会写上回调,但是handlebars会直接进入当前遍历的项目,就不用写item.id了,直接写id就可以表示里面的属性了,表示它自身用this就可以了。另外handlebars不可以写各种各样的逻辑,所以逻辑的部分一般放在数据端来进行处理
- 编译模版的方法:首先拿到模版文件,之后使用handlebars的compile方法把它编译成模版。编程模版之后就可以直接使用,也就是template(变量)。这样返回值就是html的东西。
使用模版进行渲染我们就可以通过html来读取到文件夹或者文件的内容了。我们需要使用npm安装handlebars.
app.js
[javascript]
const http = require(‘http’);
const chalk = require(‘chalk’);
const path = require(‘path’);
const route = require(‘./helper/route’)
const conf = require(‘./config/defaultConfig’);
const server = http.createServer((req,res) =&amp;gt; {
// 拿到文件路径
const filePath = path.join(conf.root, req.url);
route(req, res, filePath);
});
server.listen(conf.port, conf.hostname,() =&amp;gt; {
const addr= ‘http://${conf.hostname}:${conf.port}’;
console.info(Server started at ${chalk.green(addr)}
);
});
[/javascript]
route.js
[javascript]
const fs = require(‘fs’);
const path = require(‘path’);
const Handlebars =require(‘handlebars’);
const promisify = require(‘util’).promisify;
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
const config = require(‘../config/defaultConfig’)
const mime = require(‘./mime’);
// Use template engine
const tplPath = path.join(__dirname, ‘../template/dir.tpl’);
const source = fs.readFileSync(tplPath);
const template = Handlebars.compile(source.toString());
module.exports = async function(req, res, filePath){
try {
const stats = await stat(filePath);
if(stats.isFile()){
const contentType = mime(filePath);
res.statusCode = 200;
res.setHeader(‘Content-Type’, ‘text/plain’);
fs.createReadStream(filePath).pipe(res);
} else if(stats.isDirectory()) {
const files = await readdir(filePath);
res.statusCode =200;
res.setHeader(‘Content-Type’, ‘text/html’);
const dir = path.relative(config.root, filePath);
const data = {
title: path.basename(filePath),
dir: dir ? /${dir}
: ”,
files
};
res.end(template(data));
}
} catch(ex){
console.error(ex);
res.statusCode = 404;
res.setHeader(‘Content-Type’, ‘text/plain’);
res.end(${filePath} is not a directory or file!\n ${ex.toString()}
);
}
}
[/javascript]
dir.tpl
[html]
&lt;!DOCTYPE html&gt;
&lt;html lang=”en”&gt;
&lt;head&gt;
&lt;meta charset=”UTF-8″&gt;
&lt;meta name=”viewport” content=”width=device-width, initial-scale=1.0″&gt;
&lt;meta http-equiv=”X-UA-Compatible” content=”ie=edge”&gt;
&lt;title&gt;{{title}}&lt;/title&gt;
&lt;img src=”data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7″ data-wp-preserve=”%3Cstyle%3E%0A%20%20%20%20%20%20%20%20body%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20margin%3A%2030px%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20a%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20display%3A%20block%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20font-size%3A%2030px%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%3C%2Fstyle%3E” data-mce-resize=”false” data-mce-placeholder=”1″ class=”mce-object” width=”20″ height=”20″ alt=”&amp;lt;style&amp;gt;” title=”&amp;lt;style&amp;gt;” /&gt;
&lt;/head&gt;
&lt;body&gt;
{{#each files}}
&lt;a href=”{{../dir}}/{{this}}”&gt;{{this}}&lt;/a&gt;
{{/each}}
&lt;/body&gt;
&lt;/html&gt;
[/html]
6.自动渲染
接下来我们添加解析的方法,也就是Content-Type比如说text/plain,’text/html’,如果不进行此项更改,可能会导致解析失败(老版本浏览器图片加载不出来等等),或者加载出来的不对比如把html解析成了纯文本,之前我们是通过直接更改content的type来使其解析出来的,那么接下来我们介绍一个自动的方法来实现这个功能。通过封装一个模块,这个mime.js,这个模块可以在网上找到代码有很多,选取一些自己需要的部分,将其作为接口暴露出去来进行这个模块的使用使其自动分辨出需要使用的Content-Type。
mime.js
[javascript]
const path = require(‘path’);
const mimeTypes = {
a:”application/octet-stream”,
ai:”application/postscript”,
aif:”audio/x-aiff”,
aiff:”audio/x-aiff”,
asc:”application/pgp-signature”,
asf:”video/x-ms-asf”,
asm:”text/x-asm”,
zip:”application/zip”
};
module.exports = (filePath) =&gt; {
letext=path.extname(filePath)
.split(‘.’)
.pop()
.toLowerCase();
if(!ext){
ext=filePath;
}
returnmimeTypes[ext] ||mimeTypes[‘txt’];
};
[/javascript]
[javascript]
const fs = require(‘fs’);
const path = require(‘path’);
const Handlebars =require(‘handlebars’);
const promisify = require(‘util’).promisify;
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
const config = require(‘../config/defaultConfig’)
const mime = require(‘./mime.js’);
// Use template engine
const tplPath = path.join(__dirname, ‘../template/dir.tpl’);
const source = fs.readFileSync(tplPath);
const template = Handlebars.compile(source.toString());
module.exports = async function(req, res, filePath){
try {
const stats = await stat(filePath);
if(stats.isFile()){
const contentType = mime(filePath);
res.statusCode = 200;
res.setHeader(‘Content-Type’, contentType);
fs.createReadStream(filePath).pipe(res);
} else if(stats.isDirectory()) {
const files = await readdir(filePath);
res.statusCode =200;
res.setHeader(‘Content-Type’, ‘text/html’);
const dir = path.relative(config.root, filePath);
const data = {
title: path.basename(filePath),
dir: dir ? /${dir}
: ”,
files
};
res.end(template(data));
}
} catch(ex){
console.error(ex);
res.statusCode = 404;
res.setHeader(‘Content-Type’, ‘text/plain’);
res.end(${filePath} is not a directory or file!\n ${ex.toString()}
);
}
}
[/javascript]
我们可以在这里看一下是不是可以自动识别解析了,在静态资源服务器上打开一个json文件,安装chrome的插件json viewer能够帮助我们更好的查看json文件的代码。
可以看到在header中的content-type已经变成了application/json证明我们已经更改成功了。
7.压缩
到这里我们的静态资源服务器基本上就搭建完成了,但是仍然还有不少需要做的事情,我们随便打开一个网页查看它的Request Header的Accept-Ecoding可以看到一般是gzip或者deflate,这些都是常见的压缩算法。在对应的Response Header中就会有一个Content-Encoding和它进行对应gzip。
这里涉及到了HTTP协议,Accept-Ecoding是说当浏览器向服务器发起一个文件的请求的时候会告诉服务器支持的几种压缩方式,浏览器在知道支持的压缩方式之后就会自己挑选一个认为最好的方式来进行压缩,把压缩后的内容进行传输,同时告诉浏览器用什么方式进行压缩,浏览器在拿到相应的时候,会看一下服务器使用什么进行压缩的并选择相应的方式对应的进行解压,这样文件就呈现了正确的内容。这样的好处是减少了HTTP的传输量,压缩的肯定会小一些,对于性能优化是非常有用的。
在配置中用正则表达式通过拓展名限制一下支持的几种压缩类型
[code]
compress: /\.(html|js|css|md)/
[/code]
在写之前看一下要压缩的是什么内容,由于我们是使用createReadStream来读数据的,因此我们要对这个stream来进行压缩。我们这里只针对文件类型进行压缩。
接下来写一下压缩的方法
compress.js
[javascript]
const {createGzip, createDeflate} = require(‘zlib’);
module.exports = (rs, req, res) =&gt; {
const acceptEncoding = req.headers[‘accept-encoding’];
if(!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)){
return rs;
} else if(acceptEncoding.match(/\bgzip\b/)) {
res.setHeader(‘Content-Encoding’,’gzip’);
return rs.pipe(createGzip());
} else if(acceptEncoding.match(/\b(deflate)\b/)) {
res.setHeader(‘Content-Encoding’,’deflate’);
return rs.pipe(createGzip());
}
};
[/javascript]
router.js
[javascript]
const fs = require(‘fs’);
const path = require(‘path’);
const Handlebars =require(‘handlebars’);
const promisify = require(‘util’).promisify;
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
const config = require(‘../config/defaultConfig’)
const mime = require(‘./mime’);
const compress = require(‘./compress’)
// Use template engine
const tplPath = path.join(__dirname, ‘../template/dir.tpl’);
const source = fs.readFileSync(tplPath);
const template = Handlebars.compile(source.toString());
module.exports = async function(req, res, filePath){
try {
const stats = await stat(filePath);
if(stats.isFile()){
const contentType = mime(filePath);
res.statusCode = 200;
res.setHeader(‘Content-Type’, contentType);
let rs = fs.createReadStream(filePath);
if(filePath.match(config.compress)){
rs = compress(rs, req, res);
}
rs.pipe(res);
} else if(stats.isDirectory()) {
const files = await readdir(filePath);
res.statusCode =200;
res.setHeader(‘Content-Type’, ‘text/html’);
const dir = path.relative(config.root, filePath);
const data = {
title: path.basename(filePath),
dir: dir ? /${dir}
: ”,
files: files.map(file =&gt; {
return {
file,
icon: mime(file)
}
})
};
res.end(template(data));
}
} catch(ex){
console.error(ex);
res.statusCode = 404;
res.setHeader(‘Content-Type’, ‘text/plain’);
res.end(${filePath} is not a directory or file!\n ${ex.toString()}
);
}
}
[/javascript]
8. Range
在解决了压缩的问题后,我们进行进一步的优化,这一部分我们要优化的是范围请求,表示我们的客户端口想服务器请求数据的范围,比如说是从第几个字节到第几个字节,而不是一次要求拿回所有的内容,服务器在得到相应的请求之后,从拿到对应的文件到拿到对应的字节反馈给客户端。要做到这点只需要三个步骤(这个协议规定就是这样的):
- range: bytes = [start] – [end] 声明想要的范围,省略参数则是从0开始或一直到结束,也可以请求多个范围,使用“,”进行分隔。如果请求范围不对服务器就会直接返回200并把所有的内容返回客户端。
- Accept-Ranges: bytes 在响应中加一个响应头,表示服务器可以处理的格式
- Content-Range: bytes start-end/total 返回给的是字节,从哪儿开始哪儿结束
[javascript]
module.exports =(totalSize, req, res) => {
const range =req.headers[‘range’];
if(!range){
return{code: 200};
}
const sizes = range.match(/bytes=(\d*)-(\d*)/);
const end = size[2] || totalSize -1;
const start = size[1] || totalSize -end;
if(start > end || start < 0 || end >totalSize){
return{code: 200};
}
res.setHeader(‘Accept-Ranges’,’bytes’);
res.setHeader(‘Content-Range’, bytes ${start} - ${end}/${totalSize}
);
res.setHeader(‘Content-Length’, end – start);
return {
code:206,
start: parseInt(start),
end: parseInt(end)
};
};
[/javascript]
[javascript]
const fs = require(‘fs’);
const path = require(‘path’);
const Handlebars =require(‘handlebars’);
const promisify = require(‘util’).promisify;
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
const config = require(‘../config/defaultConfig’);
const mime = require(‘./mime’);
const compress = require(‘./compress’);
const range = require(‘./range’);
// Use template engine
const tplPath = path.join(__dirname, ‘../template/dir.tpl’);
const source = fs.readFileSync(tplPath);
const template = Handlebars.compile(source.toString());
module.exports = async function(req, res, filePath){
try {
const stats = await stat(filePath);
if(stats.isFile()){
const contentType = mime(filePath);
res.setHeader(‘Content-Type’, contentType);
let rs;
const {code, start, end} = range(stats.size, req, res);
if (code === 200){
res.statusCode = 200;
rs = fs.createReadStream(filePath);
}else{
res.statusCode = 206;
rs = fs.createReadStream(filePath, {start, end})
}
if(filePath.match(config.compress)){
rs = compress(rs, req, res);
}
rs.pipe(res);
} else if(stats.isDirectory()) {
const files = await readdir(filePath);
res.statusCode =200;
res.setHeader(‘Content-Type’, ‘text/html’);
const dir = path.relative(config.root, filePath);
const data = {
title: path.basename(filePath),
dir: dir ? /${dir}
: ”,
files: files.map(file => {
return {
file,
icon: mime(file)
}
})
};
res.end(template(data));
}
} catch(ex){
console.error(ex);
res.statusCode = 404;
res.setHeader(‘Content-Type’, ‘text/plain’);
res.end(${filePath} is not a directory or file!\n ${ex.toString()}
);
}
}
[/javascript]