说起 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 , }; }
对于基础参数的初始化,对应上文提到的配置,可以看到插件设置了两个标识
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 + "]" ); }
调用 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, }); }
这里的 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_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,以及通过子进程来实现编译的流程控制 configure、compile。
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 编辑过程中的 loaders、loaderContext通过最上层的 HappyPlugin 进行传递,来模拟实现 loader 的编译操作。
从回调函数中看到当编译完成时, fs.writeFileSync(compiledPath, source); 会将编译结果写入 compilePath 这个编译路径,并通过 done 回调返回编译结果给主进程。
3. 编译缓存初始化
happypack 会将每一个文件的编译进行缓存,这里通过
function HappyPlugin (userConfig ) {
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 编译完成时进行缓存加载、设置等操作。
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 解析
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 ( ) {
}); },
上面有提到,在加载完 HappyPlugin 时,会创建对应的 HappyThreadPool 对象以及设置数量的 HappyThread。但实际上一直没有创建真正的子进程实例,这里通过调用 threadPool.start 来进行子进程创建。
HappyThreadPool.js: start: function (done ) {
async .parallel(threads.filter(not(send('isOpen' ))).map(get('open' )), done); }
start 方法通过 send 、not、get 这三个方法来进行过滤、启动的串联。
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 消息就是对应的子进程完成编译之后会发出的消息。
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 ) {
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.resource、this.resourcePath等透传到 HappyPlugin.compile 方法进行编译。
2. plugin 编译逻辑运行
HappyPlugin.js: HappyPlugin.prototype.compile = function (loaderContext, done ) {
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 方法来判断是否可以从缓存中读取构建文件。
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() ; }
如果 nowMTime、lastMTime 两个的最后修改时间相同且不存在错误,那么说明构建可以利用缓存
2.1.1 缓存生效
如果缓存判断生效,那么开始调用 readFromCache 方法,从缓存中读取构建对应文件内容。
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 文件的存储路径,然后读取出来,完成从缓存中获取构建内容。
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 获取对应的子进程封装对象。
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,更新缓存设置。
compile: function (params, done ) {
var messageId = generateMessageId(); callbacks[messageId] = done; fd.send({
id: messageId, name: 'COMPILE' , data: params, }); }
这里的 messageId 是个从 0 开始的递增数字,完成回调方法的存储注册,方便完成编译之后找到回调方法传递信息回主进程。同时在 thread 这一层,也是将参数透传给子进程执行编译。
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.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.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 开始进行一些善后工作
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() 存储下这个缓存的映射设置。
exports.save = function ( ) {
fs.writeFileSync(cachePath, JSON .stringify(cache)); };
cache 对象的处理是会将这个文件直接写入 cachePath ,这样就能供下一次 cache.load 方法装载配置,利用缓存。
4.2. 终止子进程
其次调用 threadPool.stop 来终止掉进程
stop: function ( ) {
threads.filter(send('isOpen' )).map(send('close' )); }
类似前面提到的 start 方法,这里是筛选出来正在运行的 HappyThread 对象,调用 close方法。
close: function ( ) {
fd.kill('SIGINT' ); fd = null ; },
在 HappyThread 中,则是调用 kill 方法,完成子进程的释放。
汇总
happypack 的处理思路是将原有的 webpack 对 loader 的执行过程从单一进程的形式扩展多进程模式,原本的流程保持不变。整个流程代码结构上还是比较清晰,在使用过程中,也确实有明显提升,有兴趣的同学可以一起下来交流~
转载自:http://taobaofed.org/blog/2016/12/08/happypack-source-code-analysis/
作者:上坡