当前位置:   article > 正文

Webpack源码分析 - loader及优化_webpack loader优化

webpack loader优化

loader解析文件是Webpack中重要的一环,之所以能一切皆模块就是因为有许多强大的loader提供的支持。了解它的工作原理可以让我们从容地为项目选择合适的配置,还可以更有目的性的针对性能瓶颈分析优化,更好地做一个合格地Webpack配置工程师。

如果要了解loader内部执行原理,可以看这篇文章loader-runner

loader基础

loader的配置非常灵活,以至于关于loader的代码有一大半是在解析参数,所以要看懂代码就要先清楚常用的配置,如果在翻阅代码时看着比较奇怪,可以先回来看看这段代码到底是处理哪一类配置,下面列举了一些常用的选项:

loader执行顺序

在执行loader前,Webpack已经将其根据配置排好序,指定顺序会在创建loader阶段执行:

  • 正常情况loader执行顺序: pre -> normal -> inline -> post
  • 资源路径前使用'xxx!=!'装饰: pre -> inline -> normal -> post
  • 资源路径前使用'-!'装饰: inline -> post
  • 资源路径前使用'!'装饰: pre -> inline -> post
  • 资源路径前使用'!!'装饰: inline

全局配置

  1. rules: [
  2. // 前置
  3. { enforce: 'pre', test: /\.js$/, use: 'babel-loader' },
  4. // 正则匹配
  5. { test: /\.js$/, use: 'babel-loader' },
  6. // 后置
  7. { enforce: 'post', test: /\.js$/, use: 'babel-loader' },
  8. ]
  9. 前端学习裙:950.919261
 

inline配置

// 普通
require('babel-loader!./increment.js')
// 多个loader,从右到左执行
require('style-loader!css-loader!less-loader!./increment.less')
// 带重命名!=!,交换normalLoader和inlineLoader执行顺序
require('aa.js!=!babel-loader!./increment.js')
// 带!!前缀只执行inline-loader
require('!!babel-loader!./increment.js')
复制代码

loader匹配条件

每个模块都会筛选出它需要的loader和相应的配置:

inline匹配

  1. // 普通
  2. require('babel-loader!./increment.js')
  3. // 带参数
  4. require('./myLoader?a=11&b=22!./increment.js')
  5. /**
  6. * 指定使用全局options配置
  7. * {
  8. * test: () => false,
  9. * use: { loader: 'babel-loader', ident: 'babelLoaderOptions', options: { presets: ['@babel/preset-env'] } }
  10. * }
  11. */
  12. require('babel-loader??babelLoaderOptions!./increment.js')

全局condition条件

条件 condition 可以是这些之一,用于匹配资源绝对路径:

  1. rules: [
  2. // 字符串匹配,前缀匹配
  3. { test: path.resolve('./src/index.js'), use: 'babel-loader' },
  4. // 正则匹配
  5. { test: /\.js$/, use: 'babel-loader' },
  6. // 函数匹配,返回true表示匹配成功
  7. { test: (resourcePath) => { return true }, use: 'babel-loader' },
  8. // 数组匹配,只要一个匹配条件算匹配成功
  9. { test: [/\.js$/, /\.ts$/], use: 'babel-loader' },
  10. // 对象匹配,匹配上所有条件算匹配成功
  11. { test: { or: [/\.js$/, /.ts$/], exclude: /node_modules/ }, use: 'babel-loader' },
  12. ]

resourceQuery

