Node.js 全栈基础
1. Node.js 光速入门
1.1 Node.js 概述
Node.js 是什么
Node.js 不是一门编程语言,它是一个执行 JavaScript 代码的工具。工具是指可以安装在计算机操作系统之上的软件。
为什么浏览器和 Node.js 都可以运行 JavaScript
因为浏览器和 Node.js 都内置了 JavaScript V8 Engine。
它可以将 JavaScript 代码编译为计算机能够识别的机器码。
- 浏览器中运行的 JavaScript 和 Node.js 中运行的 JavaScript 有区别吗
在内置了 JavaScript V8 Engine 以后实际上只能执行 ECMAScript,就是语言中的语法部分。
浏览器为了能够让 JavaScript 操作浏览器窗口以及 HTML 文档,所以在 JavaScript V8 Engine 中添加了控制它们的 API, 就是 DOM 和 BOM. 所以 JavaScript 在浏览器中运行时是可以控制浏览器窗口对象和DOM文档对象的。
和浏览器不同,在 Node.js 中是没有 DOM 和 DOM 的,所以在 Node.js 中不能执行和它们相关的代码,比如 window.alert() 或者 document.getElementById(). DOM 和 DOM 是浏览器环境中特有的。在 Node.js 中,作者向其中添加了很多系统级别的 API,比如对操作系统中的文件和文件夹进行操作。获取操作系统信息,比如系统内存总量是多少,系统临时目录在哪,对系统的进程进行操作等等。
JavaScript 运行在浏览器中控制的是浏览器窗口和 DOM 文档。
JavaScript 运行在 Node.js 中控制的操作系统级别的内容。
为什么浏览器中的 JavaScript 不能控制系统级别的 API ?
浏览器是运行在用户的操作系统中的,如果能控控制系统级别的 API 就会存在安全问题。
Node.js 是运行在远程的服务器中的,访问的是服务器系统 API,不存在这方面的安全问题。
Node.js 能够做什么
我们通常使用它来构建服务器端应用和创建前端工程化工具。
JavaScript 运行在浏览器中我们就叫它客户端 JavaScript。
JavaScript 运行在 Node.js 中我们就叫它服务器端 JavaScript。
1.2 系统环境变量
系统环境变量是指在操作系统级别上定义的变量,变量中存储了程序运行时所需要的参数。
比如在使用 webpack 构建前端应用时就使用到了系统环境变量,因为 webpack 需要根据系统环境变量判断当前为开发环境还是生产环境,根据环境决定如何构建应用。
在开发环境的操作系统中定义 NODE_ENV 变量,值为 development,在生产环境的操作系统中定义 NODE_ENV 变量,值为 production。webpack 在运行时通过 process.env.NODE_ENV 获取变量的值,从而得出当前代码的运行环境是什么。
环境变量 PATH:系统环境变量 PATH 中存储的都是应用程序路径。当要求系统运行某一个应用程序又没有告诉它程序的完整路径时,此时操作系统会先在当前文件夹中查找应用程序,如果查找不到就会去系统环境变量 PATH 中指定的路径中查找。
1.3 安装 Node.js
LTS:长期支持版 (稳定版) 可以运行在生产环境中。
Current:最新版 (预览版) 不建议运行在生产环境中,因为可能有 BUG。
查看 Node 版本:node -v
查看 Npm 版本:npm -v
1.4 解决安装异常
解决在运行 node 命令时提示 “不是内部或外部命令, 也不是可运行的程序或批处理文件”。
将 Node 应用程序目录添加到系统环境变量中, 然后重新启动命令行工具再次执行 node 命令.
解决在安装 Node 的过程中出现代码为 2502 和 2503 的错误。
通过管理员权限打开命令行工具
切换到 node 安装包所在的目录
通过 msiexec /package node-v10.15.0-x64.msi 运行 Node 应用程序安装包
1.5 Node.js 初体验
function sayHello (name) {
console.log('Hello' + name)
}
sayHello('Node')
在命令行工具中通过 node JavaScript 文件 的方式执行代码。
1.6 全局对象
console.log(window) // window is not defined
在 Node.js 环境中是没有 window 的,所以 window 对象自然是未定义的。
在 Node.js 环境中全局对象为 global,在 global 对象中会存在一些和 window 对象中名字相同且作用相同的方。
global.console.log
global.setInterval
global.clearInterval
global.setTimeout
global.clearTimeout
global.setImmediate
在 Node.js 环境中声明的变量不会被添加到全局对象中,变量声明后只能在当前文件中使用。
var message = "hello"
console.log(global.message) // undefined
2. 模块系统
2.1 模块概述
在 Node.js 环境中,默认就支持模块系统,该模块系统遵循 CommonJS 规范。
一个 JavaScript 文件就是一个模块,在模块文件中定义的变量和函数默认只能在模块文件内部使用,如果需要在其他文件中使用,必须显式声明将其进行导出。
2.2 模块成员导出
在每一个模块文件中,都会存在一个 module 对象,即模块对象。在模块对象中保存了和当前模块相关信息。
在模块对象中有一个属性 exports,它的值是一个对象,模块内部需要被导出的成员都应该存储在到这个对象中。
Module {
exports: {}
}
1
2
3
// logger.js
const url = "http://mylogger.io/log";
function log (message) {
console.log(message)
}
module.exports.endPoint = url
module.exports.log = log
2.3 模块成员导入
在其他文件中通过 require 方法引入模块,require 方法的返回值就是对应模块的 module.exports 对象。
在导入模块时,模块文件后缀 .js 可以省略,文件路径不可省略。
require 方法属于同步导入模块,模块导入后可以立即使用。
// app.js
const logger = require("./logger")
console.log(logger) // { endPoint: 'http://mylogger.io/log', log: [Function: log] }
console.log(logger.endPoint) // http://mylogger.io/log
logger.log('Hello Module') // Hello Node
通过 require 方法引入模块时会执行该模块中的代码。
// logger.js
console.log("running...")
// app.js
require("./logger") // running...
在导入其他模块时,建议使用 const 关键字声明常量,防止模块被重置。
var logger = require("./logger")
logger = 1;
logger.log("Hello") // logger.log is not a function
const logger = require("./logger")
logger = 1; // Assignment to constant variable.
logger.log("Hello")
有时在一个模块中只会导出一个成员,为方便其他模块使用,可以采用以下导入方式。
// logger.js
module.exports = function (message) {
console.log(message)
}
// app.js
const logger = require("./logger")
logger("Hello")
2.4 Module Wrapper Function
Node.js 是如何实现模块的,为什么在模块文件内部定义的变量在模块文件外部访问不到?
每一个模块文件中都会有 module 对象和 require 方法,它们是从哪来的?
在模块文件执行之前,模块文件中的代码会被包裹在模块包装函数当中,这样每个模块文件中的代码就都拥有了自己的作用域,所以在模块外部就不能访问模块内部的成员了。
(function(exports, require, module, __filename, __dirname) {
// entire module code lives here
});
从这个模块包装函数中可以看到,module 和 require 实际上模块内部成员, 不是全局对象 global 下面的属性。
__filename:当前模块文件名称。
__dirname:当前文件所在路径。
exports:引用地址指向了 module.exports 对象,可以理解为是 module.exports 对象的简写形式。
exports.endPoint = url;
exports.log = log
在导入模块时最终导入的是 module.exports 对象,所以在使用 exports 对象添加导出成员时不能修改引用地址。
exports = log //这是错误的写法.
2.5 Node.js 内置模块
在 Node.js 安装完成后,会内置一些非常有用的模块。
Path:模块内提供了一些和路径操作相关的方法。
File system:文件操作系统,提供了和操作文件相关的方法。
在引入内置模块时, 使用的是模块的名字,前面不需要加任何路径。
2.5.1 Path 模块
const path = require("path")
console.log(path.parse(__filename))
{
root: '/',
dir: '/Users/administrators/Desktop/node_test',
base: 'app.js',
ext: '.js',
name: 'app'
}
2.5.2 File system 模块
const fs = require("fs")
const files = fs.readdirSync("./")
console.log(files) [ 'app.js', 'logger.js' ]
fs.readdir("./", function (error, files) {
console.log(error) // null | Error {}
console.log(files) // [ 'app.js', 'logger.js' ] | undefined
})
3.NPM
3.1 Node.js 软件包
每一个基于 Node.js 平台开发的应用程序都是 Node.js 软件包。
所有 Node.js 软件包都被托管在 www.npmjs.com 中。
3.2 什么是 NPM
Node Package Manager,Node.js 环境中的软件包管理器。随 Node.js 一起被安装。
它可以将 Node 软件包添加到我们的应用程序中并对其进行管理,比如下载,删除,更新,查看版本等等。
它没有用户界面,需要在命令行工具中通过命令的方式使用,对应的命令就是 npm。
NPM 和 Node 是两个独立的应用程序,只是被捆绑安装了,可以通过版本号证明。
3.3 package.json
Node.js 规定在每一个软件包中都必须包含一个叫做 package.json 的文件。
它是应用程序的描述文件,包含和应用程序相关的信息,比如应用名称,应用版本,应用作者等等。
通过 package.json 文件可以方便管理应用和发布应用。
创建 package.json 文件: npm init
快速创建 package.json 文件: npm init --yes
{
"name": "project-name",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
3.4 下载 Node.js 软件包
在应用程序的根目录执行命令:npm install 或者 npm i
npm install lodash
软件包下载完成后会发生三件事:
软件包会被存储在 node_modules 文件夹中,如果在应用中不存在此文件夹,npm 会自动创建。
软件包会被记录在 package.json 文件中. 包含软件包的名字以及版本号。
npm 会在应用中创建 package-lock.json 文件, 用于记录软件包及软件包的依赖包的下载地址及版本。
3.5 使用 Node.js 软件包
在引入第三方软件包时,在 require 方法中不需要加入路径信息,只需要使用软件包的名字即可,require 方法会自动去 node_modules 文件夹中进行查找。
const _ = require("lodash")
const array = ["a", "b", "c", "d"]
// chunk 对数组中的元素进行分组
// 参数一表示要进行操作的数组
// 参数二表示每一组中包含的元素个数
console.log(_.chunk(array, 2)) // [ [ 'a', 'b' ], [ 'c', 'd' ] ]
3.6 软件包依赖问题说明
比如在我的应用中要依赖 mongoose 软件包,于是我下载了它,但是在 node_modules 文件夹中除了包含 mongoose 以外还多出了很多其他软件包,为什么会多出这么多软件包呢?
实际上它们又是 mongoose 依赖的软件包。
为什么 mongoose 依赖的软件包不放在 mongoose 文件夹中呢?
在早期的 npm 版本中, 某个软件包依赖的其他软件包都会被放置在该软件包内部的 node_modules 文件夹中,但是这样做存在两个问题,第一个问题是很多软件包都会有相同的依赖,导致开发者在一个项目中会下载很多重复的软件包,比如 A 依赖 X,B 依赖 X,C 依赖 X,在这种情况下 X 就会被重复下载三次。第二个问题是文件夹嵌套层次太深,导致文件夹在 windows 系统中不能被直接删除。比如 A 依赖 B, B 依赖 C, C 依赖 D … , 就会发生文件夹依次嵌套的情况。
所有的软件包都放置在 node_modules 文件夹中不会导致软件包的版本冲突吗?
在目前的 npm 版本中,所有的软件包都会被直接放置在应用根目录的 node_modules 文件夹中,这样虽然解决了文件夹嵌套层次过深和重复下载软件包的问题,但如果只这样做肯定会导致软件包版本冲突的问题,如何解决呢?
比如 A 依赖 X 的 1 版本,B 依赖 X 的 2 版本,如果你先下载的是 A,那么 A 依赖的 X 会被放置在根目录的 node_modules 文件夹中, 当下载 B 时,由于在根目录中已经存在 X 并且版本不一致,那么 B 依赖的 X 就会被放置在 B 软件包中的 node_module 文件夹中,通过此方式解决软件包版本冲突的问题。
node_modules 文件夹中的软件包都需要提交到 git 仓库中吗?
在 node_modules 文件夹中有很多软件包,随着应用程序的增长,软件包也会越来越多,甚至会达到几百兆。
当我们将应用提交到版本库时,我们不想提交它,因为它们不是我们应用中的源代码,而且由于碎文件比较多,其他人在检出代码时需要等待的时间会很久。当其他人拿到应用程序时没有依赖软件包应用程序是运行不起来的,如何解决呢?
实际上应用程序依赖了哪些软件包在 package.json 文件中都会有记录,其他人可以通过 npm install 命令重新下载它们。为了保持下载版本一直,npm 还会根据 package-lock.json 文件中的记录的地址进行下载。
将应用程序提交到版本库之前,将 node_modules 文件夹添加到 .gitignore 文件中。
git init
git status
echo "node_modules/" > .gitignore
git status
git add .
git commit -m "our first commit"
3.7 语义版本控制
版本号规范
Major Version 主要版本:添加新功能 (破坏现有 API) -> 6.0.0
Minor version 次要版本:添加新功能 (不会破坏现有 API, 在现有 API 的基础上进行添加) -> 5.13.0
Patch version 补丁版本:用于修复 bug -> 5.12.6
版本号更新规范
^5.12.5: 主要版本不变,更新次要版本和补丁版本
~5.12.5: 主要版本和次要版本不变,更新补丁版本
5.12.5: 使用确切版本,即主要版本,次要版本,补丁版本固定
3.8 查看软件包实际版本
当过了一段时间以后,其他人从版本库中下载了你的应用程序,并通过 npm install 命令恢复了应用程序的依赖软件包,但是此时应用程序的依赖软件包版本可能会发生变化,而应用程序的 package.json 文件中记录的只是大致版本,如何查看依赖软件包的具体版本呢?
方式一:在 node_modules 文件夹中找到对应的依赖软件包,找到它的 package.json 文件,可以在这个文件中的 version 字段中找到它的具体版本。
方式二:通过 npm list 命令查看所有依赖软件包的具体版本, --depth 选项指定查看依赖包的层级。
3.9 查看软件包元数据
npm view mongoose
npm view mongoose versions
npm view mongoose dist-tags dependencies
3.10 下载特定版本的软件包
npm i <pkg>@<version>
npm i mongoose@2.4.2 lodash@4.7.0
cat package.json
npm list --depth 0
3.11 删除软件包
npm uninstall <pkg>
npm uninstall mongoose
npm un mongoose
3.12 更新软件包
通过 npm outdated 命令可以查看哪些软件包已经过期,对应的新版本是什么。
通过 npm update 更新过期的软件包,更新操作遵循语义版本控制规则。
3.13 项目依赖 VS 开发依赖
项目依赖:无论在开发环境还是线上环境只要程序在运行的过程中需要使用的软件包就是项目依赖。比如 lodash,mongoose。
开发依赖:在应用开发阶段使用,在生产环境中不需要使用的软件包,比如 TypeScript 中的类型声明文件。
在 package.json 文件中, 项目依赖和开发依赖要分别记录,项目依赖被记录在 dependencies 对象中,开发依赖被记录在 devDependencies 中,使开发者可以在不同的环境中下载不同的依赖软件包。
在下载开发依赖时,要在命令的后面加上 --save-dev 选项或者 -D 选项。npm i eslint -D
在开发坏境中下载所有依赖软件包: npm install
在生产环境中只下载项目依赖软件包: npm install --prod
3.14 本地安装与全局安装
本地安装与全局安装
本地安装:将软件包下载到应用根目录下的 node_modules 文件夹中,软件包只能在当前应用中使用。
全局安装:将软件包下载到操作系统的指定目录中,可以在任何应用中使用。
通过 -g 选项将软件包安装到全局:npm install -g
查看全局软件包安装位置:npm root -g
删除全局中的软件包: npm un npm-check-updates -g
查看全局中安装了哪些软件包: npm list -g --depth 0
查看全局中有哪些过期软件包: npm outdated -g
nodemon
问题:在 node 环境中每次修改 JavaScript 文件后都需要重新执行该文件才能看到效果。
通过 nodemon 可以解决此烦恼,它是命令工具软件包,可以监控文件变化,自动重新执行文件。
npm install nodemon@2.0.7 -g
nodemon app.js
npm-check-updates 强制更新
npm-check-updates 可以查看应用中有哪些软件包过期了,可以强制更新 package.json 文件中软件包版本
将 npm-check-updates 安装到全局: npm install npm-check-updates -g
查看过期软件包:npm-check-updates
更新 package.json:ncu -u
安装软件包:npm i
检测:npm outdated 或 npm-check-updates
3.15 发布软件包
注册 npm 账号
创建软件包
mkdir lagou-node-test && cd "$_"
npm init --yes
创建模块 index.js
module.exports = function (a, b) {
return a + b
}
登录 npm (npm 镜像地址必须为 npmjs.com)
npm login
发布软件包
npm publish
测试: 在其他应用中使用该软件包
npm install lagou-node-test
创建 index.js 模块
const lagouNodeTest = require("lagou-node-test")
console.log(lagouNodeTest.add(1, 2)) // 3
3.16 更新版本号
在软件包的源代码发生更改后, 是不能直接发布的, 应该新更新软件包的版本号然后再进行发布.
更新主要版本号:npm version major
更新次要版本号:npm version minor
更新补丁版本号:npm version patch
3.17 撤销已发布的软件包
只有在发布软件包的24小时内才允许撤销
软件包撤销后 24 小时以后才能重新发布
重新发布时需要修改包名称和版本号
npm unpublish --force
3.18 更改 npm 镜像地址
由于 npmjs.com 是国外的网站,大多数时候下载软件包的速度会比较慢,如何解决呢?
可以通过配置的方式更改 npm 工具的下载地址。
获取 npm 配置
npm config list -l --json
-l 列表所有默认配置选项
–json 以 json 格式显示配置选项
设置 npm 配置
获取 npm 下载地址:npm config get registry
获取 npm 用户配置文件: npm config get userconfig
更改 npm 镜像地址
npm config set registry https://registry.npm.taobao.org
npm config set registry https://registry.npmjs.org/
cat .npmrc
3.19 npx 命令
npx 是 npm 软件包提供的命令,它是 Node.js 平台下软件包执行器。主要用途有两个,第一个是临时安装软件包执行后删除它,第二个是执行本地安装的提供命令的软件包。
临时安装软件包执行后删除软件包
有些提供命令的软件包使用的频率并不高,比如 create-react-app 脚手架工具,我能不能临时下载使用,然后再删掉它。
npx create-react-app react-test
执行本地安装的软件包
现在有两个项目都依赖了某个命令工具软件包,但是项目 A 依赖的是它的 1 版本,项目 B 依赖的是它的 2 版本,我在全局到底应该安装什么版本呢 ?
该软件包可以在本地进行安装,在 A 项目中安装它的 1 版本, 在 B 项目中安装它的 2 版本,在应用中可以通过 npx 调用 node_modules 文件夹中安装的命令工具。
将所有软件包安装到应用本地是现在最推荐的做法,一是可以防止软件包的版本冲突问题,二是其他开发者在恢复应用依赖时可以恢复全部依赖,因为软件包安装到本地后会被 package.json 文件记录,其他开发者在运行项目时不会因为缺少依赖而报错。
3.20 配置入口文件的作用
应用程序入口文件就是应用程序执行的起点,就是启动应用程序时执行的文件。
场景一:其他开发者拿到你的软件包以后,通过该文件可以知道应用的入口文件是谁,通过入口文件启动应用。
场景二:通过 node 应用文件夹 命令启动应用。node 命令会执行 package.json 文件中 main 选项指定的入口文件,如果没有指定入口文件,则执行 index.js。
3.21 模块查找规则
在指定了查找路径的情况下
require("./server")
查找 server.js
查找 server.json
查找 server 文件夹, 查看入口文件 (package.json -> main)
查找 server 文件夹 中的 index.js 文件
在没有指令查找路径的情况下
require('server')
paths: [
'/Users/administrators/Desktop/Node/code/node_modules',
'/Users/administrators/Desktop/Node/node_modules',
'/Users/administrators/Desktop/node_modules',
'/Users/administrators/node_modules',
'/Users/node_modules',
'/node_modules'
]