6 JS的模块化 ES6模块化及webpack打包

转自:https://blog.csdn.net/u014168594/article/details/77198729

js的模块化进程

如今前端技术突飞猛进,对于同一个问题痛点,各个时段有各自的解决方案,这就带来了很大差别。今天我就打算梳理js模块化的历史进程,讲一讲这些方案要作什么,怎么作。css

js模块化进程的原由

现今的不少网页其实能够看作是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。当一个项目开发的愈来愈复杂的时候,你会遇到一些问题:命名冲突(变量和函数命名可能相同),文件依赖(引入外部的文件数目、顺序问题)等。html

JavaScript发展的愈来愈快,超过了它产生时候的自我定位。这时候js模块化就出现了。前端

什么是模块化

模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案。他按照功能将一个软件切分红许多部分单独开发,而后再组装起来,每个部分即为模块。当使用模块化开发的时候能够避免刚刚的问题,而且让开发的效率变高,以及方便后期的维护。webpack

js模块化进程

1、早期:script标签

这是最原始的 JavaScript 文件加载方式,若是把每个文件看作是一个模块,那么他们的接口一般是暴露在全局做用域下,也就是定义在 window 对象中。web

缺点: 
1.污染全局做用域 
2.只能按script标签书写顺序加载 
3.文件依赖关系靠开发者主观解决浏览器

2、发展一:CommonJS规范

容许模块经过require方法来同步加载(同步意味阻塞)所要依赖的其余模块,而后经过module.exports来导出须要暴露的接口。服务器

// module add.js
module.exports = function add (a, b) { return a + b; }