用于匹配路径参数,路径参数是引用资源时后面问号的内容,如 require('./a.js?matchMe )将能匹配下面的loader:

  1. {
  2. test: /.js$/,
  3. resourceQuery: /matchMe$/,
  4. use: 'babel-loader'
  5. }
 

oneOf

使用第一个成功匹配的规则:

  1. {
  2. test: /.css$/,
  3. oneOf: [
  4. // 匹配require('foo.css?inline')
  5. { resourceQuery: /inline/, use: 'url-loader' },
  6. // 匹配require('foo.css?external')
  7. { resourceQuery: /external/, use: 'file-loader' }
  8. ]
  9. }
 

其他

  • issure: 匹配引用这个资源的模块路径,如 foo.js , bar.js 同时引用了 inc.js ,可以指定只有 foo.js 才使用该loader。
  • compiler: 匹配编译器名,一般没啥用。

use配置

use 用于指定匹配成功后需要用到哪些loader解析:

  1. // 字符串指定
  2. use: 'babel-loader'
  3. // 数组指定: 右到左执行
  4. use: ['style-loader', 'css-loader']
  5. // 对象指定,带参数
  6. use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } }
  7. // 数组混合
  8. use: ['style-loader', { loader: 'css-loader', options: { modules: true } }, 'postcss-loader' ]

Loader处理流程

Webpack对 loader 的处理主要在模块构建期间,另外做了缓存和监听操作,主要有下面三个流程:

  • RuleSet :初始化时解析loader相关配置;
  • NormalModuleFactory.create :根据配置匹配当前模块用到的loader,并将初始化好的 loader 数组通过构造函数传入;
  • NormalModule :构建模块时使用 loader-runner 解析loader,获取处理后的文件内容;
  1. class NormalModuleFactory extends Tapable {
  2. constructor(context, resolverFactory, options) {
  3. super();
  4. // 参数解析
  5. this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
  6. }
  7. create(data, callback) {
  8. // 解析inline-loader/normal-loader
  9. resolver(data, (result) => {
  10. // ...使用解析好的loaders创建NormalModule
  11. createdModule = new NormalModule({
  12. loaders: result.loaders,
  13. resource: result.resource,
  14. });
  15. // 返回创建好的NormalModule
  16. callback(null, createdModule)
  17. })
  18. }
  19. }
  20. class NormalModule extends Module {
  21. constructor({ loaders, resource }) {
  22. this.loaders = loaders
  23. // 文件路径
  24. this.resource = resource
  25. }
  26. // 构建module
  27. doBuild(options, compilation, resolver, fs, callback) {
  28. // 上下文,loader里的this指向这个对象
  29. const loaderContext = this.createLoaderContext(resolver, options, compilation, fs);
  30. // 调用loader-runner解析loader
  31. runLoaders({
  32. resource: this.resource,
  33. loaders: this.loaders,
  34. context: loaderContext,
  35. readResource: fs.readFile.bind(fs)
  36. }, (err, result) => {
  37. if (result) {
  38. // 缓存
  39. this.buildInfo.cacheable = result.cacheable;
  40. // 文件监听依赖
  41. this.buildInfo.fileDependencies = new Set(result.fileDependencies);
  42. // 文件夹监听依赖
  43. this.buildInfo.contextDependencies = new Set(result.contextDependencies);
  44. }
  45. // 最原始的文件数据buffer
  46. const resourceBuffer = result.resourceBuffer;
  47. // loader转换后的文件内容
  48. const source = result.result[0];
  49. // 有传出sourceMap就在这里取
  50. const sourceMap = result.result.length >= 1 ? result.result[1] : null;
  51. // 其他的额外内容,如解析出的AST
  52. const extraInfo = result.result.length >= 2 ? result.result[2] : null;
  53. this._ast = extraInfo.webpackAST
  54. return callback();
  55. });
  56. }
  57. }
 

RuleSet配置解析

参数解析应该是这里面最繁琐的阶段,因为支持的配置多所以解析起来麻烦,基本上都是规范化配置,使最终得到统一格式,下面分别看看这里面的关键函数:

normalizeRule

