一. 包 (文件模块)

文件模块是由程序员基于Node环境编写并发布的代码, 通过由多个文件组成放在一个文件夹中, 所以也叫做
它不同于Nodejs中的内置模块和自定义模块,包是由第三方个人或者团队开发出来的,免费给其他人使用
好处

  • 包是基于内置模块封装出来的,提供了更高级,更方便的API,极大的提高了开发效率
  • 包和内置模块的关系,类似jquery和浏览器内置的DOM之间的关系

    1 npm包管理工具

    npm(node package manager)是由美国的一家公司提供的包管理工具,用来管理包

    1) npm基本使用

    npm在安装node时, 会被自动安装, 首先通过命令查看npm是否已经安装
    在cmd命令行执行npm -v, 查看npm的版本信息

    安装包

npm install 包名称

示例

如果我们想安装jquery, 执行npm install jquery即可

删除包

npm uninstall 包名称

示例

执行npm uninstall jquery

2) 镜像与nrm

由于npm是国外的网站, 包的下载速度比较慢, 有时不稳定会导致安装出错. 为了解决这个问题, 我们可以使用国内镜像, 镜像源就是一个和npm官网一样的网站(像镜子一样), 只不过在国内, 这样更加稳定, 并且速度也很快.

nrm是一个镜像管理工具

执行命令

  1. npm install nrm -g
  • -g: 表示全局安装

在命令行执行

  1. nrm ls

显示可用镜像
Node.js 包   手写服务器 - 图1
使用nrm use切换镜像 (如使用taobao镜像)

  1. nrm use taobao

2 package规范

每个发布在npm上的包都遵循统一的规范, 这个规范也就被称为’package规范’. 以jquery为例

  1. ├─dist // 项目发布目录
  2. ├─external
  3. └─sizzle
  4. └─dist
  5. └─src // 源代码
  6. └─package.json // 包配置文件
  7. └─README.md // 说明文件

其中, 最重要的是package.json文件

文件组成
node_modules用来存放所有已经安装到项目中的包,require导入第三方包的时候,就是从这个目录中查找并加载
package.json配置文件 用来记录node_modules目录下的每一个包的下载信息,例如包名,版本号,下载地址
src 就是源码存放的位置
dist 程序的入口文件夹

规范
1,包必须以单独的目录存在
2,包的顶级目录下必须包含 package.json这个包管理的配置文件
3,package.json配置文件必须包含name(名字) version(版本号) main(包的入口) 这三个属性

3 手写一个package

通过自己手写一个简单的包, 我们来了解package规范

1) 创建一个文件夹

node_modules下创建一个文件夹calc, 这个文件夹也就是一个package

2) 初始化

打开calc, 执行命令 npm init -y, 会在calc里生成package.json配置文件
其中, 最重要的是main, 用来指定: 其他人加载这个package时, 实际引入的文件是哪一个

  1. npm init -y
  • -y: 表示使用默认值初始化

    3) 创建包的目录结构

    1. 1,创建src文件夹,存放源码<br /> 2,创建dist文件夹,用来发布目录<br /> 3,在dist文件夹下创建index.js文件,这个程序的入口文件<br /> 4,修改package.json配置文件中的main属性值,为'dist/index.js',这个才是包的入门文件

    4) 编写代码

    1. 1,在src下创建一个add.jssub.js,编写加减法的代码并用module.exports导出<br /> 2,在dist下的index.js中,接收引入的方法,并导出

    src/add.js

  1. function add (x, y) {
  2. return x + y
  3. }
  4. module.exports = add

src/sub.js

  1. module.exports = function (x, y) {
  2. return x - y
  3. }

dist/index.js

  1. const add = require('../src/add.js')
  2. const sub = require('../src/sub.js')
  3. module.exports = {
  4. add,
  5. sub
  6. }

5) 使用自定义的package

  1. 1,导入自定义的包 const calc = require('calc');<br /> 2,使用 console.log(calc.add(1,2));
  1. const calc = require('calc')
  2. console.log(calc.add(1, 2))

4 导入规则(模块加载机制)

1) 以名称开头

示例

  1. require('fs')
  2. require('calc')
  1. Node.js会假设它是核心模块
  2. 如果不是核心模块Node.js会去当前目录下的node_modules中查找
  3. 首先看是否有该名字的JS文件
  4. 再看是否有该名字的文件夹
  5. 查看该文件夹中的package.json中的main选项确定模块入口文件
  6. 如果没有package.json文件, 加载index.js
  7. 否则找不到报错

一般规则: 先内置模块—node-module找—该名字的js文件—该名字的文件夹main—该文件加下的index.js

2) 以路径开头

