前端模块化

TIP

最后更新时间:2019年10月5日

字数:68601

没有上进心的人是可耻的,鄙视👎👎👎

主要内容

  • 前端模块化
  • CommandJS
  • AMD
  • CMD
  • UMD
  • ES6 modules
  • webpack
  • webpack开发配置
  • webpack打包配置
  • vue-cli中webpack逐行解析
  • parcel
  • rollup
  • 使用ES6

学习资源

前端模块化

什么是前端模块化

什么是模块化

  • 一个模块就是一个实现特定功能的文件,有了模块我们就可以更方便的使用别人的代码,要用什么功能就加载什么模块

模块化的优点

  • 避免变量污染,命名冲突
    • JavaScript的缺陷,首当其冲就是全局变量。
    • 这使得每想命名一个变量的时候都要三思又三思,生怕上方无穷远的地方有一个同事些的代码和自己冲突。
  • 依赖关系的管理
  • 提高代码复用率
  • 提高维护性

前端模块化发展

  • 一个js文件一个模块,用script引入
  • 后期就是各种方案了

主流模块化方案

  • CommonJS
  • AMD
  • CMD
  • UMD
  • ES6 modules
  • 模块化打包工具
    • webpack
    • parcel
    • rollup

TIP

前端模块化要解决的核心问题:

模块的定义,依赖和导出

CommonJS规范

特点

  • Nodejs规范,所以是module.exports和require
  • require是Node中少数几个同步I/O操作之一
  • 同步,require(同步)
  • 仅适用于后端,因为同步的原因,在前端使用会阻塞,下面AMD VS CommonJS有解答
  • 总共四个步骤:定义 -> 输出 -> 加载 -> 使用
  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存
  • 模块加载的顺序,按照其在代码中出现的顺序

module对象

  • Node内部提供一个Module构建函数。所有模块都是Module的实例。
  • 每个模块内部,都有一个module对象,代表当前模块。
// Module 实例
function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  // ...
  
}

// module属性
module.id 模块的识别符,通常是带有绝对路径的模块文件名。
module.filename 模块的文件名,带有绝对路径。
module.loaded 返回一个布尔值,表示模块是否已经完成加载。
module.parent 返回一个对象,表示调用该模块的模块。
module.children 返回一个数组,表示该模块要用到的其他模块。
module.exports 表示模块对外输出的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

module.parent

  • 返回一个对象,表示调用该模块的模块。
  • 如果在命令行下调用某个模块,比如node xxx.js,那么module.parent就是null
  • 如果是在脚本之中调用,比如require('./xxx.js'),那么module.parent就是调用它的模块。
  • 利用这一点,可以判断当前模块是否为入口脚本
if (!module.parent) {
    // ran with `node xxx.js`
    app.listen(8088, function() {
        console.log('app listening on port 8080');
    })
} else {
    // used with `require('/.xxx.js')`
    module.exports = app;
}
1
2
3
4
5
6
7
8
9

module.exports

  • 属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量
  • 如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出

定义模块

  • 一个单独的文件就是一个模块,并且该文件中的作用域独立
  • 当中的变量是无法被其他文件引用的,如果要使用需要将其定义为global

输出模块

  • 模块只有一个出口
  • 使用module.exports对象,将需要输出的内容放入到该对象中

加载模块

  • 通过require加载

示例代码

// 总共四个步骤: 定义 -> 输出 -> 加载 -> 使用
// 定义模块 module.js
var data = "commonJS";

function test1() {
    alert("test1");
}

function test2() {
    alert("test2");
}

// 输出模块
module.exports = {
    test1: test1,
    test2: test2
}    

// 加载模块
var module = require('./module.js');

// 使用模块功能
module.test1(); // "test1"
module.test2(); // "test2"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

TIP

CommonJS由于require是同步加载的,所以仅仅适用于后端,浏览器端同步加载页面会造成阻塞

module.exports和exports

  • module.exports
    • CommonJS规范规定,每个模块内部,module变量代表当前模块。
    • 这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。
    • 加载某个模块,其实是加载该模块的module.exports属性。
  • exports
    • 为了方便,Node为每个模块提供一个exports变量,指向module.exports。
    • 这等同在每个模块头部,有一行这样的命令
    • 于是我们可以直接在 exports 对象上添加方法,表示对外输出的接口,如同在module.exports上添加一样。
    • 注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。

TIP

如果你觉得,exportsmodule.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports

结论:不论任何时候都推荐用module.exports

AMD

  • Asynchronous Module Definition,异步模块定义
  • AMD是一套基于浏览器端模块化开发的规范,在进行页面开发时需要用到该规范的库函数,即:requireJS

require.js

  • 由于AMD不是js原生支持,使用AMD规范进行页面开发需要用到对应的函数库,也就是大名鼎鼎的RequireJS
  • 实际上AMD是RequireJS在推广过程中对模块定义的规范化的产出
  • requireJS主要解决两个问题
    • 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
    • js加载的时候浏览器会停止页面渲染,加载文件愈多,页面失去响应的时间愈长

示例代码

// 模块定义 module.js
define(
    ['dependModule', '...', ...], // 这个数组表示该模块所依赖的模块名称 
    function () {
        var data = "hello AMD !";
        function test1() {
            alert("hello test1 !);
        }

        function test2() {
            alert("hello test2 !);
        }


        return {
            test1: test1,
            test2: test2
        };
});


// 加载模块
require(['module'], function (myModule) {
    // 加载之后的module模块将会以参数形式:myModule传入到回调函数中,供使用
    // 这里是模块加载完成之后的回调函数
    myModule.test1(); // "hello test1 !"
    myModule.test2(); // "hello test2 !"
});

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
28
29

定义模块:define()

  • 全局函数,用来定义模块
  • 格式:define(id?, dependencies?, factory)
  • id:
    • 可选参数
    • 用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
  • dependencies:
    • 可选参数
    • 是当前模块依赖的模块名称组成的数组
  • factory:
    • 必选参数
    • ⼯厂方法,模块初始化要执行的函数或对象。
    • 如果为函数,它应该只被执行⼀次。
    • 如果是对象,此对象应该为模块的输出值

加载模块:require()

  • 使用异步的require()来加载模块
  • 格式:require([dependencies], function(){})
  • 参数1:
    • 一个数组,表示所依赖的模块
  • 参数2:
    • 一个回调函数
    • 当前面指定的模块都加载成功后,它将被调⽤。
    • 加载的模块将以参数形式传入该函数,从⽽在回调函数内部就可以使⽤这些模块

TIP

函数在加载依赖函数的时候是异步加载的

这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题

require.config

  • 用来配置模块的
  • 可以设置局部配置,也可以添加一个全局配置
// 将百度的jquery库地址标记为jquery,这样在require时只需要写["jquery"]就可以加载该js
require.config({
    paths : {
        "jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery"],
        "a" : "js/a"   
    }
})
require(["jquery","a"],function($){
    $(function(){
        alert("load finished");  
    })
})
1
2
3
4
5
6
7
8
9
10
11
12

通过paths的配置会使我们的模块名字更精炼,paths还有一个重要的功能,就是可以配置多个路径,如果远程cdn库没有加载成功,可以加载本地的库

require.config({
    paths : {
        "jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery", "js/jquery"],
        "a" : "js/a"   
    }
})
require(["jquery","a"],function($){
    $(function(){
        alert("load finished");  
    })
})
1
2
3
4
5
6
7
8
9
10
11
  • 这样配置后,当百度的jquery没有加载成功后,会加载本地js目录下的jquery

注意事项

  • 在使用requirejs时,加载模块时不用写.js后缀的,当然也是不能写后缀
  • 上面例子中的callback函数中发现有$参数,这个就是依赖的jquery模块的输出变量,如果你依赖多个模块,可以依次写入多个参数来使用
require(["jquery","underscore"],function($, _){
    $(function(){
        _.each([1,2,3],alert);
    })
})
// 这里的$代表jquery的输出
// 这里的下划线代表underscore的输出
1
2
3
4
5
6
7

TIP

如果某个模块不输出变量值,则没有,所以尽量将输出的模块写在前面,防止位置错乱引发误解

全局配置

  • requirejs提供了一种叫"主数据"的功能,可以添加全局配置
  • 避免了重复出现的require.config中的配置
// main.js 全局配置
require.config({
    paths : {
        "jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery", "js/jquery"],
        "a" : "js/a"   
    }
})

// 页面中使用下面的方式来使用requirejs
<script data-main="js/main" src="js/require.js"></script>
1
2
3
4
5
6
7
8
9
10

data-main

  • 作用1:

    • 这个属性指定的js将在加载完reuqire.js后处理
    • 我们把 require.config 的配置加入到 data-main 后,就可以使每一个页面都使用这个配置,然后页面中就可以直接使用 require 来加载所有的短模块名
  • 作用2:

    • 当script标签指定data-main属性时,require会默认的将data-main指定的js为根路径,当做基准开始查找,如果没有指定,就以文件所在的路径为基准开始查找
    • 上面的data-main="js/main"设定后,我们在使用require(['jquery'])后(不配置jquery的paths),require会自动加载js/jquery.js这个文件,而不是jquery.js,相当于配置了baseUrl为js目录