用于规范化单个规则,有一大半代码是在做兼容处理:

  1. normalizeRule(rule, refs, ident) {
  2. const newRule = {}
  3. // ...
  4. if (rule.test || rule.include || rule.exclude) {
  5. // 检测同级互斥关系:(rule.test || rule.include || rule.exclude) 和 resource 只能存在一项
  6. checkResourceSource("test + include + exclude");
  7. condition = {
  8. test: rule.test,
  9. include: rule.include,
  10. exclude: rule.exclude
  11. };
  12. newRule.resource = RuleSet.normalizeCondition(condition);
  13. }
  14. // 和(rule.test || rule.include || rule.exclude)做的事情一样,兼容写法
  15. if (rule.resource) {
  16. checkResourceSource("resource");
  17. newRule.resource = RuleSet.normalizeCondition(rule.resource);
  18. }
  19. if (rule.use) {
  20. // 检测同级互斥关系: use, loaders, loader, loader + options/query 配置只能存在一项
  21. checkUseSource("use");
  22. // 规范化use配置
  23. newRule.use = RuleSet.normalizeUse(rule.use, ident);
  24. }
  25. // 将有自定义id的规则记录下来,可用于替换inline-loader的options
  26. if (Array.isArray(newRule.use)) {
  27. for (const item of newRule.use) {
  28. if (item.ident) {
  29. refs[item.ident] = item.options;
  30. }
  31. }
  32. }
  33. return newRule;
  34. }
 

normalizeCondition

用于输出匹配条件,我们在规则上配置的 test, include, exclude, and, or not 将在这里转换成匹配函数,用于解析某个资源时匹配是否需要这个loader:

  1. normalizeCondition(condition) {
  2. // 字符条件串兼容
  3. if (typeof condition === "string") return str => str.indexOf(condition) === 0;
  4. // 函数条件串兼容
  5. if (typeof condition === "function") return condition;
  6. // 正则条件兼容
  7. if (condition instanceof RegExp) return condition.test.bind(condition);
  8. // 条件数组兼容
  9. if (Array.isArray(condition)) return orMatcher(condition.map(c => normalizeCondition(c)));
  10. // 对象条件兼容
  11. const matchers = [];
  12. Object.keys(condition).forEach(key => {
  13. const value = condition[key];
  14. switch (key) {
  15. case "or": case "include": case "test":
  16. if (value) matchers.push(normalizeCondition(value));
  17. break;
  18. case "and":
  19. if (value) matchers.push(andMatcher(value.map(c => normalizeCondition(c))));
  20. break;
  21. case "not": case "exclude":
  22. if (value) matchers.push(notMatcher(normalizeCondition(value)));
  23. break;
  24. }
  25. });
  26. return andMatcher(matchers);
  27. }
  28. notMatcher = matcher => str => !matcher(str);
  29. orMatcher = items => str => {
  30. for (let i = 0; i < items.length; i++) {
  31. if (items[i](str)) return true;
  32. }
  33. return false;
  34. };
  35. andMatcher = items => str => {
  36. for (let i = 0; i < items.length; i++) {
  37. if (!items[i](str)) return false;
  38. }
  39. return true;
  40. };
 

inline-loader解析

行内loader指直接在路径名前添加loader的情况,如 require('!babel-loader!./increment.js') :

  1. resolver(data, callback) {
  2. // ...
  3. // 将以!=!开头的资源名去除,得到带有inline-loader信息的资源路径
  4. // eg: 'haha!=!babel-loader!./increment.js' => 'babel-loader!./increment.js'
  5. requestWithoutMatchResource = handleMatchResourcde(data.request)
  6. // 分割出来所有的inline-loader
  7. let elements = requestWithoutMatchResource
  8. .replace(/^-?!+/, "")
  9. .replace(/!!+/g, "!")
  10. .split("!");
  11. let resource = elements.pop();
  12. // 分离inline-loader带的query参数
  13. // eg: babel-loader?a=1!./increment.js => [ { loader: 'babel-loader', options: 'a=1' } ]
  14. elements = elements.map(identToLoaderRequest);
  15. // 获取loader的路径
  16. this.resolveRequestArray(contextInfo, context, elements, loaderResolver, (err, loaders) => {
  17. // 如果指定了使用某个定义的options,替换为自定义选项
  18. // eg: babel-loader??myOptions./increment.js 将会使用配置中 rules.use.ident=myOptions 的选项
  19. for (const item of loaders) {
  20. if (typeof item.options === "string" && item.options[0] === "?") {
  21. const ident = item.options.substr(1);
  22. item.options = this.ruleSet.findOptionsByIdent(ident);
  23. item.ident = ident;
  24. }
  25. }
  26. })
  27. // ... 解析RuleSet里的loader
  28. }
 

