javascript模块化发展历程

1,833 阅读16分钟
  • 什么是模块化 ?
  • 为什么要做Javascript模块化?
  • JavaScript 模块化发展历程

什么是模块化 ?

模块化是一种处理复杂系统分解成为更好的可管理模块的方式,它可以把系统代码划分为一系列职责单一,高度解耦且可替换的模块,系统中某一部分的变化将如何影响其它部分就会变得显而易见,系统的可维护性更加简单易得。

一个模块就是实现特定功能的文件, 逻辑上相关的代码组织到同一个包内,包内是一个相对独立的王国,不用担心命名冲突什么的,那么外部使用的话直接引入对应的package即可.

就好像作家会把他的书分章节和段落;程序员会把他的代码分成模块。

就好像书籍的一章,模块仅仅是一坨代码而已。

好的代码模块分割的内容一定是很合理的,便于你增加减少或者修改功能,同时又不会影响整个系统。

为什么要做Javascript模块化?

早期前端只是为了实现简单的页面交互逻辑,随着Ajax技术的广泛应用,前端库的层出不穷,前端代码日益膨胀,JavaScript却没有为组织代码提供任何明显帮助,甚至没有类的概念,更不用说模块(module)了,这时候JavaScript极其简单的代码组织规范不足以驾驭如此庞大规模的代码.

模块化可以使你的代码低耦合,功能模块直接不相互影响。

  1. 可维护性:根据定义,每个模块都是独立的。良好设计的模块会尽量与外部的代码撇清关系,以便于独立对其进行改进和维护。维护一个独立的模块比起一团凌乱的代码来说要轻松很多。

  2. 命名空间:在JavaScript中,最高级别的函数外定义的变量都是全局变量(这意味着所有人都可以访问到它们)。也正因如此,当一些无关的代码碰巧使用到同名变量的时候,我们就会遇到“命名空间污染”的问题。

  3. 可复用性:现实来讲,在日常工作中我们经常会复制自己之前写过的代码到新项目中, 有了模块, 想复用的时候直接引用进来就行。

JavaScript 模块化发展历程

前端的先驱在刀耕火种的阶段开始,做了很多努力,在现有的运行环境中,实现"模块"的效果。

函数封装

模块就是实现特定功能的一组方法。在JavaScript中,函数是创建作用域的唯一方式, 所以把函数作为模块化的第一步是很自然的事情.

function foo(){
    //...
}

function bar(){
    //...
}

上面的,组成一个模块。使用的时候,直接调用就行了。

这种做法的缺点很明显:全局变量被污染,很容易命名冲突, 而且模块成员之间看不出直接关系。

对象写法

为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。

var MYAPP = {
    count: 0,
    foo: function(){},
    bar: function(){}
}

MYAPP.foo();

上面的代码中,函数foobar, 都封装在MYAPP对象里。使用的时候,就是调用这个对象的属性。 但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写.

立即执行函数(IIFE)写法

使用立即执行函数(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模块化需要解决那些问题:

  1. 如何安全的包装一个模块的代码?(不污染模块外的任何代码)
  2. 如何唯一标识一个模块?
  3. 如何优雅的把模块的API暴漏出去?(不能增加全局变量)
  4. 如何方便的使用所依赖的模块?

围绕着这些问题,js模块化开始了一段艰苦而曲折的征途。

JavaScript 模块规范

上述的所有解决方案都有一个共同点:使用单个全局变量来把所有的代码包含在一个函数内,由此来创建私有的命名空间和闭包作用域。

你必须清楚地了解引入依赖文件的正确顺序。就拿Backbone.js来举个例子,想要使用Backbone就必须在你的页面里引入Backbone的源文件。

然而Backbone又依赖 Underscore.js,所以Backbone的引入必须在其之后。

而在工作中,这些依赖管理经常会成为让人头疼的问题。

另外一点,这些方法也有可能引起命名空间冲突。举个例子,要是你碰巧写了俩重名的模块怎么办?或者你同时需要一个模块的两个版本时该怎么办?

还有就是协同开发的时候, 大家编写模块的方式各不相同,你有你的写法,我有我的写法, 那就乱了套.

接下来介绍几种广受欢迎的解决方案

  • CommonJS
  • AMD
  • CMD
  • ES6模块

CommonJS

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。

这标志Javascript模块化编程正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

node.js的模块系统,就是参照CommonJS规范实现的。

CommonJS定义的模块分为:

  1. 定义模块: 根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为global对象的属性。

  2. 模块输出: 模块只有一个出口,module.exports对象,我们需要把模块希望输出的内容放入该对象。module对象就代表模块本身。

  3. 加载模块: 加载模块使用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

AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

// main.js
  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // some code here
  });
  1. 用全局函数define来定义模块,用法为:define(id?, dependencies?, factory);
  2. id为模块标识,遵从CommonJS Module Identifiers规范
  3. dependencies为依赖的模块数组,在factory中需传入形参与之一一对应
  4. 如果dependencies的值中有"require"、"exports""module",则与commonjs中的实现保持一致
  5. 如果dependencies省略不写,则默认为["require", "exports", "module"]factory中也会默认传入require,exports,module.
  6. 如果factory为函数,模块对外暴漏API的方法有三种:return任意类型的数据、exports.xxx=xxx、module.exports=xxx.
  7. 如果factory为对象,则该对象即为模块的返回值