// 配置基础路径为js目录
require.config({
    baseUrl : "js"
})
1
2
3
4

第三方模块

  • 有时候我们需要加载一些非标准或者不符合AMD规范的模块
  • 关键字shim(垫)
    • 将非标准的AMD模块"垫"成可用的模块
    • 加载插件形式的非AMD模块
// 加载没有实现AMD规范的underscore类库
require.config({
    shim: {
        "underscore" : {
            exports : "_";
        }
    }
})

// 这样配置后,我们就可以在其他模块中引用underscore模块
require(["underscore"], function(_){
    _.each([1,2,3], alert);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
// 加载jquery.form插件,依赖jquery
// 将form插件"垫"到jquery中
require.config({
    shim: {
        "underscore" : {
            exports : "_";
        },
        "jquery.form" : {
            deps : ["jquery"]
        }
    }
})

// 这样配置之后我们就可以使用加载插件后的jquery了
require.config(["jquery", "jquery.form"], function($){
    $(function(){
        $("#form").ajaxSubmit({...});
    })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

TIP

shim :显式地配置模块之间的依赖项

exports:非模块的库转化为模块化的库,导出内容,必须是全局的

deps:通过 deps 来配置当前模块的依赖项

AMD VS CommonJS

  • CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
  • AMD规范则是非同步加载模块,允许指定回调函数。
  • 由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用
  • 但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。

TIP

这也是CommonJS仅适用于服务器的一个原因之一

CMD

  • Common Module Definition,通用定义模块

  • CMD规范是国内发展出来的,阿里的玉伯

  • AMD有个requireJS,CMD有个浏览器的实现SeaJS

  • SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同

TIP

AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。

示例代码

// define(id?, deps?, factory)
define(function (require, exports, module) {
    // 依赖可以就近书写
    var a = require('./a');
    a.test();  
    // ...
    if (status) {
        // 依赖可以就近书写
        var b = requie('./b');
        b.test();
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
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
28
29
  • 因为CMD推崇一个文件一个模块,所以经常就用文件名作为模块id

  • CMD推崇依赖就近,所以一般不在define的参数中写依赖,而是在factory中写

factory

  • factory有三个参数:
    • require
      • 是 factory 函数的第一个参数
      • require 是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口
    • exports
      • 是一个对象,用来向外提供模块接口
    • module
      • 是一个对象,上面存储了与当前模块相关联的一些属性和方法

AMD VS CMD

最明显的区别就是在模块定义时对依赖的处理不同

  • AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块

  • CMD推崇就近依赖,只有在用到某个模块的时候再去require

  • 他们的加载都是异步的,⽽非我们理解的同步,只不过依赖处理不同

AMD

  • 同样都是异步加载模块,AMD在加载模块完成后就会执行该模块,所有模块都加载执行完后会进入require的回调函数,执⾏主逻辑

  • 这样的效果就是依赖模块的执行顺序和书写顺序不⼀定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执⾏

CMD

  • CMD加载完某个依赖模块后并不执⾏,只是下载⽽已

  • 在所有依赖模块加载完成后进⼊主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的

TIP

这也是很多人说AMD用户体验好,因为没有延迟,依赖模块提前执行了CMD性能好,因为只有用户需要的时候才执行的原因

UMD

  • UMD规范,通用模块规范
  • AMD和CommonJS的糅合

AMD/CMD原则

  • AMD/CMD以浏览器为第一(browser-first)的原则发展,选择异步加载模块。

  • 它的模块支持对象(objects)、函数(functions)、构造器(constructors)、字符串(strings)、JSON等各种类型的模块。因此在浏览器中它非常灵活。

  • CommonJS module以服务器端为第一(server-first)的原则发展,选择同步加载模块。

  • 它的模块是无需包装的(unwrapped modules)且贴近于ES.next/Harmony的模块格式。但它仅支持对象类型(objects)模块。

UMD实现

  • UMD的实现很简单,先判断是否支持Node.js模块格式(exports是否存在),存在则使用Node.js模块格式
  • 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
  • define(factory)是一个全局函数,用来定义模块,存在就是AMD或者CMD环境
  • 前两个都不存在,则将模块公开到全局(window或global)。
//xxx.js
;(function(name, definition){
    //检测上下文环境是否为 AMD 或 CMD
    var hasDefine = typeof define === 'function',
        hasExports = typeof module !== 'undefined' && module.exports;
    if (hasDefine) {
        //AMD 环境或 CMD 环境
        define(definition);
    } else if (hasExports) {
        // 定义为普通的node模块或者CommonJS规范
        // CommonJS也就是nodejs规范
        module.exports = definition();
    } else {
        //将模块的执行结果挂在 window 变量中,在浏览器中 this 指向 window 对象
        this[name] = definition();
    }
})('xxx', function() {
    //代码主体
    var xxx = function () {};
    return xxx;
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

ES6 Modules

现状

  • 在之前的 javascript 中一直是没有模块系统的,前辈们为了解决这些问题,提出了各种规范, 最主要的有CommonJS和AMD两种。
  • 前者用于服务器,后者用于浏览器。
  • 而 ES6 中提供了简单的模块系统,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。

TIP

ES6模块化,虽然前景很好,但目前未广泛使用,需要继续发展

ES6模块化优点

  • 类似commonJS,语法更简洁
  • 类似AMD,直接支持异步加载和配置模块加载
  • 结构可以做静态分析,静态检测,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能
  • 比commonJS更好的支持循环依赖

ES6模块化特点

  • ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
  • CommonJS 和 AMD 模块,都只能在运行时确定这些东西。
  • ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
  • ES6 加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
  • ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

TIP

ES6的模块化分为导出(export)与导入(import)两个模块。

import和export都必须总是出现在它们分别被使用之处的顶层作用域。

例如,你不能把import或export放在一个if条件内部;它们必须出现在所有块儿和函数的外部。

严格模式

ES6 Module要求严格模式

严格模式介绍连接:

严格模式

命名导出(named exports)

  • 一个模块通过export声明来导出多个,需要导出的变量或函数只需要在声明最前面加上export关键词即可
  • 在需要用的地方使用import导入。
  • 这种写法非常简洁,和平时几乎没有区别,唯一的区别就是在需要导出的地方加上一个 export 关键字。
// a.jx
export const name = 'zhangsan';
export function getName(x) {
    return name;
}
export function count(x, y) {
    return x + y;
}
1
2
3
4
5
6
7
8
// b.js
import { name, getName, count } from './a.js';
console.log(getName()); // zhangsan
console.log(count(4, 3)); // 7
1
2
3
4

默认导出(default exports)

  • 使用 export default 导出模块默认的内容
  • 每个模块只能有一个 export default
  • 导入默认导出的模块时,需要指定模块名称
/*------ str.js ------*/
let version = "1.0.1"
function trim () {
    console.log('trim方法')
}
// 不需要指定变量名
// 导出一个对象
export default  {
    trim: trim,
    version: version
}


/*------ index.js ------*/
// 导入默认导出的模块时,需要指定模块名称str
import str from './str.js'

str.trim()  // 打印trim方法
console.log(str.version) // 打印1.0.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

TIP

默认导出的时候不需要指定一个变量名,它默认就是文件名。

  • 其实这个默认导出只是一个特殊的名字叫default , 你也可以直接用他的名字,把它当做命名导出来用
// 这两个是等价的
import { default as foo } from 'lib';
import foo from 'lib';


// 也可以通过显示指定 default 名字来做默认导出
export default 'zhangsan';

const name = 'zhangsan';
export { name as default };
1
2
3
4
5
6
7
8
9
10

导入命名导出

  • 导入模块时可使用大括号包含指定命名成员
  • 也可以用 *** as moduleName** 的形式把此模块的所有命名导出作为某个对象的成员。
// math.js 导出
export function add(a, b) {
    return a + b;
}
 
// app.js:指定使用math模块的add命名导出
import { add } from './math.js';
console.log(add(1, 2)); // => 3
 
// 导入所有的命名导出作为math对象的成员
import * as math from './math.js';
console.log(math.add(1, 2)); // => 3
1
2
3
4
5
6
7
8
9
10
11
12

导入默认导出

  • 导入默认导出的模块时,需要指定模块名称
// math.js
export default function add(x,y) {
    return x + y;
}
 
// app.js:导入默认导出的模块时,需要指定模块名称
import add from './math.js';
console.log(add(4,6)); // => 10
1
2
3
4
5
6
7
8

仅导入模块

  • 仅导入模块时,只会执行模块的全局函数,不会导入任何成员。
// math.js
export function add(a, b) {
    return a + b;
}
(function() {
    console.log('hello math.js');
})();
 
// app.js
import { add } from './math.js'; // => import的时候就会默认运行 hello math.js
// 可以调用add()
add(1,2) // => 3
1
2
3
4
5
6
7
8
9
10
11
12

TIP

import性能超过require

  • require:要把这个模块中的所有内容引入
  • import:可以只把需要使用的引入到文件

webpack

学习资料

什么是webpack

  • 官方描述:A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows to load parts for the application on demand. Through "loaders," modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.
  • 大致意思:一个模块化打包工具,可以打包模块。并且通过代码拆分,可以拆分出按需加载的部分。通过loaders加载器,可以解析的模块包括CommonJs,AMD,ES modules,CSS,Images,JSON,Coffeescript, LESS等等以及你自己定制的部分

TIP

  • 简单来说就是一个模块化打包工具,它做的事情是,分析你的项目结构,然后将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。

  • 还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。

  • 通过 loader 的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、 AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 LESS 等

安装webpack

全局安装

# sudo npm i -g webpack

# sudo npm i -g webpack-cli
1
2
3

TIP

安装webpack4版本后,必须安装webpack-cli

不推荐全局安装 webpack。

这会将你项目中的 webpack 锁定到指定版本,并且在使用不同的 webpack 版本的项目中,可能会导致构建失败。

本地安装

// 要安装最新版本或特定版本
# npm install --save-dev webpack
# npm install --save-dev webpack@<version>
1
2
3

TIP

对于大多数项目,我们建议本地安装。

如果全局安装,目前如果安装4+,必须安装webpack-cli,不安装,运行webpack会提示的

安装体验版

  • 体验版是直接从 webpack 的仓库中安装
# npm install webpack@beta
# npm install webpack/webpack#<tagname/branchname>
1
2

TIP

最新体验版本可能仍然包含 bug,因此不应该用于生产环境

核心概念

  • 入口(entry)
  • 输出(output)
  • loader
  • 插件(plugins)

入口(entry)

  • 指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
  • 进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
  • 每个依赖项随即被处理,最后输出到称之为 bundles 的文件中
// 1.分离 应用程序(app) 和 第三方库(vendor) 入口
const config = {
  entry: {
    app: './src/app.js',
    vendors: './src/vendors.js'
  }
};
// 2. 多页面应用程序
const config = {
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出(output)

  • output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist
  • 基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。
  • 你可以通过在配置中指定一个 output 字段,来配置这些处理过程
// 指定入口和出口
const path = require('path');
module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),// 目标输出目录 path 的绝对路径
    filename: 'my-first-webpack.bundle.js' // filename 用于输出文件的文件名
  }
};
1
2
3
4
5
6
7
8
9

loader

  • loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
  • loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
  • 本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。
  • loader配置:
    • test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
    • use 属性,表示进行转换时,应该使用哪个 loader。
const path = require('path');

const config = {
  output: {
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }
};

module.exports = config;


// 内联loader
import Styles from 'style-loader!css-loader?modules!./styles.css';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

TIP

在 webpack 配置中定义 loader 时,要定义在 module.rules 中,而不是 rules

插件(plugins)

  • loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。
  • 插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。
  • 想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。
  • 多数插件可以通过选项(option)自定义。
  • 你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建它的一个实例。
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 用于访问内置插件

const config = {
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

module.exports = config;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

mode

  • 提供 mode 配置选项,告知 webpack 使用相应模式的内置优化。
  • development:
    • 会将 process.env.NODE_ENV 的值设为 development
    • 启用 NamedChunksPluginNamedModulesPlugin
  • production:
    • 会将 process.env.NODE_ENV 的值设为 production
    • 启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin.

运行方式

  • 命令行运行
  • 配置文件

命令行运行

  • Webpack 会分析入口文件,解析包含依赖关系的各个文件。这些文件(模块)都打包到 bundle.js
  • Webpack 会给每个模块分配一个唯一的 id 并通过这个 id 索引和访问模块。
  • 在页面启动时,会先执行 entry.js 中的代码,其它模块会在运行 require 的时候再执行。
webpack <entry> [<entry>] -o <output>  --mode=development

webpack 入口文件 -o 出口   --mode=开发模式
1
2
3
  • 命令行使用loader
先安装loader
npm install css-loader style-loader

使用的时候可以这样写
require("!style-loader!css-loader!./style.css") // 载入 style.css

另一种写法,有时候单引号,有时候双引号
webpack main.js bundle.js --module-bind "css=style-loader!css-loader"
1
2
3
4
5
6
7
8

entry

  • 构建项目的入口起点,可以是一个文件,也可以是一组文件
  • 如果传递一个形式为 <name> = <request> 的键值对,则可以创建一个额外的入口起点。它将被映射到配置选项(configuration option)的 entry 属性。

output

  • 要保存的 bundled 文件的路径和文件名。
  • 它将映射到配置选项 output.pathoutput.filename

--config

  • webpack配置文件默认为 webpack.config.js,如果你想使用其它配置文件,可以加入这个参数。
webpack --config xxx.config.js
1

--env

  • 设置环境变量
  • 当配置文件是一个函数时,会将环境变量传给这个函数

输出相关参数

  • --output-path:输出文件路径

  • --output-filename:输出文件名按照自己的格式输出

  • --output-source-map-filename:映射文件名,文件打包后与源文件有一个映射关系,可以通过map找到源文件

Debug配置

  • --debug: 打开debug模式,默认false
  • --progress: 打印编译的进度,默认false
  • --devtool: 定义source map类型
  • --display-error-details: 显示错误详情,默认false

Module 参数

  • --module-bind: 绑定一个拓展程序 如:--module-bind js=babel-loader

watch参数

  • --watch:监听文件变化

Optimize 参数

  • --optimize-max-chunks:设置代码块最大数量

  • --optimize-min-chunk-size:设置代码块最小值

  • --optimize-minimize:压缩js文件

Stats 参数

  • --display: 显示打包信息,将具体信息可参考这里

  • --color和**--colors**:console里面显示颜色

高级参数

  • --bail: 如果编译过程一有error,立马停止编译

  • --hot: 热替换,在config文件中也可配置

  • --provide:提供一些模块,做全局使用 如:--provide jQuery=jquery

  • --profile: 提供每一步编译的具体时间

webpack配置

配置说明

  • 这里的配置是小峰哥总结的配置,基本可以满足我们平常的项目需求
  • 因为小峰哥也是初学者,所以许多是么有配置的,希望大家谅解

webpack开发配置

webpack打包配置

vue-cli中webpack逐行解析

config说明

目录

  • dev.env.js
  • index.js
  • prod.env.js

prod.env.js

'use strict'
module.exports = {
  NODE_ENV: '"production"'
}
1
2
3
4
  • 指明了执行环境是production(生产环境)
  • development和production一定要加双引号,不然会报错!!!

dev.env.js

'use strict'
// 引入了webpack-merge这个模块,作用是来合并两个配置文件对象并生成一个新的配置文件
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"'
})
1
2
3
4
5
6
7
8
  • vue-cli中将一些通用的配置抽出来放在一个文件内,在对不同的环境配置不同的代码,最后使用webpack-merge来进行合并,减少重复代码
  • webpack遵循不重复原则(Don't repeat yourself - DRY),不会再不同的环境中配置相同的代码

TIP

development和production一定要加双引号,不然会报错!!!

index.js

// 严格模式
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

// 引入path
const path = require('path')

module.exports = {
  dev: {

    // Paths
    assetsSubDirectory: 'static',//静态资源文件夹,默认static
    assetsPublicPath: '/',//发布路径
    proxyTable: {},//我们常用来配置代理API的地方

    // Various Dev Server settings
    // 这里大家都知道,不多说
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,//自动打开浏览器
    errorOverlay: true,//查询错误
    notifyOnErrors: true,//通知错误
    // poll是跟devserver相关的一个配置,webpack为我们提供的devserver是可以监控文件改动的,但在有些情况下却不能工作,我们可以设置一个轮询(poll)来解决
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-

    // Use Eslint Loader?
    // If true, your code will be linted during bundling and
    // linting errors and warnings will be shown in the console.
    useEslint: true,//是否使用eslint
    // If true, eslint errors and warnings will also be shown in the error overlay
    // in the browser.
    showEslintErrorsInOverlay: false,//是否展示eslint的错误提示

    /**
     * Source Maps
     */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: 'cheap-module-eval-source-map',//提供的用来方便调试的配置,它有四种模式,具体看文档

    // If you have problems debugging vue-files in devtools,
    // set this to false - it *may* help
    // https://vue-loader.vuejs.org/en/options.html#cachebusting
    cacheBusting: true,//一个配合devtool的配置,当给文件名插入新的hash导致清楚缓存时是否生成souce maps,默认在开发环境下为true

    cssSourceMap: true//是否开启cssSourceMap
  },

  build: {
    // Template for index.html
    // 编译后index.html的路径
    // path.resolve(__dirname)指的是index.js所在的绝对路径
    // 再去找“../dist”这个路径
    index: path.resolve(__dirname, '../dist/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'),//打包后的文件根路径
    assetsSubDirectory: 'static',//静态资源文件夹,默认static
    assetsPublicPath: '/',//发布路径

    /**
     * Source Maps
     */

    productionSourceMap: true,//是否开启source-map
    // https://webpack.js.org/configuration/devtool/#production
    devtool: '#source-map',//提供的用来方便调试的配置,它有四种模式,具体看文档

    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,//是否压缩
    productionGzipExtensions: ['js', 'css'],//gzip模式下需要压缩的文件的扩展名,设置后会对相应扩展名的文件进行压缩

    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report//是否开启打包后的分析报告
  }
}

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

build说明

目录

  • Build.js
  • check-versions.js
  • utils.js
  • vue-loader.conf.js
  • webpack.base.conf.js
  • webpack.dev.conf.js
  • webpack.prod.conf.js

build.js

  • 这个配置文件是命令npm run build 的入口配置文件,主要用于生产环境
// 严格模式
'use strict'
// npm和node版本检查,要求有严格版本限制
require('./check-versions')()
// 设置环境变量为production
// process是node中的global全局对象的属性,env设置环境变量
process.env.NODE_ENV = 'production'

// ora是一个命令行转圈圈动画插件
const ora = require('ora')
// rimraf插件是用来执行UNIX命令rm和-rf的用来删除文件夹和文件,清空旧的文件
const rm = require('rimraf')
// node.js路径模块
const path = require('path')
// chalk插件,用来在命令行中输入不同颜色的文字
const chalk = require('chalk')
// 引入webpack模块使用内置插件和webpack方法
const webpack = require('webpack')
// commonJs风格,引入文件模块,引入模块分为内置模块与文件模块两种
// 引入config下的index.js配置文件
const config = require('../config')
// 引入下面是生产模式的webpack配置文件
const webpackConfig = require('./webpack.prod.conf')

// 开启转圈圈动画
const spinner = ora('building for production...')
spinner.start()

// 调用rm方法,第一个参数的结果就是 dist/static,表示删除这个路径下面的所有文件
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  
  // / 如果删除的过程中出现错误,就抛出这个错误,同时程序终止
  if (err) throw err
  
  // // 没有错误,就执行webpack编译
  webpack(webpackConfig, (err, stats) => {
    // 这个回调函数是webpack编译过程中执行
    spinner.stop()// 停止转圈圈动画
    if (err) throw err// 如果有错误就抛出错误
    // 没有错误就执行下面的代码,process.stdout.write和console.log类似,输出对象
    // stats对象中保存着编译过程中的各种消息
    process.stdout.write(stats.toString({
      colors: true,// 增加控制台颜色开关
      modules: false,// 不增加内置模块信息
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.// 不增加子级信息
      chunks: false,// 允许较少的输出
      chunkModules: false// 不将内置模块的信息加到包信息
    }) + '\n\n')

    // 以上就是在编译过程中,持续打印消息
    
    // 变异失败的信息
    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }
	// 下面是编译成功的消息
    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
  })
})

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

check-versions.js

  • 用来检测node和npm版本的
// 严格模式
'use strict'
// chalk插件,作用是在控制台中输出不同的颜色的字
const chalk = require('chalk')
// semver插件,用来对特定的版本号做判断的
const semver = require('semver')
// 导入package.json文件,要使用里面的engines选项,做版本判断
// 要注意require是直接可以导入json文件的,并且requrie返回的就是json对象
const packageConfig = require('../package.json')
// shelljs:用来执行Unix系统命令
const shell = require('shelljs')

function exec (cmd) {
  // 脚本可以通过 child_process 模块新建子进程,从而执行 Unix 系统命令
  // 下面这段代码实际就是把cmd这个参数传递的值转化成前后没有空格的字符串,也就是版本号
  // https://nodejs.org/api/child_process.html这是nodejs的子进程教程
  // require('child_process') node的模块
  // execSync(cmd)创建同步进程
  return require('child_process').execSync(cmd).toString().trim()
}

const versionRequirements = [
  {
    name: 'node',// node版本的信息
    // 使用semver插件吧版本信息转化成规定格式,也就是 ' =v1.2.3 ' -> '1.2.3' 这种功能
    currentVersion: semver.clean(process.version),
    // 这是规定的pakage.json中engines选项的node版本信息 "node":">= 6.0.0"
    versionRequirement: packageConfig.engines.node
  }
]
// windows: /*shell.which('npm')  返回:C:\PROGRAM FILES\NODEJS\NPM.CMD 返回绝对路径,否则返回null*/
if (shell.which('npm')) {
  versionRequirements.push({
    name: 'npm',
    // 自动调用npm --version命令,并且把参数返回给exec函数,从而获取纯净的版本号
    currentVersion: exec('npm --version'),
    // 这是规定的pakage.json中engines选项的node版本信息 "npm": ">= 3.0.0"
    versionRequirement: packageConfig.engines.npm
  })
}

module.exports = function () {
  const warnings = []

  for (let i = 0; i < versionRequirements.length; i++) {
    const mod = versionRequirements[i]
	// /上面这个判断就是如果版本号不符合package.json文件中指定的版本号,就执行下面的代码
    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
      warnings.push(mod.name + ': ' +
        chalk.red(mod.currentVersion) + ' should be ' +
        chalk.green(mod.versionRequirement)
        // 大致意思就是 把当前版本号用红色字体 符合要求的版本号用绿色字体 给用户提示具体合适的版本
      )
    }
  }

  // 提示用户更新版本,具体不解释了,应该能看懂
  if (warnings.length) {
    console.log('')
    console.log(chalk.yellow('To use this template, you must update following to modules:'))
    console.log()

    for (let i = 0; i < warnings.length; i++) {
      const warning = warnings[i]
      console.log('  ' + warning)
    }

    console.log()
    process.exit(1)
  }
}

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

utils

  • vue开发环境的wepack相关配置文件
  • 主要用来处理css-loader和vue-style-loader
// 严格模式
'use strict'
// 引入path模块
const path = require('path')
// 引入config下的index.js配置文件
const config = require('../config')
// extract-text-webpack-plugin插件,用来将css提取到单独的css文件中
const ExtractTextPlugin = require('extract-text-webpack-plugin')
// 引入package.json文件
const packageConfig = require('../package.json')
// 导出assetsPath
exports.assetsPath = function (_path) {
  // 如果是生产环境assetsSubDirectory就是'static',否则还是'static',因为他们默认都是static
  const assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory
 
  // path.join:返回的是完整的路径
  // path.posix.join:返回的是完整路径的相对根路径,这里是返回一个干净的相对根路径
  return path.posix.join(assetsSubDirectory, _path)
}

// 导出cssLoaders的相关配置
exports.cssLoaders = function (options) {
  // options如果没值就是空对象,常见写法
  options = options || {}

  // cssLoader的基本配置
  const cssLoader = {
    loader: 'css-loader',
    options: {
      // options是用来传递参数给loader的
      // 是否开启cssmap
      sourceMap: options.sourceMap
    }
  }
	
  // postcssLoader配置
  // postcss的配置信息需要单独在外面写配置文件才可以使用
  const postcssLoader = {
    loader: 'postcss-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders (loader, loaderOptions) {
    // 将上面的基础cssLoader和postcss-loader配置放在一个数组里面
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
	// 加载对应的loader
    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        // Object.assign是es6的方法,主要用来合并对象的,浅拷贝
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
      // 注意这个extract是自定义的属性,可以定义在options里面
      // 主要作用就是当配置为true就把文件单独提取,false表示不单独提取
      // 这个可以在使用的时候单独配置
      return ExtractTextPlugin.extract({
        use: loaders,
        fallback: 'vue-style-loader'
      })
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
    // 上面这段代码就是用来返回最终读取和导入loader,来处理对应类型的文件
  }

  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
  return {
    css: generateLoaders(),// css对应 vue-style-loader 和 css-loader
    postcss: generateLoaders(),// postcss对应 vue-style-loader 和 css-loader
    less: generateLoaders('less'),// less对应 vue-style-loader 和 less-loader
    sass: generateLoaders('sass', { indentedSyntax: true }),// sass对应 vue-style-loader 和 sass-loader
    scss: generateLoaders('sass'),// scss对应 vue-style-loader 和 sass-loader
    stylus: generateLoaders('stylus'),// stylus对应 vue-style-loader 和 stylus-loader
    styl: generateLoaders('stylus')// styl对应 vue-style-loader 和 styl-loader
  }
}

// Generate loaders for standalone style files (outside of .vue)
// 下面这个主要处理import这种方式导入的文件类型的打包,上面的exports.cssLoaders是为这一步服务的
exports.styleLoaders = function (options) {
  const output = []
  // 下面就是生成的各种css文件的loader对象
  const loaders = exports.cssLoaders(options)
  // 把每一种文件的laoder都提取出来
  for (const extension in loaders) {
    const loader = loaders[extension]
    // 把最终的结果都push到output数组中
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }

  return output
}

// 它是一个原生的操作系统上发送通知的nodeJS模块。用于返回脚手架错误的函数
exports.createNotifierCallback = () => {
  const notifier = require('node-notifier')

  return (severity, errors) => {
    if (severity !== 'error') return

    const error = errors[0]
    const filename = error.file && error.file.split('!').pop()

    notifier.notify({
      title: packageConfig.name,
      message: severity + ': ' + error.name,
      subtitle: filename || '',
      icon: path.join(__dirname, 'logo.png')
    })
  }
}

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

vue-loader.conf.js

  • 配置了css加载器以及编译css之后自动添加前缀
// 严格陌陌上
'use strict'
// 引入utils文件
const utils = require('./utils')
// 引入config下面的index.js文件
const config = require('../config')
// 指定生产环境是production
const isProduction = process.env.NODE_ENV === 'production'
// 指定sourceMap选项
const sourceMapEnabled = isProduction
  ? config.build.productionSourceMap
  : config.dev.cssSourceMap

// 处理项目中的css文件,生产环境和测试环境默认是打开sourceMap,而extract中的提取样式到单独文件只有在生产环境中才需要
module.exports = {
  // css加载器,调用utils配置文件中的cssLoaders方法,用来返回配置好的css-loader和vue-style-loader
  loaders: utils.cssLoaders({
    sourceMap: sourceMapEnabled,// // 这一句话表示如何生成map文
    extract: isProduction//这一项是自定义配置项,设置为true表示生成单独样式文件
  }),
  cssSourceMap: sourceMapEnabled,
  cacheBusting: config.dev.cacheBusting,
  // 在模版编译过程中,编译器可以将某些属性,如 src 路径,转换为require调用,以便目标资源可以由 webpack 处理
  transformToRequire: {
    video: ['src', 'poster'],
    source: 'src',
    img: 'src',
    image: 'xlink:href'
  }
}

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
28
29
30
31

webpack.base.conf.js

  • 开发和生产共同使用提出来的基础配置文件
  • 主要实现:
    • 配制入口
    • 配置输出环境
    • 配置模块resolve
    • 插件
// 严格模式
'use strict'
// 引入node的模块
const path = require('path')
// 引入utils工具模块
const utils = require('./utils')
// 引入config目录下的index.js配置文件,主要用来定义一些开发和生产环境的属性
const config = require('../config')
// vue-loader.conf配置文件是用来解决各种css文件的,定义了诸如css,less,sass之类的和样式有关的loader
const vueLoaderConfig = require('./vue-loader.conf')

function resolve (dir) {
  // 此函数是用来返回当前目录的平行目录的路径,因为有个'..'
  return path.join(__dirname, '..', dir)
}
// eslint语法检测
const createLintingRule = () => ({
  // 对.js和.vue文件在编译之前进行检测,检查有没有语法错误
  test: /\.(js|vue)$/,
  loader: 'eslint-loader',
  enforce: 'pre',// 此选项指定enforce: 'pre'选项可以确保,eslint插件能够在编译之前检测,如果不添加此项,就要把这个配置项放到末尾,确保第一个执行
  // include选项指明这些目录下的文件要被eslint-loader检测,还有一个exclude表示排除某些文件夹
  include: [resolve('src'), resolve('test')],
  // ptions表示传递给eslint-loader的参数
  options: {
    // formatter是参数的名称,eslint-friendly-formatter是eslint的一个报告总结插件,也就是说eslint的检测
    formatter: require('eslint-friendly-formatter'),
    // 报告非常难看懂,这个插件就是整理这些报告方便查阅的
    emitWarning: !config.dev.showEslintErrorsInOverlay
  }
})

module.exports = {
  // 入口文件是src目录下的main.js
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './src/main.js'
  },
  // 出口
  output: {
    // 径是config目录下的index.js中的build配置中的assetsRoot,也就是dist目录
    path: config.build.assetsRoot,
    // 文件名称这里使用默认的name也就是main
    filename: '[name].js',
    // 上线地址,也就是真正的文件引用路径,如果是production生产环境,其实这里都是 '/',一样的
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  },
  // resolve是webpack的内置选项,顾名思义,决定要做的事情,
  // 也就是说当使用 import "jquery",该如何去执行这件事情就是resolve配置项要做的,
  // import jQuery from "./additional/dist/js/jquery" 这样会很麻烦,可以起个别名简化操作
  resolve: {
    //  省略扩展名,也就是说.js,.vue,.json文件导入可以省略后缀名,这会覆盖默认的配置,所以要省略扩展名在这里一定要写上
    extensions: ['.js', '.vue', '.json'],
    alias: {
      // 后面的$符号指精确匹配,也就是说只能使用 import vuejs from "vue" 这样的方式导入vue.esm.js文件,不能在后面跟上 vue/vue.js
      'vue$': 'vue/dist/vue.esm.js',
      // resolve('src') 其实在这里就是项目根目录中的src目录,使用 import somejs from "@/some.js" 就可以导入指定文件,是不是很高大上
      '@': resolve('src'),
    }
  },
  
  // module用来解析不同的模块
  module: {
    rules: [
      ...(config.dev.useEslint ? [createLintingRule()] : []),
      {
        test: /\.vue$/,
        // 对vue文件使用vue-loader,该loader是vue单文件组件的实现核心,专门用来解析.vue文件的
        loader: 'vue-loader',
        // 将vueLoaderConfig当做参数传递给vue-loader,就可以解析文件中的css相关文件
        options: vueLoaderConfig
      },
      {
        test: /\.js$/,
        // 对js文件使用babel-loader转码,该插件是用来解析es6等代码
        loader: 'babel-loader',
        // 指明src和test目录下的js文件要使用该loader
        include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        // 对图片相关的文件使用 url-loader 插件,这个插件的作用是将一个足够小的文件生成一个64位的DataURL
        // 可能有些老铁还不知道 DataURL 是啥,当一个图片足够小,为了避免单独请求可以把图片的二进制代码变成64位的
        // DataURL,使用src加载,也就是把图片当成一串代码,避免请求?
        loader: 'url-loader',
        options: {
          // 限制 10000 个字节一下的图片才使用DataURL
          limit: 10000,
          // img 表示将图片打包的img文件夹中
          // [hash:7] 表示采用hash命名方式,并且名称长度为:7个字母
          // [ext] 表示保留图片原始的后缀名称
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        // 多媒体文件处理
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('media/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        // 字体文件处理
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
        }
      }
    ]
  },
  // 以下选项是Node.js全局变量或模块,这里主要是防止webpack注入一些Node.js的东西到vue中 
  node: {
    // prevent webpack from injecting useless setImmediate polyfill because Vue
    // source contains it (although only uses it if it's native).
    setImmediate: false,
    // prevent webpack from injecting mocks to Node native modules
    // that does not make sense for the client
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty'
  }
}

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

webpack.dev.conf.js

  • vue开发环境的wepack相关配置文件
// 严格模式
'use strict'
// 引入当前目录中的utils工具配置文件
const utils = require('./utils')
// 引入webpack来使用webpack内置插件
const webpack = require('webpack')
// 引入config目录中的index.js配置文件
const config = require('../config')
// 引入webpack-merge插件用来合并webpack配置对象,也就是说可以把webpack配置文件拆分成几个小的模块,然后合并
const merge = require('webpack-merge')
// 引入nodejs中的path模块
const path = require('path')
// 引入当前目录下的webpack.base.conf.js配置文件,主要配置的是打包各种文件类型的配置
const baseWebpackConfig = require('./webpack.base.conf')
// 在webpack中拷贝文件和文件夹的插件
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 用来生成html文件的插件
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 用来把webpack的错误和日志收集起来,漂亮的展示给用户
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// 查找一个未使用的端口
const portfinder = require('portfinder')
// 获取host环境变量,用于配置开发环境域名
const HOST = process.env.HOST
// 获取post环境变量,用于配置开发环境时候的端口号
const PORT = process.env.PORT && Number(process.env.PORT)

// 合并配置对象,将这个配置文件特有的配置添加替换到base配置文件中
const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    // 下面是把utils配置中的处理css类似文件的处理方法拿过来,并且不生成cssMap文件
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // cheap-module-eval-source-map is faster for development
  // devtool是开发工具选项,用来指定如何生成sourcemap文件,cheap-module-eval-source-map此款soucemap文件性价比最高,因为速度快
  devtool: config.dev.devtool,

  // these devServer options should be customized in /config/index.js
  // 下面是对webpack-dev-server选项的基本配置,这些配置信息,我们可以在/config/index.js文件中进行自定义配置。
  devServer: {
    // 用于配置在开发工具的控制台中显示的日志级别
    clientLogLevel: 'warning',
    // 表示当使用html5的history api的时候,任意的404响应都需要被替代为index.html
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    // 启用webpack的热替换特性
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    // 一切服务都需要使用gzip压缩
	// 可以在js,css等文件的response header中发现有Content-Encoding:gzip响应头
    compress: true,
    // 指定一个host,默认是localhost
    host: HOST || config.dev.host,
    // 指定要监听请求的端口号
    port: PORT || config.dev.port,
    // 是否自动打开浏览器
    open: config.dev.autoOpenBrowser,
    // 当编译出现错误的时候,是否希望在浏览器中展示一个全屏的蒙层来展示错误信息
    // 表示只显示错误信息而不显示警告信息
	// 如果两者都希望显示,则把这两项都设置为true
	// 设置为false则表示啥都不显示
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    // 指定webpack-dev-server的根目录,这个目录下的所有的文件都是能直接通过浏览器访问的
    // 推荐和output.publicPath设置为一致
    publicPath: config.dev.assetsPublicPath,
    // 配置代理,这样我们就可以跨域访问某些接口
	// 我们访问的接口,如果符合这个选项的配置,就会通过代理服务器转发我们的请求
    proxy: config.dev.proxyTable,
    // 启用 quiet 后,除了初始启动信息之外的任何内容都不会被打印到控制台。这也意味着来自 webpack 的错误或警告在控制台不可见。
    quiet: true, // necessary for FriendlyErrorsPlugin
    // 与监视文件相关的控制选项
    watchOptions: {
      // 如果这个选项为true,会以轮询的方式检查我们的文件的变动,效率不好
      poll: config.dev.poll,
    }
  },
  // 插件
  plugins: [
    // DefinePlugin内置webpack插件,专门用来定义全局变量的,
    // 创建一个在编译时可以配置的全局变量
    /*'process.env': {
                NODE_ENV: '"development"'
            } 这样的形式会被自动转为
            'process.env': '"development"' 
            各位骚年看好了,development如果不加双引号就当做变量处理,程序会报错
    */
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    // 启用热替换模块,修改模块时不需要刷新页面
    new webpack.HotModuleReplacementPlugin(),
    // 这个插件的主要作用就是在热加载的时候直接返回更新文件的名称,而不是文件的id
    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    // 使用这个插件可以在编译出错的时候来跳过输出阶段,这样可以确保输出资源不会包含错误
    // 就是当webpack编译错误的时候,来中断打包进程,防止错误代码打包到文件中,你还不知道
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    // 这个插件主要是生成一个html文件
    new HtmlWebpackPlugin({
      // 生成的html文件的名称
      filename: 'index.html',
      // 使用的模板的名称
      template: 'index.html',
      inject: true//设置为true表示把所有的js文件都放在body标签的屁股
    }),
    // copy custom static assets
    // 复制插件
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']//忽略.*的文件
      }
    ])
  ]
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  // 查找端口号,这种方式会返回一个promise
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      // 端口被占用时就重新设置evn和devServer的端口
      // // 把获取到的端口号设置为环境变量PORT的值
      process.env.PORT = port
      // add port to devServer config
      // // 重新设置webpack-dev-server的端口的值
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      //将FriendlyErrorsPlugin添加到webpack的配置文件中,友好地输出信息
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        // 编译成功时候的输出信息
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        // 当编译出错的时候,根据config.dev.notifyOnErrors来确定是否需要在桌面右上角显示错误通知框
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))
	
      // resolve我们的配置文件
      resolve(devWebpackConfig)
    }
  })
})

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156

