chinadbo/web-front-end

前端工程化之自动构建gulp及模块打包webpack和parcel简介

chinadbo opened this issue · 0 comments

目录

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

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配置中几个核心概念: entryoutputloaderspluginschunk

  • entry:指定webpack开始构建的入口模块,从该模块开始构建并计算出直接或间接依赖的模块或库
  • output: 指定webpack如何命名输出的文件和目录
  • loaders:由于webpack只能处理JavaScript文件,所以需要对非js文件的处理模块
  • plugins: 打包📦优化、压缩🗜️、重新定义环境中的变量等能力
  • 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等方法实现对事件触发的控制,实现
    1. 多个事件连续顺序执行
    2. 并行执行
    3. 异步执行
    4. 一个接一个执行插件,前面的输出是后一个插件的输入(瀑布流执行顺序)
    5. 在允许时停止🤚插件(如某个插件返回undefined,即退出执行)
      Tapable就像nodejs的EventEmitter,提供对事件的注册on和触发emit。下例🌰:
function CustomPlugin() {}
CustomPlugin.prototype.apply = function (compiler) {
    compiler.plugin('emit', pluginFunction)
}
// 在webpack生命周期内适时的执行
this.apply*('emit', options)

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

  1. Tapable1.0 版本发生巨大变化
    不再 通过plugin注册事件、通过applyPlugins*触发事件调用
  • 暴露出很多的钩子🐶,可以使用它们为插件创造钩子函数
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
  1. Tapable详解
    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中
      }
    
    }

每次都是调用taptapSynctapPromise注册不同类型的插件钩子,通过调用callcallAsyncpromise方式调用。其实调用的时候为了按照一定的执行策略执行,调用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};\n`,
                    // ...
                }));
            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将结果透传
                })});\n`;
                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@4,webpack big changes

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 构建结果封装📦,🙅‍不可再更改
  • 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。

Gulp VS webpack

Gulp VS webpack 比较

Gulp 用自动化构建工具增强你的工作流程!

  • 通过代码优于配置的策略,Gulp 让简单的任务简单,复杂的任务可管理。
  • 利用 Node.js 流的威力,你可以快速构建项目并减少频繁的 IO 操作
  • Gulp 严格的插件

Gulp是一个�任务管理�工具🔧,而webpack的理念是:一切皆模块,模块在打包的过程中经过loader处理,它具备精细化管理能力,主要解决依赖分析问题。

Gulp主要使用 gulp.src, gulp.dest, gulp.task, gulp.watch这四个API,底层原理采用Node的Transform Streams,这是一个可读可写可做中间转换的Streams管道,由于从src到dest过程中,文件一直停留在Streams中,没有落地成为实体文件,所以整体运行效率非常高。

gulp常用插件:

  • gulp-rename: �重命名
  • gulp-uglify:文件压缩🗜️
  • gulp-concat:文件📃合并
  • gulp-sass:编译sass
  • gulp-clean-css:压缩🗜️css
  • gulp-htmlmin:压缩🗜️html
  • gulp-babel:编译js
  • gulp-jshint:jshint检查
  • gulp-imagemin:压缩 jpg、png、gif 等图片
  • gulp-livereload:当代码变化时,它可以帮我们自动刷新页面

Webpack 概念很多,但搞清楚 entry,output 和 loader 三个关键点,基本上就可以解决简单的问题了,稍微复杂的场景主要包括对资源的合并处理分拆处理多次打包等,部分这样的问题可以使用插件辅助解决,但是 Webpack 的强大并不在文件处理,而是依赖分析,所以在流程操作特别复杂的情况,webpack 并不能胜任工作,往往会被作为 gulp 的一个 task,整体工作流交给 gulp 主导。

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模块的热更新

Gulp简介

Gulp安装

  1. 全局安装
    yarn global add gulp
  2. 项目依赖
    yarn add glup --dev

配置Gulp

在项目根目录下创建gulpfile.js的配置文件

const gulp = require('gulp')
gulp.task('default', function () {
  // 默认任务代码
})

运行Gulp