示例

  1. require('./find')
  2. require('../add.js')
  1. 如果模块后缀省略,先找同名JS文件再找同名JS文件夹
  2. 如果找到了同名文件夹,找文件夹中的index.js
  3. 如果文件夹中没有index.js就会去当前文件夹中的package.json文件中查找main选项中的入口文件
  4. 如果找指定的入口文件不存在或者没有指定入口文件就会报错,模块没有被找到

二. 服务器编程

Node最显著的特点就是将js扩展到了服务端, 使得可以使用js编写服务端程序.

0 概念

1) 服务器

提供服务的机器, 本质是一台电脑电脑, 跟普通的电脑相比, 服务器一般性能更好

负责存放和对外提供资源的电脑,叫做服务器
负责获取和消费资源的电脑,叫做客户端

  • http服务器: 提供http服务的电脑
  • 数据库服务器: 提供数据存储服务的电脑

网络通讯的三要素
ip: 设备在网络中的唯一标识
端口: 程序在设备中的唯一标识
协议: 规定了数据传输的格式

2) 服务

所谓服务, 就是运行在电脑上的一个应用程序.
在计算机网络中. 通过IP地址可以唯一的确定某一台电脑, 通过端口就可以唯一的确定这个电脑提供的服务

结论

服务 = IP + Port

1 URL地址

URL(Uniform Resource Locator), 统一资源定位符
在计算机网络中, 可以通过统一资源定位符(URL)请求对应的服务器资源(Resource)
组成:

  1. Schema://host[:port]/path[?query-string]
  • Schema: 使用的协议类型, 如http/https/ftp等
  • host: 主机域名或IP
  • port: 端口号(可选)
  • path: 路径
  • query-string: 查询参数(可选)

    2 Http协议

    Http是一种网络协议(超文本传输协议), 规定了web服务器与浏览器之前的交互语言, 是一种一问一答协议 (请求—响应机制)

  • 浏览器发起请求(request)

  • 由web服务器针对请求生成响应(response)

浏览器传给服务器什么样格式的数据,服务器才能解析
服务器传给浏览器什么样格式的数据,浏览器才能解析
Node.js 包   手写服务器 - 图2
Node.js 包   手写服务器 - 图3

1) Http请求消息(req对象操作)

概念
客户端发给服务器端的消息,告诉服务器,我当前浏览器的一些信息

HTTP请求由三部分组成, 分别是:

  • 请求行
  • 请求头
  • 请求体

Node.js 包   手写服务器 - 图4

请求行

格式: 请求方式 请求的url 协议版本

  1. Method Request-URL HTTP-Version CRLF
  • Method: HTTP请求的类型, 如:GET/POST/PUT/DELETE
  • Request-URL: HTTP请求的唯一标识符, 如: /test.hmtl
  • HTTP-Version: HTTP协议版本, 如HTTP/1.1
  • CRLF: 回车换行 CR(回车\n) LF(换行\r)

例子: GET /test.html HTTP/1.1 (CRLF)
请求行以”空格”分割, 除结尾的外CR和LF外, 不允许出现单独的CR或LF字符

请求头

  1. 请求头包含许多有关的前端环境和请求正文的有用信息.<br /> ** 格式: 一组一组的键值对**<br /> user-agent:浏览器的版本型号<br /> content-type:发给服务器的数据格式<br /> accept:浏览器能够接收什么类型的返回内容<br /> Accept-Language:用户期望获得的自然语言的优先顺序

请求体

请求体主要包含前端发送给后端的数据
对于GET请求, 一般不需要请求体, 因为GET参数直接体现在URL上
只有post的请求方式中才有请求体

2) Http响应消息(res对象操作)

概念
服务器端发给浏览器端的信息,告诉浏览器我发给你的消息的数据及特点
同样, HTTP响应也是由三部分组成, 分别是:

  • 响应行
  • 响应头
  • 响应体

Node.js 包   手写服务器 - 图5

响应行

  1. **格式: 协议版本 状态码 状态码描述**
  1. HTTP-Version Status-Code Reason-Phrase CRLF
  • HTTP-Version: HTTP协议版本, 如HTTP/1.1
  • Status-Code: 响应状态码, 如200/401/500
  • Reason-Phrase: 描述信息
  • CRLF: 回车换行 CR(回车\n) LF(换行\r)

    示例

HTTP/1.1 200 OK
状态码

  • 1xx:指示信息—表示请求已接收,继续处理。
  • 2xx:成功—表示请求已被成功接收、理解、接受。
  • 3xx:重定向—要完成请求必须进行更进一步的操作。
  • 4xx:客户端错误—请求有语法错误或请求无法实现。
  • 5xx:服务器端错误—服务器未能实现合法的请求。

