当前位置: 首页 > 后端技术 > Node.js

Node.js新手上路-动手做一个静态资源服务器

时间:2023-04-03 20:15:10 Node.js

简介本文介绍了一个简单的静态资源服务器示例项目,希望能给Node.js初学者带来帮助。项目涉及http、fs、url、path、zlib、process、child_process等模块,涵盖大量常用API;还包括基于http协议的缓存策略选择、gzip压缩优化等;最终我们会把它发布到npm上,让它成为一个可以在全球范围内安装和使用的小工具。麻雀虽小,五脏俱全。想想是不是有点小激动呢?废话不多说,先把代码放进去,文中的源码地址在最后的附录中。大家可以先体验一下项目效果:安装:npmi-ghere11任意文件夹地址输入命令:herestep1新建一个项目因为我们要发布到npm上,所以先按照国际惯例,npminit,走吧!可以在命令行一路回车,一些配置会在最后的发布步骤中详细说明。目录结构如下:bin文件夹存放我们的执行代码,web作为test文件夹,里面放了一些网页。step2代码代码step2.1原型静态资源服务器,一般来说,我们在浏览器的地址栏输入一个地址比如“http://域名/test/index.html”,服务器从根目录index.html下对应文件夹,读取文件内容返回给浏览器,浏览器渲染给用户。consthttp=require("http");consturl=require("url");constfs=require("fs");constpath=require("path");constitem=(name,parentPath)=>{letpath=parentPath=`${parentPath}/${name}`.slice(1);return`