gulp // 或者 gulp default

清除文件

yarn add gulp-clean -D

// gulpfile.js
const clean = require('gulp-clean')
gulp.task('clean', function (){
  return gulp.src(['dist/css', 'dist/js'], { read: false})
    .pipe(clean())
})

编译stylus

yarn add gulp-stylus -D

// gulpfile.js
const stylus = require('gulp-stylus')
gulp.task('styles', function () {
  return gulp.src('src/stylus/*.styl')
    .pipe(stylus())
    .pipe(gulp.dest('dist/css'))
})

自动前缀

通过gulp处理css的自动前缀

  1. 安装 gulp-autoprefixer
    yarn add gulp-autoprefixer -D
  2. 编写 gulpfile.js 代码
const autoprefixer = require('gulp-autoprefixer');

gulp.task('styles', function() {
    return gulp.src('src/css/*.css') //源文件路径
        .pipe(autoprefixer()) //自动前缀
        .pipe(gulp.dest('dist/css')) //目的路径
});

base64编码

通过gulp将css中的图片转换成base65编码

  1. 安装 gulp-base64
    yarn add gulp-base64 -D
  2. 编写 gulpfile.js 代码
const base64 = require('gulp-base64');
gulp.task('styles', function() {
    return gulp.src('src/css/*.css') //源文件路径
        .pipe(base64()) //base64编码
        .pipe(gulp.dest('dist/css')) //目的路径
});

css压缩

通过gulp将css进行压缩

  1. 安装 gulp-minify-css
    yarn add gulp-minify-css -D
  2. 编写 gulpfile.js 代码
const cssmin = require('gulp-minify-css');

gulp.task('styles', function() {
    return gulp.src('src/css/*.css') //源文件路径
        .pipe(cssmin()) //css压缩
        .pipe(gulp.dest('dist/css')) //目的路径
});

排列文件顺序

通过gulp将js调整前后顺序

  1. 安装 gulp-order
    yarn add gulp-order -D
  2. 编写 gulpfile.js 代码
const order = require("gulp-order");

gulp.task('scripts', function() {
    return gulp.src('src/js/*.js')  //源文件路径
        .pipe(order([
            "src/js/config.js",
            "src/js/index.js"
        ]))
        .pipe(gulp.dest('dist/js')) //目的路径
})

合并文件

通过gulp将多个文件进行合并

  1. 安装 gulp-concat
    yarn add gulp-concat -D
  2. 编写 gulpfile.js 代码
const concat = require('gulp-concat');

gulp.task('scripts', function() {
    return gulp.src('src/js/*.js')  //源文件路径
        .pipe(concat('main.js'))  //合并文件
        .pipe(gulp.dest('dist/js')) //目的路径
})

重命名文件

通过gulp将文件名进行更改

  1. 安装 gulp-rename

yarn add gulp-rename -D
2. 编写 gulpfile.js 代码

const rename = require('gulp-rename');

gulp.task('scripts', function() {
    return gulp.src('src/js/*.js')  //源文件路径
         .pipe(rename({  
              suffix: '.min'
          }))   //修改文件名     
         .pipe(gulp.dest('dist/js')) //目的路径
})

JS文件压缩

通过gulp将js文件进行压缩

  1. 安装 gulp-uglify

yarn add gulp-uglify -D
2. 编写 gulpfile.js 代码

const rename = require('gulp-rename');

gulp.task('scripts', function() {
    return gulp.src('src/js/*.js')  //源文件路径
         .pipe(uglify())   //压缩js
         .pipe(gulp.dest('dist/js')) //目的路径
})

图片压缩

通过gulp将图片进行压缩

  1. 安装 gulp-imagemin

yarn add gulp-imagemin -D
2. 编写 gulpfile.js 代码

gulp.task('images', function() {
    return gulp.src('src/img/*')
        .pipe(cache(imagemin({
            optimizationLevel: 3,
            progressive: true,
            interlaced: true
        })))
        .pipe(gulp.dest('dist/img'))
});

处理串行任务

定义多个任务的顺序执行关系,否则默认情况下,任务会以最大的并发数同时运行。

