资讯专栏INFORMATION COLUMN

也谈 webpack 及其开发模式

huhud / 2561人阅读

摘要:比如通过安装实例一新建一个然后编辑加入打开浏览器,看到屏幕输出会给每个模块一个唯一的然后通过存取这些模块,这些模块都会被整合到里面。以上设置会输出一个的文件。

从模块化谈起

近年来,js开发涌现出了诸多模块化解决方案,例如以在浏览器环境之外(服务端)构建 JavaScript 生态系统为目标而产生的CommonJS规范,从CommonJS社区中独立出来的AMD规范(异步模块定义),还有国人制定的CMD规范等。随着遵循AMD规范的RequireJS的流行,AMD规范在前端界已被广泛认同。后来,随着npm社区的逐渐壮大,CommonJS也越来越受欢迎,于是产生了统一这两种规范的需求,即希望提供一个前后端跨平台的解决方案,也因此产生了UMD(通用模块定义)规范。

CommonJS定义的是模块的同步加载,主要用于Node端;而遵循AMD规范的RequireJS则是异步加载,适用于浏览器端。requirejs是一种在线”编译” 模块的方案,相当于在页面上加载一个AMD 解释器,以便于览器能够识别 define、exports、module,而这些东西就是用于模块化的关键。

1. CommonJS 同步式的require

Node端的模块加载遵循 CommonJS规范,该规范的核心思想是允许模块通过 require 方法来加载。

该规范首先加载所要依赖的其他模块,然后通过 exportsmodule.exports 来导出需要暴露的接口。但它的缺点也是显而易见的,即一个文件一个文件的加载很容易发生阻塞。

require("module");//find from node_modules
require("../file.js");
exports.something = function() {};
module.exports = something;

2. 使你的模块兼容AMD规范

//web.js
(function (root, factory) {
        //判断define是否存在
    if (typeof define === "function" && define.amd) {
        // 存在则使用AMD方式加载模块
        define(["b"], factory);
    } else {
        // 不存在则使用浏览器全局变量暴露模块 
        root.web = factory(root.b);
    }
}(this, function (b) {
    //use b in some fashion.

    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}));

定义一个叫web.js的模块,依赖于另一个叫b的模块。如果你不想支持浏览器全局路径,那么你可以移除root并传递this参数在函数顶部。

3. define.amd属性

为了清晰的标识全局函数(为浏览器加载script必须的)遵从AMD编程接口,任何全局函数应该有一个"amd"的属性,它的值为一个对象。

关于RequireJS的使用不在本文范围之内,因此不展开讲解,有兴趣的请移步我的另一篇文章:

详解JavaScript模块化开发:https://segmentfault.com/a/1190000000733959#articleHeader15

AMD与异步加载

因为CommonJS阻塞式的缺点,所以并不适合前端。于是有了AMD异步加载模块的规范。

Asynchronous Module Definition 规范其实只有一个主要接口 define(id?, dependencies?, factory),它要在声明模块的时候指定所有的依赖 dependencies,并且还要当做形参传到 factory 中,对于依赖的模块提前执行,依赖前置。

因为浏览器端的需求和同步require的问题,所以社区引进了异步模块加载的规范,即AMD规范。

define("module", ["dep1", "dep2"], function(d1, d2) {
  return someExportedValue;
});
require(["module", "../file"], function(module, file) { /* ... */ });

使你的模块兼容于UMD规范:

//UMD,兼容AMD和CommonJS规范
(function (root, factory) {
  if (typeof exports === "object") {
    // CommonJS
    module.exports = factory(require("b"));
  } else if (typeof define === "function" && define.amd) {
    // AMD
    define(["b"], function (b) {
      return (root.returnExportsGlobal = factory(b));
    });
  } else {
    // 浏览器全局变量,root即window
    root.returnExportsGlobal = factory(root.b);
  }
}(this, function (b) {
  // 你的实际模块
  return {};
}));

UMD规范实现的思路:

首先判断是否支持Node.js模块格式,即exports对象是否存在。

然后判断是否支持AMD格式(require是否存在),存在则使用AMD方式加载

若前两个都不存在,则将模块暴露到全局,Nodeglobal,浏览器即window。

例如,创建一个兼容UMD规范的jQuery插件:

// Uses CommonJS, AMD or browser globals to create a jQuery plugin.