全局loader解析

  1. resolver(data, callback) {
  2. // ...
  3. // 忽略normalLoader和preLoader
  4. const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
  5. // 忽略normalLoader
  6. const noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
  7. // 忽略normalLoader和preLoader和postLoader
  8. const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
  9. // RuleSet匹配loader
  10. const result = this.ruleSet.exec({
  11. resource: resource,
  12. realResource: resource.replace(/\?.*/, ""),
  13. resourceQuery,
  14. issuer: contextInfo.issuer,
  15. compiler: contextInfo.compiler
  16. });
  17. // 三种优先级loader在这里匹配
  18. const useLoadersPost = [];
  19. const useLoaders = [];
  20. const useLoadersPre = [];
  21. for (const r of result) {
  22. if (r.type === "use") {
  23. if (r.enforce === "post" && !noPrePostAutoLoaders) {
  24. useLoadersPost.push(r.value);
  25. } else if (r.enforce === "pre" && !noPreAutoLoaders && !noPrePostAutoLoaders) {
  26. useLoadersPre.push(r.value);
  27. } else if (!r.enforce && !noAutoLoaders && !noPrePostAutoLoaders) {
  28. useLoaders.push(r.value);
  29. }
  30. }
  31. }
  32. // 解析三种loader完整路径
  33. const postLoaders = this.resolveRequestArray(contextInfo, this.context, useLoadersPost, loaderResolver)
  34. const defaultLoaders = this.resolveRequestArray(contextInfo, this.context, useLoaders, loaderResolver)
  35. const preLoaders = this.resolveRequestArray(contextInfo, this.context, useLoadersPre, loaderResolver)
  36. if (matchResource === undefined) {
  37. // post -> inline -> normal -> pre
  38. loaders = postLoaders.concat(inlineLoaders, defaultLoaders, preLoaders);
  39. } else {
  40. // post -> normal -> inline -> pre
  41. loaders = postLoaders.concat(defaultLoaders, inlineLoaders, preLoaders);
  42. }
  43. callback(null, {
  44. loaders,
  45. resource,
  46. })
  47. }
 

RuleSet匹配loader

  1. _run(data, rule, result) {
  2. // 由于规则已经在解析配置时转换成了函数,所以这里使用函数调用方式判断是否需要该loader
  3. if (rule.resource && !rule.resource(data.resource)) return false;
  4. if (rule.realResource && !rule.realResource(data.realResource)) return false;
  5. if (data.issuer && rule.issuer && !rule.issuer(data.issuer)) return false;
  6. if (data.resourceQuery && rule.resourceQuery && !rule.resourceQuery(data.resourceQuery)) return false;
  7. if (data.compiler && rule.compiler && !rule.compiler(data.compiler)) return false;
  8. // use的值可以是对象,数组或函数就是在这里做的兼容
  9. // 如果资源匹配上,加到结果集里
  10. if (rule.use) {
  11. const process = use => {
  12. if (typeof use === "function") {
  13. process(use(data));
  14. } else if (Array.isArray(use)) {
  15. use.forEach(process);
  16. } else {
  17. result.push({ type: "use", value: use, enforce: rule.enforce });
  18. }
  19. };
  20. process(rule.use);
  21. }
  22. // 循环匹配
  23. if (rule.rules) {
  24. for (let i = 0; i < rule.rules.length; i++) {
  25. this._run(data, rule.rules[i], result);
  26. }
  27. }
  28. // 只要有一个rule匹配上就使用该loader
  29. if (rule.oneOf) {
  30. for (let i = 0; i < rule.oneOf.length; i++) {
  31. if (this._run(data, rule.oneOf[i], result)) break;
  32. }
  33. }
  34. return true;
  35. }
 

