美文网首页Front End
webpack群侠传(十一):webpack 1.x

webpack群侠传(十一):webpack 1.x

作者: 何幻 | 来源:发表于2018-11-21 16:14 被阅读12次

    一些老项目可能还依赖了webpack 1.x,
    虽然我们对webpack 4.x有一些了解了,但是1.x的旧逻辑还是有些不同的。

    本文记录一下webpack 1.15.0的调试过程。

    1. webpack 1.15.0

    webpack 1.x最新的版本是 1.15.0
    为了进行调试,我们得先新建一个webpack 1.x项目,并指定babel的历史版本。

    1.1 新建项目

    $ mkdir debug-webpack
    $ cd debug-webpack
    $ npm init -f
    

    1.2 安装依赖

    $ npm i -D \
    webpack@1.15.0 \
    babel-loader@6.4.1 \
    babel-core@6.26.3 \
    babel-preset-env@1.7.0
    

    1.3 webpack.config.js

    const path = require('path');
    
    module.exports = {
        entry: {
            index: path.resolve(__dirname, 'src/index.js'),
        },
        output: {
            path: path.resolve(__dirname, 'dist/'),
            filename: '[name].js',
        },
        module: {
            loaders: [
                { test: /\.js$/, loader: 'babel-loader' },
            ]
        },
    };
    

    注意这里的loader配置和webpack 4.x不同。

    (1)webpack 4.x
    配置webpack.config.js,module.rules

    ...,
    module.exports = {
        ...,
        module: {
            rules: [
                { test: /\.js$/, use: { loader: 'babel-loader', query: { presets: ['@babel/preset-env'] } } },
            ]
        },
    };
    

    (2)webpack 1.x
    配置webpack.config.js,module.loaders

    ...
    module.exports = {
        ...,
        module: {
            loaders: [
                { test: /\.js$/, loader: 'babel-loader' },
            ]
        },
    };
    

    1.4 .babelrc

    webpack 1.x 除了配置webpack.config.js之外,还需要配置babel。
    在项目根目录中新建.babelrc文件。

    {
        "presets": [
            "env"
        ]
    }
    

    1.5 npm scripts

    打开pakcage.json,添加npm scripts,

    {
      ...,
      "scripts": {
        ...,
        "build": "webpack",
      },
      ...,
    }
    

    1.6 新建src/index.js文件

    alert();
    

    1.7 查看编译结果

    $ npm run build
    
    /******/ (function(modules) { // webpackBootstrap
    /******/    // The module cache
    /******/    var installedModules = {};
    
    /******/    // The require function
    /******/    function __webpack_require__(moduleId) {
    
    /******/        // Check if module is in cache
    /******/        if(installedModules[moduleId])
    /******/            return installedModules[moduleId].exports;
    
    /******/        // Create a new module (and put it into the cache)
    /******/        var module = installedModules[moduleId] = {
    /******/            exports: {},
    /******/            id: moduleId,
    /******/            loaded: false
    /******/        };
    
    /******/        // Execute the module function
    /******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
    /******/        // Flag the module as loaded
    /******/        module.loaded = true;
    
    /******/        // Return the exports of the module
    /******/        return module.exports;
    /******/    }
    
    
    /******/    // expose the modules object (__webpack_modules__)
    /******/    __webpack_require__.m = modules;
    
    /******/    // expose the module cache
    /******/    __webpack_require__.c = installedModules;
    
    /******/    // __webpack_public_path__
    /******/    __webpack_require__.p = "";
    
    /******/    // Load entry module and return exports
    /******/    return __webpack_require__(0);
    /******/ })
    /************************************************************************/
    /******/ ([
    /* 0 */
    /***/ (function(module, exports) {
    
        "use strict";
    
        alert();
    
    /***/ })
    /******/ ]);
    

    1.8 代码压缩

    webpack 1.x默认对目标代码是不压缩的,
    要想得到压缩后的代码,我们需要添加UglifyJsPlugin插件。

    ...,
    const webpack = require('webpack');
    
    module.exports = {
        ...,
        plugins: [
            new webpack.optimize.UglifyJsPlugin({
                compress: true,
            }),
        ],
    };
    
    $ npm run build
    
    !function(r){function t(o){if(e[o])return e[o].exports;var n=e[o]={exports:{},id:o,loaded:!1};return r[o].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t){"use strict";alert()}]);
    

    2. debug webpack

    2.1 新建debug.js文件

    首先我们要有能力对webpack进行调试,在项目根目录下我们新建一个debug.js文件,
    这个文件和webpack 4.x的写法一样。

    const webpack = require('webpack');
    const options = require('./webpack.config');
    
    const compiler = webpack(options);
    compiler.run(true, (...args) => { });
    

    然后在最后一行compiler.run位置打个断点,保持焦点在本文件中,按F5进行调试。

    2.2 Compiler & Compilation

    点击单步调试后,代码进入到了Compiler.js中,第166行

    Compiler.prototype.run = function (callback) {
      ...
    };
    

    这是ES5的写法。

    轻车熟路,我们来看看资源是怎么加载的,
    在webpack 4.x中,compiler通过make这个hooks调用了compilation.addEntry,这里也是如此。
    于是,我们打开Compilation.js找到addEntry方法,位于Compilation.js 第423行

    Compilation.prototype.addEntry = function process(context, entry, name, callback) {
      this._addModuleChain(context, entry, function (module) {
        ...
      }.bind(this), function (err, module) {
        ...
      }.bind(this));
    };
    

    同样是调了compilation._addModuleChain,位于Compilation.js 第336行
    后面业务逻辑和webpack 4.x都大体相似,我们就不详细说明了。

    在资源载入阶段,比较重要的两件事,载入loader,以及使用loader载入源码。
    载入loader的过程大同小异,下面我们直接看loader载入源码的过程。

    2.3 载入资源

    我们知道,compilation._addModuleChain会调用moduleFactory创建一个module,
    然后调用compilation.buildModule载入资源,位于Compilation.js 第398行

    Compilation.prototype._addModuleChain = function process(context, dependency, onModule, callback) {
        ...,
        moduleFactory.create(context, dependency, function(err, module) {
            ...,
            this.buildModule(module, function(err) {
                ...,
            }.bind(this));
            ...,
        }.bind(this));
    };
    

    然后compilation.buildModule又调用了,module.build方法,位于NormalModule.js 第81行

    NormalModule.prototype.build = function build(options, compilation, resolver, fs, callback) {
        ...,
        return this.doBuild(options, compilation, resolver, fs, function(err) {
            ...,
        }.bind(this));
    };
    

    然而doBuild的实现却不在webpack中,在weboack-core v0.6.9里,位于NormalModuleMixin.js 第49行

    NormalModuleMixin.prototype.doBuild = function doBuild(options, moduleContext, resolver, fs, callback) {
        ...,
        function runSyncOrAsync(fn, context, args, callback) {
            ...,
            try {
                var result = (function WEBPACK_CORE_LOADER_EXECUTION() { return fn.apply(context, args) }());
                ...,
            } catch(e) {
                ...,
            }
        }
    
        (function loadPitch() {
            ...,
            runSyncOrAsync(l.module.pitch, privateLoaderContext, [remaining.join("!"), pitchedLoaders.join("!"), l.data = {}], function(err) {
                ...,
            }.bind(this));
        }.call(this));
    
        function onLoadPitchDone() {
            ...,
            if(resourcePath) {
                ...,
                fs.readFile(resourcePath, function(err, buffer) {
                    ...,
                    nextLoader(null, buffer);
                });
            } else
                nextLoader(null, null);
        }
    
        function nextLoader(err/*, paramBuffer1, param2, ...*/) {
            ...,
            runSyncOrAsync(l.module, privateLoaderContext, args, function() {
                ...,
            });
        }
    };
    

    这个函数非常长,有269行,
    总共涉及以下3个重要函数,loadPitchonLoadPitchDonerunSyncOrAsync

    (1)loadPitch
    首先会执行loadPitch代码逻辑,它会通过require载入loader,

    (function loadPitch() {
      ...,
      if(typeof __webpack_modules__ === "undefined") {
        if(require.supportQuery) {
          ...,
        } else {
          try {
            l.module = require(l.path);
          } catch (e) {
            ...,
          }
        }
      } else if(typeof __webpack_require_loader__ === "function") {
        ...,
      } else {
        ...,
      }
      ...,
      if(typeof l.module.pitch !== "function") return loadPitch.call(this);
      ...,
      runSyncOrAsync(l.module.pitch, privateLoaderContext, [remaining.join("!"), pitchedLoaders.join("!"), l.data = {}], function(err) {
        ...,
      }.bind(this));
    }.call(this));
    

    其中,l.path的值为,

    ~/Test/debug-webpack/node_modules/_babel-loader@6.4.1@babel-loader/lib/index.js
    

    注意到,loadPitch其实是一个递归函数。
    载入loader之后,又递归调用了自己两次,最终调用了onLoadPitchDone

    (2)onLoadPitchDone
    这个是doBuild的内部函数,位于第244行
    它会读取源代码文件,然后调用nextLoader去载入它,位于第259行

    function onLoadPitchDone() {
      ...,
      if(resourcePath) {
        ...,
        fs.readFile(resourcePath, function(err, buffer) {
          ...,
          nextLoader(null, buffer);
        });
      } else
        ...,
    }
    

    其中,resourcePath的值为,

    ~/Test/debug-webpack/src/index.js
    

    这就是我们项目中的源文件。

    (3)runSyncOrAsync
    nextLoader也位于doBuild函数内部,位于第265行

    function nextLoader(err/*, paramBuffer1, param2, ...*/) {
      ...,
      runSyncOrAsync(l.module, privateLoaderContext, args, function() {
        loaderContext.inputValue = privateLoaderContext.value;
        nextLoader.apply(null, arguments);
      });
    }
    

    最终它调用了runSyncOrAsync,这个函数名在webpack 4.x中也遇到过,
    就是这个函数调用了loader,用loader载入源码,位于第155行

    function runSyncOrAsync(fn, context, args, callback) {
      ...,
      try {
        var result = (function WEBPACK_CORE_LOADER_EXECUTION() { return fn.apply(context, args) }());
        ...,
      } catch(e) {
        ...,
      }
    }
    

    在这里,调用loader的函数名叫做WEBPACK_CORE_LOADER_EXECUTION
    还记得么,在webpack 4.x中,这个函数名是LOADER_EXECUTION

    它会跳入到 babel-loader v6.4.1 index.js中,位于第99行

    module.exports = function(source, inputSourceMap) {
    

    这里我们就略过不表了。

    2.4 代码压缩

    在webpack 4.x中,代码压缩是使用uglifyjs-webpack-plugin来完成的,
    webpack首先在make阶段载入资源,之后调用了compilation.seal
    在optimize-chunk-asssets这个hooks中实现了代码压缩。

    其中,uglifyjs-webpack-plugin是webpack 4.x的一个内置插件,它实现了optimize-chunk-asssets,
    内部调用了uglify-es进行了代码压缩。

    而在webpack 1.x中,这件事略有不同。

    在webpack 1.x中,默认是不压缩目标代码的,除非我们手动配置UglifyJsPlugin插件,
    这在上文第1.8节我们已经介绍过了,

    ...,
    const webpack = require('webpack');
    
    module.exports = {
        ...,
        plugins: [
            new webpack.optimize.UglifyJsPlugin({
                compress: true,
            }),
        ],
    };
    

    UglifyJsPlugin也是一个webpack的内置插件,位于lib/optimize/UglifyJsPlugin.js中,
    我们看到在第33行,它实现了optimize-chunk-asssets,

    compilation.plugin("optimize-chunk-assets", function(chunks, callback) {
    

    它先在第82行调用uligfy-js v2.7.5的uglify.parse,将未压缩的代码转换成ast

    var ast = uglify.parse(input, {
        filename: file
    });
    

    注: uglify-jsuglify-es是不同的,是两个npm包。

    然后对ast进行操作,例如压缩和混淆,

    if(options.compress !== false) {
      ast.figure_out_scope();
      var compress = uglify.Compressor(options.compress); // eslint-disable-line new-cap
      ast = ast.transform(compress);
    }
    if(options.mangle !== false) {
      ast.figure_out_scope(options.mangle || {});
      ast.compute_char_frequency(options.mangle || {});
      ast.mangle_names(options.mangle || {});
      if(options.mangle && options.mangle.props) {
        uglify.mangle_properties(ast, options.mangle.props);
      }
    }
    

    还有生成source map相关的逻辑,
    JavaScript
    if(options.sourceMap !== false) {
    var map = uglify.SourceMap({ // eslint-disable-line new-cap
    file: file,
    root: ""
    });
    output.source_map = map; // eslint-disable-line camelcase
    }

    最终将压缩后的代码通过stream写入到compilation.assets变量,

    var stream = uglify.OutputStream(output); // eslint-disable-line new-cap
    ast.print(stream);
    ...,
    asset.__UglifyJsPlugin = compilation.assets[file] = (map ?
      new SourceMapSource(stream, file, JSON.parse(map), input, inputSourceMap) :
      new RawSource(stream));
    

    其中,file是,

    index.js
    

    compilation.assets['index.js']._value的值是,

    !function(r){function t(o){if(e[o])return e[o].exports;var n=e[o]={exports:{},id:o,loaded:!1};return r[o].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t){"use strict";alert()}]);
    

    就是压缩后的目标代码了。

    注: 如果webpack.config.js中不配置UglifyJsPlugin插件,这个插件就不会被加载了,
    optimize-chunk-asssets hooks触发的时候,就不会调用UglifyJsPlugin插件中的代码逻辑了。

    3. uglify-js

    上文中,我们看到webpack 1.x使用了uglify-js v2.7.5进行代码压缩,
    然而,uglify-js v2.7.5却不能用来压缩ES6。

    3.1 let引发的错误

    我们新建一个项目来试试,并指定版本安装uglify-js,

    $ mkdir debug-uglify-js
    $ cd debug-uglify-js
    $ npm init -f
    $ npm i -S uglify-js@2.7.5 
    

    新建 index.js,

    const uglify = require('uglify-js');
    
    const source = 'let a = 1;';
    const ast = uglify.parse(source, {
        filename: 'index.js',
    });
    

    最后执行它,就报错了,

    $ node index.js
    
    undefined:1555
        throw new JS_Parse_Error(message, filename, line, col, pos);
        ^
    Error
        at new JS_Parse_Error (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:1547:18)
        at js_error (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:1555:11)
        at croak (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2094:9)
        at token_error (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2102:9)
        at unexpected (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2108:9)
        at semicolon (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2128:56)
        at simple_statement (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2319:73)
        at eval (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2188:19)
        at eval (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2141:24)
        at eval (eval at <anonymous> (~/Test/debug-uglify-js/node_modules/_uglify-js@2.7.5@uglify-js/tools/node.js:28:1), <anonymous>:2909:23)
    

    这是因为let a = 1;,包含了let,它是ES6代码,uglify-js v2.7.5不能识别它。
    不过令人奇怪的是,const a = 1;,却可以被识别。

    3.2 babel-loader

    上文我们没有强调的一点是,babel-loader加载后的代码并不是源代码,而是转换过后的ES5代码。
    因此,上文我们配置了babel-loader之后,源代码甚至源代码依赖的node_modules模块,都会被转为ES5,
    再进行uglify-js v2.7.5进行压缩就不会报错了。

    如果我们把babel-loader删掉,uglify-js v2.7.5同样也会报错。
    这需要同时满足以下几个条件,

    (1)让babel-loader失效。(只需要删除.babelrc即可)
    (2)配置UglifyJsPlugin到webpack.config.js中,并启用compress,参考本文第1.8节。
    (3)源码或者它依赖的node_modules模块中包含ES6代码。

    例如,我们修改src/index.js文件内容为,

    let a = 1;
    

    然后执行一下构建,

    $ npm run build
    
    > debug-webpack@1.0.0 build ~/Test/debug-webpack
    > webpack
    
    Hash: 99cc00b93a9d989cb93e
    Version: webpack 1.15.0
    Time: 425ms
       Asset     Size  Chunks             Chunk Names
    index.js  1.49 kB       0  [emitted]  index
        + 1 hidden modules
    
    ERROR in index.js from UglifyJs
    SyntaxError: Unexpected token: name (a) [./src/index.js:1,4]
    

    3.3 exclude

    很多项目中为了提高编译速度,会配置babel-loader的exclude属性,

    const path = require('path');
    const webpack = require('webpack');
    
    module.exports = {
        entry: {
            index: path.resolve(__dirname, 'src/index.js'),
        },
        output: {
            path: path.resolve(__dirname, 'dist/'),
            filename: '[name].js',
        },
        module: {
            loaders: [
                { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
            ]
        },
        plugins: [
            new webpack.optimize.UglifyJsPlugin({
                compress: true,
            }),
        ],
    };
    

    以上webpack.config.js中,我们配置了babel-loader的exclude属性,
    对于node_moduels目录中的文件,就会直接加载,不通过loader转换。

    值得注意是的,如果node_modules中包含ES6代码,
    载入后再通过uglify-js v2.7.5压缩也会报错。


    参考

    webpack v1.15.0
    webpack-core v0.6.9
    uglify-js v2.7.5

    相关文章

      网友评论

        本文标题:webpack群侠传(十一):webpack 1.x

        本文链接:https://www.haomeiwen.com/subject/lrycqqtx.html