vue-webpack-TODO

内容参考
webpack是一个前端模块打包package工具,需要简单的(也不简单)配置,webpack就可以完成模块的加载和打包package。

webpack的配置
const path = require('path')
const config = {
  entry: './app/entry', // string | object | array
  // webpack 打包的入口
  output: { // 定义webpack如何输出的选项
    path: path.resolve(__dirname, 'dist'), // string
    // 所有输出文件的目标路径
    filename: '[chunkhash].js', // string
    // 入口(entry chunk)文件命名模版
    publicPath: '/assets/', // string
    // 构建文件的输出目录
  },
  module: { // 模块相关配置
    rules: [ // 配置模块loaders,解析规则
      {
        test: /\.jsx?$/, // RegExp | string
        include: [ //必须匹配选项
          path.resolve(__dirname, 'app')
        ],
        exclude: [ // 必不匹配选项(优先级高于test和include)
          path.resolve(__dirname, 'app/demo-files')
        ],
        loader: 'babel-loader', //模块解析上下文
        options: { // loader的可选项
          presets: ['es2015']
        }
      }
    ]
  },
  resolve: { // 解析模块的可选项
    modules: [ // 模块的查找目录
      'node_modules',
      path.resolve(__dirname, 'app')
    ],
    extensions: ['.js', '.json', '.jsx', '.css'], // 用到的文件的扩展
    alias: { // 模块别名列表
      'module': 'new-module'
    }
  },
  devtool: 'source-map', // enum
  // 为浏览器开发者工具添加元数据增强调试
  plugins: [
    // 附件插件列表
  ]
}

module.exports = config

由上可知,webpack配置中几个核心概念: entry、output、loaders、plugins、chunk。

entry:指定webpack开始构建的入口模块,从该模块开始构建并计算出直接或间接依赖的模块或库
output: 指定webpack如何命名输出的文件和目录
loaders:由于webpack只能处理JavaScript文件,所以需要对非js文件的处理模块
plugins: 打包package优化、压缩clamp、重新定义环境中的变量等能力
chunk: coding split的产物,可以对一些代码打包成单独的chunk,比如公共模块,去重、更好的利用缓存、按需加载功能模块,优化加载时间。webpack3 l利用commonChunkPlugin将公共代码分割成chunk,实现单独加载,webpack4废除此,使用SplitChunkPlugin

webpack详解

webpack是如何运行的呢?webpack是高度复杂抽象的插件集合。
tapable
webpack本质上是一种事件流的机制,其工作流程是将各个插件串联起来,实现这一核心的就是Tapable。webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例

  1. Tapable 1.0之前(webpack3以前):
    plugin(name: string, handler: Function) 注册事件到Tapable对象中
    *apply(pluginInstances: (AnyPlugin|Function)[])调用插件的定义,将事件监听器注册到Tapable实例注册表中
    applyPlugins(name:string)多种策略细致的控制事件的触发,包括applyPluginsAsync、applyPluginsParallel等方法实现对事件触发的控制,实现
  • 多个事件连续顺序执行
  • 并行执行
  • 异步执行
  • 一个接一个执行插件,前面的输出是后一个插件的输入(瀑布流执行顺序)
  • 在允许时停止raised_back_of_hand插件(如某个插件返回undefined,即退出执行)

Tapable就像nodejs的EventEmitter,提供对事件的注册on和触发emit。下例chestnut:

function CustomPlugin() {}
CustomPlugin.prototype.apply = function (compiler) {
    compiler.plugin('emit', pluginFunction)
}
// 在webpack生命周期内适时的执行
this.apply*('emit', options)

更多Tapable1.0知识—>Tapable和事件流

  1. Tapable1.0 版本发生巨大变化
    不再 通过plugin注册事件、通过applyPlugins*触发事件调用
    暴露出很多的钩子dog,可以使用它们为插件创造钩子函数
const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
} = require("tapable");   

如何使用?


