What、Why、How?解读Webpack官方文档

17

What is Webpack?

Webpack具有Grunt、Gulp对于静态资源自动化构建的能力,但更重要的是,Webpack弥补了requireJS在模块化方面的缺陷,同时兼容AMD与CMD的模块加载规范,具有更强大的JS模块化的功能。

因此我理解的Webpack,就是一个更出色的前端自动化构建工具、模块化工具、资源管理工具。

webpack is a module bundler. webpack takes modules with dependencies and generates static assets representing those modules.

此处输入图片的描述


Why Webpack?

为什么选择Webpack,两点原因。
1、前端需要模块化:JS模块化不仅仅为了提高代码复用性,更是为了让资源文件更合理地进行缓存;

2、AMD与CMD规范日渐衰弱:原因?ES6带来了很强的模块化语法糖。虽然ES6的更多语法糖让JS可能失去了简单的优势,在一些技术社区还偶尔看到一些反ES6的文章,但感觉ES6仍然是未来发展的趋势;

module DBLayer {
  export function query(s) { ... }
  export function connection(..args) { ... }
}
import DBLayer.*;

module CanvasLib = require('http://../canvas.js');
import CanvasLib.{Triangle, rotate};

参考使用简单的JavaScript,我们为什么应该抵制ES6例子。

当然,ES6,我觉得还是未来的事情,尤其是在China地盘要全面普及支持ES6的高级浏览器,真的比证明你妈是你妈还要困难。

所以,我认为,AMD跟CMD慢慢过时的原因,是模块化前端项目的发布打包问题,requireJS跟seaJS都没有一个很好的解决方案。下面配置文件是,我曾经做过的一个backbone的项目以requireJS做模块化加载。项目初始阶段还好,当随着项目深入,模块切分得越细,最后发布上线的时候,页面对于JS的请求数量竟然多达20个以上。大量的HTTPRequest造成的后果,不用多想,大家都知道。