webpack.prod.conf.js

  • webpack生产环境的核心配置文件
// 严格模式
'use strict'
// 引入path模块
const path = require('path')
// utils工具配置文件,主要用来处理css类文件的loader
const utils = require('./utils')
// 引入webpack,来使用webpack内置插件
const webpack = require('webpack')
// config目录下的index.js配置文件,主要用来定义了生产和开发环境的相关基础配置
const config = require('../config')
// webpack的merger插件,主要用来处理配置对象合并的,可以将一个大的配置对象拆分成几个小的,合并,相同的项将覆盖
const merge = require('webpack-merge')
// webpack.base.conf.js配置文件,用来处理不同类型文件的loader
const baseWebpackConfig = require('./webpack.base.conf')
// 用来复制文件或者文件夹到指定的目录
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 生成html文件,可以设置模板
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 用来将bundle中的css等文件产出单独的bundle文件的
const ExtractTextPlugin = require('extract-text-webpack-plugin')
// 压缩css代码的,还能去掉extract-text-webpack-plugin插件抽离文件产生的重复代码,因为同一个css可能在多个模块中出现所以会导致重复代码,换句话说这两个插件是两兄弟
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
// 压缩js代码的包
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

// 如果当前环境变量NODE_ENV的值是testing,则导入 test.env.js配置文,设置env为"testing"
// 如果当前环境变量NODE_ENV的值不是testing,则设置env为"production"
const env = require('../config/prod.env')