常见的状态码

  • 200 OK:客户端请求成功。
  • 400 Bad Request:客户端请求有语法错误,不能被服务器所理解。
  • 401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate一起使用。
  • 403 Forbidden:服务器收到请求,但是拒绝提供服务。
  • 404 Not Found:请求资源不存在,举个例子:输入了错误的URL。
  • 500 Internal Server Error:服务器发生不可预期的错误。

    响应头

    1. 响应头是后端(服务器)返回给前端(客户端)的信息.<br /> content-type:告诉浏览器我发送数据的类型和编码<br /> content-length:发给浏览器内容的字节长度

    响应体

    响应体是后端(服务器)返回给前端(客户端)的数据.

    3) 常见请求方式

    get方式
    数据拼接在地址栏后面.相对的不安全,只能向客户端传递少量数据.经常用于查询某个商品等
    post方法
    数据保存在请求体当中,相对的安全.可以传递大量数据.比如文件的上传下载,表单数据的提交
    restful风格中,不同请求方式间的作用
    get:查询
    post:新增
    put:修改
    delete:删除

    3 手写http服务程序

    Node提供了Http核心模块, 方便我们快速构建一个http服务程序

    1) 最简单的http服务程序

    示例

  1. // 引入http核心模块
  2. const http = require('http')
  3. // 创建web服务器
  4. const server = http.createServer()
  5. // 处理请求
  6. server.on('request', (req, res) => {
  7. res.end('<h1>hello</h1>')
  8. })
  9. // 监听3000端口
  10. server.listen(3000)

演示

Node.js 包   手写服务器 - 图6

2) 处理中文字符

示例

如果响应里包含中文会怎样?
注意, 修改代码后要重启服务

  1. res.end('<h1>这是一个web服务器</h1>')

演示

Node.js 包   手写服务器 - 图7
我们发现会出现乱码. 我们需要在响应头里添加编码格式

  1. server.on('request', (req, res) => {
  2. // 设置响应头
  3. res.writeHead(200, {
  4. 'content-type': 'text/html;charset=utf-8',
  5. })
  6. res.end('<h1>这是一个web服务器</h1>')
  7. })

演示

Node.js 包   手写服务器 - 图8

nodemon

每次修改代码都需要手动重启服务. 这并不友好, 而且容易忘记. 这里, 大家可以安装nodemon工具

  1. npm install nodemon -g

然后, 使用nodemon来执行js文件, nodemon会监听文件的变化, 并且重新执行
Node.js 包   手写服务器 - 图9

3) 处理路由

分析URL

不论是get请求还是post请求, 作为服务端而言, 首先要知道请求的URL
在Node中, 可以通过url核心模块进行分析, 参考 官方文档

示例

  1. // 导入url模块
  2. const url = require('url')
  3. // 通过url.parse方法, 返回url对象
  4. const str = 'http://localhost:3000/index'
  5. const obj = url.parse(str, true)
  6. console.dir(obj)

输出

  1. Url {
  2. protocol: 'http:',
  3. slashes: true,
  4. auth: null,
  5. host: 'localhost:3000',
  6. port: '3000',
  7. hostname: 'localhost',
  8. hash: null,
  9. search: null,
  10. query: [Object: null prototype] {},
  11. pathname: '/index',
  12. path: '/index',
  13. href: 'http://localhost:3000/index'
  14. }

这里我们最关心的是

  • pathname: 请求的路由

通过路由, 服务端可以区分具体的资源, 比如

  • /和index: 首页
  • list: 列表页
  • detail: 详情页

    示例

  1. // 引入http核心模块
  2. const http = require('http')
  3. // 引入url核心模块
  4. const url = require('url')
  5. // 创建web服务器
  6. const server = http.createServer()
  7. // 处理请求
  8. server.on('request', (req, res) => {
  9. // 设置响应头
  10. res.writeHead('200', {
  11. 'content-type': 'text/html;charset=utf-8',
  12. })
  13. // 分析路由
  14. const { pathname } = url.parse(req.url, true)
  15. if (pathname == '/' || pathname == '/index') {
  16. res.end('首页')
  17. } else if (pathname == '/list') {
  18. res.end('列表页')
  19. } else {
  20. res.writeHead('404')
  21. res.end('Not Found')
  22. }
  23. })
  24. // 监听3000端口
  25. server.listen(3000)
  26. console.log('server is running on localhost:3000')

4) 处理GET请求

对于同一个URL, 可以发起不同类型的请求, 处理请求, 主要是分析请求的参数
由于GET参数直接在URL中, 在处理URL时, 通过query就可以得到

示例

  1. // 导入url模块
  2. const url = require('url')
  3. // 通过url.parse方法, 返回url对象
  4. const str = 'http://localhost:3000/index?username=xiaopang'
  5. const obj = url.parse(str, true)
  6. console.dir(obj)