require.config({
    //baseUrl: "scripts/vendor",
    paths: {
        underscore: '../vendor/underscore.min',
        zepto: '../vendor/zepto.min',
        backbone: '../vendor/backbone.min',
        domReady: '../vendor/domReady',
        template: '../vendor/template',
        iscroll: '../vendor/iscroll/iscroll',
        common: '../common/common'
    },
    shim: {
        underscore: {
            exports: '_'
        },
        zepto: {
            exports: '$'
        },
        backbone: {
            deps: ['underscore', 'zepto'],
            exports: 'Backbone'
        }
    },
    waitSeconds: 0
});
require([
    'zepto',
    'underscore',
    'backbone',
    'domReady',
    'common',
    '../controller/homeCtrl',
    '../controller/fadeCtrl',
    '../controller/mockCtrl'
],
function ($, _, backbone, domReady, common, homeCtrl, fadeCtrl, mockCtrl) {...}

为了解决这个问题,引入的RequireJS的优化方案:r.js Optimizer。详情:前端优化:RequireJS Optimizer 的使用和配置方法

({
    name: "ptMain",
    optimize: "uglify",//uglify
    `out: "../build/ptMain-build.js",`
    removeCombined: true,
    paths: {
        underscore: '../vendor/underscore.min',
        zepto: '../vendor/zepto.min',
        backbone: '../vendor/backbone.min',
        domReady: '../vendor/domReady',
        iscroll: '../vendor/iscroll/iscroll.min'
    },
    shim: {
        underscore: {
            exports: '_'
        },
        zepto: {
            exports: '$'
        },
        backbone: {
            deps: ['underscore', 'zepto'],
            exports: 'Backbone'
        }
    }
});

r.js同样可以对各个js进行压缩混淆优化,并最终在out配置中合并成一个JS文件,然后在页面中调用。就是说,不管三七二十一,每个页面对应引用的JS,都会被打包成一个JS,但这样的话,一个站点中多个页面之间公用的JS模块就无法缓存起来了。

说这么多,其实就是说,Webpack把以上两个问题解决了。

模块化

所有资源都是模块

大家可以回头看下Webpack官方实例图,有一点不知道大家是否注意到:Webpack处理后,输出的静态文件只剩下js与png,而css、less、jade其他的文件都合并到了js中。在Webpack当中,所有资源的都是模块,模块都需要通过AMD或者CMD规范加载,就像css样式文件,不再在HTML中以<link>标签加载。

clipboard.png

clipboard.png

content.js

module.exports = "It works from content.js.";

entry.js

//样式文件同样以模块方式引入
require("!style!css!./style.css");
//以CMD引入content.js
var content = require("./content.js");

function a() {
    document.write(content);
};
a();

style.css

body {
    background-color: yellow;
}

webpack.config.js

module.exports = {
    entry: "./entry.js",
    output: {
        path: __dirname,
        //打包输出文件
        filename: "bundle.js"
    },
    module: {
        //loaders引入加载器
        loaders: [
            { test: /\.css$/, loader: "style!css" }
        ]
    }
};

bundle.js

/***/ function(module, exports, __webpack_require__) {

    exports = module.exports = __webpack_require__(3)();
    // imports


    // module
    exports.push([module.id, "body {\r\n    background-color: yellow;\r\n}\r\n", ""]);

    // exports


/***/ },

打包好的bundle,包含了样式表在内的静态资源,而index页面下载bundle后,会将样式还原到DOM当中。如下图。

index.html

<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <script type="text/javascript" src="bundle.js" charset="utf-8"></script>
</body>
</html>

图片描述


代码切分

代码切分——抽取多个页面公用模块,打包成commonjs,便于缓存;

两大重要概念:切分点(split point)与代码块(Chunk)

AMD and CommonJs specify different methods to load code on demand.Both are supported and act as split points

AMD与CMD定义引用模块的入口就是切分点

All dependencies at a split point go into a new chunk

切分点定义中依赖的所有模块,合起来就是一个代码块。说白了就是,一个页面引用一个代码块

示例:Github common example

组织结构:build为输出结果目录
clipboard.png

逻辑结构
clipboard.png

配置代码

var path = require("path");
var CommonsChunkPlugin = require("../../node_modules/webpack/lib/optimize/CommonsChunkPlugin");

module.exports = {
    entry: {
        m1: './m1.js',
        m2: './m2.js'
    },
    output: {
        path: "build",
        filename: '[name].bundle.js'
    },
    plugins: [
        new CommonsChunkPlugin('common.js')
    ]
};

兼容AMD与CMD

CMD允许异步加载,写法:

require.ensure(["module-a", "module-b"], function(require) {
    var a = require("module-a");
    // ...
});

Note: require.ensure only loads the modules, it doesn’t evaluate them.
注意:只下载,不执行

AMD写法,与requireJS一致:

require(["module-a", "module-b"], function(a, b) {
    // ...
});

Note: AMD require loads and evaluate the modules. In webpack modules are evaluated left to right.
注意:与CMD不一样,AMD会下载并执行,执行顺序从左到右

Note: It’s allowed to omit the callback.
注意:并且允许省略回调

无论是AMD与CMD,文件组织方式与模块之间的逻辑都是一样的

AMD示例:Github AMD example

CMD示例:Github CMD example

clipboard.png

clipboard.png


丑化

webpack提供插件UglifyJsPlugin,可以优化(支持压缩、混淆)代码。插件引用方法详细,请参照。
其中混淆配置是值得注意的,由于AMD中的引用变量或方法名称混淆容易造成错误,因此混淆配置可以控制配置变量不被混淆。

A specific configuration is about mangling variable names. By default the mangle option is false. But you can say to the plugin avoid mangling a variable name passing a except list:

配置以下列表,在混淆代码时,以下配置的变量,不会被混淆

new webpack.optimize.UglifyJsPlugin({
    mangle: {
        except: ['$super', '$', 'exports', 'require']
    }
})

以上变量‘$super’, ‘$’, ‘exports’ or ‘require’,不会被混淆

Example:Github uglify example


var UglifyJsPlugin = require("../../node_modules/webpack/lib/optimize/UglifyJsPlugin");

module.exports = {
    entry: "./entry.js",
    output: {
        path: __dirname,
        filename: "bundle.js",
    },
    plugins: [
        //使用丑化js插件
        new UglifyJsPlugin({
            compress: {
                warnings: false
            },
            mangle: {
                except: ['$scope', '$']
            }
        })
    ]
};

entry.js

define("entry", function () {
    //变量 iabcdef 已引用,混淆
    var iabcdef = 11;
    //变量 $scope 已引用,但不混淆
    var $scope = "scope";

    document.write("entry module" + iabcdef);
    document.write($scope);

    //变量 ixzy 未被引用,剔除
    var ixzy = 3241;
});

版本控制

对于静态资源的版本控制,目前微信项目采取办法是版本号作为请求参数,版本号为发布日期,但有两个问题:
1、更新版本时,CDN不能及时更新;
2、没有发生变更的文件也被赋上新版本

Webpack的做法是,生成hash,区分文件。

Compute a hash of all chunks and add it.
生成所有代码块的hash

配置方法

//所有代码块添加hash
module.exports = {
    entry: "./entry.js",
    output: {
        path: "assets/[hash]/",
        publicPath: "assets/[hash]/",
        filename: "bundle.js"
    }
};

生成结果

clipboard.png

Compute a hash per chunk and add it.
生成单个代码块文件的hash

配置方法

//单个代码块添加hash
module.exports = {
    entry: "./entry.js",
    output: {
        path: "build/",
        publicPath: "build/",
        chunkFilename: "[id].[hash].bundle.js",
        filename: "output.[hash].bundle.js",
    }
};

How to use?

安装

全局安装,在任意目录,输入以下命令

$ npm install webpack -g

仅在项目在中安装,切换到项目根目录,输入以下命令

$ npm install webpack --save-dev

检查安装成功后,显示如下。

$ webpack -v

图片描述

加载插件(Plugin)

引用项目根目录node_modules

var path = require("path");

var CommonsChunkPlugin = require("../../node_modules/webpack/lib/optimize/CommonsChunkPlugin");

module.exports = {
    entry: {
        m1: './m1.js',
        m2: './m2.js'
    },
    output: {
        path: "build",
        filename: '[name].bundle.js'
    },
    plugins: [
        //引用插件
        new CommonsChunkPlugin('common.js')
    ]
};

加载加载器(Loaders)

通过加载器可以加载不同的资源文件进去各种操作,例如CoffeeScript及JSX……

安装加载器命令

$ npm install xxx-loader --save

$ npm install xxx-loader --save-dev

加载器应用方法有3种:

  1. explicit in the require statement (通过require语句,显示引用)

  2. configured via configuration (通过configuration来配置)

  3. configured via CLI (通过CLI配置)

引入方法如下:

require("./loader!./dir/file.txt");
// => uses the file "loader.js" in the current directory to transform//    "file.txt" in the folder "dir". //“!”是链接器,链接各个加载器及文件,./loader!表明loader有明确地址

require("jade!./template.jade");
// => uses the "jade-loader" (that is installed from npm to "node_modules")//    to transform the file "template.jade" //jade-laoder,可以简写为jade,并且jade加载器需要安装在“node_modules”

require("!style!css!less!bootstrap/less/bootstrap.less");
// => the file "bootstrap.less" in the folder "less" in the "bootstrap"//    module (that is installed from github to "node_modules") is//    transformed by the "less-loader". The result is transformed by the//    "css-loader" and then by the "style-loader".  //同时使用style,css,less三个加载器,并使用“!”作为链接,对应文件时bootstrap.less

配置方法如下:

{
    module: {
        loaders: [
            { test: /\.jade$/, loader: "jade" },
            // => "jade" loader is used for ".jade" files

            { test: /\.css$/, loader: "style!css" },
            // => "style" and "css" loader is used for ".css" files
            // Alternative syntax:
            { test: /\.css$/, loaders: ["style", "css"] },
        ]
    }
}

衡锋 · 2015年09月01日

Uglify虽然字面上是丑化的意思。但是这里如果翻译成丑化。那真是丑化了这个翻译啊

+2 回复

Tgor 作者 · 2015年09月01日

哈哈,翻译确实欠缺一点水平

回复

chshouyu · 2015年09月15日

“丑化”一词用的确实不太恰当

回复

learncpp · 2015年11月30日

谁告诉你CommonJS就是CMD呢??

回复

blade254353074 · 2016年01月18日

@Tgor

回复

苹果小萝卜 · 2016年02月07日

这2天使用webpack自身的uglify插件,好像就遇到了把所有变量都给丑化的问题,导致暴露给全局的变量没法被其他的模块引用了。那就得在配置文件里面将不需要被丑化的变量给配置好

回复

joebnb · 2016年03月08日

问一下版本控制中如何让html中的静态资源也同步替换成带有新hash的链接

回复

Tgor 作者 · 2016年03月19日

我之前遇到的经验是,Webpack搭配Gulp使用,使用gulp-rev、gulp-rev-collector做页面中引用地址的hash替换。具体的应用建议度娘一下。
`
var gulp = require('gulp');
var jshint = require('gulp-jshint'); //- 多个文件合并为一个;
var uglify = require('gulp-uglify');
var minifyCss = require('gulp-minify-css'); //- 压缩CSS为一行;
var rev = require('gulp-rev'); //- 对文件名加MD5后缀
var revCollector = require('gulp-rev-collector'); //- 路径替换

gulp.task('concat', function () { //- 创建一个名为 concat 的 task

gulp.src(['./js/j1.js'])                                     //- 需要处理的css文件,放到一个字符串数组里
    .pipe(jshint())
    .pipe(jshint.reporter('default'))
    .pipe(uglify())
    .pipe(rev())                                            //- 文件名加MD5后缀
    .pipe(gulp.dest('./js'))                               //- 输出文件本地
    .pipe(rev.manifest())                                   //- 生成一个rev-manifest.json
    .pipe(gulp.dest('./rev'));                              //- 将 rev-manifest.json 保存到 rev 目录内

});

gulp.task('rev', function () {

gulp.src(['./rev/*.json', './view/*.html'])   //- 读取 rev-manifest.json 文件以及需要进行css名替换的文件
    .pipe(revCollector())                                   //- 执行文件内css名的替换
    .pipe(gulp.dest('./view/'));                            //- 替换后的文件输出的目录

});

gulp.task('default', ['concat', 'rev']);
`

回复

载入中...