// main.js
var {add} = require('./math');
console.log('1 + 2 = ' + add(1,2);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

CommonJS 是以在浏览器环境以外构建JavaScript 生态系统为目标而产生的项目,好比在服务器和桌面环境中。babel

3、发展二:AMD/CMD

(1)AMD

AMD 是 RequireJS 在推广过程当中对模块定义的规范化产出(异步模块定义)。app

AMD标准中定义了如下两个API:dom

  1. require([module], callback);
  2. define(id, [depends], callback);

require接口用来加载一系列模块,define接口用来定义并暴露一个模块。

define(['./a', './b'], function(a, b) {  
        // 依赖必须一开始就写好   
        a.add1()    
        ...  
        b.add2()    
        ...
    })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

优势: 
一、适合在浏览器环境中异步加载模块 二、能够并行加载多个模块

(2)CMD

CMD 是 SeaJS 在推广过程当中对模块定义的规范化产出。(在CommomJS和AMD基础上提出)

define(function (requie, exports, module) { 
    //依赖能够就近书写 
    var a = require('./a'); 
    a.add1(); 
    ... 
    if (status) { 
        var b = requie('./b'); 
        b.add2(); 
    } 
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

优势: 
一、依赖就近,延迟执行 二、能够很容易在服务器中运行

(3)AMD 和 CMD 的区别

AMD和CMD起来很类似,可是仍是有一些细微的差异:

一、对于依赖的模块,AMD是提早执行,CMD是延迟执行。

二、AMD推崇依赖前置;CMD推崇依赖就近,只有在用到某个模块的时候再去require。

三、AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一

4、发展三:ES6模块化

EcmaScript6 标准增长了JavaScript语言层面的模块体系定义。

在 ES6 中,咱们使用export关键字来导出模块,使用import关键字引用模块。

// module math.jsx
export default class Math extends React.Component{}

// main.js
import Math from "./Math";
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

目前不多JS引擎能直接支持 ES6 标准,所以 Babel 的作法其实是将不被支持的import翻译成目前已被支持的require。

ES6详解八:模块(Module)

基本用法

命名导出(named exports)

能够直接在任何变量或者函数前面加上一个 export 关键字,就能够将它导出。 
这种写法很是简洁,和平时几乎没有区别,惟一的区别就是在须要导出的地方加上一个 export 关键字。 
好比:

export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

而后在另外一个文件中这样引用:

import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3));
  • 1
  • 2
  • 3
  • 4

你可能会注意到这个奇怪的语法 { square, diag } 不就是前面讲过的 destructing吗。因此你会觉得还能够这样写:

import lib from 'lib';
 square = lib.square;
  • 1
  • 2
  • 3

可是其实这样是错的,由于 import { square, diag } from 'lib’; 是import的特有语法,并非 destructing 语法,因此其实import的时候并非直接把整个模块以对象的形式引入的。

若是你但愿能经过 lib.square 的形式来写,你应该这样导入:

import * as lib from 'lib';
 square = lib.square;
  • 1
  • 2
  • 3

不过值得注意的一点是,若是你直接用babel编译,执行是会报错的。由于 babel 并不会彻底编译 modules,他只是把 ES6 的modules语法编译成了 CMD 的语法,因此还须要用 browserify 之类的工具再次编译一遍。 
若是你发现 browserify 找不到 lib,能够改为 from ‘./lib’ 试试。

默认导出

你们会发现上面的写法比较麻烦,由于必需要指定一个名字。其实不少时候一个模块只导出了一个变量,根本不必指定一个名字。 
还有一种用法叫默认导出,就是指定一个变量做为默认值导出:

//------ myFunc.js ------
export default function () { ... };

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

默认导出的时候不须要指定一个变量名,它默认就是文件名。 
这里的区别不只仅是不用写名字,而是 导出的默认值就是模块自己,而不是模块下面的一个属性,便是 import myFunc from 'myFunc’; 而不是 import {myFunc} from 'myFunc’;

命名导出结合默认导出

默认导出一样能够结合命名导出来使用:

export default function (obj) {
    ...
};
export function each(obj, iterator, context) {
    ...
}
export { each as forEach };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上面的代码导出了一个默认的函数,而后由导出了两个命名函数,咱们能够这样导入:

import _, { each } from 'underscore';
  • 1
  • 2

注意这个逗号语法,分割了默认导出和命名导出

其实这个默认导出只是一个特殊的名字叫 default,你也能够就直接用他的名字,把它当作命名导出来用,下面两种写法是等价的:

import { default as foo } from 'lib';
import foo from 'lib';
  • 1
  • 2
  • 3

一样的,你也能够经过显示指定 default 名字来作默认导出, 下面两种写法是同样的:

//------ module1.js ------
export default 123;

//------ module2.js ------
const D = 123;
export { D as default };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

仅支持静态导入导出

ES6规范只支持静态的导入和导出,也就是必需要在编译时就能肯定,在运行时才能肯定的是不行的,好比下面的代码就是不对的:

//动态导入
var mylib;
if (Math.random()) {
    mylib = require('foo');
} else {
    mylib = require('bar');
}
//动态导出
if (Math.random()) {
    exports.baz = ...;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

为何要这么作,主要是两点:

  1. 性能,在编译阶段即完成全部模块导入,若是在运行时进行会下降速度
  2. 更好的检查错误,好比对变量类型进行检查

各类导入和导出方式总结

总结一下,ES6提供了以下几种导入方式:

// Default exports and named exports
import theDefault, { named1, named2 } from 'src/mylib';
import theDefault from 'src/mylib';
import { named1, named2 } from 'src/mylib';

// Renaming: import named1 as myNamed1
import { named1 as myNamed1, named2 } from 'src/mylib';

// Importing the module as an object
// (with one property per named export)
import * as mylib from 'src/mylib';

// Only load the module, don’t import anything
import 'src/mylib';
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

以下几种导出方式:

//命名导出
export var myVar1 = ...;
export let myVar2 = ...;
export const MY_CONST = ...;

export function myFunc() {
    ...
}
export function* myGeneratorFunc() {
    ...
}
export class MyClass {
    ...
}
// default 导出
export default 123;
export default function (x) {
    return x
};
export default x => x;
export default class {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
};
//也能够本身列出全部导出内容
const MY_CONST = ...;
function myFunc() {
    ...
}

export { MY_CONST, myFunc };
//或者在导出的时候给他们改个名字
export { MY_CONST as THE_CONST, myFunc as theFunc };

//还能够导出从其余地方导入的模块
export * from 'src/other_module';
export { foo, bar } from 'src/other_module';
export { foo as myFoo, bar } from 'src/other_module';

浅谈webpack打包原理

模块化机制

webpack并不强制你使用某种模块化方案,而是经过兼容全部模块化方案让你无痛接入项目。有了webpack,你能够随意选择你喜欢的模块化方案,至于怎么处理模块之间的依赖关系及如何按需打包,webpack会帮你处理好的。

关于模块化的一些内容,能够看看我以前的文章:js的模块化进程

核心思想:

  1. 一切皆模块: 
    正如js文件能够是一个“模块(module)”同样,其余的(如css、image或html)文件也可视做模 块。所以,你能够require(‘myJSfile.js’)亦能够require(‘myCSSfile.css’)。这意味着咱们能够将事物(业务)分割成更小的易于管理的片断,从而达到重复利用等的目的。
  2. 按需加载: 
    传统的模块打包工具(module bundlers)最终将全部的模块编译生成一个庞大的bundle.js文件。可是在真实的app里边,“bundle.js”文件可能有10M到15M之大可能会致使应用一直处于加载中状态。所以Webpack使用许多特性来分割代码而后生成多个“bundle”文件,并且异步加载部分代码以实现按需加载。

文件管理

  • 每一个文件都是一个资源,能够用require/import导入js
  • 每一个入口文件会把本身所依赖(即require)的资源所有打包在一块儿,一个资源屡次引用的话,只会打包一份
  • 对于多个入口的状况,其实就是分别独立的执行单个入口状况,每一个入口文件不相干(可用CommonsChunkPlugin优化)

打包原理

把全部依赖打包成一个bundle.js文件,经过代码分割成单元片断并按需加载。

如图,entry.js是入口文件,调用了util1.js和util2.js,而util1.js又调用了util2.js。

打包后的bundle.js例子

/******/ ([
/* 0 */     //模块id
/***/ function(module, exports, __webpack_require__) {

    __webpack_require__(1);     //require资源文件id
    __webpack_require__(2);

/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
    //util1.js文件
    __webpack_require__(2);
    var util1=1;
    exports.util1=util1;

/***/ },
/* 2 */
/***/ function(module, exports) {
    //util2.js文件
    var util2=1;
    exports.util2=util2;

/***/ }
...
...
/******/ ]);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  1. bundle.js是以模块 id 为记号,经过函数把各个文件依赖封装达到分割效果,如上代码 id 为 0 表示 entry 模块须要的依赖, 1 表示 util1模块须要的依赖
  2. require资源文件 id 表示该文件须要加载的各个模块,如上代码_webpack_require__(1) 表示 util1.js 模块,__webpack_require__(2) 表示 util2.js 模块
  3. exports.util1=util1 模块化的体现,输出该模块