happypack 原理解析
说起 happypack 可能很多同学还比较陌生,其实 happypack 是 webpack 的一个插件,目的是通过多进程模型,来加速代码构建,目前我们的线上服务器已经上线这个插件功能,并做了一定适配,效果显著。这里有一些大致参考:
这张图是 happypack 九月逐步全量上线后构建时间的的参考数据,线上构建服务器 16 核环境。
在上这个插件的过程中,我们也发现了这个单人维护的社区插件有一些问题,我们在解决这些问题的同时,也去修改了内部的代码,发布了自己维护的版本 @ali/happypack,那么内部是怎么跑起来的,这里做一个总结记录。
webpack 加载配置
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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. 基础参数设置
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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 文件中有这样一段代码
JavaScript
1
2
3
4
5
6
7
|
function isHappy(id) {
return function(plugin) {
return plugin.name === 'HappyPack' && plugin.id === id;
};
}
happyPlugin = this.options.plugins.filter(isHappy(id))[0];
|
其次声明 state 对象标识插件的运行状态之后,开始配置信息的处理。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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. 线程池初始化
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
JavaScript
1
2
3
4
5
6
7
8
9
10
11
|
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
JavaScript
1
2
3
4
|
function HappyRPCHandler() {
this.activeLoaders = {};
this.activeCompiler = null;
}
|
对于 HappyRPCHandler
实例,可以从构造函数看到,会绑定当前运行的 loader 与 compiler ,同时在文件中,针对 loader 与 compiler 定义调用接口:
- 对应 compiler 会绑定查找解析路径的 reolve 方法:
JavaScript
12345678COMPILER_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 其中一些绑定:
JavaScript
1234567891011121314LOADER_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
方法生成实际的子进程。
JavaScript
1
2
3
4
5
6
7
8
9
10
|
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 的上层控制。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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
。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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()
方法来路由消息进行对应编译操作。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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
的代码。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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
会将每一个文件的编译进行缓存,这里通过
JavaScript
1
|
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 编译完成时进行缓存加载、设置等操作。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 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 来进行编译流程。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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,www.90168.org
resourcePath: this.resourcePath,
resourceQuery: this.resourceQuery,
target: this.target,
}, function(err, outSourceCode, outSourceMap) {
callback(null, outSourceCode, outSourceMap);
});
}
|
省略了部分代码,HappyLoader
首先拿到配置 id
,然后对所有的 webpack plugin 进行遍历
JavaScript
1
2
3
4
5
|
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
以这个事件作为流程入口,进行初始化。
JavaScript
1
2
3
4
5
|
HappyPlugin.prototype.apply = function(compiler) {
...
compiler.plugin('run', that.start.bind(that));
...
}
|
当 run
事件触发时,开始进行 start
整个流程
JavaScript
1
2
3
4
5
6
7
8
9
10
11
|
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
JavaScript
1
2
3
4
5
|
function registerCompilerForRPCs(callback) {
that.threadPool.getRPCHandler().registerActiveCompiler(compiler);
callback();
},
|
通过调用 plugin 初始化时生成的 handler 上的方法,完成对 compiler
对象的调用绑定。
2. normalizeLoaders: loader 解析
JavaScript
1
2
3
4
5
6
7
8
9
|
// webpack.config.js:
new HappyPack({
id: 'less',
loaders: ['css!less'],
threadPool: happyThreadPool,
cache: true,
verbose: true
})
|
对应中的 webpack
中的 happypackPlugin 的 loaders 配置的处理:
JavaScript
1
2
3
4
5
6
7
8
9
10
11
|
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 对应文件路径查询
JavaScript
1
2
3
4
5
6
7
8
9
10
11
|
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
方法中,进行解析
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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 加载
JavaScript
1
2
3
4
5
6
7
8
9
10
|
function loadCache(callback) {
if (that.config.cache) {
that.cache.load({
loaders: that.state.loaders,
external: that.config.cacheContext
});
}
callback();
}
|
cache 加载通过调用 cache.load
方法来加载上一次构建的缓存,快速提高构建速度。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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: 线程池启动
JavaScript
1
2
3
4
5
|
function launchAndConfigureThreads(callback) {
that.threadPool.start(function() {
// 省略 thread congigure 过程
});
},
|
上面有提到,在加载完 HappyPlugin
时,会创建对应的 HappyThreadPool
对象以及设置数量的 HappyThread
。但实际上一直没有创建真正的子进程实例,这里通过调用threadPool.start
来进行子进程创建。
JavaScript
1
2
3
4
5
|
HappyThreadPool.js:
start: function(done) {
async.parallel(threads.filter(not(send('isOpen'))).map(get('open')), done);
}
|
start
方法通过 send
、not
、get
这三个方法来进行过滤、启动的串联。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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
属性。
JavaScript
1
2
3
4
5
|
HappyThread.js
isOpen: function() {
return !!fd;
}
|
在 HappyThread
对象中 isOpen
通过判断 fd
变量来判断是否创建子进程。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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
消息就是对应的子进程完成编译之后会发出的消息。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
|
// 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: 标记启动
JavaScript
1
2
3
4
|
function markStarted(callback) {
that.state.started = true;
callback();
}
|
最后一步,在完成之前的步骤后,修改状态属性 started
为 true
,完成整个插件的启动过程。
编译运行
1. loader 传递 在 webpack 流程中,在源码文件完成内容读取之后,开始进入到 loader 的编译执行阶段,这时 HappyLoader
作为编译逻辑入口,开始进行编译流程。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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.resource
、this.resourcePath
等透传到 HappyPlugin.compile
方法进行编译。
2. plugin 编译逻辑运行
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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
方法来判断是否可以从缓存中读取构建文件。
JavaScript
1
|
// 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
返回的是编译完成时的修改时间。
JavaScript
1
2
3
|
function getMTime(filePath) {
return fs.statSync(filePath).mtime.getTime();
}
|
如果 nowMTime
、lastMTime
两个的最后修改时间相同且不存在错误,那么说明构建可以利用缓存
2.1.1 缓存生效
如果缓存判断生效,那么开始调用 readFromCache
方法,从缓存中读取构建对应文件内容。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 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 文件的存储路径,然后读取出来,完成从缓存中获取构建内容。
JavaScript
1
2
3
4
5
6
7
8
9
|
// 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
编译流程。
JavaScript
1
2
3
4
5
|
HappyPlugin.prototype.compileInBackground = function(loaderContext, done) {
this._performCompilationRequest(this.threadPool.get(), loaderContext, done);
}
|
在调用 _performCompilationRequest
前, 还有一步是从 ThreadPool
获取对应的子进程封装对象。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 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. 编译开始
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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
,更新缓存设置。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// HappyThread.js
compile: function(params, done) {
var messageId = generateMessageId();
callbacks[messageId] = done;
fd.send({
id: messageId,
name: 'COMPILE',
data: params,
});
}
|
这里的 messageId 是个从 0 开始的递增数字,完成回调方法的存储注册,方便完成编译之后找到回调方法传递信息回主进程。同时在 thread
这一层,也是将参数透传给子进程执行编译。
子进程接到消息后,调用 worker.compile
方法 ,同时进一步传递构建参数。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// 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
来获取子进程的编译内容。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 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
开始进行一些善后工作
JavaScript
1
2
3
4
5
6
7
8
9
10
11
|
// 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()
存储下这个缓存的映射设置。
JavaScript
1
2
3
4
5
|
// HappyFSCache.js
exports.save = function() {
fs.writeFileSync(cachePath, JSON.stringify(cache));
};
|
cache 对象的处理是会将这个文件直接写入 cachePath
,这样就能供下一次 cache.load
方法装载配置,利用缓存。
4.2. 终止子进程
其次调用 threadPool.stop
来终止掉进程
JavaScript
1
2
3
4
5
|
// HappyThreadPool.js
stop: function() {
threads.filter(send('isOpen')).map(send('close'));
}
|
类似前面提到的 start
方法,这里是筛选出来正在运行的 HappyThread
对象,调用 close
方法。
JavaScript
1
2
3
4
5
6
|
// HappyThread.js
close: function() {
fd.kill('SIGINT');
fd = null;
},
|
在 HappyThread
中,则是调用 kill
方法,完成子进程的释放。
汇总
happypack 的处理思路是将原有的 webpack 对 loader 的执行过程从单一进程的形式扩展多进程模式,原本的流程保持不变。整个流程代码结构上还是比较清晰,在使用过程中,也确实有明显提升,有兴趣的同学可以一起下来交流~
happypack 原理解析的更多相关文章
- [原][Docker]特性与原理解析
Docker特性与原理解析 文章假设你已经熟悉了Docker的基本命令和基本知识 首先看看Docker提供了哪些特性: 交互式Shell:Docker可以分配一个虚拟终端并关联到任何容器的标准输入上, ...
- 【算法】(查找你附近的人) GeoHash核心原理解析及代码实现
本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...
- Web APi之过滤器执行过程原理解析【二】(十一)
前言 上一节我们详细讲解了过滤器的创建过程以及粗略的介绍了五种过滤器,用此五种过滤器对实现对执行Action方法各个时期的拦截非常重要.这一节我们简单将讲述在Action方法上.控制器上.全局上以及授 ...
- Web APi之过滤器创建过程原理解析【一】(十)
前言 Web API的简单流程就是从请求到执行到Action并最终作出响应,但是在这个过程有一把[筛子],那就是过滤器Filter,在从请求到Action这整个流程中使用Filter来进行相应的处理从 ...
- GeoHash原理解析
GeoHash 核心原理解析 引子 一提到索引,大家脑子里马上浮现出B树索引,因为大量的数据库(如MySQL.oracle.PostgreSQL等)都在使用B树.B树索引本质上是对索引字段 ...
- alibaba-dexposed 原理解析
alibaba-dexposed 原理解析 使用参考地址: http://blog.csdn.net/qxs965266509/article/details/49821413 原理参考地址: htt ...
- 支付宝Andfix 原理解析
支付宝Andfix 原理解析 使用参考地址: http://blog.csdn.net/qxs965266509/article/details/49802429 原理参考地址: http://blo ...
- JavaScript 模板引擎实现原理解析
1.入门实例 首先我们来看一个简单模板: <script type="template" id="template"> <h2> < ...
- Request 接收参数乱码原理解析三:实例分析
通过前面两篇<Request 接收参数乱码原理解析一:服务器端解码原理>和<Request 接收参数乱码原理解析二:浏览器端编码原理>,了解了服务器和浏览器编码解码的原理,接下 ...
随机推荐
- JS函数的其他用法【备于取用】
//随机数生成器 Math.random() 日期时间函数(需要用变量调用): var b = new Date(); //获取当前时间 b.getTime() //获取时间戳 b.getFull ...
- AJAX 三级联动
新的封装类 <?php class DBDA { public $host="localhost";//服务器地址 public $uid="root"; ...
- AJAX JSON类型返回
文本样式和下拉样式 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://w ...
- MVC学习笔记---ModelBinder
http://www.cnblogs.com/terrysun/archive/2010/04/05/1704666.html http://www.cnblogs.com/aehyok/archiv ...
- Android4.4 以太网和DHCP启动过程介绍
转自:http://blog.csdn.net/wlwl0071986/article/details/51451843 Android4.4已经加入了以太网的支持.现在对以太网的初始化流程.网络策略 ...
- ****php:require_once(dirname(__FILE__)."/./config_uc.php");
Q:麻烦清楚地讲解一下这句的意思,具体路径是怎样的,这个文解在 根目录,如果我想放在根目录下的tieba文件夹里,应该怎么修改/./ 这个是表示什么? A: require_once(dirname( ...
- 重温WCF之一个服务实现多个契约(二)
public class ServiceImp : IService1,IService2,IService3 { public string SayHelloA() { return "你 ...
- Delphi中的异常处理
转载:http://www.cnblogs.com/doit8791/archive/2012/05/08/2489471.html 以前写Delphi程序一直不注意异常处理,对其异常处理的机制总是一 ...
- 【131031】html:hidden的使用
一般来说,我们在使用Struts时,如果要在JSP隐式的传值给Action有两种情况: 1.要传的值是FromBean中的一个字段,你说的情况应该就是这种情况,例如需要在Edit页面中保存theID, ...
- POJ3208 Apocalypse Someday(二分 数位DP)
数位DP加二分 //数位dp,dfs记忆化搜索 #include<iostream> #include<cstdio> #include<cstring> usin ...