Html-webpack-plugin: Is it possible to indicate different inject position ('body' or 'head') to different chunk?

Created on 6 Dec 2017  路  10Comments  路  Source: jantimon/html-webpack-plugin

I have a multi-page project. Besides each page js file, each html file need import a common js file set_font.js. However, I need to put this set_font.js in <head> instead of <body> as it refers to style setting code to avoid screen flash or shake. This means for each html page. I have at lease 2 js file, for example:
for index.html, I need set_font.js and index.js, here is my config file

    entry: {
      'setFont': './src/util/set_font.js',
      'index': './src/pages/index/index.js',      
    },

    new HtmlWebpackPlugin({
        template: __dirname + '/src/views/index.html',
        chunks: ['setFont' , 'index'],
        inject: 'head'
      }),

now both js files are in the <head>. Any suggestions? Thanks!

Most helpful comment

You can set inject to false and use a template interpolation engine, the plugin supports lodash out of the box, to customize the insertion point. It is documented in README.md, under the section Writing Your Own Templates.

As an example, in my company we use Angular and Preact to power new and refactored sections in a Python web app that uses Mako as a render engine. Due to conflicts in syntax, we set our Webpack plugin configuration to

new HtmlWebpackPlugin({
      filename: OUTPUT_FILE,
      template: INPUT_FILE.tpl,
      hash: true,
      inject: false,
      minify: false,
})

And added a loader to customize lodash templates' interpolation:

{
      test: /\.tpl$/,
      loader: 'ejs-loader',
      query: {
        interpolate: /\{\{(.+)\}\}/g,
        escape: '<$-(.+?)$>',
        evaluate: /\[\[(.+)\]\]/g,
        engine: 'lodash',
      },
}

Then, inserting the scripts was as simple as looping htmlWebpackPlugin.files.js inside the template:

[[ for (var chunk in htmlWebpackPlugin.files.js) { ]] 
<script src="{{ htmlWebpackPlugin.files.js[chunk] }}"></script>
[[ } ]]

This is just a full example, but as you can see it is possible to do almost anything that you need just tinkering the webpack configuration in documented ways and using custom templates.

All 10 comments

I have a same question. I need to put the 'vendors.js' in , or before the 'app,js', but It seems no solution.

@ahutyaoyj
i found two solutions. First one, I used file-loader to import the js file in html

<script src="<%= require('file-loader?name=[name]_[hash:8].[ext]!../util/set_font.js') %>"></script>

Well, as my set_font.js has only 3 rows, I can also create a html template file by html-loader.

<!-- set_font.html -->
<script>
    var html = document.getElementsByTagName('html')[0];
    var width = html.getBoundingClientRect().width;
    html.style.fontSize = width/18.75 + 'px';
</script>

then, insert the following code in the html file

<!-- index.html -->
<%= require('html-loader!./set_font.html') %>

Now, I think the second solution is even better as long as the size of template file is small, as it reduces the number of http connection

You can adjust tags on html-webpack-plugin-alter-asset-tags event;

class FilterInjectionPlugin {
  constructor(opts) {
    const { bodyFilter, headFilter } = opts || {};
    this.bodyFilter = bodyFilter;
    this.headFilter = headFilter;
  }

  apply (compiler) {
    compiler.plugin('compilation', (compilation) => {
      compilation.plugin('html-webpack-plugin-alter-asset-tags', (object, callback) => {
        (this.bodyFilter || this.headFilter) && (() => {
          const head = [];
          const body = [];
          const allAssets = [...object.head, ...object.body];
          allAssets.forEach((asset) => {
            const { attributes: { src } } = asset;
            this.bodyFilter && this.bodyFilter(src) && body.push(asset);
            this.headFilter && this.headFilter(src) && head.push(asset);
            this.headFilter && !this.headFilter(src) && !this.bodyFilter && body.push(asset);
          })
          object.head = head;
          object.body = body;
        })();
        callback(null, object);
      });
    });
  }
}

module.exports = FilterInjectionPlugin;

You can set inject to false and use a template interpolation engine, the plugin supports lodash out of the box, to customize the insertion point. It is documented in README.md, under the section Writing Your Own Templates.

As an example, in my company we use Angular and Preact to power new and refactored sections in a Python web app that uses Mako as a render engine. Due to conflicts in syntax, we set our Webpack plugin configuration to

new HtmlWebpackPlugin({
      filename: OUTPUT_FILE,
      template: INPUT_FILE.tpl,
      hash: true,
      inject: false,
      minify: false,
})

And added a loader to customize lodash templates' interpolation:

{
      test: /\.tpl$/,
      loader: 'ejs-loader',
      query: {
        interpolate: /\{\{(.+)\}\}/g,
        escape: '<$-(.+?)$>',
        evaluate: /\[\[(.+)\]\]/g,
        engine: 'lodash',
      },
}

Then, inserting the scripts was as simple as looping htmlWebpackPlugin.files.js inside the template:

[[ for (var chunk in htmlWebpackPlugin.files.js) { ]] 
<script src="{{ htmlWebpackPlugin.files.js[chunk] }}"></script>
[[ } ]]

This is just a full example, but as you can see it is possible to do almost anything that you need just tinkering the webpack configuration in documented ways and using custom templates.

The easiest way to do this is to use templateParameters.

new HtmlWebpackPlugin({
  template: path.resolve(__dirname, './src/index.ejs'),
  inject: false,
  templateParameters: function(compilation, assets, options) {
    return {
      title: 'Document title',
      files: assets,
      options: options,
      webpackConfig: compilation.options,
      webpack: compilation.getStats().toJson()
    }
  }
})

Use it like this in the standard ejs template (assuming you have a chunk named "head" that you want to put in the <head>):

<!DOCTYPE html>
<html>

<head>
  <title><%= title %></title>
  <% files.css.forEach(function (src) { %>
    <link rel="stylesheet" href="<%= src %>">
  <% }) %>
  <% files.js.forEach(function (src) { %>
    <% if (src.indexOf('head') !== -1) { %>
      <script src="<%= src %>"></script>
    <% } %>
  <% }) %>
</head>

<body>
  <div id="app"></div>
  <% files.js.forEach(function (src) { %>
    <% if (src.indexOf('head') === -1) { %>
      <script src="<%= src %>"></script>
    <% } %>
  <% }) %>
</body>

</html>

@wolthers webpack: compilation.getStats().toJson() is quite slow so you should skip it if you don't need it inside the template

@jantimon Good tip. Was just trying to mimmick the default. As far as I can tell, the default does this already as per https://github.com/jantimon/html-webpack-plugin/blob/75eef8876bb54e80f9493a544759493ed385d7e0/index.js#L719

@wolthers yes but this was removed for html-webpack-plugin 4.x alpha

Alright :)

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings