认识新伙伴nodejs

    科技2026-04-14  5

    入门之前-学习新语法-ES6

    在学习nodejs之前,为了能及时用上语言的新特性,我们应该先学习以下JavaScript的基本新语法,那么是什么呢,那就是ES6啦, 全称 ECMAScript 6.0 ,是 JavaScript 的下一个版本标准,2015.06 发版。

    那么什么是ES6?

    由于JavaScript是上个世纪90年代,由Brendan Eich在用了10天左右的时间发明的;虽然语言的设计者很牛逼,但是也扛不住"时间紧,任务重"。因此,JavaScript在早期有很多的设计缺陷;而它的管理组织为了修复这些缺陷,会定期的给JS添加一些新的语法特性。JavaScript前后更新了很多个版本,我们要学的是ES6这个版本。 ES6是JS管理组织在2015年发布的一个版本,这个版本和之前的版本大不一样,包含了大量实用的,拥有现代化编程语言特色的内容,比如:Promise, async/await, class继承等。因此,我们可以认为这是一个革命性的版本。

    下面我来说一说ES6中一些新的东西,我觉得都是大家应该要掌握的。

    ES6 let 与 const ES2015(ES6) 新增加了两个重要的 JavaScript 关键字: let 和 const。

    let 声明的变量只在 let 命令所在的代码块内有效。

    const 声明一个只读的常量,一旦声明,常量的值就不能改变。

    最佳实践:优先用const,如果变量需要被修改就用let。

    // 定义常量 const index = 1 // 定义变量 let num = 5 num = 6 let age = 18 //100 行代码 // 下面的代码,执行的时候会报错,不允许同一个变量重复声明 // 如果是var就不会报错 // let age=20 // 如果使用var声明i,在for循环的外面可以访问到i // 如果使用let声明i,在for循环的外面访问不到i for (let i = 0; i < 10; i++) { console.log(i) } console.log("for循环外部的值:" + i)

    解构赋值 ES6 允许我们按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

    数组的解构赋值 const arr = [1, 2, 3] //我们得到了一个数组 let [a, b, c] = arr //可以这样同时定义变量和赋值 console.log(a, b, c); // 1 2 3 对象的解构赋值(常用) const obj = { name: '俊哥',address:'深圳', age: '100'} //我们得到了一个对象 let {name, age} = obj //可以这样定义变量并赋值 console.log(name, age); //俊哥 100 函数参数的解构赋值(常用) const person = { name: '小明', age: 11} function printPerson({name, age}) { // 函数参数可以解构一个对象 console.log(`姓名:${name} 年龄:${age}`); } printPerson(person) // 姓名:小明 年龄:11

    函数扩展

    ES6 对函数增加了很多实用的扩展功能。

    参数默认值,从ES6开始,我们可以为一个函数的参数设置默认值。 function foo(name, address = '深圳') { console.log(name, address); } foo("小明") // address将使用默认值 箭头函数,将function换成=>定义的函数,就是箭头函数。 function getSum(x, y) { return x + y; } // 箭头函数 // let getSum2=(x, y)=> { // return x + y; // } // 箭头函数简写,只能用在函数体只有一行代码的情况 let getSum2=(x, y)=> x + y; ​ let result = getSum(1, 2); // 自执行函数,定义函数以后,立刻调用函数,代码可读性比较差,不推荐这种写法 console.log( ((x, y)=> x + y)(1,2) )

    Class继承

    由于js一开始被设计为函数式语言,万物皆函数。所有对象都是从函数原型继承而来,通过继承某个函数的原型来实现对象的继承。但是这种写法会让新学者产生疑惑,并且和传统的OOP语言差别很大。ES6 封装了class语法来大大简化了对象的继承。

    class Person { constructor(name, age){ this.name = name this.age = age } // 注意:没有function关键字 sayHello(){ console.log(`大家好,我叫${this.name}`); } } class Man extends Person{ constructor(name, age){ super(name, age) } //重写父类的方法 sayHello(){ console.log('我重写了父类的方法!'); } } let p = new Person("小明", 33) //创建对象 p.sayHello() // 调用对象p的方法,打印 大家好,我叫小明 let m = new Man("小五", 33) m.sayHello() // 我重写了父类的方法!

    小结: ES6的新语法有非常多,有人将它总结为了一本书。当然,ES6提出的只是标准,各大浏览器和node基本实现了90%以上的新特性,极其个别还没有实现。我刚说的仅仅是一些基本以及常用的语法,还有很多没有说到,我们可以一起学习。 学习资源 ES6 入门教程:http://es6.ruanyifeng.com/

    各大浏览器的支持程度:http://kangax.github.io/compat-table/es6/

    初入nodejs

    环境搭建 官网下载nodejs并安装(安装教程网上都有我就不再赘述)

    Node的发展历史和异步IO机制

    浏览器之战 随着互联网的不断普及和Web的迅速发展,几家巨头公司开始了浏览器之战。微软推出了IE系列浏览器,Mozilla推出了Firefox浏览器,苹果推出了Safari浏览器,谷歌推出了Chrome浏览器。其中,微软的IE6由于推出的早,并和Windows系统绑定,在早期成为了浏览器市场的霸主。没有竞争就没有发展。微软认为IE6已经非常完善,几乎没有可改进之处,就解散了IE6的开发团队。而Google却认为支持现代Web应用的新一代浏览器才刚刚起步,尤其是浏览器负责运行JavaScript的引擎性能还可提升10倍,于是自己偷偷开发了一个高性能的Javascript解析引擎,取名V8,并且开源。在浏览器大战中,微软由于解散了最有经验、战斗力最强的浏览器团队,被Chrome远远的抛在身后。 nodejs的诞生 浏览器大战和Node有何关系?

    话说有个叫Ryan Dahl的歪果仁,他的工作是用C/C++写高性能Web服务。对于高性能,异步IO、事件驱动是基本原则,但是用C/C++写就太痛苦了。于是这位仁兄开始设想用高级语言开发Web服务。他评估了很多种高级语言,发现很多语言虽然同时提供了同步IO和异步IO,但是开发人员一旦用了同步IO,他们就再也懒得写异步IO了,所以,最终,Ryan瞄向了JS。因为JavaScript是单线程执行,根本不能进行同步IO操作,只能使用异步IO。

    另一方面,因为V8是开源的高性能JavaScript引擎。Google投资去优化V8,而他只需拿来改造一下。

    于是在2009年,Ryan正式推出了基于JavaScript语言和V8引擎的开源Web服务器项目,命名为Node.js。虽然名字很土,但是,Node第一次把JavaScript带入到后端服务器开发,加上世界上已经有无数的JavaScript开发人员,所以Node一下子就火了起来。 浏览器端JS和Node端JS的区别 相同点就是都使用了Javascript这门语言来开发。

    浏览器端的JS,受制于浏览器提供的接口。比如浏览器提供一个弹对话框的Api,那么JS就能弹出对话框。浏览器为了安全考虑,对文件操作,网络操作,操作系统交互等功能有严格的限制,所以在浏览器端的JS功能无法强大,就像是压在五行山下的孙猴子。

    NodeJs完全没有了浏览器端的限制,让Js拥有了文件操作,网络操作,进程操作等功能,和Java,Python,Php等语言已经没有什么区别了。而且由于底层使用性能超高的V8引擎来解析执行,和天然的异步IO机制,让我们编写高性能的Web服务器变得轻而易举。Node端的JS就像是被唐僧解救出来的齐天大圣一样,法力无边。

    理解NodeJS的事件驱动和异步IO NodeJS在用户代码层,只启动一个线程来运行用户的代码。每当遇到耗时的IO操作,比如文件读写,网络请求,则将耗时操作丢给底层的事件循环去执行,而自己则不会等待,继续执行下面的代码。当底层的事件循环执行完耗时IO时,会执行我们的回调函数来作为通知。

    同步就是你去银行排队办业务,排队的时候啥也不能干(阻塞);异步就是你去银行用取号机取了一个号,此时你可以自由的做其他事情,到你的时候会用大喇叭对你进行事件通知。而银行系统相当于底层的事件循环,不断的处理耗时的业务(IO)。

    但是NodeJs只有一个线程用来执行用户代码,如果耗时的是CPU计算操作,比如for循环100000000次,那么在循环的过程中,下面的代码将会无法执行,阻塞了唯一的一个线程。所以,Node适合大并发的IO处理,不适合CPU密集型的计算操作。Web开发大部分都是耗时IO操作,所以Node非常适合进行Web开发。如果真的遇到了CPU密集的计算,比如从1亿个用户中计算出哪些人和你兴趣相投的这个功能,就非常耗CPU,那这个功能就交由C++,C,Go,Java这些语言实现。像淘宝,京东这种大型网站绝对不是一种语言就可以实现的。

    语言只是工具,让每一种语言做它最擅长的事,才能构建出稳定,强大的系统。

    nodejs常用模块 前言:

    在浏览器端写JS,其实就是使用浏览器给我们提供的功能和方法来写代码。

    在Node端写JS,就是用Node封装好的一系列功能模块来写代码。NodeJS封装了网络,文件,安全加密,压缩等等很多功能模块,我们只需要学会常用的一些,然后在需要的时候去查询文档即可。

    API: npm介绍

    npm是Nodejs自带的包管理器,当你安装Node的时候就自动安装了npm。通俗的讲,当我们想使用一个功能的时候,而Node本身没有提供,那么我们就可以从npm上去搜索并下载这个模块。每个开发语言都有自己的包管理器,比如,java有maven,python有pip。而npm是目前世界上生态最丰富,可用模块最多的一个社区,没有之一。基本上,你所能想到的功能都不用自己手写了,它已经在npm上等着你下载使用了。

    npm的海量模块,使得我们开发复杂的NodeJs的程序变得更为简单。 学习2个知识点:

    怎么生成package.json怎么从npm安装包,并保存到package.json文件中? 全局变量

    全局变量是指我们在任何js文件的任何地方都可以使用的变量。

    __dirname:当前文件的目录 __filename:当前文件的绝对路径 console:控制台对象,可以输出信息 process:进程对象,可以获取进程的相关信息,环境变量等 setTimeout/clearTimeout:延时执行 setInterval/clearInterval:定时器

    示例:

    // 获取进程信息 // console.log(global.process) // 获取当前文件夹路径 console.log(__dirname) // 获取当前文件的绝对路径 console.log(__filename) // 开始标记 // console.time("timer") // setTimeout(()=>{ // console.log("一秒后开始执行") // },1000) // 结束标记 // console.timeEnd("timer") // setInterval(()=>{ // console.log("执行") // },1000)

    path模块

    path模块供了一些工具函数,用于处理文件与目录的路径

    path.basename:返回一个路径的最后一部分 path.dirname:返回一个路径的目录名 path.extname:返回一个路径的扩展名 path.join:用于拼接给定的路径片段 path.normalize:将一个路径正常化

    示例:

    let path = require("path"); // 获取路径当中的最后一部分 let basename = path.basename(__dirname); console.log(basename) // 获取上一级目录的路径 let dirname = path.dirname(__filename); console.log(dirname) // 获取后缀名 let extname = path.extname("a.jpg"); console.log(extname) // 拼接路径 let join = path.join("D:", "aa", "bb", "c.txt"); console.log(join) // 标准化路径 let normalize = path.normalize("D:/aa//bb/c.txt"); console.log(normalize)

    fs模块

    文件操作相关的模块

    fs.stat/fs.statSync:访问文件的元数据,比如文件大小,文件的修改时间

    let fs = require("fs"); // 获取文件的属性 fs.stat("a.txt", (err, stats) => { if (err) { console.log(err) return } console.log(stats.birthtime) console.log(stats.isFile()) console.log(stats.isDirectory()) }) - `fs.readFile/fs.readFileSync`:异步/同步读取文件 - `fs.writeFile/fs.writeFileSync`:异步/同步写入文件

    示例:

    let fs = require("fs"); // 读取文件 // fs.readFile("a.txt", (err, data) => { // if (err) { // throw err; // } // // console.log(data.toString()) // }) // 写入文件 fs.writeFile("b.txt", "这是我使用API生成的文件内容", (err) => { if (err) { throw err; } }) - `fs.readdir/fs.readdirSync`:读取文件夹内容 let fs = require("fs"); let path = require("path"); let dirName = "aa"; // 读取文件夹中的内容(获取到的是文件夹中文件的名字) fs.readdir(dirName, (err, files) => { if (err) { throw err } // 遍历文件夹中的内容, files.forEach((fileName) => { // 拼接文件路径 let absPath = path.join(dirName, fileName); // 获取文件属性 fs.stat(absPath, (err, stats) => { if (err) { throw err } console.log(fileName + "是否是文件夹:" + stats.isDirectory()) }) }) }) - `fs.unlink/fs.unlinkSync`:删除文件 - `fs.rmdir/fs.rmdirSync`:只能删除空文件夹,思考:如何删除非空文件夹? > 使用`fs-extra` 第三方模块来删除。 let fs = require("fs"); let fs2 = require("fs-extra"); // 删除文件 // fs.unlink("aa/a.txt", (err) => { // console.log(err) // }) // 删除文件夹 // fs.rmdir("aa", (err) => { // if (err) { // console.log(err) // } // }) // 使用第三方模块删除文件 // fs2.remove("aa", (err) => { // if (err) { // throw err // } else { // console.log("success") // } // }) // 同步方式读取文件 let readFileSync = fs.readFileSync("demo10_read_dir.js"); console.log(readFileSync.toString()) - `fs.watchFile`:监视文件的变化 let fs = require("fs"); fs.watchFile("a.txt", {interval: 100}, (current, previous) => { console.log("current:" + current.mtime) console.log("previous:" + previous.mtime) })

    Promise和asnyc/await 我们知道,如果我们以同步的方式编写耗时的代码,那么就会阻塞JS的单线程,造成CPU一直等待IO完成才去执行后面的代码;而CPU的执行速度是远远大于硬盘IO速度的,这样等待只会造成资源的浪费。异步IO就是为了解决这个问题的,异步能尽可能不让CPU闲着,它不会在那等着IO完成;而是传递给底层的事件循环一个函数,自己去执行下面的代码。等磁盘IO完成后,函数就会被执行来作为通知。

    虽然异步和回调的编程方式能充分利用CPU,但是当代码逻辑变的越来越复杂后,新的问题出现了。请尝试用异步的方式编写以下逻辑代码:

    先判断一个文件是文件还是目录,如果是目录就读取这个目录下的文件,找出结尾是txt的文件,然后获取它的文件大小。

    示例:

    function withoutpromise() { // 获取文件属性 fs.stat(dirName, (err, stats) => { if (err) { throw err } // 如果是文件夹 if (stats.isDirectory) { // 读取文件夹 fs.readdir(dirName, (err, files) => { if (err) { throw err } // 遍历文件 files.forEach((fileName) => { // 获取后缀名 let extname = path.extname(fileName); // 如果是txt文件 if (extname === ".txt") { // 获取文件属性 fs.stat(path.join(dirName, fileName), (err, stats) => { if (err) { throw err } // 获取文件大小 console.log(fileName + " = " + stats.size) }) } }) }) } }) }

    完成上面任务后你将进入终极关卡:Callback hell回调地狱! 为了解决Callback hell的问题,Promise和async/await诞生。

    promise的作用是对异步回调代码包装一下,把原来的一个回调函数拆成2个回调函数,这样的好处是可读性更好。语法如下:

    语法注意:Promise内部的resolve和reject方法只能调用一次,调用了这个就不能再调用了那个;如果调用,则无效。

    let promise = new Promise((resolve, reject) => { //resolve: 代码执行过程中没有发生异常,正常执行的时候,会调用的方法 //reject: 代码执行过程中发生了异常的时候,会调用的方法 fs.readFile("aa/a1.txt", (err, data) => { if (err) { reject(err) } else { resolve(data) } }) }); promise.then((data) => { console.log("代码正常执行了," + data) }).catch((err) => { console.log("代码执行过程中发生了异常,"+err) })

    async/await的作用是**直接将Promise异步代码变为同步的写法,注意,代码仍然是异步的。**这项革新,具有革命性的意义。

    语法要求:

    await只能用在async修饰的方法中,但是有async不要求一定有await。 await后面只能跟async方法和promise。 假设拥有了一个promise对象,现在使用async/await可以这样写:

    async function asyncDemo() { try { // 当promise的then方法执行的时候 let data = await promise; console.log(data.toString()) } catch (e) { console.log("捕获到的异常:" + e) } } asyncDemo()

    异步代码的终极写法:

    先使用promise包装异步回调代码,可使用node提供的util.promisify方法; 使用async/await编写异步代码。

    http模块

    封装了http server 和 client的功能,就是说可以充当server处理请求,也可以发出请求。

    http.createServer:创建server对象

    let http = require("http"); ​ let fs = require("fs"); ​ let server = http.createServer((request, response) => { ​ ​ let url = request.url; ​ response.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'}); console.log("收到了请求") // 返回普通字符串 // response.end("你好") let person = { name: "zhangsan", age: 20 } // 输出json字符串 // response.end(JSON.stringify(person)) if (url == "/a") { let readFileSync = fs.readFileSync("a.html"); response.end(readFileSync) } else if (url == "/b") { let readFileSync = fs.readFileSync("b.html"); response.end(readFileSync) } ​ }) server.listen(8000)

    http.get:执行http get请求

    let http = require("http"); ​ ​ http.get("http://www.baidu.com", (response) => { ​ ​ let data; ​ response.on("data", (chunk) => { ​ data += chunk.toString(); ​ }) ​ response.on("end", () => { console.log(data) }) })

    小案例:

    功能需求:启动一个服务,当用户访问服务时,给用户展示指定目录下的所有文件;如果子文件是目录,则能继续点进去浏览。

    let http = require("http"); ​ let fs = require("fs"); ​ let path = require("path"); ​ ​ let server = http.createServer((request, response) => { ​ ​ showDir(request, response) ​ }) ​ // 绑定端口 ​ server.listen(8000) ​ // 显示文件夹中的内容 function showDir(request, response) { // 获取请求路径 let url = request.url; let dirPath = "aa"; // 如果请求的不是根目录,获取用户的请求路径 if (request.url != "/") { dirPath = request.url dirPath = dirPath.substring(1, dirPath.length) } // 读取文件夹 fs.readdir(dirPath, (err, files) => { // 有异常,向外抛出 if (err) { throw err } // 拼接数据 let data = ""; files.forEach(fileName => { // 拼接路径 let joinPath = path.join(dirPath, fileName); // 获取属性 let stats = fs.statSync(joinPath); // 如果是文件夹,拼接a标签 if (stats.isDirectory()) { data += `<li><a href="${joinPath}"> ${fileName}</a></li>` } else { // 不是文件夹,不拼a标签 data += `<li>${fileName}</li>` } }) response.end(buildHTML(data)) }) } // 用于生成html页面 function buildHTML(data) { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> *{ padding: 0; margin: 0; } li{ list-style: none; padding: 0.5em 1em; background-color:#ddd; border-top: 1px solid #eee; } li:hover{ background-color:#aaa; } </style> </head> <body> <ul> ${data} </ul> </body> </html>` }

    引自OwEe_icebare,nodejs简介。

    Processed: 0.015, SQL: 10