当前位置: 首页 > 科技观察

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

时间:2023-03-15 08:47:12 科技观察

简介本文介绍了一个简单的静态资源服务器示例项目,希望能给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;//去掉searchletparentPath=_path;_path=path.join(__dirname,_path);try{//获取路径对应的文件描述对象letstats=fs.statSync(_path);if(stats.isFile()){//是一个文件,返回文件内容letfile=fs.readFileSync(_path);res.end(file);}elseif(stats.isDirectory()){//是一个目录,返回一个目录list,让用户可以继续点击letdirArray=fs.readdirSync(_path);res.end(list(dirArray,parentPath));}else{res.end();}}catch(err){res.writeHead(404,"NotFound");res.end();}});constport=2234;consthostname="127.0.0.1";server.listen(端口,主机名,()=>{console.log(`serverisrunningonhttp://${hostname}:${port}`);});以上代码是我们的核心代码,核心功能已经实现,在本地运行可以看到返回文件目录,点击文件名可以浏览对应的网页、图片、文字。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压缩带来的性能(用户访问体验)提升非常明显。开启gzip压缩可以大大减少js、css等文件资源的体积,提高用户访问速度。作为一个静态资源服务器,我们当然要加上这个功能。node中有一个zlib模块,它提供了很多压缩相关的API,我们用它来实现:constzlib=require("zlib");if(stats.isFile()){//是一个文件,返回文件内容res.setHeader("content-encoding","gzip");constgzip=zlib.createGzip();letreadStream=fs.createReadStream(_path);readStream.pipe(gzip).pipe(res);}带流经验,再看这段代码会更容易理解。首先将文件流定向到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()){//是一个文件,返回文件的内容//加入判断文件是否发生变化的逻辑,返回304ifthereisnochange//从请求头中获取修改时间letIfModifiedSince=req.headers["if-modified-since"];//获取文件的修改日期——时间戳格式letmtime=stats.mtime;//如果服务器上的文件修改时间小于等于请求头如果携带了修改时间,则判定文件没有发生变化if(IfModifiedSince&&mtime<=newDate(IfModifiedSince).getTime()){//return304res.writeHead(304,"notmodify");returnres.end();}//第一次请求或文件修改后,返回新的修改时间给客户端res.setHeader("last-modified",newDate(mtime).toString());res.setHeader("content-encoding","gzip");letreg=/\.html$/;//不同的文件类型设置不同的cache-controlif(reg.test(_path)){//我们e对html文件执行每次向服务器验证资源有效性的策略res.setHeader("cache-control","no-cache");}else{//我们对其余部分采用强缓存策略静态资源文件,不需要向服务器索取res.setHeader("cache-control",`max-age=${1*60*60*24*30}`);}//执行gzip压缩constgzip=zlib.createGzip();letreadStream=fs.createReadStream(_path);readStream.pipe(gzip)。pipe(res);}这样的缓存策略在现代前端项目系统中比较合适,尤其是spa应用。我们希望index.html能够每次验证是否有更新到服务器,其余文件在本地缓存一个月(自行决定);通过webpack或者其他工程方式打包后,如果js和css的内容发生变化,文件名也会随之更新,index.html中插入的manifest(或者脚本链接,链接链接等)列表也会更新确保用户可以实时获取新资源。当然,缓存路径有几万条,适合业务的才重要,可以灵活定制。4、命令行参数作为一个在命令行上执行的工具,怎么能不支持多个参数的符号化呢?constconfig={//从命令行获取端口号,如果没有设置,使用默认端口:process.argv[2]||2234,hostname:"127.0.0.1"}server.listen(config.port,config.hostname,()=>{console.log(`serverisrunningonhttp://${config.hostname}:${config.port}`);});这里举一个简单的例子,大家可以自由发挥!5.自动打开浏览器虽然用处不大,但还是要加上。我只想让你知道我添加后你会是什么样子:-(duang~constexec=require("child_process").exec;server.listen(config.port,config.hostname,()=>{console.日志(`服务器运行在http://${config.hostname}:${config.port}`);exec(`openhttp://${config.hostname}:${config.port}`);});6.process.cwd()将__dirname替换为process.cwd()。我们最终想做一个可以在任意目录调用的全局命令,所以拼接路径的代码修改如下://__dirname为当前文件的目录地址,process.cwd()返回脚本的路径execution_path=path.join(process.cwd(),_path);step3发布基本上我们的代码就写完了,可以考虑发布了!step3.1package.json得到一个json文件,配置类似如下:{"name":"here11","version":"0.0.13","private":false,"description":"anodestaticassetsserver","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