class Order {
  constructor() {
    this.hooks = {
      // hooks
      goods: new SyncHook(['goodsId', 'number']),
      comsumer: new AsyncParallelHook(['userId','orderId'])
    }
  }

  queryGoods(goodsId, number) {
    this.hooks.goods.call(goodsId, number)
  }
  consumerInfoPromise(userId, orderId) {
    this.hooks.consumer.promise(UserId, orderId).then(() => {/*TODO*/})
  }
  consumerInfoAsync(userId, orderId) {
    this.hooks.consumer.callAsync(userId, orderId, (err, data) => {
      //TODO
    })
  }
}

// 对于所有的构造函数均接受一个可选的string类型的数组
const hook = new SyncHook(['arg1', 'arg2', 'arg3'])

// 调用tap方法注册一个consumer
order.hooks.goods.tap('QueryPlugin', (goodsId, number) => {
    return fetchGoods(goodsId, number);
})
// 再添加一个
order.hooks.goods.tap('LoggerPlugin', (goodsId, number) => {
    logger(goodsId, number);
})

// 调用
order.queryGoods('10000000', 1)

对于一个SyncHook,我们通过tap来添加消费者,通过call来触发钩子的执行孙婿。
对于u 一个非Sync类型的钩子,即async类型的钩子,还能以以下方式注册消费者和调用

// 注册一个sync 钩子

order.hooks.consumer.tap('LoggerPlugin', (userId, orderId) => {
   logger(userId, orderId);
})

order.hooks.consumer.tapAsync('LoginCheckPlugin', (userId, orderId, callback) => {
    LoginCheck(userId, callback);
})

order.hooks.consumer.tapPromise('PayPlugin', (userId, orderId) => {
    return Promise.resolve();
})

// 调用
// 返回Promise
order.consumerInfoPromise('user007', '1024');
//回调函数
order.consumerInfoAsync('user007', '1024')

以上大致说明Tapable用法:

  • 插件注册数量
  • 插件注册类型(sync,async,promise)
  • 调用的方式(sync,async,promise)
  • 实例钩子的时候参数数量
  • 是否使用了interception

Tapable详解
对于Sync*类型的钩子:
注册在该钩子下的插件的执行顺序都是顺序执行
只能使用tap注册, 不能使用tapPtomise和tapAsync注册

// 所有的钩子都继承于Hook
class Sync* extends Hook { 
    tapAsync() { // Sync*类型的钩子不支持tapAsync
        throw new Error("tapAsync is not supported on a Sync*");
    }
    tapPromise() {// Sync*类型的钩子不支持tapPromise
        throw new Error("tapPromise is not supported on a Sync*");
    }
    compile(options) { // 编译代码来按照一定的策略执行Plugin
        factory.setup(this, options);
        return factory.create(options);
    }
}

对于Async*类型钩子:

支持tap、tapPromise、tapAsync注册

class AsyncParallelHook extends Hook {
  constructor(args) {
      super(args);
      this.call = this._call = undefined;
  }

  compile(options) {
      factory.setup(this, options);
      return factory.create(options);
  }
}

class Hook { 
  constructor(args) {

    if(!Array.isArray(args)) args = [];
    this._args = args; // 实例钩子的时候的string类型的数组
    this.taps = []; // 消费者
    this.interceptors = []; // interceptors
    this.call = this._call =  // 以sync类型方式来调用钩子
    this._createCompileDelegate("call", "sync");
    this.promise = 
    this._promise = // 以promise方式
    this._createCompileDelegate("promise", "promise");
    this.callAsync = 
    this._callAsync = // 以async类型方式来调用
    this._createCompileDelegate("callAsync", "async");
    this._x = undefined; 
  }

  _createCall(type) {

    return this.compile({
        taps: this.taps,
        interceptors: this.interceptors,
        args: this._args,
        type: type
    });
  }

  _createCompileDelegate(name, type) {

    const lazyCompileHook = (...args) => {
        this[name] = this._createCall(type);
        return this[name](...args);
    };
    return lazyCompileHook;
  } 


