webpack源码解析二(html-webpack-plugin插件)

我们接着上一节从源码研究一下html-webpack-plugin插件,感兴趣的童鞋可以看一下我上一篇文章 webpack源码解析一
文章开始先上一下源码:html-webpack-plugin源码地址,不废话,我们开车咯~~

首先安装:

yarn add html-webpack-plugin

配置webpack.config.js:

	plugins: [
		new HtmlPlugin({
			title: 'webpack demo',
			template: './index.html'
		})
	],

因为我是直接在webpack的源码工程项目,所以我就直接执行打包脚本了
执行打包命令():

./bin/webpack.js

我们可以看到报错了:

TypeError: Cannot add property htmlWebpackPluginAlterChunks, object is not extensible
    at compiler.hooks.compilation.tap.compilation (/Users/yinqingyang/ThirdProject/webpack/webpack/node_modules/html-webpack-plugin/index.js:59:56)
 

想必小伙伴都认识这个错误,当某个对象用了Object.freeze方法后,我们再对这个对象做扩展的时候就会报这个错误
本次demo我们用的webpack的源码版本号是:5.0.0-beta.9,html-webpack-plugin的版本号是:3.2.0
所以html-webpack-plugin还不是完全兼容webpack5.0.0-beta.9,为了测试,我们修改一下webpack的源码:

lib/Compilation.js

class Compilation {
	/**
	 * Creates an instance of Compilation.
	 * @param {Compiler} compiler the compiler which created the compilation
	 */
	constructor(compiler) {
		const getNormalModuleLoader = () => deprecatedNormalModuleLoaderHook(this);
		this.hooks = {
		}

我们直接去掉了对this.hooks的Object.freeze操作

我们继续执行打包脚本:

./bin/webpack.js

又报错了(哈哈,看来html-webpack-plugin想要兼容webpack5.0还需要改一波代码呀,我们做个好心人帮它改改把):

TypeError: this.cache.get is not a function
    at asyncLib.forEach (/Users/yinqingyang/ThirdProject/webpack/webpack/lib/Compilation.js:2337:18)
    at arrayEach (/Users/yinqingyang/ThirdProject/webpack/webpack/node_modules/neo-async/async.js:2405:9)

html-webpack-plugin/lib/compiler.js:

在这里插入图片描述
可以看到,我们注释掉了一段代码~

我们再次运行打包脚本:

./bin/webpack.js

在这里插入图片描述
在这里插入图片描述

可以看到,我们打包过程已经ok了,也自动生成了index.html文件

下面我们正式走一遍html-webpack-plugin的源码,在分析源码之前我们先理一下我们的思路,通过上一节 webpack源码解析一我们知道,想要解析目标文件得有对应的loader加载器,比如webpack内置的js、json加载器(上一节有介绍,我就不重复介绍了),同样! 想要加载我们的html文件的话也要有对应的htmlloader才行,然后加载完html文件后,我们肯定得对加载的文件做处理,比如我们写在index.html中的<%=htmlWebpackPlugin.options.title%>跟<%=htmlWebpackPlugin.options.author%>:

  <meta charset="UTF-8">
		<title><%=htmlWebpackPlugin.options.title%></title>
    <meta name="author" content="<%=htmlWebpackPlugin.options.author%>">

然后当我们配置在webpack.config.js的时候:

plugins: [
		new HtmlPlugin({
			title: 'webpack demo',
			template: './demo/index.html',
			author: 'yasin'
		})
	],

我们最后打包看到index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
		<title>webpack demo</title>
    <meta name="author" content="yasin">
</head>
<body>
	<div id="container"></div>
	<noscript>we're sorry but webpack demo doesn't work properly without javascript enabled. please enable it to continue</noscript>
<script type="text/javascript" src="main.js"></script></body>
</html>

可以看到,我们设置的变量被自动替换了~

好啦! 我们从webpack编译开始分析html-webpack-plugin~

webpack.config.js配置插件

/**
 * @author YASIN
 * @version [React-Native V01, 2019/12/11]
 * @date 2019/12/11
 * @description webpack.config
 */
const path = require("path");
const HtmlPlugin = require("html-webpack-plugin");
module.exports = {
	entry: "./demo/src/main.js",
	output: {
		path: path.resolve(__dirname, "./demo/dist"),
	},
	module: {
		rules: [
			{
				test: /\.js$/,
				use: {
					loader: "babel-loader",
					options: {
						presets: [
							"@babel/env"
						]
					}
				},
				exclude: /node_modules/
			}
		]
	},
	plugins: [
		new HtmlPlugin({
			title: 'webpack demo',
			template: './demo/index.html',
			author: 'yasin'
		})
	],
	mode: "development"
};

执行脚本后webpack就会创建一个Compiler对象,然后执行所有的插件(webpack流程不是很熟的童鞋可以看我上一篇文章)

lib/webpack.js

const createCompiler = options => {
	options = new WebpackOptionsDefaulter().process(options);
	const compiler = new Compiler(options.context);
	//执行所有的插件
	if (Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				plugin.call(compiler, compiler);
			} else {
				plugin.apply(compiler);
			}
		}
	}
};

