happypack 原理解析

说起 happypack 可能很多同学还比较陌生,其实 happypack 是 webpack 的一个插件,目的是通过多进程模型,来加速代码构建,目前我们的线上服务器已经上线这个插件功能,并做了一定适配,效果显著。这里有一些大致参考:

这张图是 happypack 九月逐步全量上线后构建时间的的参考数据,线上构建服务器 16 核环境。

在上这个插件的过程中,我们也发现了这个单人维护的社区插件有一些问题,我们在解决这些问题的同时,也去修改了内部的代码,发布了自己维护的版本 @ali/happypack,那么内部是怎么跑起来的,这里做一个总结记录。

webpack 加载配置

var HappyPack = require('happypack');var happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

// 省略其余配置module: {  loaders: [      {        test: /\.less$/,        loader: ExtractTextPlugin.extract(            'style',            path.resolve(__dirname, './node_modules', 'happypack/loader') + '?id=less'        )      }    ]  },  plugins: [      new HappyPack({        id: 'less',        loaders: ['css!less'],        threadPool: happyThreadPool,        cache: true,        verbose: true      })  ]

这个示例只单独抽取了配置 happypack 的部分。可以看到,类似 extract-text-webpack-plugin 插件,happypack 也是通过 webpack 中 loader 与 plugin 的相互调用协作的方式来运作。

loader 配置直接指向 happypack 提供的 loader, 对于文件实际匹配的处理 loader ,则是通过配置在 plugin 属性来传递说明,这里 happypack 提供的 loader 与 plugin 的衔接匹配,则是通过 id=less 来完成。

happypack 文件解析

HappyPlugin.js

对于 webpack 来讲,plugin 是贯穿在整个构建流程,同样对于 happypack 配置的构建流程,首先进入逻辑的是 plugin 的部分,从初始化的部分查看 happypack 中与 plugin 关联的文件。

1. 基础参数设置