大名鼎鼎的require.js就是AMD规范的实现.

require.js要求,每个模块是一个单独的js文件。这样的话,如果加载多个模块,就会发出多次HTTP请求,会影响网页的加载速度。因此,require.js提供了一个优化工具(Optimizerr.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

CMD 即Common Module Definition, CMD是sea.js的作者在推广sea.js时提出的一种规范.

CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

define(function(require, exports, module) {
    // 模块代码
    // 使用require获取依赖模块的接口
    // 使用exports或者module或者return来暴露该模块的对外接口
})
  1. 也是用全局的define函数定义模块, 无需罗列依赖数组,在factory函数中需传入形参require,exports,module.
  2. 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推崇依赖前置,在定义模块的时候就要声明其依赖的模块,
  • CMD推崇就近依赖,只有在用到某个模块的时候再去require,
  • AMD和CMD最大的区别是对依赖模块的执行时机处理不同

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

CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。

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

ES6模块

上述的这几种方法都不是JS原生支持的, 在ECMAScript 6 (ES6)中,引入了模块功能, ES6 的模块功能汲取了CommonJS 和 AMD 的优点,拥有简洁的语法并支持异步加载,并且还有其他诸多更好的支持。

简单来说,ES6 模块的设计思想就是:一个 JS 文件就代表一个 JS 模块。在模块中你可以使用 import 和 export 关键字来导入或导出模块中的东西。

ES6 模块主要具备以下几个基本特点:

  • 自动开启严格模式,即使你没有写 use strict
  • 每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域
  • 模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等
  • 每一个模块只加载一次,每一个 JS 只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取。

补充: Typescript 识别模块的模式

一般来讲,组织声明文件的方式取决于库是如何被使用的。 在JavaScript中一个库有很多使用方式,这就需要你书写声明文件去匹配它们.

通过库的使用方法及其源码来识别库的类型。

  1. 全局库

全局库是指能在全局命名空间下访问的,许多库都是简单的暴露出一个或多个全局变量。 比如jQuery.

当你查看全局库的源代码时,你通常会看到:

  • 顶级的var语句或function声明
  • 一个或多个赋值语句到window.someName
  1. 模块化库

一些库只能工作在模块加载器的环境下。 比如,像 express只能在Node.js 里工作所以必须使用CommonJSrequire函数加载。

模块库至少会包含下列具有代表性的条目之一:

  • 无条件的调用requiredefine
  • import * as a from 'b'; or export c;这样的声明
  • 赋值给exportsmodule.exports
  1. 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
  • Webpack
  • Browserify
  • ......

简单的说,Grunt / Gulp 和 browserify / webpack不是一回事。

  1. Gulp / Grunt Gulp / Grunt 是一种工具,能够优化前端工作流程。比如自动刷新页面、combo、压缩css、js、编译less等等。简单来说,就是使用Gulp/Grunt,然后配置你需要的插件,就可以把以前需要手工做的事情让它帮你做了。

  2. 说到 browserify / webpack ,那还要说到 seajs / requirejs 。这四个都是JS模块化的方案。其中seajs / require 是一种类型,browserify / webpack 是另一种类型。seajs / require : 是一种在线"编译" 模块的方案,相当于在页面上加载一个 CMD/AMD 解释器。这样浏览器就认识了 define、exports、module这些东西。也就实现了模块化。

  3. browserify / webpack : 是一个预编译模块的方案,相比于上面 ,这个方案更加智能, 首先,它是预编译的,不需要在浏览器中加载解释器。另外,你在本地直接写JS,不管是 AMD / CMD / ES6风格的模块化,它都能认识,并且编译成浏览器认识的JS。这样就知道,Gulp是一个工具,而webpack等等是模块化方案。Gulp也可以配置seajs、requirejs甚至webpack的插件。

Grunt

每次运行grunt 时,他就利用node提供的require()系统查找本地安装的 Grunt

如果找到一份本地安装的 Gruntgrunt-CLI就将其加载,并传递Gruntfile中的配置信息,然后执行你所指定的任务。

  • 安装grunt-cli
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

gulp是基于Nodejs的自动化任务运行器,它能自动化地完成javascript/sass/less/html/image/css 等文件的的测试、检查、合并、压缩、格式化、浏览器自动刷新、部署文件生成,并监听文件在改动后重复指定的这些步骤。

使用Gulp的优势就是利用流的方式进行文件的处理,使用管道(pipe)思想,前一级的输出,直接变成后一级的输入,通过管道将多个任务和操作连接起来,因此只有一次I/O的过程,流程更清晰,更纯粹。Gulp去除了中间文件,只将最后的输出写入磁盘,整个过程因此变得更快。

使用Gulp,可以避免浏览器缓存机制,性能优化(文件合并,减少http请求;文件压缩)以及效率提升(自动添加CSS3前缀;代码分析检查)

browserify

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

官网对webpack的定义是MODULE BUNDLER(模块打包器),他的目的就是把有依赖关系的各种文件打包成一系列的静态资源。 请看下图

Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:main.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。

webpack核心概念

1. 入口(entry):

webpack将创建所有应用程序的依赖关系图表(dependency graph)。

entry配置项告诉Webpack应用的根模块或起始点在哪里, 入口起点告诉 webpack 从哪里开始,并遵循着依赖关系图表知道要打包什么。可以将应用程序的入口起点认为是根上下文或 app 第一个启动文件。它的值可以是字符串、数组或对象.

//webpack.config.js
const config = {
 entry: {
   app: './src/app.js',
   vendors: './src/vendors.js'
 }
};

2. 出口(output)

将所有的资源(assets)合并在一起后,我们还需要告诉 webpack 在哪里打包我们的应用程序。output 选项控制 webpack 如何向硬盘写入编译文件。注意,即使可以存在多个入口起点,但只指定一个输出配置。

output: {
    path: helpers.root('dist/nonghe'),
    publicPath: '/',
    filename: 'js/[name].[chunkhash].bundle.js',
    chunkFilename: 'js/[name].[chunkhash].bundle.js'
 }

3. 加载器(loader)

在webpack的世界里, 一切皆模块, 通过 loader 的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、 AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 LESS 等。而且 webpack 只理解 JavaScript。

对比 Node.js 模块,webpack 模块能够以各种方式表达它们的依赖关系:

  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD define 和 require 语句
  • css/sass/less 文件中的 @import 语句。
  • 样式(url(...))或 HTML 文件(<img src=...>)中的图片链接

webpack compiler在碰到上面那些语句的时候, 通过与其相对应的loader将这些文件进行转换,而转换后的文件会被添加到依赖图表中。

module: {
        loaders: [{
            test: /\.scss$/,
            loaders: 'style!css!sass'
        }, {
            test: /\.(png|jpg|svg)$/,
            loader: 'url?limit=20480' //20k
        }]
    }}