输出

  1. Url {
  2. protocol: 'http:',
  3. slashes: true,
  4. auth: null,
  5. host: 'localhost:3000',
  6. port: '3000',
  7. hostname: 'localhost',
  8. hash: null,
  9. search: '?username=xiaopang',
  10. query: [Object: null prototype] { username: 'xiaopang' },
  11. pathname: '/users/index',
  12. path: '/users/index?username=xiaopang',
  13. href: 'http://localhost:3000/users/index?username=xiaopang'
  14. }

处理get请求

  1. // 引入http核心模块
  2. const http = require('http')
  3. // 引入url核心模块
  4. const url = require('url')
  5. // 创建web服务器
  6. const server = http.createServer()
  7. // 处理请求
  8. server.on('request', (req, res) => {
  9. // 设置响应头
  10. res.writeHead('200', {
  11. 'content-type': 'text/html;charset=utf-8',
  12. })
  13. // 分析路由
  14. const { query, pathname } = url.parse(req.url, true)
  15. if (pathname == '/' || pathname == '/index') {
  16. // 处理get请求
  17. if (req.method == 'GET') {
  18. // 打印在后端控制台
  19. console.log(query)
  20. // 返回给浏览器
  21. res.end(query.username)
  22. }
  23. } else if (pathname == '/list') {
  24. res.end('列表页')
  25. } else {
  26. res.writeHead('404')
  27. res.end('Not Found')
  28. }
  29. })
  30. // 监听3000端口
  31. server.listen(3000)
  32. console.log('server is running on localhost:3000')

5) 处理POST请求

对于POST请求, 由于参数在数据报文中, 只有等数据传输完成才可以进行处理.
主要使用request提供的dataend事件来处理

示例

  1. // 引入http核心模块
  2. const http = require('http')
  3. // 引入url核心模块
  4. const url = require('url')
  5. // 创建web服务器
  6. const server = http.createServer()
  7. // 处理请求
  8. server.on('request', (req, res) => {
  9. // 设置响应头
  10. res.writeHead('200', {
  11. 'content-type': 'text/html;charset=utf-8',
  12. })
  13. // 分析路由
  14. const { query, pathname } = url.parse(req.url, true)
  15. if (pathname == '/' || pathname == '/index') {
  16. // 处理get请求
  17. if (req.method == 'GET') {
  18. // 显示页面
  19. console.log(query)
  20. } else if (req.method == 'POST') {
  21. let post_data = ''
  22. // post数据传递
  23. req.on('data', (data) => (post_data += data))
  24. // post数据传输完成
  25. req.on('end', () => {
  26. console.log(post_data)
  27. res.end(post_data)
  28. })
  29. }
  30. } else if (pathname == '/list') {
  31. res.end('列表页')
  32. } else {
  33. res.writeHead('404')
  34. res.end('Not Found')
  35. }
  36. })
  37. // 监听3000端口
  38. server.listen(3000)
  39. console.log('server is running on localhost:3000')

监听request的两个事件

  • data: 当服务端收到post数据时调用
  • end: 当服务端收集完post数据时调用

    6) 处理静态资源

    静态资源

像html, js, css, 图片这些数据都属于静态资源
如果我们直接在end方法里通过字符串返回html, 显然不够友好.
最好是能以文件的形式保存, 通过读取文件的方式返回

示例

  1. // 引入http核心模块
  2. const http = require('http')
  3. // 引入url核心模块
  4. const url = require('url')
  5. // 引入path核心模块
  6. const path = require('path')
  7. // 引入fs核心模块
  8. const fs = require('fs')
  9. // 创建web服务器
  10. const server = http.createServer()
  11. // 读取静态资源
  12. function resolveStatic(file) {
  13. // 将网络路由转换成服务器端真实路径
  14. const realPath = path.join(__dirname, 'public/' + file)
  15. // 同步读取文件
  16. return fs.readFileSync(realPath)
  17. }
  18. // 处理请求
  19. server.on('request', (req, res) => {
  20. // 设置响应头
  21. res.writeHead('200', {
  22. 'content-type': 'text/html;charset=utf-8',
  23. })
  24. // 分析路由
  25. const { pathname } = url.parse(req.url, true)
  26. if (pathname == '/' || pathname == '/index') {
  27. const html = resolveStatic('index.html')
  28. res.end(html)
  29. } else if (pathname == '/list') {
  30. res.end('列表页')
  31. } else {
  32. res.writeHead('404')
  33. res.end('Not Found')
  34. }
  35. })
  36. // 监听3000端口
  37. server.listen(3000)
  38. console.log('server is running on localhost:3000')