(function (factory) {
    if (typeof define === "function" && define.amd) {
        // AMD. Register as an anonymous module.
        define(["jquery"], factory);
    } else if (typeof module === "object" && module.exports) {
        // Node/CommonJS
        module.exports = function( root, jQuery ) {
            if ( jQuery === undefined ) {
                // require("jQuery") returns a factory that requires window to
                // build a jQuery instance, we normalize how we use modules
                // that require this pattern but the window provided is a noop
                // if it"s defined (how jquery works)
                if ( typeof window !== "undefined" ) {
                    jQuery = require("jquery");
                }
                else {
                    jQuery = require("jquery")(root);
                }
            }
            factory(jQuery);
            return jQuery;
        };
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function ($) {
    $.fn.jqueryPlugin = function () { return true; };
CMD

Common Module Definition 规范和 AMD 很相似,尽量保持简单,并与 CommonJSNode.jsModules 规范保持了很大的兼容性。

define(function(require, exports, module) {
  var $ = require("jquery");
  var Spinning = require("./spinning");
  exports.doSomething = ...
  module.exports = ...
})

CMD规范地址:https://github.com/seajs/seajs/issues/242

ES6 module

ECMAScript6 內建的用法:

import "jquery";
export function doStuff() {}
module "localModule" {}
为什么只载入JavaScript文件?

为什么模块化系统只帮助开发者处理JavaScript?然而还有其他静态资源需要被处理,比如:

stylesheets
images
webfonts
html for templating
其他..
coffeescript ➞ javascript
less stylesheet ➞ css
jade ➞ html
i18n ➞ something
require("./style.css");
require("./style.less");
require("./template.jade");
require("./image.png");

因为上面这些动机,所以有了webpack

webpack

webpack是一款模块封装工具(module bundler,是打包工具,也是模块加载工具,各种资源都可以当成模块来处理),webpack 会将模块与其他相关联的模块,函数库,其他需要预编译的文件等整合,编译输出此模块的静态资源文件。

// webpack.config.js `like=>`  gulpfile.js/gruntfile.js

module.exports = {
    entry: "./entry.js",
    output: {
        path: __dirname,
        filename: "bundle.js"
    },
    //module 对象用于添加loaders
    module: {
        //一个用于加载loader的数组
        loaders: [
            { test: /.css$/, loader: "style!css" }
        ]
    }
}; 

module具有如下属性:

test: 需要满足的条件A condition that must be met

exclude: 不需要满足的条件A condition that must not be met

include: 需要满足的条件A condition that must be met

loader: !用于分隔loaders

loaders: 一个loaders数组An array of loaders as string

"include" 通常被用于匹配目录:

  include: [
    path.resolve(__dirname, "app/src"),
    path.resolve(__dirname, "app/test")
  ],

简单的说,webpack会把我们常用的 .less, .scss, .jade, .jsx 等等文件编译成纯 js + 图片(图片有时也可以被编译成 base64 格式的 dataUrl)。

webpack的优势和特点:

将依赖项分块,按需加载

尽可能减少初始化载入的时间

使每一个静态资源都能够作为组件使用

有能力整合其他第三方函数库为模块

高度可配置化

适合大型项目

webpack拥有更聪明的解析工具可以处理几乎所有的第三方函数库。甚至允许在相依性设定上使用表达式,例如: require("./templates/" + name + ".jade"),这几乎能处理大部分的模块化标准(CommonJS, AMD)。

webpack常用命令
webpack 最基本的启动webpack命令
webpack -w 提供watch方法,实时进行打包更新
webpack -p 对打包后的文件进行压缩
webpack -d 提供SourceMaps,方便调试
webpack --colors 输出结果带彩色,比如:会用红色显示耗时较长的步骤
webpack --profile 输出性能数据,可以看到每一步的耗时
webpack --display-modules 默认情况下 node_modules 下的模块会被隐藏,加上这个参数可以显示这些被隐藏的模块
在项目中使用webpack

首先在项目根目录新建一个package.json或者通过$ npm init指令来产生:

接着通过npm指令安装webpack

$ npm install webpack --save-dev

or => $ cnpm i webpack -g

单纯的编译指令

$ webpack  
配置对象内容

context:用来指明entry选项的基础目录(绝对路径)。默认值为process.cmd(),即webpack.config.js文件所在路径

对象entry

定义了打包后的入口文件,可以是数组(所有文件打包生成一个filename文件),对象或者字符串

{
    entry: {
        page1: "./page1",
        page2: ["./entry1", "./entry2"]
    },
    output: {
        // 在 output.filename 中使用 [name]或者[id],当使用多个entry points时
        filename: "[name].bundle.js",
        path: "dist/js/page",
        chunkFilename: "[id].bundle.js" //chunkFilename是非主入口的文件名
    }
}

该段代码最终会生成一个page1.bundle.jspage2.bundle.js,并存放到 ./dist/js/page 文件夹下

chunkFilename是非主入口的文件名,当按需异步加载模块的时候,这时生成的文件名是以chunkname配置的

output:该参数是个对象,定义了输出文件的位置及名字:

path: 打包文件存放的绝对路径

publicPath: 网站运行时的访问路径URL

filename:打包后的文件名

你可以使用= 的格式来替代 entry point 建立一个別名:

// webpack.config.js
module.exports = {
  output: {
    filename: "[name].bundle.js"
  }
}

接着执行如下命令:

$ webpack index=./entry.js
>> Output a file that is named index.bundle.js

对象output

表示欲输出的路径,其会被映射到设定档中的 output.path 以及 output.filename

output.filename:指定每一个在磁盘上输出的文件名,不允许指定绝对路径

output.path:输出绝对路径目录(必须)

output.publicPath:指定在浏览器端引用的文件公开URL地址

对象resolve

webpack在构建包的时候会按目录进行文件的查找,resolve属性中的extensions数组可用于配置程序可以自行补全哪些文件后缀。extensions 第一个是空字符串,对应不需要后缀的情况。比如,为了查找CoffeeScript文件,你的数组应当包含字符串".coffee"。使用extensions,在引入模块的时候就不需要写后缀,会自动补全

resolve: {
        extensions: ["", ".js", ".jsx",".es6","css","scss","png","jpg"]
    },

resolve.alias 定义别名

alias:{
       "react-dom":path.join(nodeModulesPath,"/dist/react-dom"),
        "redux": path.join(nodeModulesPath,"dist/redux")
    },

对象externals

当我们想在项目中require一些其他的类库或者API,而又不想让这些类库的源码被构建到运行时文件中,
这在实际开发中很有必要。此时我们就可以通过配置externals参数来解决这个问题:

 externals: {
     "jquery": "jQuery"
 }

这样我们就可以放心的在项目中使用这些API了:

var $ = require(“jquery”);

配置项详情:https://webpack.github.io/docs/configuration.html

css样式和图片的加载

你可以在你的js文件里引入css文件和图片,例如:

require("./bootstrap.css");
require("./style.less");
require("../../main.scss");

var img = document.createElement("img");
img.src = require("./myImg.png");

当你require了CSS(less或者其他)文件,webpack会在页面中插入一个内联的