4. 插件(plugin)

plugin 插件,用于扩展webpack的功能,在webpack构建生命周期的节点上加入扩展hookwebpack加入功能。

LoadersPlugins常常被弄混,但是他们其实是完全不同的东西,可以这么来说,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构建到输出结果经历了一系列过程,它们是:

  1. 解析webpack配置参数,合并从shell传入和webpack.config.js文件里配置的参数,生产最后的配置结果。
  2. 注册所有配置的插件,让插件监听webpack构建生命周期的事件节点,以做出对应的反应。
  3. 从配置的entry入口文件开始解析文件构建依赖图谱,找出每个文件所依赖的文件,递归下去。
  4. 在解析文件递归的过程中根据文件类型和loader配置找出合适的loader用来对文件进行转换。
  5. 递归完后得到每个文件的最终结果,根据entry配置生成代码块chunk
  6. 输出所有chunk到文件系统。

代码拆分(Code Splitting)

代码拆分是 webpack 中最引人注目的特性之一。你可以把代码分离到不同的 bundle 中,然后就可以去按需加载这些文件.

  1. 分离资源,实现缓存资源
  • 分离第三方库(vendor) CommonsChunkPlugin
  • 分离 CSS
  1. 传统的模块打包工具(module bundlers)最终将所有的模块编译生成一个庞大的bundle.js文件。因此Webpack使用许多特性来分割代码然后生成多个“bundle”文件,而且异步加载部分代码以实现按需加载
  • 使用 require.ensure() 按需分离代码

    require.ensure(dependencies: String[], callback: function(require), chunkName: String)
    

模块热替换(Hot Module Replacement)

模块热替换功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载页面。这使得你可以在独立模块变更后,无需刷新整个页面,就可以更新这些模块.

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.ioclient 代码,可以和 webpack-dev-server 进行 websocket 通讯。

其他配置选项

  • --hot 开启 Hot Module Replacement功能
  • --quiet 控制台中不输出打包的信息
  • --compress 开启gzip压缩
  • --progress 显示打包的进度