官方定义:Node.js是一个基于V8JavaScript引擎的Js运行时环境(V8引擎用来执行JavaScript代码)
V8可以嵌入到任何的C++程序中,无论是Chrome还是Node.js,事实上都是嵌入了V8引擎来执行JavaScript代码。但是在Chrome中,还需要解析渲染HTML、CSS等相关的引擎,另外还需要提供支持浏览器操作的API等
另外在Node.js中我们也需要进行一些额外操作,比如文件系统读写,网络IO,加密,压缩解压文件等操作。
Node是用什么语言编写的?C、C++、和少部分JS
LTS版本(Long Term Support):长期支持,相对稳定,线上使用。真正开发一定要使用LTS版本
Current版本:最新的版本,包含很多新特性
.msi microsoft install 微软安装程序
安装成功
本身不支持windows系统。
有大佬在windows系统中复刻了一个nvm,我们在windows上使用的是nvm。
官网:https://github.com/coreybutler/nvm-windows
nvm install latest 安装最新的node版本nvm list 展示目前安装的所有版本nvm use 切换版本点击是即可。
1 | npm install -g n |
1 | // 给程序输入内容 |
1 | R:\code\github\workspace\nodejs>node 输入输出.js num=20 |
为什么叫做argv?
1 | // 特殊的全局对象 |
1 | R:\code\github\workspace\nodejs>node 输入输出.js |
很大的一个对象,里面包含了很多东西
1 | // 定时器方法 |
在新标准中使用globalThis替代global
是什么?
上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程;
无论你多么喜欢JavaScript,以及它现在发展的有多好,它都有很多的缺陷:
在网页开发的早期,Brendan Eich开发JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的,这个时候我们只需要讲JavaScript代码写到<script>标签中即可,并没有必要放到多个文件中来编写;甚至流行:通常来说 JavaScript 程序的长度只有一行。
但是随着前端和JavaScript的快速发展,JavaScript代码变得越来越复杂了:
所以,模块化已经是JavaScript一个非常迫切的需求:
但是JavaScript本身,直到ES6(2015)才推出了自己的模块化方案;在此之前,为了让JavaScript支持模块化,涌现出了很多不同的模块化规范:AMD、CMD、CommonJS等;
早期没有模块化带来了很多的问题:比如命名冲突的问题
当然,我们有办法可以解决上面的问题:立即函数调用表达式(IIFE) IIFE (Immediately Invoked Function Expression)
但是,我们其实带来了新的问题:
所以,我们会发现,虽然实现了模块化,但是我们的实现过于简单,并且是没有规范的。
我们需要制定一定的规范来约束每个人都按照这个规范去编写模块化的代码;
这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性;
JavaScript社区为了解决上面的问题,涌现出一系列好用的规范,接下来我们就学习具有代表性的一些规范。
CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS。
浏览器中默认不支持CommonJS
所以,Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
前面我们提到过模块化的核心是导出和导入,Node中对其进行了实现:
utils.js
1 | FILE_NAME = "utils.js" |
main.js
1 | // 导入时解构 |
实际上,exports是一个对象,向其中添加的属性将会被导出。
在另一个文件中使用require实际上是拿到了这个exports对象,也就是require通过各种查找的方式,最终找到了export对象,并且把这个exports对象赋值给了另一个文件中的变量,这个变量就是exports对象了。
exports的本质就是一种引用赋值,在被引用的文件中修改导出值的时候,在引用的文件中也会变化。
utils.js
1 | let name = "bar" |
main.js
1 | // 探讨require的本质 |
开发中的常见写法:
utils.js
1 | user_name = 'jack' |
main.js
1 | user = require('./utils') |
但是Node中我们经常导出东西的时候,又是通过module.exports导出的:module.exports和exports有什么关系或者区别呢?
我们追根溯源,通过维基百科中对CommonJS规范的解析:
但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module;所以在Node中真正用于导出的其实根本不是exports,而是module.exports;因为module才是导出的真正实现者;
但是,为什么exports也可以导出呢?
情况一. X是一个Node核心模块,比如path、http
情况二:X是以 ./ 或 ../ 或 /(根目录)开头的
第一步:将X当做一个文件在对应的目录下查找;
第二步:没有找到对应的文件,将X作为一个目录,查找目录下面的index文件
如果没有找到,那么报错:not found
情况三:直接是一个X(没有路径),并且X不是一个核心模块
/Users/coderwhy/Desktop/Node/TestCode/04_learn_node/05_javascript-module/02_commonjs/main.js中编写 require(‘why’)
如果上面的路径中都没有找到,那么报错:not found
结论一:模块在被第一次引入时,模块中的js代码会被运行一次
结论二:模块被多次引入时,会缓存,最终只加载(运行)一次
为什么只会加载运行一次呢?
这是因为每个模块对象module都有一个属性:loaded。
为false表示还没有加载,为true表示已经加载;
结论三:如果有循环引入,那么加载顺序是深度优先算法
如果出现右图模块的引用关系,那么加载顺序是什么呢?
Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb
CommonJS加载模块是同步的:
同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;
如果将它应用于浏览器呢?
浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行;那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
所以在浏览器中,我们通常不使用CommonJS规范:
当然在webpack中使用CommonJS是另外一回事;因为它会将我们的代码转成浏览器可以直接执行的代码;
在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ES Module代码的转换;AMD和CMD已经使用非常少了;
AMD主要是应用于浏览器的一种模块化规范:
AMD是Asynchronous Module Definition(异步模块定义)的缩写,它采用的是异步加载模块。
事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了。我们提到过,规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用。
AMD实现的比较常用的库是require.js和curl.js;
CMD规范也是应用于浏览器的一种模块化规范:
CMD 是Common Module Definition(通用模块定义)的缩写;
它也采用的也是异步加载模块,但是它将CommonJS的优点吸收了过来;
但是目前CMD使用也非常少了;
CMD也有自己比较优秀的实现方案:SeaJS
导出方式1:
1 | const name = 'codryjc' |
导出方式2:
1 | const name = 'codryjc' |
导出方式3:
1 | export const name = 'codryjc' |
声明时导出,导入的时候同1
1 | import {name, age, sayhello} from './util.js' |
1 | import {name as fname, age as fname, sayhello} from './util.js' |
1 | import * as util from "./utils.js" |
如果我们想要把一个文件作为工具文件,则可以这样写
1 | // ./util/utils.js |
导入:
1 | import {getUserList, getPostList} from './util' |
或者
1 | import * from './util' |
前面我们学习的导出功能都是有名字的导出(named exports):
还有一种导出叫做默认导出(default export)
注意:在一个模块中,只能有一个默认导出(default export)
默认导出在导入的时候不用写大括号
==方法一==
1 | // utils.js |
1 | // main.js |
注意:如果导出的时候有大括号,那么在导入的时候也应该写上大括号
==方法二==
匿名函数导出
1 | // utils.js |
1 | // main.js |
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
1 | let flag = true |
import声明必须写在顶层。
为什么会出现这个情况呢?
这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系,由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况,甚至拼接路径的写法也是错误的:因为我们必须到运行时能确定path的值。
但是某些情况下,我们确确实实希望动态的来加载某一个模块:
如果根据不懂的条件,动态来选择加载模块的路径,这个时候我们需要使用 import() 函数来动态加载,import函数返回一个Promise,可以通过then获取结果;
1 | let flag = true |
import中有个meta属性,了解即可。
https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record);
阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;
Node Package Manager Node包管理工具
我们发布自己的包其实是发布到registry上面的,当我们安装一个包时其实是从registry上面下载的包。
新建文件夹
假如我们要使用dayjs,那么我们可以使用npm install dayjs来安装dayjs
安装成功后的文件夹结构:
多了一个文件夹和两个文件
然后在main.js中:
1 | const dayjs = require('dayjs') |
输出为:
可以做到比较方便的引入
安装npm包分两种情况:
1 | npm install yarn -g |
但是很多人对全局安装有一些误会:
默认安装开发和生产依赖
1 | npm install axios |
开发依赖
1 | npm install webpack --save-dev |
根据package.json中的依赖包
1 | npm install |
安装之后生成了node_modules目录,关于项目如何从目录中查找文件,可以参考这个[[Node模块化开发#require查找规则]]
这个配置文件会记录着你项目的名称、版本号、项目描述等,也会记录着你项目所依赖的其他库的信息和依赖库的版本号。
如何创建这个配置文件:
npm init –y(所有信息都是默认的)npm init 也可以创建项目,但是要自己输入一些项目相关的参数必须填写的属性:name、version
private属性:
main属性用来设置程序的入口:
下图,这个属性规定了从index.js查找文件
==scripts==属性:
scripts属性用于配置一些脚本命令,以键值对的形式存在,配置后我们可以通过 npm run 命令的key来执行这个命令;
npm start和npm run start的区别是什么?
比如
npm run dev来启动vue-element-admin,就是在scripts属性中有一个”dev”:”具体命令”
dependencies属性
dependencies属性是指定无论开发环境还是生成环境都需要依赖的包,通常是我们项目实际开发用到的一些库模块vue、vuex、vue-router、react、react-dom、axios等等;
与之对应的是devDependencies;使用npm install自动安装依赖的库
devDependencies属性
一些包在生产环境是不需要的,比如webpack、babel等,这个时候我们会通过 npm install webpack –save-dev,将它安装到devDependencies属性中;使用npm install xxx --save-dev来安装只有在开发环境使用的库
peerDependencies属性
还有一种项目依赖关系是对等依赖,也就是你依赖的一个包,它必须是以另外一个宿主包为前提的,比如element-plus是依赖于vue3的,ant design是依赖于react、react-dom;
engines属性
browserslist属性
我们会发现安装的依赖版本出现:^2.0.3或~2.0.3,这是什么意思呢?
npm的包通常需要遵从semver版本规范:
semver版本规范是X.Y.Z:
做了重大更新的时候彩更新X主版本号
^和~的区别:
npm install会检测是有package-lock.json文件:
没有lock文件
有lock文件
package-lock.json文件解析:
当前项目依赖axios,但是axios依赖follow-redireacts;
axios中的属性如下:
卸载某个依赖包
1 | npm uninstall package |
强制重新build
1 | npm rebuild |
清除缓存
1 | npm cache clean |
更多的命令,可以根据需要查阅官方文档 https://docs.npmjs.com/cli-documentation/cli
yarn 是为了弥补 早期npm 的一些缺陷而出现的;
早期的npm存在很多的缺陷,比如安装依赖速度很慢、版本依赖混乱等等一系列的问题;
虽然从npm5版本开始,进行了很多的升级和改进,但是依然很多人喜欢使用yarn;
yarn也是生成node_modules
由于一些特殊的原因,某些情况下我们没办法很好的从 https://registry.npmjs.org 下载下来一些需要的包
查看npm镜像:
1 | npm config get registry # npm config get registry |
我们可以直接设置npm的镜像:
1 | npm config set registry https://registry.npm.taobao.org |
但是对于大多数人来说(比如我),并不希望将npm镜像修改了:
第一,不太希望随意修改npm原本从官方下来包的渠道;
第二,担心某天淘宝的镜像挂了或者不维护了,又要改来改去;
这个时候,我们可以使用cnpm,并且将cnpm设置为淘宝的镜像:
1 | npm install -g cnpm --registry=https://registry.npm.taobao.org |
npx是npm5.2之后自带的一个命令。
npx的作用非常多,但是比较常见的是使用它来调用项目中的某个模块的指令。
我们以webpack为例:
全局安装的是webpack5.1.3
项目安装的是webpack3.6.0
如果我在终端执行 webpack –version使用的是哪一个命令呢?
显示结果会是 webpack 5.1.3,事实上使用的是全局的,为什么呢?
原因非常简单,在当前目录下找不到webpack时,就会去全局找,并且执行命令;
如何解决这个问题呢?
那么如何使用项目(局部)的webpack,常见的是两种方式:
◼ 方式一:在终端中使用如下命令(在项目根目录下)
1 | ./node_modules/.bin/webpack --version |
◼ 方式二:修改package.json中的scripts
1 | "scripts": { |
方式三:==使用npx==
1 | npx webpack --version |
npx的原理非常简单,它会到当前目录的node_modules/.bin目录下查找对应的命令;
performant npm 高性能NPM
原来的工具(npm、cnpm、yarn等)存在的问题:
认知:硬连接和软连接
当使用 npm 或 Yarn 时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么, 你在硬盘上就需要保存 100 份该相同依赖包的副本。
如果是使用 pnpm,依赖包将被存放在一个统一的位置,因此:
其结果是,源码可以访问 本不属于当前项目所设定的依赖包。
当使用 npm 或 Yarn Classic 安装依赖包时,所有软件包都将被提升到 node_modules 的 根目录下。
安装pnpm
1 | npm install -g pnpm |
更多命令和用法可以参考pnpm的官网 https://pnpm.io/zh/
在pnpm7.0之前,统一的存储位置是 ~/.pnpm-score中的;
在pnpm7.0之后,统一的存储位置进行了更改:<pnpm home directory>/store
在 Linux 上,默认是 ~/.local/share/pnpm/store
在 Windows 上: %LOCALAPPDATA%/pnpm/store
在 macOS 上: ~/Library/pnpm/store
我们可以通过一些终端命令获取这个目录:获取当前活跃的store目录
1 | pnpm store path |
另外一个非常重要的store命令是prune(修剪):从store中删除当前未被引用的包来释放store的空间
1 | pnpm store prune |