resolveRequestArray

NormalModuleFactory.resolveRequestArray 主要处理以下功能:

  • 获取loader的完整路径地址
  • 兼容处理不规范的配置写法
  1. resolveRequestArray(contextInfo, context, array, resolver, callback) {
  2. // 循环所有loader配置
  3. asyncLib.map(array, (item, callback) => {
  4. // 解析loader完整路径地址
  5. resolver.resolve(contextInfo, context, item.loader, {}, (err, result) => {
  6. // 如果没找到loader是因为省略了-loader,抛出异常提示
  7. if (err && /^[^/]*$/.test(item.loader) && !/-loader$/.test(item.loader)) {
  8. return resolver.resolve(contextInfo, context, item.loader + "-loader", {}, err2 => {
  9. if (!err2) {
  10. err.message = err.message + "loader不支持省略 '-loader' 后缀 \n" +
  11. `You need to specify '${item.loader}-loader' instead of '${item.loader}',\n`
  12. }
  13. callback(err);
  14. });
  15. }
  16. if (err) return callback(err);
  17. // 格式化并输出结果
  18. const optionsOnly = item.options ? { options: item.options } : undefined;
  19. callback(null, Object.assign({}, item, identToLoaderRequest(result), optionsOnly))
  20. });
  21. }, callback)
  22. }
 

输出结果

result[source, sourceMap, extraInfo]

  • source: 是经过loader处理后的内容;
  • sourceMap: 如果有sourceMap,可以在这里获取;
  • extraInfo: 这里放额外的输出内容,比如AST等;

resourceBuffer

这是最原始文件的资源,没有经过loader

fileDependencies / contextDependencies

如果文件需要 foo-loader 处理,那么 foo-loader 会默认将该文件添加到自己的依赖上,开启文件监听后如果文件改变,那么会重新执行 foo-loader 。

默认情况下loader只会添加匹配到的文件作为依赖,如果在loader执行过程中,需要用到其他文件如 data.txt ,且更新后要重新输出结果,那么可以使用 this.addDependency 来将其添加为依赖项,这样 data.txt 更新后会重新执行loader输出。

cacheable

如果一个loader的输入和相关依赖没变化时输出结果不变,那么这个loader应该设置为可以缓存,在解析完所有loader后,会将结果缓存下来。

在监听文件情况下如果输出结果没变,文件不会重新输出。

默认情况下loader会开启缓存,在loader中可以使用 this.cacheable(false) 关闭缓存。

优化

loader是打包耗时的大块头,比如 babel-loader 在执行时能明显感觉到非常慢,所以了解了loader的基本原理,我们就可以针对性的对我们的项目做些优化:

缩小loader作用范围

最主要的优化还是在排除掉不必要的解析,用好 exclude 等选项基本上能满足大多数场景。

loader缓存

有些loader本身提供了缓存的功能,比如 babel-loader 的 cacheDirectory 等,使用loader时需要我们熟悉它们的配置。

预编译

DllPlugin 用于将代码预先打包抽离出来,使用时Webpack将不会重新编译直接引用。对于我们项目中的代码,不能用排除方法去除loader时,可以考虑先将比较稳定的代码预先编译一次,下次使用就可以不需要经过loader直接使用啦。

多进程编译

thread-loader 或 HappyPack 可以让Webpack使用多个进程同时对文件执行loader。我们知道node是单线程模型,用一个线程处理当然很慢,如果能发挥多核CPU并行优势同时编译的话,编译速度能快不少。使用时要注意它们和loader的兼容性。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/502303
推荐阅读
相关标签
  

闽ICP备14008679号