  // 调用tap 类型注册 
  tap(options, fn) {
    options = Object.assign({ type: "sync", fn: fn }, options);
    this._insert(options);  // 添加到 this.taps中
  } 
  
  // 注册 async类型的钩子 
  tapAsync(options, fn) {
    options = Object.assign({ type: "async", fn: fn }, options);
    this._insert(options); // 添加到 this.taps中
  } 
  //注册 promise类型钩子 
  tapPromise(options, fn) {
    options = Object.assign({ type: "promise", fn: fn }, options);
    this._insert(options); // 添加到 this.taps中
  }

}

每次都是调用tap、tapSync、tapPromise注册不同类型的插件钩子,通过调用call、callAsync 、promise方式调用。其实调用的时候为了按照一定的执行策略执行,调用compile方法快速编译出一个方法来执行这些插件。


const factory = new Sync*CodeFactory();
class Sync* extends Hook { 
    compile(options) { // 编译代码来按照一定的策略执行Plugin
        factory.setup(this, options);
        return factory.create(options);
    }
}

class Sync*CodeFactory extends HookCodeFactory {
    content({ onError, onResult, onDone, rethrowIfPossible }) {
        return this.callTapsSeries({
            onError: (i, err) => onError(err),
            onDone,
            rethrowIfPossible
        });
    }
}

compile中调用HookCodeFactory#create方法编译生成执行代码。


class HookCodeFactory {
    constructor(config) {
        this.config = config;
        this.options = undefined;
    }