function HappyPlugin(userConfig) {  if (!(this instanceof HappyPlugin)) {    return new HappyPlugin(userConfig);  }

this.id = String(userConfig.id || ++uid);  this.name = 'HappyPack';  this.state = {    started: false,    loaders: [],    baseLoaderRequest: '',    foregroundWorker: null,  };

// 省略 config}

对于基础参数的初始化,对应上文提到的配置,可以看到插件设置了两个标识

  • id: 在配置文件中设置的与 loader 关联的 id 首先会设置到实例上,为了后续 loader 与 plugin 能进行一对一匹配
  • name: 标识插件类型为 HappyPack,方便快速在 loader 中定位对应 plugin,同时也可以避免其他插件中存在 id 属性引起错误的风险

对于这两个属性的应用,可以看到 loader 文件中有这样一段代码

function isHappy(id) {  return function(plugin) {    return plugin.name === 'HappyPack' && plugin.id === id;  };}

happyPlugin = this.options.plugins.filter(isHappy(id))[0];

其次声明 state 对象标识插件的运行状态之后,开始配置信息的处理。

function HappyPlugin(userConfig) {  // 省略基础标识设置  this.config = OptionParser(userConfig, {    id:                       { type: 'string' },    tempDir:                  { type: 'string', default: '.happypack' },    threads:                  { type: 'number', default: 3 },    threadPool:               { type: 'object', default: null },    cache:                    { type: 'boolean', default: true },    cachePath:                { type: 'string' },    cacheContext:             { type: 'object', default: {} },    cacheSignatureGenerator:  { type: 'function' },    verbose:                  { type: 'boolean', default: true },    debug:                    { type: 'boolean', default: process.env.DEBUG === '1' },    enabled:                  { type: 'boolean', default: true },    loaders:                  {      validate: function(value) {          ...      },    }  }, "HappyPack[" + this.id + "]");

// 省略 threadPool 、HappyFSCache 初始化}

调用 OptionParser 函数来进行插件过程中使用到的参数合并,在合并函数的参数对象中,提供了作为数据合并依据的一些属性,例如合并类型 type、默认值 default 以及还有设置校验函数的校验属性 validate 完成属性检查。

这里对一些运行过车中的重要属性进行解释:

  • tmpDir: 存放打包缓存文件的位置
  • cache: 是否开启缓存,目前缓存如果开启,(注: 会以数量级的差异来缩短构建时间,很方便日常开发)
  • cachePath: 存放缓存文件映射配置的位置
  • verbose: 是否输出过程日志
  • loaders: 因为配置中文件的处理 loader 都指向了 happypack 提供的 loadr ,这里配置的对应文件实际需要运行的 loader

2. 线程池初始化

function HappyPlugin(userConfig) {

// 省略基础参数设置

this.threadPool = this.config.threadPool || HappyThreadPool({    id: this.id,    size: this.config.threads,    verbose: this.config.verbose,    debug: this.config.debug,  });

// 省略 HappyFSCache 初始化}

这里的 thread 其实严格意义说是 process,应该是进程,猜测只是套用的传统软件的一个主进程多个线程的模型。这里不管是在配置中,配置的是 threads 属性还是 threadPool 属性,都会生成一个 HappyThreadPool 对象来管理生成的子进程对象。

2.1. HappyThreadPool.js

function HappyThreadPool(config) { var happyRPCHandler = new HappyRPCHandler();

var threads = createThreads(config.size, happyRPCHandler, {   id: config.id,   verbose: config.verbose,   debug: config.debug, });

// 省略返回对象部分 }

在返回 HappyThreadPool 对象之前,会有两个操作:

2.1.1. HappyRPCHandler.js

function HappyRPCHandler() {  this.activeLoaders = {};  this.activeCompiler = null;}

对于 HappyRPCHandler 实例,可以从构造函数看到,会绑定当前运行的 loader 与 compiler ,同时在文件中,针对 loader 与 compiler 定义调用接口:

  • 对应 compiler 会绑定查找解析路径的 reolve 方法:
COMPILER_RPCs = {  resolve: function(compiler, payload, done) {    var resolver = compiler.resolvers.normal;    var resolve = compiler.resolvers.normal.resolve;    // 省略部分判断    resolve.call(resolver, payload.context, payload.context, payload.resource, done);  },};
  • 对应 loader 其中一些绑定:
LOADER_RPCS = {  emitWarning: function(loader, payload) {    loader.emitWarning(payload.message);  },  emitError: function(loader, payload) {      loader.emitError(payload.message);  },  addDependency: function(loader, payload) {      loader.addDependency(payload.file);  },  addContextDependency: function(loader, payload) {      loader.addContextDependency(payload.file);  },};

通过定义调用 webpack 流程过程中的 loader、compiler 的能力来完成功能,类似传统服务中的 RPC 过程。

2.1.2. 创建子进程 (HappyThread.js)

传递子进程数参数 config.size 以及之前生成的 HappyRPCHandler 对象,调用 createThreads 方法生成实际的子进程。

function createThreads(count, happyRPCHandler, config) {  var set = []

for (var threadId = 0; threadId < count; ++threadId) {    var fullThreadId = config.id ? [ config.id, threadId ].join(':') : threadId;    set.push(HappyThread(fullThreadId, happyRPCHandler, config));  }

return set;}

fullThreadId 生成之后,传入 HappyThread 方法,生成对应的子进程,然后放在 set 集合中返回。调用 HappyThread 返回的对象就是 Happypack 的编译 worker 的上层控制。

HappyThread: {    open: function(onReady) {      fd = fork(WORKER_BIN, [id], {        execArgv: []      });      // 省略进程消息绑定处理    },    configure: function(compilerOptions, done) {      // 省略具体过程    },    compile: function(params, done) {      // 省略具体过程    },

isOpen: function() {      return !!fd;    },

close: function() {      fd.kill('SIGINT');      fd = null;    },

对象中包含了对应的进程状态控制 open 、close,以及通过子进程来实现编译的流程控制 configurecompile

2.1.2.1 子进程执行文件 HappyWorkerChannel.js

上面还可以看到一个信息是,fd 子进程的运行文件路径变量 WORKER_BIN,这里对应的是相同目录下的 HappyWorkerChannel.js 。

var HappyWorker = require('./HappyWorker');

if (process.argv[1] === __filename) {  startAsWorker();}

function startAsWorker() {  HappyWorkerChannel(String(process.argv[2]), process);}

function HappyWorkerChannel(id, stream) {  var worker = new HappyWorker({ compiler: fakeCompiler });

stream.on('message', accept);  stream.send({ name: 'READY' });

function accept(message) {   // 省略函数内容  }}

精简之后的代码可以看到 fork 子进程之后,最终执行的是 HappyWorkerChannel 函数,这里的 stream 参数对应的是子进程的 process 对象,用来与主进程进行通信。

函数的逻辑是通过 stream.on('messgae') 订阅消息,控制层 HappyThread 对象来传递消息进入子进程,通过 accept() 方法来路由消息进行对应编译操作。

function accept(message) {    if (message.name === 'COMPILE') {      worker.compile(message.data, function(result) {        stream.send({          id: message.id,          name: 'COMPILED',          sourcePath: result.sourcePath,          compiledPath: result.compiledPath,          success: result.success        });      });    }    else if (message.name === 'COMPILER_RESPONSE') {      // 省略具体流程    }    else if (message.name === 'CONFIGURE') {      // 省略具体流程    }    else {      // 省略具体流程    }}

对于不同的上层消息进行不通的子进程处理。

2.1.2.1.1 子进程编译逻辑文件 HappyWorker.js

这里的核心方法 compile ,对应了一层 worker 抽象,包含 Happypack 的实际编译逻辑,这个对象的构造函数对应 HappyWorker.js 的代码。

HappyWorker.js

HappyWorker.prototype.compile = function(params, done) {

applyLoaders({    compiler: this._compiler,    loaders: params.loaders,    loaderContext: params.loaderContext,  }, params.loaderContext.sourceCode, params.loaderContext.sourceMap, function(err, source, sourceMap) {    // 省略部分判断    var compiledPath = params.compiledPath;    var success = false;

// 省略错误处理    fs.writeFileSync(compiledPath, source);    fs.writeFileSync(compiledPath + '.map', SourceMapSerializer.serialize(sourceMap));

success = true;

done({      sourcePath: params.loaderContext.resourcePath,      compiledPath: compiledPath,      success: success    });  });

从 applyLoaders 的参数看到,这里会把 webpack 编辑过程中的 loadersloaderContext通过最上层的 HappyPlugin 进行传递,来模拟实现 loader 的编译操作。

从回调函数中看到当编译完成时, fs.writeFileSync(compiledPath, source); 会将编译结果写入 compilePath 这个编译路径,并通过 done 回调返回编译结果给主进程。

3. 编译缓存初始化

happypack 会将每一个文件的编译进行缓存,这里通过

function HappyPlugin(userConfig) {  // 省略基础参数设置  // 省略 threadPool 初始化

this.cache = HappyFSCache({    id: this.id,    path: this.config.cachePath ?      path.resolve(this.config.cachePath.replace(/\[id\]/g, this.id)) :      path.resolve(this.config.tempDir, 'cache--' + this.id + '.json'),    verbose: this.config.verbose,    generateSignature: this.config.cacheSignatureGenerator  });

HappyUtils.mkdirSync(this.config.tempDir); }

这里的 cachePath 默认会将 plugin 的 tmpDir 的目录作为生成缓存映射配置文件的目录路径。同时创建好 config.tempDir 目录。

3.1 happypack 缓存控制 HappyFSCache.js
HappyFSCache 函数这里返回对应的 cache 对象,在编译的开始和 worker 编译完成时进行缓存加载、设置等操作。

// HappyFSCache.js

exports.load = function(currentContext) {};exports.save = function() {};exports.getCompiledSourceCodePath = function(filePath) {  return cache.mtimes[filePath] && cache.mtimes[filePath].compiledPath;};

exports.updateMTimeFor = function(filePath, compiledPath, error) {    cache.mtimes[filePath] = {      mtime: generateSignature(filePath),      compiledPath: compiledPath,      error: error    };};

exports.getCompiledSourceMapPath = function(filePath) {};exports.hasChanged = function(filePath) {};exports.hasErrored = function(filePath) {};exports.invalidateEntryFor = function(filePath) {};exports.dump = function() {};

对于编译过程中的单个文件,会通过 getCompiledSourceCodePath 函数来获取对应的缓存内容的文件物理路径,同时在新文件编译完整之后,会通过 updateMTimeFor 来进行缓存设置的更新。

HappyLoader.js

在 happypack 流程中,配置的对应 loader 都指向了 happypack/loader.js ,文件对应导出的是 HappyLoader.js 导出的对象 ,对应的 bundle 文件处理都通过 happypack 提供的 loader 来进行编译流程。

function HappyLoader(sourceCode, sourceMap) {  var happyPlugin, happyRPCHandler;  var callback = this.async();  var id = getId(this.query);

happyPlugin = this.options.plugins.filter(isHappy(id))[0];

happyPlugin.compile({    remoteLoaderId: remoteLoaderId,    sourceCode: sourceCode,    sourceMap: sourceMap,    useSourceMap: this._module.useSourceMap,    context: this.context,    request: happyPlugin.generateRequest(this.resource),    resource: this.resource,    resourcePath: this.resourcePath,    resourceQuery: this.resourceQuery,    target: this.target,  }, function(err, outSourceCode, outSourceMap) {    callback(null, outSourceCode, outSourceMap);  });}

省略了部分代码,HappyLoader 首先拿到配置 id ,然后对所有的 webpack plugin 进行遍历

function isHappy(id) {  return function(plugin) {    return plugin.name === 'HappyPack' && plugin.id === id;  };}

找到 id 匹配的 happypackPlugin。传递原有 webpack 编译提供的 loaderContext (loader 处理函数中的 this 对象)中的参数,调用 happypackPlugin 的 compile 进行编译。

上面是 happypack 的主要文件,作者在项目介绍中也提供了一张图来进行结构化描述:

实际运行

从前面的文件解析,已经把 happypack 的工程文件关联结构大致说明了一下,这下结合日常在构建工程的一个例子,将整个流程串起来说明。

启动入口

在 webpack 编译流程中,在完成了基础的配置之后,就开始进行编译流程,这里 webpack 中的 compiler 对象会去触发 run 事件,这边 HappypackPlugin 以这个事件作为流程入口,进行初始化。

HappyPlugin.prototype.apply = function(compiler) {  ...  compiler.plugin('run', that.start.bind(that));  ...}

当 run 事件触发时,开始进行 start 整个流程

HappyPlugin.prototype.start = function(compiler, done) {  var that = this;  async.series([    function registerCompilerForRPCs(callback) {},    function normalizeLoaders(callback) {},    function resolveLoaders(callback) {},    function loadCache(callback) {},    function launchAndConfigureThreads(callback) {},    function markStarted(callback) {}  ], done);};

start函数通过 async.series 将整个过程串联起来。

1. registerCompilerForRPCs: RPCHandler 绑定 compiler

function registerCompilerForRPCs(callback) {  that.threadPool.getRPCHandler().registerActiveCompiler(compiler);

callback();},

通过调用 plugin 初始化时生成的 handler 上的方法,完成对 compiler 对象的调用绑定。

2. normalizeLoaders: loader 解析

// webpack.config.js: 

new HappyPack({    id: 'less',    loaders: ['css!less'],    threadPool: happyThreadPool,    cache: true,    verbose: true  })

对应中的 webpack 中的 happypackPlugin 的 loaders 配置的处理:

function normalizeLoaders(callback) {  var loaders = that.config.loaders;

// 省略异常处理

that.state.loaders = loaders.reduce(function(list, entry) {    return list.concat(WebpackUtils.normalizeLoader(entry));  }, []);

callback(null);}

对应配置的 loaders ,经过 normalizeLoader 的处理后,例如 [css!less] 会返回成一个 loader 数组 [{path: 'css'},{path: 'less'}],复制到 plugin 的 this.state 属性上。

3.resolveLoaders: loader 对应文件路径查询

function resolveLoaders(callback) {  var loaderPaths = that.state.loaders.map(function(loader) { return loader.path; });

WebpackUtils.resolveLoaders(compiler, loaderPaths, function(err, loaders) {    that.state.loaders = loaders;    that.state.baseLoaderRequest = loaders.map(function(loader) {      return loader.path + (loader.query || '');    }).join('!');    callback();  });}

为了实际执行 loader 过程,这里将上一步 loader 解析 处理过后的 loaders 数组传递到 resolveLoaders 方法中,进行解析

exports.resolveLoaders = function(compiler, loaders, done) {  var resolve = compiler.resolvers.loader.resolve;  var resolveContext = compiler.resolvers.loader;

async.parallel(loaders.map(function(loader) {    return function(callback) {      var callArgs = [ compiler.context, loader, function(err, result) {        callback(null, extractPathAndQueryFromString(result));      }];      resolve.apply(resolveContext, callArgs);    };  }), done);};

而 resolveLoaders 方法采用的是借用原有 webpack 的 compiler 对象上的对应 resolvers.loader 这个 Resolver 实例的 resolve 方法进行解析,构造好解析参数后,通过 async.parallel 并行解析 loader 的路径

4.loadCache: cache 加载

function loadCache(callback) {  if (that.config.cache) {    that.cache.load({      loaders: that.state.loaders,      external: that.config.cacheContext    });  }

callback();}

cache 加载通过调用 cache.load 方法来加载上一次构建的缓存,快速提高构建速度。

exports.load = function(currentContext) {  var oldCache, staleEntryCount;  cache.context = currentContext;

try {    oldCache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));  } catch(e) {    oldCache = null;  }

cache.mtimes = oldCache.mtimes;  cache.context = currentContext;

staleEntryCount = removeStaleEntries(cache.mtimes, generateSignature);

return true;};

load 方法会去读取 cachePath 这个路径的缓存配置文件,然后将内容设置到当前 cache 对象上的 mtimes 上。

在 happypack 设计的构建缓存中,存在一个上述的一个缓存映射文件,里面的配置会映射到一份编译生成的缓存文件。

5.launchAndConfigureThreads: 线程池启动

function launchAndConfigureThreads(callback) {  that.threadPool.start(function() {    // 省略 thread congigure 过程  });},

上面有提到,在加载完 HappyPlugin 时,会创建对应的 HappyThreadPool 对象以及设置数量的 HappyThread。但实际上一直没有创建真正的子进程实例,这里通过调用 threadPool.start 来进行子进程创建。

HappyThreadPool.js:

start: function(done) {      async.parallel(threads.filter(not(send('isOpen'))).map(get('open')), done);    }

start 方法通过 send 、notget 这三个方法来进行过滤、启动的串联。

HappyThreadPool.js:    function send(method) {      return function(receiver) {        return receiver[method].call(receiver);      };    }

function not(f) {      return function(x) {        return !f(x);      };    }

function get(attr) {      return function(object) {        return object[attr];      };    }

传递 'isOpen' 到 send 返回函数中,receiver 对象绑定调用 isOpen 方法;再传递给 not返回函数中,返回前面函数结构取反。传递给 threads 的 filter 方法进行筛选;最后通过 get 传递返回的 open 属性。

HappyThread.js 

isOpen: function() {      return !!fd;    }

在 HappyThread 对象中 isOpen 通过判断 fd 变量来判断是否创建子进程。

open: function(onReady) {  var emitReady = Once(onReady);

fd = fork(WORKER_BIN, [id], {    execArgv: []  });

fd.on('error', throwError);  fd.on('exit', function(exitCode) {    if (exitCode !== 0) {      emitReady('HappyPack: worker exited abnormally with code ' + exitCode);    }  });

fd.on('message', function acceptMessageFromWorker(message) {    if (message.name === 'READY') {      emitReady();    }    else if (message.name === 'COMPILED') {      var filePath = message.sourcePath;

callbacks[message.id](message);      delete callbacks[message.id];    }  });}

HappyThread 对象的 open 方法首先将 async.parallel 传递过来的 callback 钩子通过 Once方法封装,避免多次触发,返回成 emitReady 函数。

然后调用 childProcess.fork 传递 HappyWorkerChannel.js 作为子进程执行文件来创建一个子进程,绑定对应的 error 、exit 异常情况的处理,同时绑定最为重要的 message 事件,来接受子进程发来的处理消息。而这里 COMPILED 消息就是对应的子进程完成编译之后会发出的消息。

// HappyWorkerChannel.js

function HappyWorkerChannel(id, stream) {  var fakeCompiler = new HappyFakeCompiler(id, stream.send.bind(stream));  var worker = new HappyWorker({ compiler: fakeCompiler });

stream.on('message', accept);  stream.send({ name: 'READY' });

// 省略消息处理}

在子进程完成创建之后,会向主进程发送一个 READY 消息,表明已经完成创建,在主进程接受到 READY 消息后,会调用前面封装的 emitReady ,来反馈给 async.parallel 表示完成 open 流程。

6.markStarted: 标记启动

function markStarted(callback) {  that.state.started = true;  callback();}

最后一步,在完成之前的步骤后,修改状态属性 started 为 true,完成整个插件的启动过程。

编译运行

1. loader 传递
在 webpack 流程中,在源码文件完成内容读取之后,开始进入到 loader 的编译执行阶段,这时 HappyLoader 作为编译逻辑入口,开始进行编译流程。

function HappyLoader(sourceCode, sourceMap) { // 省略 Plugin 查找

happyPlugin.compile({    remoteLoaderId: remoteLoaderId,    sourceCode: sourceCode,    sourceMap: sourceMap,    useSourceMap: this._module.useSourceMap,    context: this.context,    request: happyPlugin.generateRequest(this.resource),    resource: this.resource,    resourcePath: this.resourcePath,    resourceQuery: this.resourceQuery,    target: this.target,  }, function(err, outSourceCode, outSourceMap) {    callback(null, outSourceCode, outSourceMap);  });}

loader 中将 webpack 原本的 loaderContext(this指向) 对象的一些参数例如 this.resourcethis.resourcePath等透传到 HappyPlugin.compile 方法进行编译。

2. plugin 编译逻辑运行

HappyPlugin.js: 

HappyPlugin.prototype.compile = function(loaderContext, done) {  // 省略 foregroundWorker 情况  return this.compileInBackground(loaderContext, done);};

HappyPlugin.prototype.compileInBackground = function(loaderContext, done) {  var cache = this.cache;  var filePath = loaderContext.resourcePath;

if (!cache.hasChanged(filePath) && !cache.hasErrored(filePath)) {    var cached = this.readFromCache(filePath);

return done(null, cached.sourceCode, cached.sourceMap);  }

this._performCompilationRequest(this.threadPool.get(), loaderContext, done);};

HappyPlugin 中的 compile 方法对应 build 过程,通过调用 compileInBackground 方法来完成调用。

2.1 构建缓存判断

在 compileInBackground 中,首先会代用 cache 的 hasChanged 和 hasErrored 方法来判断是否可以从缓存中读取构建文件。

// HappyFSCache.js  exports.hasChanged = function(filePath) {    var nowMTime = generateSignature(filePath);    var lastMTime = getSignatureAtCompilationTime(filePath);

return nowMTime !== lastMTime;  };

exports.hasErrored = function(filePath) {    return cache.mtimes[filePath] && cache.mtimes[filePath].error;  };

function getSignatureAtCompilationTime(filePath) {    if (cache.mtimes[filePath]) {      return cache.mtimes[filePath].mtime;    }  }

hasError 判断的是更新缓存的时候的 error 属性是否存在。

hasChanged 中会去比较 nowMTime 与 lastMTime 两个是否相等。实际上这里 nowMTime 通过调用 generateSignature(默认是 getMTime 函数) 返回的是文件目前的最后修改时间,lastMTime 返回的是编译完成时的修改时间。

function getMTime(filePath) {  return fs.statSync(filePath).mtime.getTime();}

如果 nowMTimelastMTime 两个的最后修改时间相同且不存在错误,那么说明构建可以利用缓存

2.1.1 缓存生效

如果缓存判断生效,那么开始调用 readFromCache 方法,从缓存中读取构建对应文件内容。

// HappyPlugin.js: 

HappyPlugin.prototype.readFromCache = function(filePath) {  var cached = {};  var sourceCodeFilePath = this.cache.getCompiledSourceCodePath(filePath);  var sourceMapFilePath = this.cache.getCompiledSourceMapPath(filePath);

cached.sourceCode = fs.readFileSync(sourceCodeFilePath, 'utf-8');

if (HappyUtils.isReadable(sourceMapFilePath)) {    cached.sourceMap = SourceMapSerializer.deserialize(      fs.readFileSync(sourceMapFilePath, 'utf-8')    );  }

return cached;};

函数的意图是通过 cache 对象的 getCompiledSourceCodePath 、getCompiledSourceMapPath 方法获取缓存的编译文件及 sourcemap 文件的存储路径,然后读取出来,完成从缓存中获取构建内容。

// HappyFSCache.js

exports.getCompiledSourceCodePath = function(filePath) {    return cache.mtimes[filePath] && cache.mtimes[filePath].compiledPath;  };

exports.getCompiledSourceMapPath = function(filePath) {    return cache.mtimes[filePath] && cache.mtimes[filePath].compiledPath + '.map';  };

获取的路径是通过在完成编译时调用的 updateMTimeFor 进行存储的对象中的 compiledPath编译路径属性。

2.1.2 缓存失效

在缓存判断失效的情况下,进入 _performCompilationRequest ,进行下一步 happypack 编译流程。

HappyPlugin.prototype.compileInBackground = function(loaderContext, done) {

this._performCompilationRequest(this.threadPool.get(), loaderContext, done);

}

在调用 _performCompilationRequest 前, 还有一步是从 ThreadPool 获取对应的子进程封装对象。

// HappyThreadPool.js

get: RoundRobinThreadPool(threads),

function RoundRobinThreadPool(threads) {  var lastThreadId = 0;

return function getThread() {    var threadId = lastThreadId;

lastThreadId++;

if (lastThreadId >= threads.length) {      lastThreadId = 0;    }

return threads[threadId];  }}

这里按照递增返回的 round-robin,这种在服务器进程控制中经常使用的简洁算法返回子进程封装对象。

3. 编译开始

HappyPlugin.prototype._performCompilationRequest = function(worker, loaderContext, done) {  var cache = this.cache;  var filePath = loaderContext.resourcePath;

cache.invalidateEntryFor(filePath);

worker.compile({    loaders: this.state.loaders,    compiledPath: path.resolve(this.config.tempDir,      HappyUtils.generateCompiledPath(filePath)),    loaderContext: loaderContext,  }, function(result) {    var contents = fs.readFileSync(result.compiledPath, 'utf-8')    var compiledMap;

if (!result.success) {      cache.updateMTimeFor(filePath, null, contents);      done(contents);    }    else {      cache.updateMTimeFor(filePath, result.compiledPath);      compiledMap = SourceMapSerializer.deserialize(        fs.readFileSync(cache.getCompiledSourceMapPath(filePath), 'utf-8')      );

done(null, contents, compiledMap);    }  });};

首先对编译的文件,调用 cache.invalidateEntryFor 设置该文件路径的构建缓存失效。然后调用子进程封装对象的 compile 方法,触发子进程进行编译。

同时会生成衔接主进程、子进程、缓存的 compiledPath,当子进程完成编译后,会将编译后的代码写入 compiledPath,之后发送完成编译的消息回主进程,主进程也是通过 compiledPath 获取构建后的代码,同时传递 compiledPath 以及对应的编译前文件路径 filePath,更新缓存设置。

// HappyThread.js

compile: function(params, done) {      var messageId = generateMessageId();

callbacks[messageId] = done;

fd.send({        id: messageId,        name: 'COMPILE',        data: params,      });    }

这里的 messageId 是个从 0 开始的递增数字,完成回调方法的存储注册,方便完成编译之后找到回调方法传递信息回主进程。同时在 thread 这一层,也是将参数透传给子进程执行编译。

// HappyWorkerChannel.js

function accept(message) {    if (message.name === 'COMPILE') {      worker.compile(message.data, function(result) {        stream.send({          id: message.id,          name: 'COMPILED',          sourcePath: result.sourcePath,          compiledPath: result.compiledPath,          success: result.success        });      });    }

//省略其他操作}

子进程接到消息后,调用 worker.compile 方法 ,同时进一步传递构建参数。

// HappyWorker.js

HappyWorker.prototype.compile = function(params, done) {

applyLoaders({    compiler: this._compiler,    loaders: params.loaders,    loaderContext: params.loaderContext,  }, params.loaderContext.sourceCode, params.loaderContext.sourceMap, function(err, source, sourceMap) {    var compiledPath = params.compiledPath;    var success = false;

if (err) {      console.error(err);      fs.writeFileSync(compiledPath, serializeError(err), 'utf-8');    }    else {      fs.writeFileSync(compiledPath, source);      fs.writeFileSync(compiledPath + '.map', SourceMapSerializer.serialize(sourceMap));

success = true;    }

done({      sourcePath: params.loaderContext.resourcePath,      compiledPath: compiledPath,      success: success    });  });};

在 HappyWorker.js 中的 compile 方法中,调用 applyLoaders 进行 loader 方法执行。applyLoaders 是 happypack 中对 webpack 中 loader 执行过程进行模拟,对应 NormalModuleMixin.js 中的 doBuild 方法。完成对文件的字符串处理编译。

根据 err 判断是否成功。如果判断成功,则将对应文件的编译后内容写入之前传递进来的 compiledPath,反之,则会把错误内容写入。

在子进程完成编译流程后,会调用传递进来的回调方法,在回调方法中将编译信息返回到主进程,主进程根据 compiledPath 来获取子进程的编译内容。

// HappyPlugin.js

HappyPlugin.prototype._performCompilationRequest = function(worker, loaderContext, done) {

var contents = fs.readFileSync(result.compiledPath, 'utf-8')    var compiledMap;

if (!result.success) {      cache.updateMTimeFor(filePath, null, contents);      done(contents);    }    else {      cache.updateMTimeFor(filePath, result.compiledPath);      compiledMap = SourceMapSerializer.deserialize(        fs.readFileSync(cache.getCompiledSourceMapPath(filePath), 'utf-8')      );

done(null, contents, compiledMap);    }

}

获取子进程的编译内容 contents 后,根据 result.success 属性来判断是否编译成功,如果失败的话,会将 contents 作为错误传递进去。

在完成调用 updateMTimeFor 缓存更新后,最后将内容返回到 HappyLoader.js 中的回调中,返回到 webpack 的原本流程。

4. 编译结束

当 webpack 整体编译流程结束后, happypack 开始进行一些善后工作

// HappyPlugin.js

compiler.plugin('done', that.stop.bind(that));

HappyPlugin.prototype.stop = function() {  if (this.config.cache) {    this.cache.save();  }

this.threadPool.stop();};

4.1. 存储缓存配置

首先调用 cache.save() 存储下这个缓存的映射设置。

// HappyFSCache.js

exports.save = function() {    fs.writeFileSync(cachePath, JSON.stringify(cache));  };

cache 对象的处理是会将这个文件直接写入 cachePath ,这样就能供下一次 cache.load 方法装载配置,利用缓存。

4.2. 终止子进程

其次调用 threadPool.stop 来终止掉进程

// HappyThreadPool.js

stop: function() {      threads.filter(send('isOpen')).map(send('close'));    }

类似前面提到的 start 方法,这里是筛选出来正在运行的 HappyThread 对象,调用 close方法。

// HappyThread.js

close: function() {      fd.kill('SIGINT');      fd = null;    },

在 HappyThread 中,则是调用 kill 方法,完成子进程的释放。

汇总

happypack 的处理思路是将原有的 webpack 对 loader 的执行过程从单一进程的形式扩展多进程模式,原本的流程保持不变。整个流程代码结构上还是比较清晰,在使用过程中,也确实有明显提升,有兴趣的同学可以一起下来交流~

转载自:http://taobaofed.org/blog/2016/12/08/happypack-source-code-analysis/

作者:上坡

时间: 2024-08-04 00:14:39

happypack 原理解析的相关文章

Skinned Mesh原理解析和一个最简单的实现示例

Skinned Mesh原理解析和一个最简单的实现示例   作者:n5 Email: happyfirecn@yahoo.com.cn Blog: http://blog.csdn.net/n5 2008-10月   Histroy: Version:1.01  Date:2008-11-01        修改了一些不精确的用语 Version:1.00 Date:2008-10-19     讲述骨骼动画的资料很多,但大部分都是针对DX8或DX9的SkinnedMesh进行讲解.我觉得对于骨

[IT]JSONP跨域的原理解析

JavaScript是一种在Web开发中经常使用的前端动态脚本技术.在JavaScript中,有一个很重要的安全性限制,被称为"Same-Origin Policy"(同源策略).这一策略对于JavaScript代码能够访问的页面内容做了很重要的限制,即JavaScript 只能访问与包含它的文档在同一域下的内容. JavaScript这个安全策略在进行多iframe或多窗口编程.以及Ajax编程时显得尤为重要.根据这个策略,在baidu.com下的页面中包含的JavaScript代码

Android中微信抢红包插件原理解析及开发思路_Android

一.前言 自从去年中微信添加抢红包的功能,微信的电商之旅算是正式开始正式火爆起来.但是作为Android开发者来说,我们在抢红包的同时意识到了很多问题,就是手动去抢红包的速度慢了,当然这些有很多原因导致了.或许是网络的原因,而且这个也是最大的原因.但是其他的不可忽略的因素也是要考虑到进去的,比如在手机充电锁屏的时候,我们并不知道有人已经开始发红包了,那么这时候也是让我们丧失了一大批红包的原因.那么关于网络的问题,我们开发者可能用相关技术无法解决(当然在Google和Facebook看来的话,他们

Android代码入侵原理解析(一)

Android代码入侵原理解析(一)           1.代码入侵原理 代码入侵,或者叫代码注入,指的是让目标应用/进程执行指定的代码.代码入侵,可以在应用进行运行过程中进行动态分析,也是对应用进行攻击的一种常见方式.我把代码入侵分为两种类型:静态和动态.静态代码入侵是直接修改相关代码,在应用启动和运行之前,指定代码就已经和应用代码关联起来.动态代码入侵是应用启动之后,控制应用运行进程,动态加载和运行指定代码. 2.静态代码入侵 静态代码入侵,有直接和间接的手段. 直接手段是修改应用本身代码

秋色园QBlog技术原理解析:性能优化篇:字节、缓存、并发(十二)

文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文件的作用 2: 秋色园QBlog技术原理解析:认识整站处理流程(二) --介绍秋色园业务处理流程 3: 秋色园QBlog技术原理解析:UrlRewrite之无后缀URL原理(三) --介绍如何实现无后缀URL 4: 秋色园QBlog技术原理解析:UrlRewrite之URL重定向体系(四) --介绍URL如何定位到处理程序 5: 秋色园QBlog技术原理解析:Module之页面基类设计(五) --介绍创建

秋色园QBlog技术原理解析:性能优化篇:access的并发极限及超级分库分散并发方案(十六)

上节回顾:   上节 秋色园QBlog技术原理解析:性能优化篇:数据库文章表分表及分库减压方案(十五) 中, 介绍了 秋色园QBlog 在性能优化方面,从技术的优化手段,开始步入数据库设计优化,并从数据的使用情况上进行了分析,从而将文章内容进行分离,得到新的分表,由于内容比较大,进而分了库,达到一种基础减压.   本节内容:   本节将介绍秋色园 QBlog 的Super分库方案,以及何以如此Super分库的原因.   描述说明:   在进行上了上节的分库方案后,虽然感觉一度秋色园QBlog的访

SQL Server 内存数据库原理解析

原文:SQL Server 内存数据库原理解析 前言 关系型数据库发展至今,细节上以做足文章,在寻求自身突破发展的过程中,内存与分布式数据库是当下最流行的主题,这与性能及扩展性在大数据时代的需求交相辉映.SQL Server作为传统的数据库也在最新发布版本SQL Server 2014中提供了新利器 SQL Server In-Memory OLTP(Hekaton),使得其在OLTP系统中的性能有了几十倍甚至上百倍的性能提升,本篇文章为大家探究一二.         大数据时代的数据如何组织应

秋色园QBlog技术原理解析:性能优化篇:用户和文章计数器方案(十七)

上节概要:   上节 秋色园QBlog技术原理解析:性能优化篇:access的并发极限及分库分散并发方案(十六) 中, 介绍了 Access的并发上限,及从某种程度上 秋色园QBlog 针对并发上限进行了多个数据的划分,从而最大并发上限从64提升到64*N(个数据库),虽然总和的最大并发值是上升了,但是单个库的最大值并没有变化,或者说单个表的最大并发值没有发生变化,上限仍是64. 于是,对于频繁产生更新操作的访问计数器(用户表及文章表),是该进入优化的方案了.   本节概要:   本节将介绍秋色

秋色园QBlog技术原理解析:性能优化篇:数据库文章表分表及分库减压方案(十五)

文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文件的作用 2: 秋色园QBlog技术原理解析:认识整站处理流程(二) --介绍秋色园业务处理流程 3: 秋色园QBlog技术原理解析:UrlRewrite之无后缀URL原理(三) --介绍如何实现无后缀URL 4: 秋色园QBlog技术原理解析:UrlRewrite之URL重定向体系(四) --介绍URL如何定位到处理程序 5: 秋色园QBlog技术原理解析:Module之页面基类设计(五) --介绍创建