//清除任务
gulp.task('clean', function() {
    return gulp.src('dist/css', { read: false })
        .pipe(clean());
});

//编译任务
gulp.task('styles', function() {
    return gulp.src('src/less/*.less') //源文件路径
        .pipe(less()) //less编译                       
        .pipe(gulp.dest('dist/css')) //目的路径
});

//先清空目录,然后再执行编译CSS
gulp.task('default', ['clean'], function() {
    gulp.start('styles')
});

热加载服务

使用 BrowserSync 服务实现文件变更的实时编译调试

  1. 安装 browser-sync

yarn add browser-sync -D
2. 编写 gulpfile.js 代码

const browserSync = require('browser-sync').create();

gulp.task('dev', function() {
    //初始化browser-sync服务
    browserSync.init({
        server: {
            baseDir: "./dist"
        }
    });
    
    //检测less文件是否更改,来调用重新编译css
    gulp.watch('src/less/*', ['styles']);  

    //如果css文件更改过则刷新服务器
    gulp.watch( ['./dist/sys/css/*'] ).on("change", browserSync.reload)
});

webpack简介

配置见开篇

...

调试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构建,可以实现代码的热加载功能,可以通过它方便的进行代码的开发。其构建方法如下:

  1. 安装webpack-dev-server
    yarn add webpack-dev-server
  2. 修改配置文件
// 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文件的变化必须依赖插件来进行监控。

  1. 安装 html-webpack-plugin
    yarn add html-webpack-plugin -D
  2. 修改配置文件
// 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编译完之后自动打开浏览器;

  1. 安装 open-browser-webpack-plugin
    yarn add open-browser-webpack-plugin -D
  2. 修改配置文件
// 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 文件中调用。

  1. 在项目根目录下面创建 config.json 文件,内容如下
{
    "name": "demo",
    "type": "HTML5"
}
  1. 修改 index.js
const config = require('../../config.json')
const lib = require('./common.js')
lib.printmsg(config.name)
  1. 修改配置文件 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'
        },
        ]

配置公共库抽取

  1. 安装 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"
        })
    ]
}

配置模块分析器

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

  1. 安装 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()
    ]
};

parcel打包工具

  • 🚀 极速打包 ----> Parcel 使用 worker 进程去启用多核编译。同时有文件系统缓存,即使在重启构建后也能快速再编译。
  • 📦 将你所有的资源打包 ----> Parcel 具备开箱即用的对 JS, CSS, HTML, 文件 及更多的支持,而且不需要插件。
  • 🐠 自动转换 ----> 如若有需要,Babel, PostCSS, 和PostHTML甚至 node_modules 包会被用于自动转换代码.
  • ✂️ 零配置代码分拆 ----> 使用动态 import() 语法, Parcel 将你的输出文件束(bundles)分拆,因此你只需要在初次加载时加载你所需要的代码。
  • 🔥 热模块替换 ----> Parcel 无需配置,在开发环境的时候会自动在浏览器内随着你的代码更改而去更新模块。
  • 🚨 友好的错误日志 ----> 当遇到错误时,Parcel 会输出 语法高亮的代码片段,帮助你定位问题。

实例
✏️ index.html

<html>
<body>
  <script src="./index.js"></script>
</body>
</html>

🛠 index.js

// 引入另一个组件
import main from './main';

main();

🛠 main.js

// 引入一个 CSS 模块
import classes from './main.css';

export default () => {
  console.log(classes.main);
};

💅 main.css

.main {
  /* 引用一张图片 */
  background: url('./images/background.png');
  color: red;
}

只需要运行 parcel index.html 去启动一个开发服务器。引入 JavaScript, CSS, images, 和更多的资源,然后便大功告成! 👌

快速�🔜开始

Yarn:

yarn global add parcel-bundler

// package.json

"scripts": {
  "start": "parcel index.html",
  "build": "parcel index.html -d build/output --no-minify --no-cache"
}

// --no-minify 禁用压缩🗜️
// --no-cache 禁用文件系统缓存