    create(options) {
        this.init(options);
        switch(this.options.type) {
            case "sync":  // 编译生成sync, 结果直接返回
                return new Function(this.args(), 
                "\"use strict\";\n" + this.header() + this.content({
                    onResult: result => `return ${result};
                }));
            case "async": // async类型, 异步执行,最后将调用插件执行结果来调用callback,
                return new Function(this.args({
                    after: "_callback"
                }), "\"use strict\";\n" + this.header() + this.content({
                    onResult: result => `_callback(null, ${result});\n`,
                    onDone: () => "_callback();\n"
                }));
            case "promise": // 返回promise类型,将结果放在resolve中
                code += "return new Promise((_resolve, _reject) => {\n";
                code += "var_sync = true;\n";
                code += this.header();
                code += this.content({
                    onResult: result => `_resolve(${result});\n`,
                    onDone: () => "_resolve();\n"
                });
                return new Function(this.args(), code);
        }
    }
    // callTap 就是执行一些插件,并将结果返回
    callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
        let code = "";
        let hasTapCached = false;
        code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
        const tap = this.options.taps[tapIndex];
        switch(tap.type) {
            case "sync":
                if(onResult) {
                    code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
                        before: tap.context ? "_context" : undefined
                    })});\n`;
                } else {
                    code += `_fn${tapIndex}(${this.args({
                        before: tap.context ? "_context" : undefined
                    })});\n`;
                }

                if(onResult) { // 结果透传
                    code += onResult(`_result${tapIndex}`);
                }
                if(onDone) { // 通知插件执行完毕,可以执行下一个插件
                    code += onDone();
                }
                break;
            case "async": //异步执行,插件运行完后再将结果通过执行callback透传
                let cbCode = "";
                if(onResult)
                    cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
                else
                    cbCode += `_err${tapIndex} => {\n`;
                cbCode += `if(_err${tapIndex}) {\n`;
                cbCode += onError(`_err${tapIndex}`);
                cbCode += "} else {\n";
                if(onResult) {
                    cbCode += onResult(`_result${tapIndex}`);
                }

                cbCode += "}\n";
                cbCode += "}";
                code += `_fn${tapIndex}(${this.args({
                    before: tap.context ? "_context" : undefined,
                    after: cbCode //cbCode将结果透传
                })});
                break;
            case "promise": // _fn${tapIndex} 就是第tapIndex 个插件,它必须是个Promise类型的插件
                code += `var _hasResult${tapIndex} = false;\n`;
                code += `_fn${tapIndex}(${this.args({
                    before: tap.context ? "_context" : undefined
                })}).then(_result${tapIndex} => {\n`;
                code += `_hasResult${tapIndex} = true;\n`;
                if(onResult) {
                    code += onResult(`_result${tapIndex}`);
                }
                break;
        }
        return code;
    }
    // 按照插件的注册顺序,按照顺序递归调用执行插件
    callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
        const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
        const next = i => {
            const done = () => next(i + 1);
            return this.callTap(i, {
                onResult: onResult && ((result) => {
                    return onResult(i, result, done, doneBreak);
                }),
            });
        };
        return next(0);
    }

    callTapsLooping({ onError, onDone, rethrowIfPossible }) {

        const syncOnly = this.options.taps.every(t => t.type === "sync");
        let code = "";
        if(!syncOnly) {
            code += "var _looper = () => {\n";
            code += "var _loopAsync = false;\n";
        }
        code += "var _loop;\n";
        code += "do {\n";
        code += "_loop = false;\n";
        code += this.callTapsSeries({
            onResult: (i, result, next, doneBreak) => { // 一旦某个插件返回不为undefined,  即一只调用某个插件执行,如果为undefined,开始调用下一个
                let code = "";
                code += `if(${result} !== undefined) {\n`;
                code += "_loop = true;\n";
                if(!syncOnly)
                    code += "if(_loopAsync) _looper();\n";
                code += doneBreak(true);
                code += `} else {\n`;
                code += next();
                code += `}\n`;
                return code;
            },
        })
        code += "} while(_loop);\n";
        return code;
    }
    // 并行调用插件执行
    callTapsParallel({ onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) => run() }) {
        // 遍历注册都所有插件,并调用
        for(let i = 0; i < this.options.taps.length; i++) {
            code += "if(_counter <= 0) break;\n";
            code += onTap(i, () => this.callTap(i, {
                onResult: onResult && ((result) => {
                    let code = "";
                    code += "if(_counter > 0) {\n";
                    code += onResult(i, result, done, doneBreak);
                    code += "}\n";
                    return code;
                }),
            }), done, doneBreak);
        }
        return code;
    }
}

在HookCodeFactory#create中调用到content方法,此方法将按照此钩子的执行策略,调用不同的方法来执行编译 生成最终的代码。

SyncHook中调用callTapsSeries编译生成最终执行插件的函数,callTapsSeries做的就是将插件列表中插件按照注册顺序遍历执行。

class SyncHookCodeFactory extends HookCodeFactory {
 content({ onError, onResult, onDone, rethrowIfPossible }) {
   return this.callTapsSeries({
       onError: (i, err) => onError(err),
       onDone,
       rethrowIfPossible
   });
 }
}
SyncWaterfallHook中上一个插件执行结果当作下一个插件的入参
class SyncWaterfallHookCodeFactory extends HookCodeFactory {
  content({ onError, onResult, onDone, rethrowIfPossible }) {
  return this.callTapsSeries({
      onResult: (i, result, next) => {
          let code = "";
          code += `if(${result} !== undefined) {\n`;
          code += `${this._args[0]} = ${result};\n`;
          code += `}\n`;
          code += next();
          return code;
      },
      onDone: () => onResult(this._args[0]),
  });
}
}
AsyncParallelHook调用callTapsParallel并行执行插件
class AsyncParallelHookCodeFactory extends HookCodeFactory {
  content({ onError, onDone }) {
  return this.callTapsParallel({
      onError: (i, err, done, doneBreak) => onError(err) + doneBreak(true),
      onDone
  });
  }
}
webpack流程篇 webpack4

webpack入口文件
在webpack项目的package.json文件中,我们找到了入口执行函数,在函数中引入webpack,入口将是 lib/webpack.js;如果是在shell中执行,入口将是./bin/webpack.js。

lib/webpack.js
{
  "name": "webapck",
  "version": "4.1.1",
  "main": "lib/webpack.js",
  "web": "lib/webpack.web.js",
  "bin": "./bin/webpack.js",
}

webpack入口

const webpack = (options, callback) => {
  // 验证options正确性
  // 预处理options
  options = new webapckOptionsDefaulter().process(options); // webpack 4 默认配置
  compiler = new Compiler(options.context); // 实例compiler
  // 若options.watch === true && callback 则开启watch线程
  compiler.watch(watchOptions, callback)
  compiler.run(callback)
  return compiler
}

webpack的入口文件就实例了compiler并且调用run方法开启编译,webpack的编译按以下钩子调用顺序执行:

before-run清除缓存
run 注册缓存数据钩子
before-compile
compile开始编译
make 从入口分析依赖以及间接依赖模块,创建模块对象
build-module 模块构建
seal 构建结果封装package,不可再更改
after-compile 完成构建,缓存数据
emit 输出到dist目录
编译 构建流程
webpack中负责编译和构建都是Compilation

class Compilation extends Tapable {
  constructor(compiler) {
    super()
    this.hooks = {}
  }
  this.compiler = compiler
  // template
  this.mainTemplate = new MainTemplate(this.outputOptions)
  this.chunkTemplate = new ChunkTemplate(this.outputOptions)
  this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate(this.outputOptions)
  this.runtimeTemplate = new RuntimeTemplate(
    this.outputOptions,
    this.requestShortener
  )
  this.moduleTemplates = {
    new ModuleTemplate(this.runtimeTemplate),
    webassembly: new ModuleTemplate(this.runtimeTemplate)
  }

  // 构建生成的资源
  this.chunks = []
  this.chunkGroups = []
  this.modules = []
  this.additionalChunkAssets = []
  this.assets = {}
  this.children = []
}
buildModule(module, optional, origin, dependencies, thisCallback) {
  // 调用module.build方法进行编译代码,build中利用acorn编译生成AST
  this.hooks.buildModule.call(module)
  module.build(/**param*/)
}
// 将模块添加到列表中,并编译模块
_addModuleChain(context, dependency, onModule, callback) {
  // moduleFactory.create创建模块,这里会先利用loader处理文件,然后生成模块对象
  moduleFactory.create(
    {
      contextInfo: {
        issuer: "",
        compiler: this.compiler.name
      },
      context: context,
      dependencies: [dependency]
    },
    (err, module) => {
      const addModuleResult = this.addModule(module)
      module = addModuleResult.module
      onModule(module)
      dependency,module = module
      // 调用buildModule编译模块
      this.buildModule(module, false, null, null, err => {})
    }
  )
}

// 添加入口文件,开始编译、构建
addEntry(context, entry, name, callback) {
  this._addModuleChain( // 调用——addModuleChain添加模块
    context,
    entry,
    module => {
      this.entries.push(module)
    }
  )
}

seal(callback) {
  this.hooks.seal.call()
  const chunk = this.addChunk(name)
  this.entrypoint = new EntryPoint(name)
  entrypoint.setRuntimeChunk(chunk)
  entrypoint.addOrigin(null, name, prepareEntrypoint.request)
  this.namedChunkGroups.set(name, entrypoint)
  this.entrypoints.set(name, entrypoint)
  this.chunkGroups.push(entrypoint)

  GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
        GraphHelpers.connectChunkAndModule(chunk, module);

        chunk.entryModule = module;
        chunk.name = name;

         // ...
        this.hooks.beforeHash.call();
        this.createHash();
        this.hooks.afterHash.call();
        this.hooks.beforeModuleAssets.call();
        this.createModuleAssets();
        if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
            this.hooks.beforeChunkAssets.call();
            this.createChunkAssets();
        }
        // ...
    }


    createHash() {
        // ...
    }

    // 生成 assets 资源并 保存到 Compilation.assets 中 给webpack写插件的时候会用到
    createModuleAssets() {
        for (let i = 0; i < this.modules.length; i++) {
            const module = this.modules[i];
            if (module.buildInfo.assets) {
                for (const assetName of Object.keys(module.buildInfo.assets)) {
                    const fileName = this.getPath(assetName);
                    this.assets[fileName] = module.buildInfo.assets[assetName]; 
                    this.hooks.moduleAsset.call(module, fileName);
                }
            }
        }
    }

    createChunkAssets() {
     // ...
    }
  }
}

在webpack make钩子中,tapAsync注册了一个DllEntryPlugin,就是将入口模块通过调用compilation。addEntry方法将所有的入口添加到编译构建队列中,开启编译流程。

compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => {
      compilation.addEntry(
          this.context,
          new DllEntryDependency(
              this.entries.map((e, idx) => {
                  const dep = new SingleEntryDependency(e);
                  dep.loc = `${this.name}:${idx}`;
                  return dep;
              }),
              this.name
          ),
          // ...
      );
  });

随后在addEntry中调用_addModuleChain开始编译,在_addModuleChain首先会生成模块,最后构建

class NormalModuleFactory extends Tapable {
    create(data, callback) {
        this.hooks.beforeResolve.callAsync(
            {
                contextInfo,
                resolveOptions,
                context,
                request,
                dependencies
            },
            (err, result) => {
                if (err) return callback(err);
                // Ignored
                if (!result) return callback();
                // factory 钩子会触发 resolver 钩子执行,而resolver钩子中会利用acorn 处理js生成AST,再利用acorn处理前,会使用loader加载文件
                const factory = this.hooks.factory.call(null);

                factory(result, (err, module) => {
                    if (err) return callback(err);

                    if (module && this.cachePredicate(module)) {
                        for (const d of dependencies) {
                            d.__NormalModuleFactoryCache = module;
                        }
                    }
                    callback(null, module);
                });
            }
        );
    }
}

在编译完成后,调用compilation.seal方法封闭,生成资源,这些资源保存在compilation.assets, compilation.chunk, 在给webpack写插件的时候会用到

class Compiler extends Tapable {
    constructor(context) {
        super();
        this.hooks = {
            beforeRun: new AsyncSeriesHook(["compilation"]),
            run: new AsyncSeriesHook(["compilation"]),
            emit: new AsyncSeriesHook(["compilation"]),
            afterEmit: new AsyncSeriesHook(["compilation"]),
            compilation: new SyncHook(["compilation", "params"]),
            beforeCompile: new AsyncSeriesHook(["params"]),
            compile: new SyncHook(["params"]),
            make: new AsyncParallelHook(["compilation"]),
            afterCompile: new AsyncSeriesHook(["compilation"]),
            // other hooks
        };
    }
    run(callback) {
        const startTime = Date.now();
        const onCompiled = (err, compilation) => {
            this.emitAssets(compilation, err => {
                if (err) return callback(err);
                if (compilation.hooks.needAdditionalPass.call()) {
                    compilation.needAdditionalPass = true;
                    const stats = new Stats(compilation);
                    stats.startTime = startTime;
                    stats.endTime = Date.now();
                    this.hooks.done.callAsync(stats, err => {
                        if (err) return callback(err);
                        this.hooks.additionalPass.callAsync(err => {
                            if (err) return callback(err);
                            this.compile(onCompiled);
                        });
                    });
                    return;
                }
            });
        };
        this.hooks.beforeRun.callAsync(this, err => {
            if (err) return callback(err);
            this.hooks.run.callAsync(this, err => {
                if (err) return callback(err);

                this.readRecords(err => {
                    if (err) return callback(err);
                    this.compile(onCompiled);
                });
            });
        });
    }
    // 输出文件到构建目录
    emitAssets(compilation, callback) {
        this.hooks.emit.callAsync(compilation, err => {
            if (err) return callback(err);
            outputPath = compilation.getPath(this.outputPath);
            this.outputFileSystem.mkdirp(outputPath, emitFiles);
        });
    }
    newCompilationParams() {
        const params = {
            normalModuleFactory: this.createNormalModuleFactory(),
            contextModuleFactory: this.createContextModuleFactory(),
            compilationDependencies: new Set()
        };
        return params;
    }
    compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            if (err) return callback(err);
            this.hooks.compile.call(params);
            const compilation = this.newCompilation(params);

            this.hooks.make.callAsync(compilation, err => {
                if (err) return callback(err);
                compilation.finish();
                // make 钩子执行后,调用seal生成资源
                compilation.seal(err => {
                    if (err) return callback(err);
                    this.hooks.afterCompile.callAsync(compilation, err => {
                        if (err) return callback(err);
                        // emit, 生成最终文件
                        return callback(null, compilation);
                    });
                });
            });
        });
    }
}

最后输出 seal执行后,便会调用emit钩子,根据webpackconfig文件的output配置的path属性,将文件输出到指定的path。

webpack(<@4.0) 常用的 loader 和 plugin:

stylus-loader:处理样式
url-loader, file-loader:两个都必须用上。否则超过大小限制的图片无法生成到目标文件夹中
babel-loader: js 处理,转码
expose-loader: 将 js 模块暴露到全局
NormalModuleReplacementPlugin:匹配 resourceRegExp,替换为 newResource
ContextReplacementPlugin:替换上下文的插件
IgnorePlugin:不打包匹配文件
PrefetchPlugin:预加载的插件,提高性能
ResolverPlugin:替换上下文的插件
DedupePlugin:删除重复或者相似的文件
LimitChunkCountPlugin:限制打包文件的个数
UglifyJsPlugin:JS文件的压缩
CommonsChunkPlugin:共用模块提取
HotModuleReplacementPlugin:runtime时候的模块热替换
NoErrorsPlugin:跳过编译时出错的代码并记录,使编译后运行时的包不会发生错误。
HtmlWebpackPlugin:HTML模块的热更新

调试webpack

开发总是离不开调试,如果可以更加方便的调试当然就能提高开发效率,不过打包后的文件有时候你是不容易找到出错了的地方对应的源代码的位置的,Source Maps就是来帮我们解决这个问题的。通过简单的配置后,Webpack在打包时可以为我们生成的source maps,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。

devtool选项 配置结果
source-map 在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map,但是它会减慢打包文件的构建速度;
cheap-module-source-map 在一个单独的文件中生成一个不带列映射的map,不带列映射提高项目构建速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便;
eval-source-map 使用eval打包源文件模块,在同一个文件中生成干净的完整的source map。这个选项可以在不影响构建速度的前提下生成完整的sourcemap,但是对打包后输出的JS文件的执行具有性能和安全的隐患。不过在开发阶段这是一个非常好的选项,但是在生产阶段一定不要用这个选项;
cheap-module-eval-source-map 这是在打包文件时最快的生成source map的方法,生成的Source Map 会和打包后的JavaScript文件同行显示,没有列映射,和eval-source-map选项具有相似的缺点;
开发阶段可用eval-source-map:

const path = require('path')
module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'bundle.[chunkHash:8].js',
    path: path.resolve(__dirname, 'dist')
  },
  devtool: 'eval-source-map'
}

建立本地开发服务器
Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js构建,可以实现代码的热加载功能,可以通过它方便的进行代码的开发。其构建方法如下:

安装webpack-dev-server
yarn add webpack-dev-server
修改配置文件

// webpack.config.js

//output 
devServer: {
  contentBase: "./public",//本地服务器所加载的页面所在的目录
  port: 9000,
  historyApiFallback: true,//不跳转
  inline: true//实时刷新
}

// package.json
{
  "script": {
    "dev": "webpack-dev-server"
  }
}

配置HTML代码热加载
webpack-dev-server 只能监控入口文件(JS/LESS/CSS/IMG)的变化,因此 HTML文件的变化必须依赖插件来进行监控。

安装 html-webpack-plugin
yarn add html-webpack-plugin -D
修改配置文件
// webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // entry
  // output
  plugins: [
    new HtmlWebpackPlugin({ // html代码热加载
      template: './index.html'
    })
  ],
}

此时可以取消 html 文件内的 js 引用,因为 html-webpack-plugin 会自动加载编译完的 js 文件

配置自动打开浏览器
通过配置 open-browser-webpack-plugin 可以在webpack编译完之后自动打开浏览器;

安装 open-browser-webpack-plugin
yarn add open-browser-webpack-plugin -D
修改配置文件
// webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')
const OpenBrowserPlugin = require('open-browser-webpack-plugin')

module.exports = {
  // entry
  // output
  plugins: [
    new HtmlWebpackPlugin({ // html代码热加载
      template: './index.html'
    }),
    new OpenBrowserPlugin({ //自动打开浏览器
      url: 'http://localhost:9000'
    })
  ],
}

配置加载器
配置json加载器
使用 json 解析器可以将常量数据定义在 json文件中,然后在 js 文件中调用。

在项目根目录下面创建 config.json 文件,内容如下

{
    "name": "demo",
    "type": "HTML5"
}

修改 index.js

const config = require('../../config.json')
const lib = require('./common.js')
lib.printmsg(config.name)
修改配置文件 webpack.config.js
const path = require('path');

module.exports = {
    entry: './src/js/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    module: { 
        rules: [{
            test: /\.json$/,
            loader: "json-loader"
        }]
    }
};

配置stylus加载器

rules: [
        {
            test: /\.styl$/, // stylus解析器
            loader: 'style-loader!css-loader!stylus-loader'
        },
        ]

配置公共库抽取
安装 chunk-manifest-webpack-plugin webpack-chunk-hash 库
npm install chunk-manifest-webpack-plugin webpack-chunk-hash --save-dev
2. 修改配置文件 webpack.config.js

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const WebpackChunkHash = require("webpack-chunk-hash");
const ChunkManifestPlugin = require("chunk-manifest-webpack-plugin");

module.exports = {
    devtool: 'source-map',
    entry: { 
        main: './src/js/index.js',
        vendor: ['jquery']
    },
    output: {
        filename: '[name].[chunkhash].js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [ {
            test: /\.less$/, // less解析器
            loader: 'style-loader!css-loader!less-loader'
        }, {
            test: /\.(png|jpg)$/, // img压缩器
            loader: 'url-loader?limit=8192'
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({ // html代码热加载
            template: './index.html'
        }),
        new webpack.ProvidePlugin({ //jquery解析器
            $: "jquery",
            jQuery: "jquery",
            "window.jQuery": "jquery"
        }),
        new webpack.optimize.CommonsChunkPlugin({  //公共库抽取
            name: ["vendor", "manifest"], // vendor libs + extracted manifest
            minChunks: Infinity,
        }),
        new webpack.HashedModuleIdsPlugin(),
        new WebpackChunkHash(),
        new ChunkManifestPlugin({
          filename: "chunk-manifest.json",
          manifestVariable: "webpackManifest"
        })
    ]
}

配置模块分析器
在项目复杂的情况下,为了分析多个模块的相互依赖以及打包的关系,通常引入模块打包分析工具,可以清晰的给出每个模块的依赖关系。

安装 webpack-bundle-analyzer 库
npm install webpack-bundle-analyzer --save-dev
2. 修改配置文件 webpack.config.js

const path = require('path');
const { BundleAnalyzerPlugin }  = require('webpack-bundle-analyzer')

module.exports = {
    devtool: 'source-map',
    entry: { 
        main: './src/js/index.js',
        vendor: ['jquery']
    },
    output: {
        filename: '[name].[chunkhash].js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new BundleAnalyzerPlugin()
    ]
};

源码地址:https://github.com/qqxhb/vue-webpack
效果图:
demo效果图

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值