Node.js
一、包与npm命令
1. 使用moment
项目目录下打开终端,输入命令,等待下载:
在代码中引入、使用:
结果:
2. npm 命令的使用
上面的代码,我们使用npm安装了moment来进行格式化时间的处理,这就是使用第三方模块;
而我们使用的npm就是node中自带的包(模块)管理工具;
借助NPM可以帮助我们快速安装和管理依赖包,使Node与第三方模块之间形成了一个良好的生态系统;
我们也可以直接输入npm,查看帮助引导:
3. 包的初始化
一个项目,不可能只是使用一个第三方包,而包越多,管理起来就越麻烦,
而 npm init 给我们提供了项目初始化的功能,也解决了多个包的管理问题:
一直按回车键enter
最后:
内容:
{
"name": "test", // 项目名
"version": "1.0.0", // 版本号
"main": "http.js", // 入口文件
"dependencies": { // 用户发布环境,生成上所需要的依赖包
"moment": "^2.24.0" // 包名以及版本
},
"devDependencies": {}, // 用于本地环境开发时候所需要的依赖包
"scripts": { // npm 设置的一些指令
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "", // 作者
"license": "ISC", // 当前项目的协议
"description": "" //项目描述
}
dependencies与devDependencies的区别:
-
后面部分为–save -dev 的情况会使得下载的插件放在package.json文件的devDpendencies对象里面。
-
后面部分为–save的情况会使得下载的插件放在package.json文件的dependencies对象里面。
-
devDependencies下的依赖包,只是我们在本地或开发坏境下运行代码所依赖的,若发到线上,其实就不需要devDependencies下的所有依赖包;(比如各种loader,babel全家桶及各种webpack的插件等)只用于开发环境,不用于生产环境,因此不需要打包;
-
dependencies是我们线上(生产坏境)下所要依赖的包,比如vue,我们线上时必须要使用的,所以要放在dependencies下;dependencies依赖的包不仅开发环境能使用,生产环境也能使用。
4. 包的结构
包实际上就是一个储存文件,即一个目录直接打包成为.zip或者tar.gz格式的文件,安装后解压还原为目录。完全符合规范的包目录应该包含如下文件:
- package.json:包描述文件
- bin:用于存放二进制文件的目录
- lib:用于存放JavaScript代码的目录
- doc:用于存放文档的目录
- test:用于存放单元测试实例的代码目录
5. 解决 npm 被墙问题
npm 存储包文件的服务器在国外,有时候会被墙,速度很慢,所以我们需要解决这个问题。
http://npm.taobao.org/ 淘宝的开发团队把 npm 在国内做了一个备份。
安装淘宝的 cnpm:
# 在任意目录执行都可以
# --global 表示安装到全局,而非当前目录
# --global 不能省略,否则不管用
npm install --global cnpm
接下来你安装包的时候把之前的 npm
替换成 cnpm
。
举个例子:
# 这里还是走国外的 npm 服务器,速度比较慢
npm install moment
# 使用 cnpm 就会通过淘宝的服务器来下载 jquery
cnpm install moment
如果不想安装 cnpm
又想使用淘宝的服务器来下载:
npm install jquery --registry=https://registry.npm.taobao.org
但是每一次手动这样加参数很麻烦,所我们可以把这个选项加入配置文件中:
# 配置到淘宝服务器
npm config set registry https://registry.npm.taobao.org
# 查看 npm 配置信息
npm config list
只要经过了上面命令的配置,则你以后所有的 npm install
都会默认通过淘宝的服务器来下载。
6. package.json 与 package-lock.json 文件
如果后期开发过程中,需要项目迁移,我们只需要将package.json文件迁移即可,在新项目下执行npm install
,根据package.json文件再次下载所需的依赖包。
两者的区别:
- package.json文件
- 管理和记录了你项目正在使用的第三方插件
- package-lock.json文件
- 详细记录了每个插件的内容,地址,唯一码
当我们使用npm管理包时,package.json 及package-lock.json 的内容都会自动更新。
7. 服务端页面渲染
之前的案例中,我们时通过前端浏览器发送ajax请求获取服务器数据的,前端获取数据后进行遍历展示;
流程图:
缺点就是发送多次请求、不利于搜索引擎查找;我们修改改为后端渲染数据;
什么叫后端渲染?
客户端发送请求,服务端获取所有数据,读取index.html并将数据组合在html中一起响应给客户端,浏览器进行渲染显示。
如何实现?
使用第三方模块 art-template: https://www.npmjs.com/package/art-template
先来做一个小实验,了解art-template的使用方法:
npm install art-template
JavaScript:(art-test.js)
/* 2.引入art-template */
var art = require('art-template');
/* 3.设置当前路径 */
art.defaults.root = './';
var html = art('./art-test.html',{data:[{name:123,age:345},{a:678,b:987}]});
console.log(html);
HTML:(art-test.html)
<body>
<h1>nihoa</h1>
<h2>{{data[0].name}}</h2>
</body>
结果:
实现原理就是先获取 ./art-test.html
,根据 ./art-test.html
中的{{data[0].name}}
组合数据,最后整体响应。
重构案例
- 重新创建目录,并初始化项目:
npm init
- 将之前写好的后台文件 http.js 和 前台模板页面 index.html 复制到新项目目录中;
- 安装时间处理模块:
npm install moment
- 安装模板引擎模块:
npm install art-template
- 修改 后台文件 http.js 和 前台模板页面 index.html 文件
修改JavaScript:(http.js)
/* 引入核心模块 */
var http = require('http');
var fs = require('fs');
/* 第三方模块 */
var moment = require('moment');
var template = require('art-template');
/* 设置为当前路径 */
template.defaults.root = './';
/* 创建服务器 */
var server = http.createServer();
server.listen(8080,function(){
console.log('启动成功');
});
server.on('request',function(req,res){
/* 获取请求url */
var urls = req.url;
if(urls == '/'){
/* 此处获取当前路径下的所有文件各自的信息 */
/* 以一个数组的形式存在 */
/**
*[
*{name:'http.js',mtime:'2019-09-04T05:18:11.635Z',size:'866'}
*{name:'index.html',mtime:'2019-09-04T05:18:11.635Z',size:'866'}
*...
*]
*/
/* 读取当前路径下的文件夹名 */
fs.readdir('./','utf8',function(err,data){
//定义一个空数组
var arr = [];
var cont = 0;
/* 循环遍历每一个文件名,获取其中的信息 */
for(let i = 0;i<data.length;i++){
//每一个文件一个对象,用于保存信息
arr[i] = {};
/* for循环中存在异步,解决方法闭包 */
(function(i){
/* 获取每个文件的所有信息 */
fs.stat(data[i],function(err,datas){
/* 分清是文件还是文件夹,自定义一个type属性,响应式用于分别图标 */
if(datas.isFile()){//用于判断是否是文件
arr[i].type = 'text';
}else{
arr[i].type = 'folder'
}
arr[i].name = data[i];
arr[i].mtime = moment(datas.mtime).format('YYYY-MM-DD hh:mm:ss');
arr[i].size = datas.size;
cont++;
if(cont == data.length){
/* 获取index.html,组合所有数组,整体响应 */
var html = template('./index.html',{data:arr});
res.end(html);
}
})
})(i)
}
})
}else{
fs.readFile('.' + req.url, function (err, data) {
res.end(data);
})
}
})
修改HTML:(index.html)
<body>
<h2>Index of /框架</h2>
<table>
<tbody>
<tr>
<th valign="top"><img src="./img/blank.gif" alt="[ICO]"></th>
<th><a href="http://localhost/%e6%a1%86%e6%9e%b6/?C=N;O=D">Name</a></th>
<th><a href="http://localhost/%e6%a1%86%e6%9e%b6/?C=M;O=A">Last modified</a></th>
<th><a href="http://localhost/%e6%a1%86%e6%9e%b6/?C=S;O=A">Size</a></th>
<th><a href="http://localhost/%e6%a1%86%e6%9e%b6/?C=D;O=A">Description</a></th>
</tr>
<tr>
<th colspan="5">
<hr>
</th>
</tr>
<tr>
<td valign="top"><img src="./img/back.gif" alt="[PARENTDIR]"></td>
<td><a href="http://localhost/">Parent Directory</a> </td>
<td> </td>
<td align="right"> - </td>
<td> </td>
</tr>
<!-- 使用模板语法 -->
<!-- 循环模板
{{each target}}
{{$index}}表示循环下标 {{$value}}表示循环的值,也是一个对象,可以直接使用点语法
{{/each}}
-->
<!-- 判断模板
{{if value}} ... {{/if}}
{{if v1}} ... {{else if v2}} ... {{/if}}
-->
{{each data}}
<tr>
{{if $value.type == 'text'}}
<td valign="top"><img src="./img/text.gif" alt="[DIR]"></td>
{{else}}
<td valign="top"><img src="./img/folder.gif" alt="[DIR]"></td>
{{/if}}
<td><a href="#">{{$value.name}}/</a> </td>
<td align="right">{{$value.mtime}}</td>
<td align="right">{{$value.size}}</td>
<td> </td>'
</tr>
{{/each}}
<tr>
<th colspan="5">
<hr>
</th>
</tr>
</tbody>
</table>
<address>Apache/2.4.39 (Win64) PHP/7.2.18 Server at localhost Port 80</address>
</body>
效果:
那么我们在项目中应该使用 客户端渲染还是服务端渲染:
答:两者都用,根据数据的不同作用而定;
推荐:
推举一个node开发时使用的小工具 nodemon
npm install nodemon -g
安装成功后,使用 nodemon 运行代码,
代码一旦被保存,nodemon便会自动重新运行新代码
二、Node的模块实现及CommonJS规范
1. CommonJS规范
(1)commomJs 的出发点
commonJs 规范的提出,主要是为了弥补当前JavaScript没有标准的缺陷,以达到像python、Java具有开发大型项目的基础能力,而不是停留在小脚本程序阶段。并且期望那些使用commonJs API写出的应用可以具有跨宿主环境执行能力,这样不经可以利用JavaScript开发客户端应用,而且还能编写以下应用。
- 服务器端JavaScript应用程序。
- 命令行工具
- 桌面图形界面应用程序
- 混合应用
Electron 跨平台的桌面应用框架: https://electronjs.org/
但是目前,他依旧在成长过程中,这些规范涵盖了模块、二进制、Buffer、字符集编码、I/O流、进程环境、文件系统、套接字、单元测试、Web服务器网关接口、包管理等等。
What I’m describing here is not a technical problem. It’s a matter of people getting together and making a decision to step forward and start building up something bigger and cooler together.
我在这里描述的不是一个技术问题。这是一个人们聚在一起,决定向前一步,开始一起建立更大更酷的东西的问题。
–Kevin Dangoor
下述是Node与浏览器以及W3C组织、commomJs组织、ECMAscript组织之间的关系:
(2)commomJs 的模块化规范
commomJs对模块的定义很简单,只要分为模块引用,模块定义,模块的标识三部分
1. 模块的引用
var fs = require('fs');
在commonJs规范中,存在require()方法,这个方法接受模块的标识,从此引入一个模块的API到当前上下文中
2. 模块的定义与导出
在模块中,上下文提供了require()方法来引入外部的模块.对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口.在模块中还存在一个module对象,它代表模块自身,而exports是module的属性.在node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式:
// test.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
module.exports = { firstName, lastName, year };
在另一个文件中,我们通过require()方法引入模块后就可以定义或者调用属性与方法:
// demo.js
const test = require('./test.js');
console.log(test); // {firstName: "Michael", lastName: "Jackson", year: 1958}
其实exports 对象就是module.exports 的引用; exports === module.exports
3. 模块标识
模块标识其实就是传递给require()方法的参数,它必须符合小驼峰命名法的命名法则,或者以 .或者…开头的的相对路径或者绝对路径.(注意:它可以没有后缀名.js)
重点注意 : 模块中的方法和变量的作用于尽在模块内部,每个模块具有独立的空间,互不干扰;
CommonJS 构建的模块机制中的引入与导出是我们完全不用考虑变量污染或者替换的问题,相比与命名空间
的机制,简直就是天才和菜鸟的区别;
2. Node 的模块实现
在Node中引入模块,需要经历以下三个步骤:
(1)路径分析
(2)文件定位
(3)编译执行
在Node中,模块分为两类:1)Node提供的模块,称为核心模块;2)另一类是用户编写的模块,称为文件模块。
核心模块:在node源码编译过程中,编译进了二进制文件。在node进程启动时,部分核心模块就被直接加载进了内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤就可以省略,并且在路径分析中优先判断,所以加载速度是最快的。
文件模块:在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。
加载过程
(1)优先从缓存中加载
与前端浏览器会缓存静态脚本文件以提高性能一样。Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于浏览器仅仅缓存文件,而node缓存的是编译和执行后的对象。
不论是核心模块还是文件模块,require()方法对相同模块的二次加载一律采用缓存优先的方式,只是第一优先级。不同的是核心模块的缓存检查优先于文件模块的缓存检查。
(2)路径分析与文件的定位
无非就是模块标识符的分析:
- 核心模块
- . 或者 … 开头的相对路径文件模块
- 以/开头的绝对路径文件模块
- 非路径形式的文件模块,比如自定义模块
第三方模块的加载规则:
- 先在当前文件的模块所属目录去找 node_modules目录
- 如果找到,则去该目录中找 模块名的目录 如 : moment
- 如果找到 moment 目录, 则找该目录中的 package.json文件
- 如果找到 package.json 文件,则找该文件中的 main属性
- 如果找到main 属性,则拿到该属性对应的文件
- 如果找到 moment 目录之后,
- 没有package.json
- 或者有 package.json 没有 main 属性
- 或者有 main 属性,但是指向的路径不存在
- 则 node 会默认去看一下 moment 目录中有没有 index.js --> index.json–> index.node 文件
- 如果找不到index 或者 找不到 moment 或者找不到 node_modules
- 则进入上一级目录找 node_moudles 查找(规则同上)
- 如果上一级还找不到,继续向上,一直到当前文件所属磁盘的根目录
- 如果到磁盘概目录还没有找到,直接报错
(3)模块的编译
各类文件的载入编译方式:
- .js文件的编译。通过fs模块同步读取文件后进行编译执行。
编译执行过程:
1)头尾包装:
比如上述art-template的小实验:
(funtion(exports,require,module,_filename,_dirname){
var art = require('art-template');
art.defaults.root = './';
var html = art('./art-test.html',{data:[{name:123,age:345},{a:678,b:987}]});
console.log(html);
})
2)通过vm原生模块的runThisContext()方法执行(类似于eval,只是明确上下文,不会污染全局),返回一个具体的function对象。
3)最后将当前模块对象的exports属性,require()方法、module(模块对象自身)、以及在文件定位中的得到的完整文件路径和文件目录作为参数传递给这个function()执行。
- .json文件的编译。通过fs模块同步读取文件后,用JSON.parse()解析返回结果。
编译执行过程:
直接调用JSON.parse()方法得到对象,然后将其复制给模块对象的exports,以供外部调用。
- 其他扩展名文件的编译。它们都被当作.js文件载入。
3. 模块化封装案例
对服务端页面渲染进行模块化封装:
修改JavaScript:(http.js)
http.js ---- 服务器启动模块,用于启动服务器
/* 引入核心模块 */
var http = require('http');
/* 引入router模块 */
var router = require('./router')
/* 创建服务器 */
var server = http.createServer();
/* 将server传到router.js中,监听请求事件 */
router.server(server);
server.listen(8080,function(){
console.log('启动成功');
});
router.js ---- 路由模块,用于接受请求并处理响应
var contrllor = require('./controller');
var fs = require('fs');
/* 创建一个函数供给http.js调用并传参 */
function server(server) {
server.on('request', function (req, res) {
var urls = req.url;
if (urls == '/') {
/* 获取数据 */
var html = controller.html;
res.setHeader('Content-Type', 'text/html;charset=utf-8');
/* 响应数据 */
res.end(html);
}
else {
fs.readFile('.' + urls, function (error, data) {
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.end(data);
})
}
});
}
/* 导出模块,此处非常重要!!! */
exports.server = server;
controller.js ---- 业务逻辑模块,用于获取数据
var fs = require('fs');
var moment = require('moment');
var template = require('art-template');
template.defaults.root = './';
fs.readdir('./', 'utf8', function (err, data) {
var arr = [];
var cont = 0;
for (var i = 0; i < data.length; i++) {
arr[i] = {};
(function (i) {
fs.stat(data[i], function (err, datas) {
if (datas.isFile()) {
arr[i].type = 'text';
} else {
arr[i].type = 'folder';
}
arr[i].name = data[i];
arr[i].size = datas.size;
arr[i].mtime = moment(datas.mtime).format('YYYY-MM-DD hh:mm');
cont++;
if (cont == data.length) {
var html = template('./index.html',{data:arr});
/* 导出数据 */
exports.html = html;
}
})
})(i);
}
})
效果: