Node.js基础

282 阅读8分钟

Node.js是什么

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

特性

Node.js 可以解析JS代码(没有浏览器安全级别的限制)提供很多系统级别的API,如:

  • 文件的读写 (File System)
  • 进程的管理 (Process)
  • 网络通信 (HTTP/HTTPS)
  • ……

举例

浏览器安全级别的限制

Ajax测试

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>browser-safe-sandbox</title>
</head>
<body>
  <div>browser-safe-sandbox</div>
  <script>
    const xhr = new XMLHttpRequest()
    xhr.open('get', 'https://m.maoyan.com/ajax/moreClassicList?sortId=1&showType=3&limit=10&offset=30&optimus_uuid=A5518FF0AFEC11EAAB158D7AB0D05BBBD74C9789D9F649898982E6542C7DD479&optimus_risk_level=71&optimus_code=10', false)
    xhr.send()
  </script>
</body>
</html>

浏览器预览

browser-sync start --server --files **/* --directory

文件的读写 (File System)

const fs = require('fs')

fs.readFile('./ajax.png', 'utf-8', (err, content) => {
    console.log(content)
})

进程管理(Process)

// process.js

function main(argv) {
    console.log(argv)
}

main(process.argv.slice(2))

运行

node process.js argv1 argv2

网络通信(HTTP/HTTPS)

const http = require('http')

http.createServer((req,res) => {
    res.wirteHead(200, {
        'content-type' : 'text/plain'
    })
    res.write('hello nodejs')
    res.end()
}).listen(3000)

Node 相关工具

npm脚本

Node 开发离不开 npm,而脚本功能是 npm 最强大、最常用的功能之一。

什么是 npm 脚本?

npm 允许在 package.json 文件里面,使用 scripts 字段定义脚本命令。

{
  // ...
  "scripts": {
    "build": "node build.js"
 }

执行顺序

如果 npm 脚本里面需要执行多个任务,那么需要明确它们的执行顺序。

// script1.js

var x = 0
console.log(x)
// script2.js

var y = 0
console.log(y)
// package.json

"scripts": {
  "script1": "node script1.js",
  "script2": "node script2.js"
}

如果是并行执行(即同时的平行执行),可以使用 & 符号。

npm run script1.js & npm run script2.js

如果是继发执行(即只有前一个任务成功,才执行下一个任务),可以使用 && 符号。

npm run script1.js && npm run script2.js

简写形式

常用的 npm 脚本简写形式。

npm start // npm run start

变量

npm 脚本有一个非常强大的功能,就是可以使用 npm 的内部变量。

首先,通过 npm_package_ 前缀,npm 脚本可以拿到 package.json 里面的字段。比如,下面是一个 package.json。

注意:一定要在 npm 脚本中运行(如:npm run view)才可以,直接在命令行中运行JS(如:node view.js)是拿不到值的

{
  "name": "foo", 
  "version": "1.2.5",
  "scripts": {
    "view": "node view.js"
  }
}

那么,变量 npm_package_name 返回 foo,变量 npm_package_version 返回 1.2.5。

// view.js

console.log(process.env.npm_package_name); // foo
console.log(process.env.npm_package_version); // 1.2.5

上面代码中,我们通过环境变量 process.env 对象,拿到 package.json 的字段值。

npmpackage前缀也支持嵌套的package.json字段。

"repository": {
  "type": "git",
  "url": "xxx"
},
scripts: {
  "view": "echo $npm_package_repository_type"
}

上面代码中,repository 字段的 type 属性,可以通过 npm_package_repository_type 取到。

下面是另外一个例子。

"scripts": {
  "install": "foo.js"
}

上面代码中,npm_package_scripts_install 变量的值等于 foo.js。

然后,npm 脚本还可以通过 npmconfig 前缀,拿到 npm 的配置变量,即 npm config get xxx 命令返回的值。比如,当前模块的发行标签,可以通过 npm_config_tag 取到。

"view": "echo $npm_config_tag",

注意,package.json 里面的 config 对象,可以被环境变量覆盖。

{ 
  "name" : "foo",
  "config" : { "port" : "8080" },
  "scripts" : { "start" : "node server.js" }
}

上面代码中,npm_package_config_port 变量返回的是 8080。这个值可以用下面的方法覆盖。

npm config set foo:port 80

最后,env命令可以列出所有环境变量。

"env": "env"

NPX: npm package extention

npm 从5.2版开始,增加了 npx 命令。它有很多用处,本文介绍该命令的主要使用场景。

Node 自带 npm 模块,所以可以直接使用 npx 命令。万一不能用,就要手动安装一下。

调用项目安装的模块

npx 想要解决的主要问题,就是调用项目内部安装的模块。比如,项目内部安装了Mocha。

npm install -g npx

一般来说,调用 Mocha ,只能在项目脚本和 package.json 的scripts字段里面,如果想在命令行下调用,必须像下面这样。

// 项目的根目录下执行
node-modules/.bin/mocha --version

npx 就是想解决这个问题,让项目内部安装的模块用起来更方便,只要像下面这样调用就行了。

npx mocha --version

npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。

由于 npx 会检查环境变量PATH,所以系统命令也可以调用。

# 等同于 ls
npx ls

注意,Bash 内置的命令不在$PATH里面,所以不能用。比如,cd是 Bash 命令,因此就不能用npx cd。

避免全局安装模块

除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app 这个模块是全局安装,npx 可以运行它,而且不进行全局安装。

npx create-react-app my-react-app

上面代码运行时,npx 将 create-react-app 下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载 create-react-app。

注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。

npx http-server

--no-install 参数和 --ignore-existing 参数

如果想让 npx 强制使用本地模块,不下载远程模块,可以使用--no-install参数。如果本地不存在该模块,就会报错。

npx --no-install http-server

反过来,如果忽略本地的同名模块,强制安装使用远程模块,可以使用--ignore-existing参数。比如,本地已经安装了http-server,但还是想使用远程模块,就用这个参数。

npx --ignore-existing http-server

模块/包与CommonJS

模块/包分类

Node.js 有三类模块,即内置的模块、第三方的模块、自定义的模块。

内置模块

Node.js 内置模块又叫核心模块,Node.js安装完成可直接使用。如:

const path = require('path')
var extname = path.extname('index.html')
console.log(extname)

第三方的Node.js模块

第三方的Node.js模块指的是为了实现某些功能,发布的npmjs.org上的模块,按照一定的开源协议供社群使用。如:

npm install chalk
const chalk = require('chalk')
console.log(chalk.blue('Hello world!'))

自定义的Node.js模块

自定义的Node.js模块,也叫文件模块,是我们自己写的供自己使用的模块。同时,这类模块发布到npmjs.org上就成了开源的第三方模块。

自定义模块是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程、速度相比核心模块稍微慢一些,但是用的非常多。

模块定义、接口暴露和引用接口

我们可以把公共的功能 抽离成为一个单独的 js 文件 作为一个模块,默认情况下面这个模块里面的方法或者属性,外面是没法访问的。如果要让外部可以访问模块里面的方法或者属性,就必须在模块里面通过 exports 或者 module.exports 暴露属性或者方法。

// m1.js

const name = 'gp19'

const sayName = () => {
    console.log(name)
}

console.log('module 1')

// 接口暴露方法一
module.exports = {
    say: sayName
}

// 接口暴露方法二
exports.say = sayName

// 错误!
exports = {
    say: sayName
}
// main.js
const m1 = require('./m1')
m1.say()

模块的循环引用

由于 exports 使用方式方式不对,会在两个不同 js 循环引用的情况下,导致其中一个 js 无法获取另外一个 js 的方法,从而导致执行报错。如:

// a.js

exports.done = false
const b = require('./b.js')
console.log('in a, b.done = %j', b.done)
exports.done = true
console.log('a done')
// b.js

console.log('b starting')
exports.done = false
const a = require('./a.js')
console.log('in b, a.done = %j', a.done)
exports.done = true
console.log('b done')
// main.js

console.log('main starting')
const a = require('./a.js')
const b = require('./b.js')
console.log('in main, a.done = %j, b.done = %j', a.done, b.done)

main.js 首先会 load a.js, 此时执行到const b = require('./b.js');的时候,程序会转去loadb.js, 在b.js中执行到const a = require('./a.js'); 为了防止无限循环,将a.jsexports的未完成副本返回到b.js模块。然后b.js完成加载,并将其导出对象提供给a.js模块。

我们知道nodeJs的对每个js文件进行了一层包装称为module,module中有一个属性exports,当调用require('a.js')的时候其实返回的是module.exports对象,module.exports初始化为一个{}空的object,所以在上面的例子中,执行到b.js中const a = require('./a.js');时不会load新的a module, 而是将已经load但是还未完成的a module的exports属性返回给b module,所以b.js拿到的是a module的exports对象,即:{done:false}, 虽然在a.js中exports.done被修改成了true,但是由于此时a.js未load完成,所以在b.js输出的a module的属性done为false,而在main.js中输出的a module的属性done为true. Nodejs通过上面这种返回未完成exports对象来解决循环引用的问题。

常用内置模块

这里介绍几个常用的内置模块:url, querystring, http, events, fs, stream, readline, crypto, zlib

url

parse

url.parse(urlString[, parseQueryString[, slashesDenoteHost]])

const url = require('url')
const urlString = 'https://www.baidu.com:443/ad/index.html?id=8&name=mouse#tag=110'
const parsedStr = url.parse(urlString)
console.log(parsedStr)

format

url.format(urlObject)

const url = require('url')
const urlObject = {
  protocol: 'https:',
  slashes: true,
  auth: null,
  host: 'www.baidu.com:443',
  port: '443',
  hostname: 'www.baidu.com',
  hash: '#tag=110',
  search: '?id=8&name=mouse',
  query: { id: '8', name: 'mouse' },
  pathname: '/ad/index.html',
  path: '/ad/index.html?id=8&name=mouse',
  href: 'https://www.baidu.com:443/ad/index.html?id=8&name=mouse#tag=110'
}
const parsedObj = url.format(urlObject)
console.log(parsedObj)

resolve

url.resolve(from, to)

const url = require('url')
var a = url.resolve('/one/two/three', 'four')
var b = url.resolve('http://example.com/', '/one')
var c = url.resolve('http://example.com/one', '/two')
console.log(a + "," + b + "," + c) 

// 输出结果 /one/two/four,http://example.com/one,http://example.com/two

querystring

parse

querystring.parse(str[, sep[, eq[, options]]])

const querystring = require('querystring')
var qs = 'x=3&y=4'
var parsed = querystring.parse(qs)
console.log(parsed)

stringify

querystring.stringify(obj[, sep[, eq[, options]]])

const querystring = require('querystring')
var qo = {
  x: 3,
  y: 4
}
var parsed = querystring.stringify(qo)
console.log(parsed)

escape/unescape

querystring.escape(str)

const querystring = require('querystring')
var str = 'id=3&city=北京&url=https://www.baidu.com'
var escaped = querystring.escape(str)
console.log(escaped)

querystring.unescape(str)

const querystring = require('querystring')
var str = 'id%3D3%26city%3D%E5%8C%97%E4%BA%AC%26url%3Dhttps%3A%2F%2Fwww.baidu.com'
var unescaped = querystring.unescape(str)
console.log(unescaped)

http/https

get

var http = require('http')
var https = require('https')

const server = http.createServer((request, response) => {
    var url = request.url.substr(1)
    
    var data = ''
    
    response.writeHeader(200, {
        'content-type': 'application/json;charset=utf-8',
        'Access-Control-Arrow-Origin': '*'
    })
    
    https.get(`https://m.lagou.com/listmore.json${url}`, (res) => {
    res.on('data', (chunk) => {
        data += chunk
    })
    
    res.on('end', () => {
        response.end(JSON.stringify({
            ret: true,
            data
        }))
       })
     })
})

server.listen(8080, () => {
    console.log('listening at localhost:8080')
})

post: 服务器提交

const https = require('https')
const querystring = require('querystring')

const postData = querystring.stringify({
  province: '上海',
  city: '上海',
  district: '宝山区',
  address: '同济支路199号智慧七立方3号楼2-4层',
  latitude: 43.0,
  longitude: 160.0,
  message: '求购一条小鱼',
  contact: '13666666',
  type: 'sell',
  time: 1571217561
})

const options = {
  protocol: 'https:',
  hostname: 'ik9hkddr.qcloud.la',
  method: 'POST',
  port: 443,
  path: '/index.php/trade/add_item',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': Buffer.byteLength(postData)
  }
}

function doPost() {
  let data

  let req = https.request(options, (res) => {
    res.on('data', chunk => data += chunk)
    res.on('end', () => {
      console.log(data)
    })
  })

  req.write(postData)
  req.end()
}

跨域:jsonp

const http = require('http')
const url = requre('url')

const app = http.createServer((req, res) => {
    let urlObj = url.parse(req.url, true)
    
    switch(urlObj.pathname) {
        case '/api/user':
            res.end(`${urlObj.query.cb}({"name": "gp145"})`)
            break
        default:
            res.end('404')
            break
     }
})

app.listen(8080, () => {
    console.log('localhost: 8080')
})

跨域: CROS

const http = require('http')
const url = require('url')
const querystring = require('querystring')

const app = http.createServer((req, res) => {
    let data = ''
    let urlObj = url.parse(req.url, true)
    
    res.wirteHead(200, {
        'content-type': 'application/json;charset=utf-8',
        'Access-Control-Allow-Origin': '*'
    })
    
    req.on('data', (chunk) => {
        data += chunk
    }
    
    req.on('end', () => {
        responseResult(querystring.parse(data))
    })
    
    function responseResult(data) {
        switch(urlObj.pathname) {
            case '/api/login':
                res.end(JSON.stringify({
                    message: data
                ))
                break
            default:
                res.end('404.')
                break
         }
     }
})

app.listen(8080, () => {
    console.log('localhost:8080')
})

跨域:middleware(http-proxy-middware)

const http = require('http')
const proxy = require('http-proxy-middleware')

http.createServer((req, res) => {
    let url = req.url
    
    res.writeHead(200, {
        'Access-Control-Allow-Origin': '*'
    })
    
    if (/^\/api/.test(url)) {
        let apiProxy = proxy('/api', {
            target: 'https: //m.lagou.com',
            changeOrigin: true,
            pathRewrite: {
                '^/api': ''
            }
        )
    //  http-proy-middleware 在Node.js中使用的方法
    apiProxy(req, res)
    } else {
        switch(url) {
            case '/index.html':
                res.end('index.html')
                break
            case '/search.html':
                res.end('search.html')
                break
            default:
                res.end('404 page not found')
         }
    }
}).listen(8080)

Events

const EventEmitter = require('event')

class MyEventEmitter extends EventEmitter {}

const event = new myEventEmitter()

event.on('play', (movie) => {
    console.log(movie)
})

event.emit('play', '我和我的祖国')

File System

const fs = require('fs')
const fsP = require('fs').promises

// 创建文件夹
fs.mkdir('./log', (err) => {
    console.log('done')
}

// 文件夹改名
fs.rename('./log', '/err', () => {
    console.log('done')
})

// 删除文件夹
fs.rmdir('./err', () => {
    console.log('done')
}

// 写内容到文件
fs.wirteFile('./logs/log.txt', 'hello world', (err) => {
    // 错误优先回调函数
    if (err) {
        console.log(err.message)
    } else {
        console.log('创建文件成功')
    }
})

// 给文件追加内容
fs.appendFile('./logs/log.txt', '\nhello node', () => {
    console.log('done')
})

// 读取文件内容
fs.readFile('./logs/log.txt', 'utf-8', (err, data) => {
    if (err) {
        throw Error(err)
    }
    console.log(data)
})

// 删除文件
fs.unlink('./logs/log.txt', (err) => {
    console.log('done')
})

// 读取文件/目录信息
fs.readdir('./', (err, data) => {
    data.forEach((val, index) => {
        fs.stat(`./${val}`, (err, stats) => {
            console.log(value + ' is ' + (stats.isDirectory() ? 'directory' : 'file'))
        })
    })
})

// 异步读取文件: 方法一
fs.readFile('./logs/log.txt', 'utf-8', (err, content) => {
    console.log(content)
})

// 异步读取文件: 方法二
fs.readFile('./logs/log.txt', 'utf-8').then(result => {
    console.log(result)
}

//  异步读取文件: 方法三
function getFile() {
    return new Promise((resolve, reject) => {
        fs.readFile('./logs/log.txt', 'utf-8', (err, data) => {
            if (err) reject(err)
            resolve(data)
        })
    })
}

;(async () => await getFile())()

// 异步读取文件:方法四
const fsp = fsP.readFile('./logs/log.txt', 'utf-8').then((result) => {
  console.log(result)
})

// watch 检测文件变化
fs.watch('./logs/log.txt', () => {
    console.log('change')
})

Stream

const fs = require('fs')

const readstream = fs.createReadStream('./note.txt')
const writestream = fs.createWriteStream('./note2.txt')

writestream.write(readstream)

Cryto

const cryto = require('crypto')

const secret = 'abc123'

const hash = cryto.createHash('md5', secret).update('node.js').digest('hex')

路由

var http = require('http')
var fs = require('fs')

http.createServer( function ( req, res ) {

  switch ( req.url ) {
    case '/home':
      res.write('home')
      res.end()
      break
    case '/mine':
      res.write('mine')
      res.end()
      break
    case '/login': 
      fs.readFile( './static/login.html',function ( error , data ) {
        if ( error ) throw error  
        res.write( data )
        res.end()
      })
      break
    default: 
      break
   }

 }).listen( 8000, 'localhost', function () {
   console.log( '服务器运行在: http://localhost:8000' )
 })