// 把当前的配置对象和基础的配置对象合并
const webpackConfig = merge(baseWebpackConfig, {
  module: {
    // 下面就是把utils配置好的处理各种css类型的配置拿过来,和dev设置一样,就是这里多了个extract: true,此项是自定义项,设置为true表示,生成独立的文件
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true,
      usePostCSS: true
    })
  },
  // devtool开发工具,用来生成个sourcemap方便调试
  // 按理说这里不用生成sourcemap多次一举,这里生成了source-map类型的map文件,只用于生产环境
  devtool: config.build.productionSourceMap ? config.build.devtool : false,
  output: {
    // 打包后的文件放在dist目录里面
    path: config.build.assetsRoot,
    // 文件名称使用 static/js/[name].[chunkhash].js, 其中name就是main,chunkhash就是模块的hash值,用于浏览器缓存的
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    // chunkFilename是非入口模块文件,也就是说filename文件中引用了chunckFilename
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    // 利用DefinePlugin插件,定义process.env环境变量为env
    new webpack.DefinePlugin({
      'process.env': env
    }),
    // UglifyJsPlugin插件是专门用来压缩js文件的
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          // 在删除未使用的变量等时,显示警告信息,默认就是false
          warnings: false
        }
      },
      // 压缩后生成map文件
      // 使用 source map 将错误信息的位置映射到模块(这会减慢编译的速度)
	  // 而且这里不能使用cheap-source-map
      sourceMap: config.build.productionSourceMap,
      // 使用多进程并行运行和文件缓存来提高构建速度
      parallel: true
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      // 生成独立的css文件,下面是生成独立css文件的名称
      // 提取之后css文件存放的地方 // 其中[name]和[contenthash]都是占位符 
      // [name]就是指模块的名称 	   
      // [contenthash]根据提取文件的内容生成的 hash
      filename: utils.assetsPath('css/[name].[contenthash].css'),
      // Setting the following option to `false` will not extract CSS from codesplit chunks.
      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 
      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
      // 从所有额外的 chunk(additional chunk) 提取css内容 
      // (默认情况下,它仅从初始chunk(initial chunk) 中提取) 
      // 当使用 CommonsChunkPlugin 并且在公共 chunk 中有提取的 chunk(来自ExtractTextPlugin.extract)时 
      // 这个选项需要设置为true

      allChunks: true,
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    // 使用这个插件压缩css,主要是因为,对于不同组件中相同的css可以剔除一部分
    new OptimizeCSSPlugin({
      // 这个选项的所有配置都会传递给cssProcessor 
      // cssProcessor使用这些选项决定压缩的行为
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { safe: true }
    }),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    // 生成html页面
    new HtmlWebpackPlugin({
      // 非测试环境生成index.html
      filename: config.build.index,
      // 模板是index.html加不加无所谓
      template: 'index.html',
      // 把script和link标签放在body底部
      inject: true,
      minify: {
        // 移除注释
        removeComments: true,
        // 移除空格和换行
        collapseWhitespace: true,
        // 尽可能移除属性中的引号和空属性
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      // 分类要插到html页面的模块
      chunksSortMode: 'dependency'
    }),
    
    // keep module.id stable when vendor modules does not change
    // 该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 建议用于生产环境
    new webpack.HashedModuleIdsPlugin(),
    // enable scope hoisting
    // webpack 打包时的一个取舍是将 bundle 中各个模块单独打包成闭包。这些打包函数使你的 JavaScript 在浏览器中处理的更慢。相比之下,一些工具像 Closure Compiler 和 RollupJS 可以提升(hoist)或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度
    new webpack.optimize.ModuleConcatenationPlugin(),
    // split vendor js into its own file
    // 将第三方的包分离出来
    // 通过将公共模块提取出来之后,最终合成的文件能够在最开始的时候加载一次 
    // 然后缓存起来供后续使用,这会带来速度上的提升。
    new webpack.optimize.CommonsChunkPlugin({
      // 这是 common chunk 的名称
      name: 'vendor',
      // 把所有从mnode_modules中引入的文件提取到vendor中
      minChunks (module) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    // 为了将项目中的第三方依赖代码抽离出来,官方文档上推荐使用这个插件,当我们在项目里实际使用之后, 
    // 发现一旦更改了 app.js 内的代码,vendor.js 的 hash 也会改变,那么下次上线时, 
    // 用户仍然需要重新下载 vendor.js 与 app.js——这样就失去了缓存的意义了。所以第二次new就是解决这个问题的
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    // This instance extracts shared chunks from code splitted chunks and bundles them
    // in a separate chunk, similar to the vendor chunk
    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),

    // copy custom static assets
    // 拷贝静态资源到build文件夹中
    // 复制,比如打包完之后需要把打包的文件复制到dist里面
    new CopyWebpackPlugin([
      {
        // 定义要拷贝的资源的源目录
        from: path.resolve(__dirname, '../static'),
        // 定义要拷贝的资源的目标目录
        to: config.build.assetsSubDirectory,
        // 忽略拷贝指定的文件,可以使用模糊匹配
        ignore: ['.*']
      }
    ])
  ]
})