html-webpack-plugin/index.js:

apply (compiler) {
    //获取带有loader的文件全路径
    this.options.template = this.getFullTemplatePath(this.options.template, compiler.context);
    ....
  }
   getFullTemplatePath (template, context) {
    // If the template doesn't use a loader use the lodash template loader
    if (template.indexOf('!') === -1) {
      template = require.resolve('./lib/loader.js') + '!' + path.resolve(context, template);
    }
    // Resolve template path
    return template.replace(
      /([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
      (match, prefix, filepath, postfix) => prefix + path.resolve(filepath) + postfix);
  }

首先第一行就很关键, this.getFullTemplatePath这个方法返回的是“获取带有loader的文件全路径”,什么叫“带有loader的文件全路径”?有什么用呢?

比如我们加载的是index.html文件,然后调用了getFullTemplatePath方法后返回的是:

      html-webpack-plugin/lib/loader.js!index.html
	

“!”符号前面的表示“加载器”,“!”符号后面表示需要加载的文件,那到底是不是这样呢?其实上一节中我们也是带过了一下,并没有解释完全,好了,我们再走一遍webpack的loader。

lib/NormalModuleFactory.js:

	this.hooks.resolve.tapAsync(
			/** @type {TODO} */ ({
				name: "NormalModuleFactory",
				stage: 100
			}),
			(data, callback) => { 
		const rawElements = requestWithoutMatchResource
					.slice(
						noPreAutoLoaders || noPrePostAutoLoaders ? 2 : noAutoLoaders ? 1 : 0
					)
					.split(/!+/);
				const unresolvedResource = rawElements.pop();
				const elements = rawElements.map(identToLoaderRequest);
}

可以看到webpack的模块resolve获取模块的loader的时候是以“!”符号切割的,然后获取模块对应的loader加载器

好啦! 说完html-webpack-plugin的getFullTemplatePath方法后,我们继续往下走~

html-webpack-plugin/index.js:

apply (compiler) {
    this.options.template = this.getFullTemplatePath(this.options.template, compiler.context);
	// 注册一个compiler.hooks.make的钩子函数
    (compiler.hooks ? compiler.hooks.make.tapAsync.bind(compiler.hooks.make, 'HtmlWebpackPlugin') : compiler.plugin.bind(compiler, 'make'))((compilation, callback) => {
      // 开始编译模版html文件
      compilationPromise = childCompiler.compileTemplate(self.options.template, compiler.context, self.options.filename, compilation)
        .then(compilationResult => {
          // 获取编译结果
          callback();
          return compilationResult.content;
        });
    });
}

那么“compiler.hooks.make”是干什么呢?

lib/Compiler.js(上一节有解释):

	const params = this.newCompilationParams();
		this.hooks.beforeCompile.callAsync(params, err => {
			
			logger.time("make hook");
			//开始执行打包
			this.hooks.make.callAsync(compilation, err => {

当调用this.hooks.make的时候EntryPlugin开始根据入口文件信息开始打包操作~
从webpack的源码我们可以知道,我们自定的插件是先于默认的插件执行的,所以我们会在EntryPlugin开始前执行我们的make钩子函数,这个时候webpack处于一个打包前的状态。

html-webpack-plugin/lib/compiler.js:

module.exports.compileTemplate = function compileTemplate (template, context, outputFilename, compilation) {
//创建一个跟当前Compiler一样配置的Compiler(childCompiler)对象
const childCompiler = compilation.createChildCompiler(compilerName, outputOptions);
  childCompiler.context = context;
	// Compile and return a promise
  return new Promise((resolve, reject) => {
  	//开始执行childCompiler编译器的run操作(独立于主编译器外的编译器)
    childCompiler.runAsChild((err, entries, childCompilation) => {
        resolve({
          // Hash of the template entry point
          hash: entries[0].hash, //当前模块(index.html)的hash值
          // Output name
          outputName: outputName, //当前模块(index.html)的名称
          // Compiled code
          content: childCompilation.assets[outputName].source()//当前模块(index.html)编译过后的源码
        });
}

经过了html-webpack-plugin/lib/compiler.js的compileTemplate方法后,其实我们已经加载了我们的模版index.html文件,
最后在webpack生成打包文件之前,我们需要获取所有的打包文件,然后动态的添加到我们的index.html模版文件中。

html-webpack-plugin/index.js:

 apply (compiler) {
	    (compiler.hooks ? compiler.hooks.emit.tapAsync.bind(compiler.hooks.emit, 'HtmlWebpackPlugin') : compiler.plugin.bind(compiler, 'emit'))((compilation, callback) => {
	    	
	    }
}

可以看到,注册了一个“hooks.emit”钩子函数,那么“hooks.emit”钩子函数又是哪调用的呢?上一节有介绍的,当webpack编译完项目所有的模块后,然后就是输出打包文件,在生成打包文件前会调用“hooks.emit”钩子函数

lib/Compiler.js:

emitAssets(compilation, callback) {
		let outputPath;this.hooks.emit.callAsync(compilation, err => {
			if (err) return callback(err);
			outputPath = compilation.getPath(this.outputPath, {});
			mkdirp(this.outputFileSystem, outputPath, emitFiles);
		});
	}

那我们要在这个时候干什么呢?
1、我们需要设置在模版index.html的参数、设置favicon、把生成的assets(js、css)资源文件放入模版index.html代码
2、我们要把我们处理完毕后的html模块放到webpack的主加载器模块中去,当作webpack主加载器的assets资源文件
3、通知webpack可以生成打包文件了

1、我们需要设置在模版index.html的参数、设置favicon、把生成的assets(js、css)资源文件放入模版index.html代码

 /**
   * Evaluates the child compilation result
   * Returns a promise
   */
  evaluateCompilationResult (compilation, source) {
    if (!source) {
      return Promise.reject('The child compilation didn\'t provide a result');
    }

    //创建一个上下文环境
    const vmContext = vm.createContext(_.extend({HTML_WEBPACK_PLUGIN: true, require: require}, global));
    //用node的vm创建一个可执行代码环境
    const vmScript = new vm.Script(source, {filename: template});
    // Evaluate code and cast to string
    let newSource;
    try {
    //执行代码获取(构建index.html源码的方法)
      newSource = vmScript.runInContext(vmContext);
    } catch (e) {
      return Promise.reject(e);
    }
    if (typeof newSource === 'object' && newSource.__esModule && newSource.default) {
      newSource = newSource.default;
    }
    return typeof newSource === 'string' || typeof newSource === 'function'
      ? Promise.resolve(newSource)
      : Promise.reject('The loader "' + this.options.template + '" didn\'t return html.');
  }

那么我们这里说的源码指的是什么呢? 还记得我们一开始说的那个loader吗?
对的! 源码其实就是loader加载的

html-webpack-plugin/lib/loader.js:

const _ = require('lodash');
const loaderUtils = require('loader-utils');

module.exports = function (source) {
  if (this.cacheable) {
    this.cacheable();
  }
  const allLoadersButThisOne = this.loaders.filter(function (loader) {
    // Loader API changed from `loader.module` to `loader.normal` in Webpack 2.
    return (loader.module || loader.normal) !== module.exports;
  });
  // This loader shouldn't kick in if there is any other loader
  if (allLoadersButThisOne.length > 0) {
    return source;
  }
  // Skip .js files
  if (/\.js$/.test(this.resourcePath)) {
    return source;
  }

  // The following part renders the tempalte with lodash as aminimalistic loader
  //
  // Get templating options
  const options = this.query !== '' ? loaderUtils.parseQuery(this.query) : {};
  const template = _.template(source, _.defaults(options, { variable: 'data' }));
  // Require !!lodash - using !! will disable all loaders (e.g. babel)
  return 'var _ = require(' + loaderUtils.stringifyRequest(this, '!!' + require.resolve('lodash')) + ');' +
    'module.exports = function (templateParams) { with(templateParams) {' +
      // Execute the lodash template
      'return (' + template.source + ')();' +
    '}}';
};

这里用到了loadsh的template方法去加载模版代码的,也就是我们在index.html中设置的:

<%=htmlWebpackPlugin.options.title%>
<%=htmlWebpackPlugin.options.author%>

loadsh的template方法我就不做解释了,小伙伴自己去看loadsh的文档。

通过loader加载之后我们获取到的源码:

return 'var _ = require(' + loaderUtils.stringifyRequest(this, '!!' + require.resolve('lodash')) + ');' +
    'module.exports = function (templateParams) { with(templateParams) {' +
      // Execute the lodash template
      'return (' + template.source + ')();' +
    '}}';

我们可以简单的看到我们的index.html文件经过loader之后变成了node里面的一个模块,然后是以commonjs的方式,通过传入的templateParams建议一个当前this环境,然后获取templateParams里面的属性,所以我们才能在index.html中用“htmlWebpackPlugin.options.title”这种表达式。

html-webpack-plugin/index.js:

 /**
   * 把webpack的所以assets插入到html文件中
   */
  generateHtmlTags (assets) {
    // Turn script files into script tags
    const scripts = assets.js.map(scriptPath => ({
      tagName: 'script',
      closeTag: true,
      attributes: {
        type: 'text/javascript',
        src: scriptPath
      }
    }));
    // Make tags self-closing in case of xhtml
    const selfClosingTag = !!this.options.xhtml;
    // Turn css files into link tags
    const styles = assets.css.map(stylePath => ({
      tagName: 'link',
      selfClosingTag: selfClosingTag,
      voidTag: true,
      attributes: {
        href: stylePath,
        rel: 'stylesheet'
      }
    }));
    // Injection targets
    let head = this.getMetaTags();
    let body = [];

    // If there is a favicon present, add it to the head
    if (assets.favicon) {
      head.push({
        tagName: 'link',
        selfClosingTag: selfClosingTag,
        voidTag: true,
        attributes: {
          rel: 'shortcut icon',
          href: assets.favicon
        }
      });
    }
    // Add styles to the head
    head = head.concat(styles);
    // Add scripts to body or head
    if (this.options.inject === 'head') {
      head = head.concat(scripts);
    } else {
      body = body.concat(scripts);
    }
    return {head: head, body: body};
  }

2、我们要把我们处理完毕后的html模块放到webpack的主加载器模块中去,当作webpack主加载器的assets资源文件

html-webpack-plugin/index.js:

 .then(html => {
          // Replace the compilation result with the evaluated html code
          compilation.assets[self.childCompilationOutputName] = {
            source: () => html,
            size: () => html.length
          };
        })

3、通知webpack可以生成打包文件了

  // Let webpack continue with it
        .then(() => {
          callback();
        });

好啦! html-webpack-plugin所有流程跟源码我们就分析完毕了

之后还会把webpack的分包、优化等等插件分析一波

欢迎志同道合的童鞋一起学习,一起进步,欢迎入群!!

晚安~~

vv_小虫 CSDN认证博客专家 JavaScript CSS 前端框架
6 年开发经验,前端架构师,目前主要负责企业级应用前端技术平台建设工作,在前端工程化实现、Node 应用开发、Android 技术、Vue 技术、React 技术、移动开发等方向有丰富实践。
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:白松林 返回首页