${name}
`;}constlist=(arr,parentPath)=>{returnarr.map(name=>item(name,parentPath)).join("");}constserver=http.createServer((req,res)=>{let_path=url.parse(req.url).pathname;//去掉搜索letparentPath=_path;_path=path.join(__dirname,_path);try{//获取路径对应的文件描述对象letstats=fs.statSync(_path);if(stats.isFile()){//是一个file,返回文件内容letfile=fs.readFileSync(_path);res.end(file);}elseif(stats.isDirectory()){//是一个目录,返回目录列表,这样用户就可以继续点击letdirArray=fs.readdirSync(_path);res.end(list(dirArray,parentPath));}else{重发();}}catch(err){res.writeHead(404,"NotFound");重发();}});constport=2234;consthostname="127.0.0.1";server.listen(port,hostname,()=>{console.log(`serverisrunningonhttp://${hostname}:${端口}`);});以上代码是我们的核心代码,核心功能已经实现,在本地运行可以看到返回的文件目录。点击文件名可以浏览对应的网页、图片、文字。step2.2优化功能已经实现,但是我们可以在某些方面进行优化,提高实用性,顺便多学几个api(装逼技巧)。1.stream我们目前读取一个文件返回给浏览器的操作是通过readFile一次性读出,一次性返回。这样当然可以实现功能,但是我们有更好的方法——使用流(stream)进行IO操作。Stream并不是node.js特有的概念,而是操作系统最基本的运行形式,所以理论上任何服务端语言都可以实现streamAPI。为什么使用流是更好的方法?因为一次性读取和操作大文件,内存和网络都难以承受,尤其是用户访问量比较大的时候;借助流,数据可以一点一点地流动和操作,从而提高性能。代码修改如下:if(stats.isFile()){//是一个文件,返回文件内容//createServer在request事件中加入时传入的回调函数,两个形参回调函数req和res//分别是http.IncomingMessage对象和http.ServerResponse对象//它们都实现了stream接口letreadStream=fs.createReadStream(_path);readStream.pipe(res);}编码实现很简单,需要返回文件内容,我们创建一个可读流,指向res对象。2、gzip压缩gzip压缩带来的性能(用户访问体验)提升非常明显,尤其是在现在spa应用时代,启用gzip压缩可以大大减少js、css等文件资源的体积,提高用户访问速度.作为一个静态资源服务器,我们当然要加上这个功能。node中有一个zlib模块,提供了很多压缩相关的API,我们用它来实现:constzlib=require("zlib");if(stats.isFile()){//它是一个文件,返回文件内容res.setHeader("content-encoding","gzip");constgzip=zlib.createGzip();让readStream=fs.createReadStream(_path);readStream.pipe(gzip).pipe(res);}和stream有了使用经验,再看这段代码会更容易理解。首先将文件流定向到gzip对象,然后再定向到res对象。另外,在使用gzip压缩时需要注意一件事:需要将响应头中的content-encoding设置为gzip。否则浏览器会显示一堆乱码。3.HttpCache缓存是让人又爱又恨的东西。用得好,可以提升用户体验,减轻服务器压力;如果使用不当,可能会面临各种奇怪的问题。一般来说,浏览器的http缓存分为强缓存(非验证缓存)和协商缓存(验证缓存)。什么是强缓存?强缓存是通过cache-control和expires这两个头域来控制的,现在一般都是用cache-control。比如我们设置cache-control:max-age=31536000响应头,告诉浏览器这个资源有一年的缓存期,不需要再向浏览器发送请求服务器一年内,直接从缓存中读取资源。协商缓存使用if-modified-since/last-modified、if-none-match/etag等头域,配合强缓存。当强缓存未命中(或通知浏览器无缓存)时,发送请求,确认资源有效性,决定从缓存中读取或返回新资源。有了上面的概念,我们就可以制定我们的缓存策略了:if(stats.isFile()){//是一个文件,返回文件的内容//加入判断文件是否发生变化的逻辑,返回304iftherenochange//从请求头中获取修改时间letIfModifiedSince=req.headers["if-modified-since"];//获取文件的修改日期——时间戳格式letmtime=stats.mtime;//如果服务器上的文件修改时间小于等于如果请求头中携带的修改时间,则认为文件没有发生变化if(IfModifiedSince&&mtime<=newDate(IfModifiedSince).getTime()){//return304res.writeHead(304,"不修改");返回res.end();}//第一次请求或文件修改后,返回新的修改时间给客户端res.setHeader("last-modified",newDate(mtime).toString());res.setHeader("内容编码","gzip");让reg=/\.html$/;//针对不同的文件类型设置不同的cache-controlif(reg.test(_path)){//我们执行每一个Res.setHeader("cache-control","no-cache");}else{//我们对其余的静态资源文件采用强缓存策略,一个月内不需要发送到服务器Requestres.setHeader("cache-control",`max-age=${1*60*60*24*30}`);}//执行gzip压缩constgzip=zlib.createGzip();让readStream=fs.createReadStream(_path);readStream.pipe(gzip).pipe(res);}这样的缓存策略在现代前端项目系统中比较合适,尤其是spa应用,我们希望index.html能够保证每次都和服务端验证是否有为更新,其余文件在本地缓存一个月(自行决定);通过webpack或者其他工程方式打包后,如果js和css内容发生变化,文件名会相应更新,index.html插入的manifest(或者脚本链接,link链接等)列表也会更新,保证用户可以实时获取最新资源。当然,缓存路径有几万条,适合业务的才重要,可以灵活定制。4、命令行参数作为一个在命令行上执行的工具,怎么能不支持多个参数的符号化呢?constconfig={//从命令行获取端口号,如果没有设置,使用默认端口:process.argv[2]||2234,hostname:"127.0.0.1"}server.listen(config.port,config.hostname,()=>{console.log(`serverisrunningonhttp://${config.hostname}:${config端口}`);});这里有一个简单的例子,大家可以自由发挥!5.自动打开浏览器虽然用处不大,但还是要加上。我只是想让你知道我添加后你的样子:-(duang~constexec=require("child_process").exec;server.listen(config.port,config.hostname,()=>{console.log(`服务器正在http://${config.hostname}:${config.port}`上运行);exec(`openhttp://${config.hostname}:${config.port}`);});6.process.cwd()将__dirname替换为process.cwd()。我们最终想做一个可以在任意目录调用的全局命令,所以拼接path的代码修改如下://__dirname为当前文件的目录地址,process.cwd()返回脚本的路径execution_path=path.join(process.cwd(),_path);step3发布基本上我们的代码已经写好了,可以考虑发布了!(为什么npm上没有发布?)step3.1package.json得到一个json文件,配置类似如下:{"name":"here11","version":"0.0.13","private":false,"description":"节点静态资产服务器","bin":{"here":"./bin/index.js"},"repository":{"type":"git","url":"https://github.com/gww666/here.git"},"scripts":{"test":"nodebin/index.js"},"keywords":["node"],"author":"gw666","license":"ISC"}其中bin和private比较重要,其余根据自己项目情况填写。bin配置表示在npmi-gxxx后运行here命令执行的文件。“这里”这个名字可以随意命名。Step3.2声明脚本的执行类型,在index.js文件开头添加:#!/usr/bin/envnode否则在linux上运行会报错。step3.3注册一个npm账号,发布一手命令。我自己不知道怎么做。百度:如果没有账号,先添加一个,执行:npmadduser然后填写Username:你的名字Password:你的密码Email:yourmailnpm会给你发一封验证邮件,记得点击,否则会发布失败。执行登录命令:npmlogin执行发布命令:npmpublish发布的时候记得改项目名,版本号,作者,仓库等,不要填我的。还有一个readme文件可以写,告诉别人怎么用,基本和文章开头说的用法一样。好吧,住在一起。step3.4还等什么,赶快把命令npmi-gxxx发给你的小伙伴吧。什么?你没有朋友吗?告别!附上本项目源码地址:https://github.com/gww666/here如果对你有帮助,欢迎star!