Javascript
模块化?模块化是一种处理复杂系统分解成为更好的可管理模块的方式,它能够把系统代码划分为一系列职责单一,高度解耦且可替换的模块,系统中某一部分的变化将如何影响其它部分就会变得显而易见,系统的可维护性更加简单易得。javascript
一个模块就是实现特定功能的文件, 逻辑上相关的代码组织到同一个包内,包内是一个相对独立的王国,不用担忧命名冲突什么的,那么外部使用的话直接引入对应的package
便可.css
就好像做家会把他的书分章节和段落;程序员会把他的代码分红模块。html
就好像书籍的一章,模块仅仅是一坨代码而已。前端
好的代码模块分割的内容必定是很合理的,便于你增长减小或者修改功能,同时又不会影响整个系统。java
Javascript
模块化?早期前端只是为了实现简单的页面交互逻辑,随着Ajax
技术的普遍应用,前端库的层出不穷,前端代码日益膨胀,JavaScript
却没有为组织代码提供任何明显帮助,甚至没有类的概念,更不用说模块(module
)了,这时候JavaScript
极其简单的代码组织规范不足以驾驭如此庞大规模的代码.node
模块化可使你的代码低耦合,功能模块直接不相互影响。jquery
可维护性:根据定义,每一个模块都是独立的。良好设计的模块会尽可能与外部的代码撇清关系,以便于独立对其进行改进和维护。维护一个独立的模块比起一团凌乱的代码来讲要轻松不少。webpack
命名空间:在JavaScript中,最高级别的函数外定义的变量都是全局变量(这意味着全部人均可以访问到它们)。也正因如此,当一些无关的代码碰巧使用到同名变量的时候,咱们就会遇到“命名空间污染”的问题。git
可复用性:现实来说,在平常工做中咱们常常会复制本身以前写过的代码到新项目中, 有了模块, 想复用的时候直接引用进来就行。程序员
前端的先驱在刀耕火种的阶段开始,作了不少努力,在现有的运行环境中,实现"模块"的效果。
模块就是实现特定功能的一组方法。在JavaScript中,函数是建立做用域的惟一方式, 因此把函数做为模块化的第一步是很天然的事情.
function foo(){
//...
}
function bar(){
//...
}
复制代码
上面的,组成一个模块。使用的时候,直接调用就好了。
这种作法的缺点很明显:全局变量被污染,很容易命名冲突, 并且模块成员之间看不出直接关系。
为了解决上面的缺点,能够把模块写成一个对象,全部的模块成员都放到这个对象里面。
var MYAPP = {
count: 0,
foo: function(){},
bar: function(){}
}
MYAPP.foo();
复制代码
上面的代码中,函数foo
和bar
, 都封装在MYAPP对象里。使用的时候,就是调用这个对象的属性。 可是,这样的写法会暴露全部模块成员,内部状态能够被外部改写.
使用当即执行函数(Immediately-Invoked Function Expression,IIFE),能够达到不暴露私有成员的目的。
var Module = (function(){
var _private = "safe now";
var foo = function(){
console.log(_private)
}
return {
foo: foo
}
})()
Module.foo();
Module._private; // undefined
复制代码
这种方法的好处在于,你能够在函数内部使用局部变量,而不会意外覆盖同名全局变量,但仍然可以访问到全局变量, 在模块外部没法修改咱们没有暴露出来的变量、函数.
将全局变量当成一个参数传入到匿名函数而后使用
var Module = (function($){
var _$body = $("body"); // we can use jQuery now!
var foo = function(){
console.log(_$body); // 特权方法
}
// Revelation Pattern
return {
foo: foo
}
})(jQuery)
Module.foo();
复制代码
jQuery
的封装风格曾经被不少框架模仿,经过匿名函数包装代码,所依赖的外部变量传给这个函数,在函数内部可使用这些依赖,而后在函数的最后把模块自身暴漏给window
。
若是须要添加扩展,则能够做为jQuery
的插件,把它挂载到$上。 这种风格虽然灵活了些,但并未解决根本问题:所需依赖仍是得外部提早提供、仍是增长了全局变量。
从以上的尝试中,能够概括出js模块化须要解决那些问题:
围绕着这些问题,js模块化开始了一段艰苦而曲折的征途。
上述的全部解决方案都有一个共同点:使用单个全局变量来把全部的代码包含在一个函数内,由此来建立私有的命名空间和闭包做用域。
你必须清楚地了解引入依赖文件的正确顺序。就拿Backbone.js
来举个例子,想要使用Backbone
就必须在你的页面里引入Backbone
的源文件。
然而Backbone
又依赖 Underscore.js
,因此Backbone
的引入必须在其以后。
而在工做中,这些依赖管理常常会成为让人头疼的问题。
另一点,这些方法也有可能引发命名空间冲突。举个例子,要是你碰巧写了俩重名的模块怎么办?或者你同时须要一个模块的两个版本时该怎么办?
还有就是协同开发的时候, 你们编写模块的方式各不相同,你有你的写法,我有个人写法, 那就乱了套.
接下来介绍几种广受欢迎的解决方案
2009年,美国程序员Ryan Dahl
创造了node.js
项目,将javascript
语言用于服务器端编程。
这标志Javascript
模块化编程正式诞生。由于老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;可是在服务器端,必定要有模块,与操做系统和其余应用程序互动,不然根本无法编程。
node.js
的模块系统,就是参照CommonJS
规范实现的。
CommonJS
定义的模块分为:
定义模块: 根据CommonJS
规范,一个单独的文件就是一个模块。每个模块都是一个单独的做用域,也就是说,在该模块内部定义的变量,没法被其余模块读取,除非定义为global
对象的属性。
模块输出: 模块只有一个出口,module.exports
对象,咱们须要把模块但愿输出的内容放入该对象。module
对象就表明模块自己。
加载模块: 加载模块使用require
方法,该方法读取一个文件并执行,返回文件内部的module.exports
对象。
// math.js
exports.add = function(a, b){
return a + b;
}
复制代码
// main.js
var math = require('math') // ./math in node
console.log(math.add(1, 2)); // 3
复制代码
这种实现模式有两点好处:
可是, 因为一个重大的局限,使得CommonJS
规范不适用于浏览器环境。
看上面的main.js
代码, 第二行的math.add(1, 2)
,在第一行require('math')以后运行,所以必须等math.js
加载完成。也就是说,若是加载的依赖不少, 时间很长,整个应用就会停在那里等。
咱们分析一下浏览器端的js和服务器端js都主要作了哪些事,有什么不一样:
服务器端JS | 浏览器端JS |
---|---|
相同的代码须要屡次执行 | 代码须要从一个服务器端分发到多个客户端执行 |
CPU和内存资源是瓶颈 | 带宽是瓶颈 |
加载时从磁盘中加载 | 加载时须要经过网络加载 |
这对服务器端不是一个问题,由于全部的模块都存放在本地硬盘,能够同步加载完成,等待时间就是硬盘的读取时间。可是,对于浏览器,这倒是一个大问题,由于模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
所以,浏览器端的模块,不能采用"同步加载"(synchronous
),只能采用"异步加载"(asynchronous
)。这就是AMD
规范诞生的背景。
AMD 即
Asynchronous Module Definition
,中文名是异步模块定义的意思。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到加载完成以后,这个回调函数才会运行。
// main.js
  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // some code here
  });