if (config.build.productionGzip) {
  // 如果开启了生产环境的gzip
  const CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      // 目标资源的名称
	  // [path]会被替换成原资源路径
	  // [query]会被替换成原查询字符串
      asset: '[path].gz[query]',
      // gzip算法
	  // 这个选项可以配置成zlib模块中的各个算法
	  // 也可以是(buffer, cb) => cb(buffer)
      algorithm: 'gzip',
      // 处理所有匹配此正则表达式的资源
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      // 只处理比这个值大的资源
      threshold: 10240,
      // 只有压缩率比这个值小的资源才会被处理
      minRatio: 0.8
    })
  )
}

if (config.build.bundleAnalyzerReport) {
  // 如果需要生成一分bundle报告,则需要使用下面的这个插件
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = webpackConfig

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221

parcel

特点

  • 极速打包

    • Parcel 使用 worker 进程去启用多核编译。
    • webpack据说5x之后会支持多核编译
    • 同时有文件系统缓存,即使在重启构建后也能快速再编译
  • 将你所有的资源打包

    • Parcel 具备开箱即用的对 JS, CSS, HTML, 文件 及更多的支持,而且不需要插件
  • 自动转换

    • 如若有需要,Babel, PostCSS, 和PostHTML甚至 node_modules 包会被用于自动转换代码.
  • 零配置代码分拆

    • 使用动态 import() 语法, Parcel 将你的输出文件束(bundles)分拆,因此你只需要在初次加载时加载你所需要的代码。
  • 热模块替换

    • Parcel 无需配置,在开发环境的时候会自动在浏览器内随着你的代码更改而去更新模块
  • 友好的错误日志

    • 当遇到错误时,Parcel 会输出 语法高亮的代码片段,帮助你定位问题。
    • 类似webpack的devServer.overlay

安装

  • Yarn:
yarn global add parcel-bundler
1
  • npm:
npm install -g parcel-bundler
1

使用

  • Parcel 可以将任何类型的文件作为 入口点(entry point) ,但是推荐 HTML 或 JavaScript 文件做入口。
  • 如果你使用相对路径将你的主 JavaScript 文件链接到 HTML 中,Parcel 也会为你处理,并将该引用替换为输出文件的 URL 。
parcel index.html --open --p 1234
1
  • 文件下缓存cache文件加速
  • 默认端口1234
  • parcel使用vue的时候,不用做配置,非常简单

热模块替换

  • 模块热替换 (HMR) 在运行时自动更新浏览器中的模块优化开发体验,无需刷新整个页面。
  • 这意味着在您代码小幅更改时可以保留应用程序的状态。
  • Parcel 的 HMR 实现支持开箱即用的 JavaScript 和 CSS 资源。
  • 当在生产模式下打包时,HMR 自动被禁用。

热更新说明

  • 保存文件时,Parcel 将重新编译所更改的内容,并将包含新代码的更新发送到任何正在运行的客户端。
  • 新的代码会替换旧版本的代码,并与所有顶层资源一起被重新计算。
  • 你可以使用module.hot API 对此过程进行 hook ,编写的这段代码会在你处理模块时或有新版本进入时通知您。
  • 类似项目 react-hot-loader 可以帮助你完成该过程,并通过 Parcel 实现开箱即用。

热更新API

  • module.hot.accept
  • module.hot.dispose
if (module.hot) {
  module.hot.dispose(function () {
    // 模块即将被替换时
  });

  module.hot.accept(function () {
    // 模块或其依赖项之一刚刚更新时
  });
}
1
2
3
4
5
6
7
8
9

watch

  • parcel watch xxx

打包

  • parcel build index.html 即可
  • 不用package.json装文件,他会自己装

TIP

parcel内部集成很多东西,不用像webpack配置插件和loader,parcel都替我们自动安装和解决

也不用npm安装包,他会自动下载,很方便

rollup

安装

  • 需要nodejs+npm
npm install rollup --global
1
  • 运行目录 rollup
rollup src/main.js -o bundle.js -f cjs
1
  • -i, --input 要打包的文件(必须)
  • -o, --output.file 输出的文件 (如果没有这个参数,则直接输出到控制台)
  • -f, --output.format [es] 输出的文件类型 (amd, cjs, es, iife, umd)
  • -c 用 --config 或 -c 来使用配置文件

rollup.config.js

import babel from 'rollup-plugin-babel'
import resolve from 'rollup-plugin-node-resolve'

export default {
    entry : 'src/index.js', //入口
    format:'umd', //规范 兼容 script导入 amd commonjs
    plugins:[
        resolve(),
        babel({
            exclude:'node_modules/**'
        })
    ],
    dest:'build/bundle.js'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

rollup-plugin-node-resolve

npm install --save-dev rollup-plugin-node-resolve
1

rollup-plugin-babel

  • 使用 Babel 和 Rollup 的最简单方法是使用 [rollup-plugin-babel]
npm i -D rollup-plugin-babel
1

使用ES6

babel-core

  • babel的核心存在,babel的核心api都在这个模块里面

TIP

  • 如果用 webpack, 我们会看到npm install babel-loader babel-core --save--dev
  • 如果用gulp, 安装gulp-babel插件,其实都是安装的babel-core 这个核心。

babel-preset-env

  • 灵活决定加载哪些插件和polyfill,不过还是得开发者手动进行一些配置

babel-plugin-external-helpers

  • 把 helpers 收集到一个共享模块或共享文件。
  • helper 函数是 babel 在 transform 时候常用的工具函数,对编译模块时,会将用到的 helpers 放到模块顶部。
  • 如果编译多个文件,就会重复引用,导致每个模块都定义一份

babel-preset-latest

  • 就是把所有es2015, es2016, es2017 全部包含在一起了

babrlrc

  • 安装好的插件或预设, 就需要在.babelrc 配置文件中进行配置
  • 如果安装插件,就在 plugins 列出
  • 如是安装预设,就在 presets 列出
// .babelrc文件
{
  "presets": [], // 预设
  "plugins": []  // 插件
}
1
2
3
4
5
npm install --save-dev babel-preset-es2015

{
  "presets": [
      "es2015"
  ], // 预设
  "plugins": []  // 插件
}
1
2
3
4
5
6
7
8
babel-preset-stage-0
babel-preset-stage-1
babel-preset-stage-2
babel-preset-stage-3

没有stage4,因为它就是 es2015 预设

0-3是相互依赖的,但是目前只需要安装你想要的阶段就可以了
npm install --save-dev babel-preset-stage-2

// babelrc文件配置
{
	"presets": [
		"es2015",
		"stage-2"
	],
	"plugins": []
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

babel-polyfill

  • polyfill 即是在当前运行环境中用来复制(意指模拟性的复制,而不是拷贝)尚不存在的原生 api 的代码。
  • 能让你提前使用还不可用的 APIs,Array.from 就是一个例子。
npm install --save babel-polyfill
1

babel-runtime

  • 为了实现 ECMAScript 规范的细节,Babel 会使用“助手”方法来保持生成代码的整洁。
  • 由于这些助手方法可能会特别长并且会被添加到每一个文件的顶部,因此你可以把它们统一移动到一个单一的“运行时(runtime)”中去。
npm install --save-dev babel-plugin-transform-runtime
npm install --save babel-runtime

// babelrc 文件配置
{
    "plugins": [
     "transform-runtime"
    ]
}
1
2
3
4
5
6
7
8
9

Gulp

自动化构建工具

对于需要反复执行的任务,例如:压缩、编译、单元测试、代码ESlint检查等工作,可以通过自动化工具来减轻自己的劳动,简化自己的工作!

当配置文件定义好任务后,任务运行器就会帮你自动完成一系列的工作!

Gulp是基于流的自动化构建工具,采用管道流方式对大部分文件进行处理

安装

全局安装

npm install --global gulp

# macOS
sudo npm install --global gulp
1
2
3
4

作为依赖项安装(本地安装)

npm install --save-dev gulp
1

gulpfile.js

  • gulp核心配置文件,我们允许gulp命令后,会自动执行这个文件

第一步构建

'use strict'

const gulp = require('gulp');
gulp.task('task1',()=> {
    console.log('task1')
})

gulp.task('task2',()=> {
    console.log('task2')
})

gulp.task('default',() => {
    console.log('你好,Gulp!!!')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

第二步运行

# gulp 会执行默认任务default
gulp 
结果:你好,Gulp!!!

# 命令格式
gulp task1 task2
结果:
task1
task2
1
2
3
4
5
6
7
8
9

插件

插件安装

npm install 插件名 --save-dev
1

gulp.src(globs,options)

被执行任务的文件

  • globs
  • options
    • 一个可选的参数对象
    • 一般情况下我们用不到

gulp.dest(path,options)

执行任务完毕后,输出到的文件

  • path
    • 为写入文件的路径
  • options
    • 一个可选的参数对象
    • 通常我们不需要用到

Gulp工作流程

  • 通过gulp.src()方法获取到我们想要处理的文件流
  • 然后把文件流通过pipe方法导入到gulp的插件中
  • 最后把经过插件处理后的流再通过pipe方法导入到gulp.dest()中
  • gulp.dest()方法则把流中的内容写入到文件中

gulp.dest()说明

  • 我们给gulp.dest()传入的路径参数,只能用来指定要生成的文件的目录,而不能指定生成文件的文件名
  • 它生成文件的文件名使用的是导入到它的文件流自身的文件名,所以生成的文件名是由导入到它的文件流决定的
  • 即使我们给它传入一个带有文件名的路径参数,然后它也会把这个文件名当做是目录名
gulp.task('task1',()=> {
    gulp.src('./a.js')
    .pipe(gulp.dest('./b.js'))
    console.log('task1')
})
// 生成的文件是:./b.js/a/.js
// 这里的b.js是一个目录,而不是一个文件
1
2
3
4
5
6
7

TIP

最终生成的文件路径是:./b.js/a/.js,而不是:/b.js

压缩JS

gulp-uglify

npm install gulp-uglify --save-dev
1
const gulp = require('gulp')
const uglify = require('gulp-uglify')

gulp.task('default',function(){
    // 压缩js任务
    gulp.src('./js/a.js')
    .pipe(uglify())
    .pipe(gulp.dest('./dist/js'))
})

// 生成的文件在/dist/js/a.js
1
2
3
4
5
6
7
8
9
10
11

重命名

gulp-rename

npm install gulp-rename --save-dev
1
const gulp = require('gulp')
const uglify = require('gulp-uglify')
const rename = require('gulp-rename')
gulp.task('default',function(){
    // 指定入库
    gulp.src('./js/a.js')
    // 压缩js
    .pipe(uglify())
    // rename:方法一
    // 重命名:result.min.js
    // .pipe(rename('result.min.js'))

    // rename:方法二:
    .pipe(rename({
        "dirname":"", // 目录
        "basename":"result2", // 文件名
        "prefix":"", // 前缀
        "suffix":"", // 后缀
        "extname":".min.js", // 扩展名
    }))
    // 指定出口
    .pipe(gulp.dest('./dist/js'))
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

less编译

gulp-less

cnpm install gulp-less --save-dev
1
'use strict'

const gulp = require('gulp');

// 重命名
const rename = require('gulp-rename')
// less编译
const less = require('gulp-less')

// less
gulp.task('less',()=>{
    gulp.src('./css/*.less')
    .pipe(less())
    .pipe(rename({
        suffix:".min"
    }))
    .pipe(gulp.dest('dist/css'))
})
gulp.task('default',['less'],() => {
    console.log('你好,Gulp!!!')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

TIP

Gulp-less编译的less文件,其实是没有压缩和合并的,如果要添加压缩和合并,还需要另外的插件

压缩css

gulp-clean-css

cnpm install gulp-clean-css --save-dev
1
'use strict'

const gulp = require('gulp')

// 重命名
const rename = require('gulp-rename')
// less编译
const less = require('gulp-less')
// 压缩css
const minifyCss = require('gulp-clean-css')

// less
gulp.task('less',()=>{
    gulp.src('./css/*.less')
    .pipe(less())
    .pipe(rename({
        suffix:".min"
    }))
    .pipe(gulp.dest('dist/css'))
})

// 压缩css
gulp.task('minifyCss',() => {
    gulp.src('./dist/css/*.css')
    .pipe(minifyCss())
    .pipe(rename({
        suffix:'.min2'
    }))
    .pipe(gulp.dest('dist/css'))
})
// 合并写法
gulp.task('minifyCss2',()=>{
    gulp.src('./css/*.less')
    .pipe(less())
    .pipe(minifyCss())
    .pipe(rename({
        suffix:".min3"
    }))
    .pipe(gulp.dest('dist/css'))
})
// 先编译less为css,在压缩css,也可以合并
gulp.task('default',['less','minifyCss','minifyCss2'],() => {
    console.log('你好,Gulp!!!')
})
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

文件合并

gulp-concat

npm install gulp-concat --save-dev
1
'use strict'

const gulp = require('gulp')

// 重命名
// const rename = require('gulp-rename')
// less编译
const less = require('gulp-less')
// 压缩css
const minifyCss = require('gulp-clean-css')
// 合并文件
const concat = require('gulp-concat')

// 合并写法
gulp.task('minifyCss2',()=>{
    gulp.src('./css/*.less')
    .pipe(less())
    .pipe(minifyCss())
    .pipe(concat('result.css')) // 这里添加合并文件的名
    .pipe(gulp.dest('dist/css'))
})
gulp.task('default',['minifyCss2'],() => {
    console.log('你好,Gulp!!!')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

压缩image

gulp-imagemin

  • 可以用来压缩PNG, JPEG, GIF和SVG图片
cnpm install gulp-imagemin --save-dev
1
'use strict'

const gulp = require('gulp')

// 重命名
const rename = require('gulp-rename')

const imagemin = require('gulp-imagemin')
// 合并写法
gulp.task('imagemin',()=>{
    gulp.src('./image/img1.png')
    .pipe(imagemin())
    .pipe(rename({
        suffix:".min"
    }))
    .pipe(gulp.dest('dist/image'))
})
gulp.task('default',['imagemin'],() => {
    console.log('你好,Gulp!!!')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

压缩html

gulp-htmlmin

cnpm install gulp-htmlmin --save-dev
1
'use strict'

const gulp = require('gulp')

// 重命名
const rename = require('gulp-rename')
// 压缩html
const htmlmin = require('gulp-htmlmin')

// 合并写法
gulp.task('htmlmin',()=>{
    gulp.src('./index.html')
    .pipe(htmlmin({
        collapseWhitespace: true, // 压缩HTML,清除空格,这个最重要,可以节省大部分资源
        removeComments: true, // 清除注释
        collapseBooleanAttributes: true,//省略布尔属性的值 <input checked="true"/> ==> <input />
        removeEmptyAttributes: true,//删除所有空格作属性值 <input id="" /> ==> <input />
        removeScriptTypeAttributes: true,//删除<script>的type="text/javascript"
        removeStyleLinkTypeAttributes: true,//删除<style>和<link>的type="text/css"
        minifyJS: true,//压缩页面JS
        minifyCSS: true//压缩页面CSS
    }))
    .pipe(rename({
        basename:"index"
    }))
    .pipe(gulp.dest('dist'))
})
gulp.task('default',['htmlmin'],() => {
    console.log('你好,Gulp!!!')
})
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
28
29
30

watch

  • gulp.watch用于监听文件变化,以触发任务

  • 通常把gulp.watch自身也写成一个任务

  • Gulp内置的,不需要安装插件

  • watch是不可以监听新增加文件的变化的

  • watch的2种写法

    • 常规任务列表式写法

    • watch()回调函数式写法,放弃任务列表,直接写回调函数

// 常规任务列表式写法
'use strict'
// gulp
const gulp = require('gulp')
// 编译less
const less = require('gulp-less')
// 重命名
const rename = require('gulp-rename')
// 文件合并
const concat = require('gulp-concat')
gulp.task('less',()=> {
    gulp.src('./css/*.less')
    .pipe(less())
    .pipe(concat('result.css'))
    .pipe(rename('index.css'))
    .pipe(gulp.dest('dist/css'))
})

gulp.task('default',() => {
    // gulp.watch(参数1,参数2)
    // 参数1:监听哪些文件或者文件夹发生了变化
    // 参数2:调用哪个任务,数组
    // 注意,任务列表是个数组,即使只有一个元素
    gulp.watch('./css/*.less',['less'])
})

// 常规的另一种写法
gulp.task('default',() => {
    
    let watch = gulp.watch('./css/*.less',['less'])
    
    // 回调函数会被传入一个名为 event 的对象,这个对象描述了所监控到的变动
    // event.type  类型: String  发生的变动的类型:added, changed 或者 deleted。
    // event.path  类型: String  触发了该事件的文件的路径
    
    watch.on('change',function(event){
        console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');

        if (event.type === 'added') {
            // ...
        } else if (event.type === 'deleted') {
            // ...
        }
    })
    
})
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// watch()回调函数式写法
'use strict'
// gulp
const gulp = require('gulp')
// 编译less
const less = require('gulp-less')
// 重命名
const rename = require('gulp-rename')
// 文件合并
const concat = require('gulp-concat')

gulp.task('default',() => {
    
    gulp.watch('./css/*.less',function(event){
        if (event.type === 'added') {
            // ...
        } else if (event.type === 'deleted') {
            // ...
        }
        gulp.src('./css/*.less') // 引入资源文件
            .pipe(less())
            .pipe(concat('result.css'))
            .pipe(rename('index.css'))
            .pipe(gulp.dest('dist/css'))
    })
})
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