预热
介绍模块化编程,因为Node.js是一个高度模块化的平台,学习模块化可以帮助我们快速理解和使用Node.js。详见如下,这是摘自我去年写的一篇博客笔记(博客目前已经没有在维护了)
title:前端模块化演变 date:2018-10-23 23:27:56 tags:模块化
现状
前端产品的交付是基于浏览器,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统,这个理想中的模块化系统是前端工程师多年来一直探索的难题。
-
异步概念 js是单线程的,由于执行ajax请求会消耗一定的时间,甚至出现了网络故障而迟迟得不到返回结果;这时,如果同步执行的话,就必须等到ajax返回结果以后才能执行接下来的代码,如果ajax请求需要1分钟,程序就得等1分钟。如果是异步执行的话,就是告诉ajax代码“老兄,既然你迟迟不返回结果,我先不等你了,我还有一大堆代码要执行,等你执行完了给我说一下”
-
模块系统主要解决模块的定义、依赖和导出,先来看看已经存在的模块系统。
模块系统的演进
原始的 JavaScript 文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中,不同模块的接口调用都是一个作用域中,一些复杂的框架,会使用命名空间的概念来组织这些模块的接口,典型的例子如 YUI 库
这种原始的加载方式暴露了一些显而易见的弊端:
1.全局作用域下容易造成变量冲突
2.文件只能按照 <script>
的书写顺序进行加载
3.开发人员必须主观解决模块和代码库的依赖关系
4.在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪
CommonJS
服务器端的 Node.js 遵循 CommonJS规范,该规范的核心思想是允许模块通过 require 方法来同步加载所要依赖的其他模块,然后通过 exports 或 module.exports 来导出需要暴露的接口。
require("module");module.exports = module;复制代码
优点:
- 服务器端模块便于重用
- NPM 中已经有将近20万个可以使用模块包
- 简单并容易使用
缺点:
- 同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的
- 不能非阻塞的并行加载多个模块
实现:
- 服务器端的 Node.js
- Browserify,浏览器端的 CommonJS 实现,可以使用 NPM 的模块,但是编译打包后的文件体积可能很大
- modules-webmake,类似Browserify,还不如 Browserify 灵活
- wreq,Browserify 的前身
AMD
Asynchronous Module Definition 规范其实只有一个主要接口 define(id?, dependencies?, factory),它要在声明模块的时候指定所有的依赖 dependencies,并且还要当做形参传到 factory 中,对于依赖的模块提前执行,依赖前置。
define("module", ["dep1", "dep2"], function(d1, d2) { return someExportedValue;});require(["module", "../file"], function(module, file) { /* ... */ });复制代码
优点:
- 适合在浏览器环境中异步加载模块
- 可以并行加载多个模块
- 缺点:
- 提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
- 不符合通用的模块化思维方式,是一种妥协的实现
实现:
- RequireJS
- curl
CMD
Common Module Definition 规范和 AMD 很相似,尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。
define(function(require, exports, module) { var $ = require('jquery'); var Spinning = require('./spinning'); exports.doSomething = ... module.exports = ...})复制代码
优点:
- 依赖就近,延迟执行
- 可以很容易在 Node.js 中运行
缺点:
- 依赖 SPM 打包,模块的加载逻辑偏重
实现:
- Sea.js
- coolie
UMD
Universal Module Definition 规范类似于兼容 CommonJS 和 AMD 的语法糖,是模块定义的跨平台解决方案。
ES6 模块
ECMAScript6 标准增加了 JavaScript 语言层面的模块体系定义。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。
import "jquery";export function doStuff() {}module "localModule" {}复制代码
优点:
- 容易进行静态分析
- 面向未来的 ECMAScript 标准
缺点:
- 原生浏览器端还没有实现该标准
- 全新的命令字,新版的 Node.js才支持
实现:
- Babel
前端模块加载
前端模块要在客户端中执行,所以他们需要增量加载到浏览器中。 模块的加载和传输,我们首先能想到两种极端的方式,一种是每个模块文件都单独请求,另一种是把所有模块打包成一个文件然后只请求一次。显而易见,每个模块都发起单独的请求造成了请求次数过多,导致应用启动速度慢;一次请求加载所有模块导致流量浪费、初始化过程慢。这两种方式都不是好的解决方案,它们过于简单粗暴。 分块传输,按需进行懒加载,在实际用到某些模块的时候再增量更新,才是较为合理的模块加载方案。 要实现模块的按需加载,就需要一个对整个代码库中的模块进行静态分析、编译打包的过程。
所有资源都是模块
在上面的分析过程中,我们提到的模块仅仅是指JavaScript模块文件。然而,在前端开发过程中还涉及到样式、图片、字体、HTML 模板等等众多的资源。这些资源还会以各种方言的形式存在,比如 coffeescript、 less、 sass、众多的模板库、多语言系统(i18n)等等。 如果他们都可以视作模块,并且都可以通过require的方式来加载,将带来优雅的开发体验,比如:如何使用reuqire管理.
require("./style.css");require("./style.less");require("./template.jade");require("./image.png");复制代码
静态分析
在编译的时候,要对整个代码进行静态分析,分析出各个模块的类型和它们依赖关系,然后将不同类型的模块提交给适配的加载器来处理。比如一个用 LESS 写的样式模块,可以先用 LESS 加载器将它转成一个CSS 模块,在通过 CSS 模块把他插入到页面的 <style>
标签中执行。Webpack 就是在这样的需求中应运而生。 同时,为了能利用已经存在的各种框架、库和已经写好的文件,我们还需要一个模块加载的兼容策略,来避免重写所有的模块。 那么接下来,让我们开始 Webpack 的神奇之旅吧。
模块化解决问题:
命名冲突,文件依赖,多人协作,可维护性,开发效率
演变过程-案例说明
以一个计算器模块为案例说明
1. 全局函数:所有变量和函数全部暴露在全局
```javascriptfunction add(x,y){ return pareseInt(x) + parseInt(y)}function subtract(x,y){ return pareseInt(x) - parseInt(y)}function multiply(x,y){ return pareseInt(x) * parseInt(y)}function divide(x,y){ return pareseInt(x) / parseInt(y)}```复制代码
2. 对象命名空间
>用来解决上述问题 但是命名冲突还在,如果有子命名空间就会导致命名空间越来越长```javascriptvar calculator={};calculator.add=function(x,y){ return pareseInt(x) + parseInt(y)}......calculator.add(1,3) //命名空间调用......//如果有子命名空间就会导致命名空间越来越长calculator.sub={ foo:function(){ ... }}calculator.sub.foo()```复制代码
3. 函数作用域(闭包)
> 全局函数和对象命名空间都不能很好的解决命名冲突的问题,而且开发中会有一些不想被外部访问的私有属性,那么我们可以通过封装函数的私有空间去让一些属性和方法私有化,也就是闭包```javascriptvar calculator=(function(){ function add(x,y){ return parseInt(x) + parseInt(y) } function subtract(x,y){ return pareseInt(x) - parseInt(y) } function multiply(x,y){ return pareseInt(x) * parseInt(y) } function divide(x,y){ return pareseInt(x) / parseInt(y) } return { add:add, subtarct:subtract, multiply:multiply, divide:divide }})()calculator.add(x,y) //通过匿名函数.函数名 ```复制代码
4. 维护和扩展
需求:添加一个取余的方法,如果这个计算模块由第三方提供解决:通过参数的形式将原来的模块和第三方库传递进去```javascriptvar calculator=(function(cal){function add(x,y){ return parseInt(x) + parseInt(y)}function subtract(x,y){ return pareseInt(x) - parseInt(y)}function multiply(x,y){ return pareseInt(x) * parseInt(y)}function divide(x,y){ return pareseInt(x) / parseInt(y)} cal.add:add, cal.subtarct:subtract, cal.multiply:multiply, cal.divide:divide return cal;})(calculator||{})var carculator=(function(){ cal.mod=function(x,y){ return x % y } return cal;})(calculator||{})```复制代码
Node.js基本介绍
Node.js的特点
1. js的runtime,让js脱离浏览器在服务端单独执行2. 依赖于V8引擎进行解析代码3. 事件驱动 事件触发才会执行响应的处理函数,这种机制就是标准的事件驱动机制4. 非阻塞IO 5. 轻量可伸缩,适用于实时数据交互应用 利用socket可以实现双向通信,例如聊天室的6. 单线程和单进程 进程就是一个application的一次执行过程, 而线程是进程中的一部分,进程包含多个线程在运行,单线程就是进程中只有一个线程 阻塞Io模式下一个线程只能处理一个任务非阻塞Io下,一个线程永远在处理任务,这样的cpu利用率是100% 所以node.js采用单线程,利用事件驱动的异步编程模式来实现了非阻塞Io复制代码
global对象和模块作用域
node中的global类似浏览器的window对象,用于定义全局命名空间,所以除了global之外都是他的属性 node中定义的变量默认是当前文件下的,不是全局global的,但是我们可以手动挂载到globa上 var foo=10; global.foo=foo; consoloe.log(global.foo) 复制代码
require/exports/module.exports
exports只是返回一个Object对象,不能单独定义并返回数据类型module.exports可以单独定义,返回数据类型复制代码
全局可用变量,函数,对象
全局作用域中任何变量函数和对象都是global对象的一个属性全局可用就是node提供的一下全局可用变量函数以及对象不需要进行模块加载就可以直接使用如:require是可以在每个模块作用域中存在,不加载就可以使用,可以说他全局可用而不是全局函数全局变量 _dirname 当前文件所在目录 _filename 当前正在执行脚本的文件名,输出文件所在位置的绝对路径,如果在模块中则显示模块文件的路径全局函数 定时器 console对象 info error warn dir time timeEnd trace(测试函数运行) 复制代码
require()的模块加载规则
加载模块分为两大类:文件模块和核心模块文件模块: -使用“/”开头的模块表示 执行当前文件所属的盘符根路径 c盘d盘... - 以"./"或"../"开头的相对路径模块标识 可以省略后缀名js json node核心模块:是被编译的二进制文件,保存在源码lib目录下 全局对象 常用工具 事件机制 文件系统访问 http服务器与客户端复制代码
模块的缓存
foo.js ==> console.log('foo模块被加载了')index.js 下面只会加载一次(输出一次)。因为模块的缓存 reuqire("./foo") reuqire("./foo") reuqire("./foo") reuqire("./foo") 不希望模块缓存可以在被加载的模块(foo.js)中添加如下代码 delete require.cache[module.filename] 结果会输出四次复制代码
异步编程
js的执行环境是单线程,一次只能完成一个任务,所以常见的浏览器无响应就是因为某代码运行时间过长,导致后面任务无法执行,所以NodeJS加入了异步编程的概念,解决单线程阻塞问题
1. 同步和异步
异步案例 setTimeout(()=>{},0)复制代码
2. 回调函数
-同步代码中使用try...catch处理异常-异步代码中不能使用try...catch处理异常 对于异步代码 try..catch是无法捕捉异步代码中出现的异常的复制代码
3. 使用回调函数接受异步代码的执行结果
异步编程提出了回调函数的设计三个约定: 1.优先把callback当做最后一个形参 2.将代码出现的错误作为callback第一个参数 callback(err,result) 3.将代码成功返回的结果作为callback的第二个参数 callback(err,result)** 异步编程的’事件驱动‘思路 异步函数执行时,不确定何时执行完毕,回调函数会被压入到一个事件循环(Event Loop)队列, 然后往下执行其他代码,直至异步函数执行完毕后,才会开始处理事件循环,调用响应的回调函数 EventL Loop是一个先进先出的队列复制代码
NodeJS中的包和npm
CommonJS是规范,开篇预热中已经介绍过,Node.js是这种规范的部分实现
规范的包目录结构:
-package.json 顶层目录的包描述文件,说明文件(开发者拿到第三方包的时候一目了然) 文件属性说明 属性和值通过json字符串形式描述 -name 包名 -description 包的描述 -version 版本号 -keywords 关键词用于npm包市场搜索 -author 包的作者 -main 配置包的入口,默认就是模块根目录下的index.js -dependencies 包的依赖项,npm会自行下载 -scripts 指定运行脚本命令的npm命令行缩写-bin 存放可执行的二进制文件-lib 存放js的目录-doc 存放文档的目录 -test 存放单元测试用例的代码复制代码
npm常用命令
-npm init --yes 初始一个package.json文件-npm install 名字 安装包-npm install 包名 --save 将安装的包添加到package.json的依赖中-npm install 包名 -g 全局安装一个命令行工具-npm install docs 查看包文档,非常有用-npm root -g 查看全局包安装路径-npm config set prefix '路径' 修改全局包安装路径-npm list 查看当前目录下安装的所有包-npm list -g 查看全局包的安装路径下的所有包-npm uninstall 包 卸载当前目录下的某个包-npm uninstall 包 -g-npm update 包 更新当前目录下的某个包复制代码
Node.js文件模块
基本文件操作
API: File System 简写fs开发中建议使用异步函数,比起同步函数性能更高,速度更快,没有阻塞方法:同步 Sync和异步 1.文件写入 fs.writeFileSync(file,data) 同步必须使用tryCatc捕获,防止出错程序意外退出 fs.writeFile(file,data,cllback) 在回调中判断err参数 2.向文件中追加内容 fs.appendFile(file,data,callback) 3.文件读取 fs.readFile(file,callback(err,data)) data.toString()转换二进制数据 4.文件复制 node没有提供这个函数,我们自己可以封装,思路:读取一个文件写入另一个文件 5.获取文件信息 fs.stat(path,callback)复制代码
案例:控制歌词滚动
1.创建歌词文件lrc.txt
[ti:我爱你中国(Live)][ar:汪峰][al:歌手第二季 歌王之战][by:天龙888][00:00.00]汪峰 - 我爱你中国(Live)[00:00.02]词:汪峰[00:00.03]曲:汪峰[00:00.04]原唱:汪峰[00:00.05]编曲:黄毅[00:00.06]Program:黄毅[00:00.07]现场Program:汪涛[00:00.08]制作人:黄毅[00:00.09]音乐总监:梁翘柏.....复制代码
2.编写js文件
const fs = require("fs");fs.readFile("./lrc.txt", function(err, data) { if (err) { return console.log("读取歌词文件失败") } data = data.toString(); var lines = data.split("\n"); var reg = /\[(\d{2})\:(\d{2})\.(\d{2})\]\s*(.+)/; for (var i = 0; i < lines.length; i++) { (function(index) { var line = lines[index]; var matches = reg.exec(line); if (matches) { var m = parseFloat(matches[1]); //获取分 var s = parseFloat(matches[2]); //获取秒 var ms = parseFloat(matches[3]); //获取毫秒 var content = matches[4] //获取定时器要输出的内容 var time = m * 60 * 1000 + s * 1000 + ms; //将分+秒+毫秒转化为毫秒 setTimeout(() => { console.log(content) }, time); } })(i) }})复制代码
文件操作的相关Api
1.Path模块之路径字符串模块 basename 获取文件名 dirname 获取文件目录 extname 获取文件扩展名 isAbsolute 判断是否为绝对路径 join(path1,path2,..) 拼接路径字符串 \\转义后为\ normalize(p) 将非标准路径转换为标准路径 sep 获取操作系统的文件路径分隔符 2.目录操作 对文件目录增加,读取,删除等操作 fs.mkdir(path,mode,callback) mode设置目录权限 默认0777 fs.rmdir(path,callback) 回调无参数 删除目录时目录中必须为空目录,需要提前读取目录和删除目录中的文件复制代码
Node.js中处理数据I/O
Buffer缓冲区
Buffer缓冲区 为Node提供存储原始数据的方法,用来在内存区域创建一个存放二进制数据的缓冲区
首先知道什么是二进制数据和乱码
鼠标右键直接创建文件编码一般为ANSI 这时候文件中包含中文字符,这个编码不支持中文字符。所以会出现乱码可以修改.txt文件编码为UTF-8,重新打印就没问题了复制代码
Buffer的构造函数
** 缓冲区模块支持开发者在缓冲区结构中创建,读取,写入和操作二进制数据 ** 此模块是全局的,使用时不需要require()函数来加载 创建方式: 1.传入字节 var buf=new Buffer(size) size=5代表创建了一个5字节的内存空间 2.传入数组 var buf=new Buffer([10,20,30,40]) 3.传入字符串和编码 var buf=new Buffer('hello world','utf-8') utf-8为默认支持的编码方式可以省略 写入缓冲区: 首先将源文件的数据读取处理然后写入到Buffer缓冲区中 从缓冲区读取数据 buf.toString(encoding,start,end) encoding 编码默认uft-8 start 指定开始读取的索引位置,默认0 end 缓冲区结束位置 拼接缓冲区 实际开发中遇到的需求: 输出多个缓冲区的内容的组合 buf.concat(list,totalLength) list是合并的Buffer对象数组列表 totalLength用于指定合并后的Buffer对象总长度复制代码
Stream文件流
问题:由于Buffer缓冲区限制在1GB,超过限制的文件无法直接完成读写操作,读写大文件的时候,读写资源一直持续不停,node将无法继续其他工作,所以采用文件流的方式
解决: stream文件流来解决大数据文件操作问题,node读写很容易是内存爆仓(1GB),所以文件流的读写会防止这一现象1.文件流的概念: stream文件流方式: 读一部分,写一部分,好处是可以提前处理,缩短等待时间,提高速度 案例模拟:观看在线视频的时候,下载一点播放一点 Stream有4中流类型: 1.Readable 可读操作(可读流) 2.Writable 可写操作(可写流) 3.Duplex 可读可写操作(双向流,双工流) 4.Transform 操作被写入数据,然后读出结果(变换流) NodeJS中很多模块涉及流的读写,如下: HTTP requests and responses Standard input/output File reads and writes Node.js中的I/O是异步的,所以对磁盘和网络的读写需要通过回到函数来读取数据,而回调函数需要通过事件来触发,所有的Stream对象都是EventEmitter(时间触发器)的实例 Stream常用事件如下: 1.data 当有数据可读时触发 2.end 没有更多的数据可读时触发 3.error 在接受和写入的过程中发生错误时触发 4.finish 所有的数据被写入到底层系统时触发2. Node.js的可读流和可写流 与buffer的读写操作类似,Stream中的可读流和可写流也用于读写操作 1.使用文件流进行文件复制,首先要创建一个可读流(Readable Stream) 可读流可以让用户在源文件中分块读取文件中的数据,然后再从可读流中读取数据 fs.createReadStream(path,options) options是一组 key-value值,常用设置如下 flags 对文件如何操作,默认为r 读文件 encoding start end 2.可写流(Writable Stream) fs.createWriteStream(path,options) 方法:write将一个数据写入到可写流中 3.使用pipe()处理大文件 如果把数据比作水,chunk就相当于盆,使用盆来完成水的传递 在可读流中还有一个函数叫做pipe() 是一个很高效的文件处理方式,可以简化复制文件的操作 pipe中文管子,相当于用管子替换盆,通过管道来完成数据的读取和写入复制代码