复制代码
define
来定义模块,用法为:define(id?, dependencies?, factory)
;id
为模块标识,听从CommonJS Module Identifiers
规范dependencies
为依赖的模块数组,在factory
中需传入形参与之一一对应dependencies
的值中有"require"、"exports"
或"module"
,则与commonjs
中的实现保持一致dependencies
省略不写,则默认为["require", "exports", "module"]
,factory
中也会默认传入require,exports,module
.factory
为函数,模块对外暴漏API
的方法有三种:return
任意类型的数据、exports.xxx=xxx、module.exports=xxx
.factory
为对象,则该对象即为模块的返回值大名鼎鼎的require.js
就是AMD规范的实现.
require.js
要求,每一个模块是一个单独的js
文件。这样的话,若是加载多个模块,就会发出屡次HTTP
请求,会影响网页的加载速度。所以,require.js
提供了一个优化工具(Optimizer
)r.js
,当模块部署完毕之后,能够用这个工具将多个模块合并在一个文件中,实现前端文件的压缩与合并, 减小HTTP请求数。
咱们来看一个require.js
的例子
//a.js
define(function(){
console.log('a.js执行');
return {
hello: function(){
console.log('hello, a.js');
}
}
});
复制代码
//b.js
define(function(){
console.log('b.js执行');
return {
hello: function(){
console.log('hello, b.js');
}
}
});
复制代码
//main.js
require.config({
paths: {
"jquery": "../js/jquery.min"
},
});
require(['jquery','a', 'b'], function($, a, b){
console.log('main.js执行');
a.hello();
$('#btn').click(function(){
b.hello();
});
})
复制代码
上面的main.js被执行的时候,会有以下的输出: a.js执行 b.js执行 main.js执行 hello, a.js
在点击按钮后,会输出: hello, b.js
可是若是细细来看,b.js
被预先加载而且预先执行了,(第二行输出),b.hello
这个方法是在点击了按钮以后才会执行,若是用户压根就没点,那么b.js
中的代码应不该该执行呢?
这其实也是AMD/RequireJs
被吐槽的一点,因为浏览器的环境特色,被依赖的模块确定要预先下载的。问题在于,是否须要预先执行?若是一个模块依赖了十个其余模块,那么在本模块的代码执行以前,要先把其余十个模块的代码都执行一遍,无论这些模块是否是立刻会被用到。这个性能消耗是不容忽视的。
另外一点被吐槽的是,在定义模块的时候,要把全部依赖模块都罗列一遍,并且还要在factory
中做为形参传进去,要写两遍很大一串模块名称,像这样:
define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){ ..... })
复制代码
CMD 即
Common Module Definition
, CMD是sea.js
的做者在推广sea.js
时提出的一种规范.
在 CMD
规范中,一个模块就是一个文件。代码的书写格式以下:
define(function(require, exports, module) {
// 模块代码
// 使用require获取依赖模块的接口
// 使用exports或者module或者return来暴露该模块的对外接口
})
复制代码
define
函数定义模块, 无需罗列依赖数组,在factory
函数中需传入形参require,exports,module
.require
用来加载一个 js
文件模块,require
用来获取指定模块的接口对象 module.exports
。//a.js
define(function(require, exports, module){
console.log('a.js执行');
return {
hello: function(){
console.log('hello, a.js');
}
}
});
复制代码
//b.js
define(function(require, exports, module){
console.log('b.js执行');
return {
hello: function(){
console.log('hello, b.js');
}
}
});
复制代码
//main.js
define(function(require, exports, module){
console.log('main.js执行');
var a = require('a');
a.hello();
$('#b').click(function(){
var b = require('b');
b.hello();
});
});
复制代码
上面的main.js执行会输出以下: main.js执行 a.js执行 hello, a.js
a.js和b.js都会预先下载,可是b.js中的代码却没有执行,由于尚未点击按钮。当点击按钮的时候,会输出以下: b.js执行 hello, b.js
Sea.js
加载依赖的方式
AMD vs CMD
一样都是异步加载模块,AMD在加载模块完成后就会执行改模块,全部模块都加载执行完后会进入require的回调函数,执行主逻辑.
CMD加载完某个依赖模块后并不执行,只是下载而已,在全部依赖模块加载完成后进入主逻辑,遇到require
语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是彻底一致的。
这也是不少人说AMD用户体验好,由于没有延迟,依赖模块提早执行了,CMD性能好,由于只有用户须要的时候才执行的缘由。
上述的这几种方法都不是JS原生支持的, 在
ECMAScript 6 (ES6)
中,引入了模块功能, ES6 的模块功能汲取了CommonJS 和 AMD 的优势,拥有简洁的语法并支持异步加载,而且还有其余诸多更好的支持。
简单来讲,ES6 模块的设计思想就是:一个 JS 文件就表明一个 JS 模块。在模块中你可使用 import 和 export 关键字来导入或导出模块中的东西。
ES6 模块主要具有如下几个基本特色:
通常来说,组织声明文件的方式取决于库是如何被使用的。 在JavaScript中一个库有不少使用方式,这就须要你书写声明文件去匹配它们.
经过库的使用方法及其源码来识别库的类型。
全局库是指能在全局命名空间下访问的,许多库都是简单的暴露出一个或多个全局变量。 好比jQuery.
当你查看全局库的源代码时,你一般会看到:
一些库只能工做在模块加载器的环境下。 好比,像 express
只能在Node.js
里工做因此必须使用CommonJS
的require
函数加载。
模块库至少会包含下列具备表明性的条目之一:
require
或define
import * as a from 'b'; or export c
;这样的声明exports
或module.exports
UMD (Universal Module Definition)
库
UMD创造了一种同时使用两种规范的方法,而且也支持全局变量定义。因此UMD的模块能够同时在客户端和服务端使用。
本质上,UMD 是一套用来识别当前环境支持的模块风格的 if/else 语句。下面是一个解释其功能的例子:
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define(["libName"], factory);
} else if (typeof module === "object" && module.exports) {
module.exports = factory(require("libName"));
} else {
root.returnExports = factory(root.libName);
}
}(this, function (b) {})
复制代码
简单的说,Grunt / Gulp 和 browserify / webpack
不是一回事。
Gulp / Grunt
Gulp / Grunt 是一种工具,可以优化前端工做流程。好比自动刷新页面、combo、压缩css、js、编译less等等。简单来讲,就是使用Gulp/Grunt,而后配置你须要的插件,就能够把之前须要手工作的事情让它帮你作了。
说到 browserify / webpack
,那还要说到 seajs / requirejs
。这四个都是JS模块化的方案。其中seajs / require
是一种类型,browserify / webpack
是另外一种类型。seajs / require
: 是一种在线"编译" 模块的方案,至关于在页面上加载一个 CMD/AMD
解释器。这样浏览器就认识了 define、exports、module
这些东西。也就实现了模块化。
browserify / webpack
: 是一个预编译模块的方案,相比于上面 ,这个方案更加智能, 首先,它是预编译的,不须要在浏览器中加载解释器。另外,你在本地直接写JS,无论是 AMD / CMD / ES6
风格的模块化,它都能认识,而且编译成浏览器认识的JS。这样就知道,Gulp
是一个工具,而webpack
等等是模块化方案。Gulp
也能够配置seajs、requirejs
甚至webpack
的插件。
每次运行grunt
时,他就利用node
提供的require()
系统查找本地安装的 Grunt
。
若是找到一份本地安装的 Grunt
,grunt-CLI
就将其加载,并传递Gruntfile
中的配置信息,而后执行你所指定的任务。
npm install -g grunt-cli
复制代码
gruntfile.js
文件module.exports = function (grunt) {
// 项目配置.
grunt.initConfig({
// 定义Grunt任务
});
// 加载可以提供"uglify"任务的插件。
grunt.loadNpmTasks('grunt插件');
// Default task(s).
grunt.registerTask('default', ['任务名']);
}
复制代码
gulp
是基于Nodejs的自动化任务运行器,它能自动化地完成javascript/sass/less/html/image/css
等文件的的测试、检查、合并、压缩、格式化、浏览器自动刷新、部署文件生成,并监听文件在改动后重复指定的这些步骤。
使用Gulp
的优点就是利用流的方式进行文件的处理,使用管道(pipe
)思想,前一级的输出,直接变成后一级的输入,经过管道将多个任务和操做链接起来,所以只有一次I/O
的过程,流程更清晰,更纯粹。Gulp
去除了中间文件,只将最后的输出写入磁盘,整个过程所以变得更快。
使用Gulp
,能够避免浏览器缓存机制,性能优化(文件合并,减小http请求;文件压缩)以及效率提高(自动添加CSS3前缀;代码分析检查)
Browserify
是一个模块打包器,它遍历代码的依赖树,将依赖树中的全部模块打包成一个文件。有了 Browserify
,咱们就能够在浏览器应用程序中使用 CommonJS
模块。
browserify模块化的用法和node是同样的,因此npm上那些本来仅仅用于node环境的包,在浏览器环境里也同样能用.
webpack官网有对两者的使用方法进行对比,能够看一下:webpack for browserify users
browserify main.js -o bundle.js
复制代码
Compare Webpack vs Browserify vs RequireJS
官网对webpack
的定义是MODULE BUNDLER
(模块打包器),他的目的就是把有依赖关系的各类文件打包成一系列的静态资源。 请看下图
Webpack
的工做方式是:把你的项目当作一个总体,经过一个给定的主文件(如:main.js
),Webpack
将从这个文件开始找到你的项目的全部依赖文件,使用loaders
处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript
文件。
webpack
将建立全部应用程序的依赖关系图表(dependency graph
)。
entry配置项告诉Webpack应用的根模块或起始点在哪里, 入口起点告诉 webpack
从哪里开始,并遵循着依赖关系图表知道要打包什么。能够将应用程序的入口起点认为是根上下文或 app
第一个启动文件。它的值能够是字符串、数组或对象.
//webpack.config.js
const config = {
entry: {
app: './src/app.js',
vendors: './src/vendors.js'
}
};
复制代码
将全部的资源(assets
)合并在一块儿后,咱们还须要告诉 webpack
在哪里打包咱们的应用程序。output
选项控制 webpack
如何向硬盘写入编译文件。注意,即便能够存在多个入口起点,但只指定一个输出配置。
output: {
path: helpers.root('dist/nonghe'),
publicPath: '/',
filename: 'js/[name].[chunkhash].bundle.js',
chunkFilename: 'js/[name].[chunkhash].bundle.js'
}
复制代码
在webpack的世界里, 一切皆模块, 经过
loader
的转换,任何形式的资源均可以视做模块,好比CommonJs 模块、 AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 LESS
等。并且 webpack 只理解 JavaScript。
对比 Node.js 模块,webpack 模块可以以各类方式表达它们的依赖关系:
url(...
))或 HTML 文件(<img src=...>
)中的图片连接webpack compiler
在碰到上面那些语句的时候, 经过与其相对应的loader
将这些文件进行转换,而转换后的文件会被添加到依赖图表中。
module: {
loaders: [{
test: /\.scss$/,
loaders: 'style!css!sass'
}, {
test: /\.(png|jpg|svg)$/,
loader: 'url?limit=20480' //20k
}]
}}
复制代码
plugin
插件,用于扩展webpack
的功能,在webpack
构建生命周期的节点上加入扩展hook
为webpack
加入功能。
Loaders
和Plugins
经常被弄混,可是他们实际上是彻底不一样的东西,能够这么来讲,loaders
是在打包构建过程当中用来处理源文件的(js,ts, Scss,Less..),一次处理一个,一般做用于包生成以前或生成的过程当中。
插件并不直接操做单个文件,它直接对整个构建过程其做用。
几款经常使用的插件
HtmlWebpackPlugin : 这个插件的做用是依据一个简单的html
模板,生成一个自动引用打包后的JS文件的新index.html
。
Hot Module Replacement: 它容许你在修改组件代码后,自动刷新实时预览修改后的效果。
CommonsChunkPlugin: 对于有多个入口文件的, 能够抽取公共的模块,最终合成的文件可以在最开始的时候加载一次,便存起来到缓存中供后续使用。
DefinePlugin: 容许你建立一个在编译时能够配置的全局常量。这可能会对开发模式和发布模式的构建容许不一样的行为很是有用。
ExtractTextWebpackPlugin: 它会将打包在js
代码中的样式文件抽离出来, 放到一个单独的 css
包文件 (styles.css)当中, 这样js
代码就能够和css
并行加载.
UglifyjsWebpackPlugin: 这个插件使用 UglifyJS 去压缩你的JavaScript代码。
从启动webpack构建到输出结果经历了一系列过程,它们是:
webpack
配置参数,合并从shell
传入和webpack.config.js
文件里配置的参数,生产最后的配置结果。entry
入口文件开始解析文件构建依赖图谱,找出每一个文件所依赖的文件,递归下去。loader
配置找出合适的loader
用来对文件进行转换。entry
配置生成代码块chunk
。chunk
到文件系统。代码拆分是 webpack
中最引人注目的特性之一。你能够把代码分离到不一样的 bundle
中,而后就能够去按需加载这些文件.
CommonsChunkPlugin
module bundlers
)最终将全部的模块编译生成一个庞大的bundle.js
文件。所以Webpack使用许多特性来分割代码而后生成多个“bundle”文件,并且异步加载部分代码以实现按需加载使用 require.ensure()
按需分离代码
require.ensure(dependencies: String[], callback: function(require), chunkName: String)
复制代码
模块热替换功能会在应用程序运行过程当中替换、添加或删除模块,而无需从新加载页面。这使得你能够在独立模块变动后,无需刷新整个页面,就能够更新这些模块.
webpack-dev-server
支持热模式,在试图从新加载整个页面以前,热模式会尝试使用 HMR 来更新。
webpack-dev-server 主要是启动了一个使用 express 的 Http服务器 。它的做用 主要是用来伺服资源文件 。此外这个 Http服务器 和 client 使用了 websocket 通信协议,原始文件做出改动后, webpack-dev-server 会实时的编译,可是最后的编译的文件并无输出到目标文件夹, 实时编译后的文件都保存到了内存当中。
"server": "webpack-dev-server --inline --progress --hot",
复制代码
webpack-dev-server 支持2种自动刷新的方式:
Iframe mode
Iframe mode 是在网页中嵌入了一个 iframe ,将咱们本身的应用注入到这个 iframe 当中去,所以每次你修改的文件后,都是这个 iframe 进行了 reload 。
inline mode
而 Inline-mode ,是 webpack-dev-server 会在你的 webpack.config.js 的入口配置文件中再添加一个入口,
module.exports = {
entry: {
app: [
'webpack-dev-server/client?http://localhost:8080/',
'./src/js/index.js'
]
},
output: {
path: './dist/js',
filename: 'bundle.js'
}
}
复制代码
这样就完成了将 inlinedJS
打包进 bundle.js
里的功能,同时 inlinedJS
里面也包含了 socket.io
的 client
代码,能够和 webpack-dev-server
进行 websocket
通信。
其余配置选项
Hot Module Replacement
功能