如何在 Webpack 4 中从 header/footer(和 css/js 注入)为静态页面生成 html 模板(lodash 模板不起作用)?

How to generate html template for static page from header/footer (and css/js inject) in Webpack 4 (lodash template not working)?

我正在尝试使用 webpack 4 构建静态 html 页面。我正在使用 html-webpack-plugin。我想定义一些 header.htmlfooter.html 并稍后在所有 html 页面中导入它们。但我还希望我的输出 js 和 css 文件自动注入此页眉和页脚。像这样:

header.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Some Title</title>
    <% for(var i=0; i < htmlWebpackPlugin.files.css.length; i++) {%>
        <link type="text/css" rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[i] %>">
    <% } %>
</head>
<body>

footer.html:

    <% for(var i=0; i < htmlWebpackPlugin.files.js.length; i++) {%>
        <script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[i] %>"></script>
    <% } %>
</body>
</html>

index.hbs(或index.html):

<%= _.template(require('./../includes/header.html'))() %>

    <div class="content">
        <img src="img/bg/desktop/bg-index-03.jpg" width="500" height="500"/>
        <div class="imgtest"></div>
    </div> <!-- .content -->

<%= _.template(require('./../includes/footer.html'))() %>

但我遇到了 dist/index.html 一些错误。这些行仍然相同,不加载 header/footer 到 index.html:

<%= _.template(require('./../includes/header.html'))() %>
...
<%= _.template(require('./../includes/footer.html'))() %>

出于某种原因,lodash 不起作用...

更新:

我把src/html/views/index.hbs改成了src/html/views/index.html (在插件中)

new HtmlWebpackPlugin({
  template: './src/html/views/index.html',
...

在此处添加 include 行:

{
    test: /\.html$/,
    include: path.resolve(__dirname, 'src/html/includes'),
    use: ['html-loader']
},

在 header/footer 中,我删除了 lodash 代码 <% ... %>,只留下干净的 html - 这样就可以了! Header/footer 在 index.html 中导入,但没有 css/js.

如果我 return lodash 回到 footer.html(或 header.html)

<% for(var i=0; i < htmlWebpackPlugin.files.js.length; i++) {%>
    <script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[i] %>"></script>
<% } %>

我收到错误:

ERROR in Template execution failed: ReferenceError: htmlWebpackPlugin is not defined

ERROR in   ReferenceError: htmlWebpackPlugin is not defined

  - lodash.templateSources[2]:10 eval
    lodash.templateSources[2]:10:19

  - index.html:102 
    D:/.../src/html/views/index.html:102:110

  - index.html:104 ./node_modules/html-webpack-plugin/lib/loader.js!./src/html/views/index.html.module.exports
    D:/.../src/html/views/index.html:104:3

  - index.js:393 
    [project]/[html-webpack-plugin]/index.js:393:16

  - runMicrotasks

  - task_queues.js:93 processTicksAndRejections
    internal/process/task_queues.js:93:5

  - async Promise.all

为什么会这样?怎么了?如何让 lodash 在 header/footer?

中工作

或者生成 html 模板的最佳方法是什么?

文件树:

dist
│   index.html
├───css
│       main.css
│       main.css.map
├───fonts
├───img
├───js
│       main.js
│       main.js.map
│       vendors~main.js
│       vendors~main.js.map
src
├───favicon
├───fonts
├───html
│   ├───includes
│   │       footer.html
│   │       header.html
│   └───views
│           index.hbs
├───img
├───js
│       index.js
├───scss
│       fonts.scss
│       icomoon.scss
│       style.scss
package.json
package-lock.json
webpack.config.js

webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const OptimizeCssAssetWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

const isDev = process.env.NODE_ENV === 'development';
const isProd = !isDev;

const optimization = () => {
  const config = {
    splitChunks: {
      chunks: 'all'
    }
  }
  if (isProd) {
    config.minimizer = [
      new OptimizeCssAssetWebpackPlugin(),
      new TerserWebpackPlugin()
    ]
  }
  return config
}

const filename = ext => isDev ? `[name].${ext}` : `[name].[hash].${ext}`;

const cssLoaders = extra => {
  const loaders = [
    {
      loader: MiniCssExtractPlugin.loader,
      options: {
        hmr: isDev,
        reloadAll: true
      },
    },
    {
      loader: 'css-loader',
      options: {
        url: false
      }
    }
  ];
  if (extra) {
    loaders.push(extra)
  }
  return loaders
}

const babelOptions = preset => {
  const opts = {
    presets: [
      '@babel/preset-env'
    ],
    plugins: [
      '@babel/plugin-proposal-class-properties'
    ]
  }

  if (preset) {
    opts.presets.push(preset)
  }

  return opts
}

module.exports = {
  mode: 'development',
  entry: [
    '@babel/polyfill',
    './src/js/index.js'
  ],
  output: {
    filename: 'js/' + filename('js'),
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    port: 4250,
    hot: isDev
  },
  devtool: isDev ? 'source-map' : '',
  resolve: {
    //extensions: ['.js', '.json', '.png'],
    alias: {
      '@views': path.resolve(__dirname, 'src/html/views'),
      '@includes': path.resolve(__dirname, 'src/html/includes'),
      '@scss': path.resolve(__dirname, 'src/scss'),
      '@': path.resolve(__dirname, 'src'),
    }
  },
  optimization: optimization(),
  module: {
    rules: [
      {
        test: /\.html$/,
        //include: path.resolve(__dirname, 'src/html/includes'),
        use: ['html-loader']
      },
      {
        test: /\.hbs$/,
        loader: 'handlebars-loader'
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: {
          loader: 'babel-loader',
          options: babelOptions()
        }
      },
      {
        test: /\.css$/,
        use: cssLoaders()
      },
      {
        test: /\.s[ac]ss$/,
        use: cssLoaders('sass-loader')
      },
      {
        test: /\.(png|jpg|svg|gif)$/,
        exclude: path.resolve(__dirname, 'src/fonts'),
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
              outputPath: path.resolve(__dirname, 'dist/img')
            }
          }
        ]
      },
      {
        test: /\.(ttf|otf|svg|woff|woff2|eot)$/,
        exclude: path.resolve(__dirname, 'src/img'),
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
              outputPath: path.resolve(__dirname, 'dist/fonts')
            }
          }
        ]
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/html/views/index.hbs',
      minify: {
        collapseWhitespace: isProd
      },
      inject: false
    }),
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: 'css/' + filename('css'),
    }),
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, 'src/favicon'),
        to: path.resolve(__dirname, 'dist')
      },
      {
        from: path.resolve(__dirname, 'src/fonts'),
        to: path.resolve(__dirname, 'dist/fonts')
      },
      {
        from: path.resolve(__dirname, 'src/img'),
        to: path.resolve(__dirname, 'dist/img')
      }
    ])
  ]
};

package.json:

{
  "name": "Name",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack --mode development",
    "build": "cross-env NODE_ENV=production webpack --mode production",
    "watch": "cross-env NODE_ENV=development webpack --mode development --watch",
    "start": "cross-env NODE_ENV=development webpack-dev-server --mode development --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.9.0",
    "@babel/plugin-proposal-class-properties": "^7.8.3",
    "@babel/preset-env": "^7.9.5",
    "babel-loader": "^8.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "copy-webpack-plugin": "^5.1.1",
    "cross-env": "^7.0.2",
    "css-loader": "^3.5.2",
    "file-loader": "^6.0.0",
    "html-loader": "^1.1.0",
    "html-webpack-plugin": "^4.2.0",
    "mini-css-extract-plugin": "^0.9.0",
    "node-sass": "^4.13.1",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "raw-loader": "^4.0.1",
    "resolve-url-loader": "^3.1.1",
    "sass-loader": "^8.0.2",
    "style-loader": "^1.1.4",
    "terser-webpack-plugin": "^2.3.5",
    "webpack": "^4.42.1",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.10.3"
  },
  "browserslist": "defaults",
  "dependencies": {
    "@babel/polyfill": "^7.8.7",
    "bootstrap": "^4.4.1",
    "handlebars": "^4.7.6",
    "handlebars-loader": "^1.7.1",
    "jquery": "^3.5.0",
    "popper.js": "^1.16.1"
  }
}

已解决!

我在 HtmlWebpackPlugin 中将 inject 更改为 true

而且我还添加了函数 generateHtmlPlugins() 以自动将所有 html 文件添加到来自 ./src/html/views 的插件。

src/html/includes/header.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Some Title</title>
</head>
<body>
<header>header</header>

src/html/includes/footer.html:

    <footer>footer</footer>
</body>
</html>

src/html/views/index.html:

<%= _.template(require('./../includes/header-main.html'))() %>
    <div class="content">
        <img src="img/bg/desktop/bg-index-03.jpg" width="500" height="500"/>
        <div class="imgtest"></div>
    </div>
<%= _.template(require('./../includes/footer.html'))() %>

dist/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
    <title>Some Title</title>
<link href="css/main.43f2075150009f972ed4.css" rel="stylesheet"></head>
<body>
<header>header</header>
    <div class="content">
        <img src="img/bg/desktop/bg-index-03.jpg" width="500" height="500">
        <div class="imgtest"></div>
    </div>
    <footer>footer</footer>
<script src="js/vendors~main.43f2075150009f972ed4.js"></script><script src="js/main.43f2075150009f972ed4.js"></script></body>
</html>

webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const OptimizeCssAssetWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const fs = require('fs');

const isDev = process.env.NODE_ENV === 'development';
const isProd = !isDev;

function generateHtmlPlugins(templateDir) {
  const templateFiles = fs.readdirSync(path.resolve(__dirname, templateDir));
  return templateFiles.map(item => {
    const parts = item.split('.');
    const name = parts[0];
    const extension = parts[1];
    return new HtmlWebpackPlugin({
      filename: `${name}.html`,
      template: path.resolve(__dirname, `${templateDir}/${name}.${extension}`),
      minify: {
        collapseWhitespace: isProd
      },
      inject: true,
    })
  })
}

const htmlPlugins = generateHtmlPlugins('./src/html/views')

const optimization = () => {
  const config = {
    splitChunks: {
      chunks: 'all'
    }
  }
  if (isProd) {
    config.minimizer = [
      new OptimizeCssAssetWebpackPlugin(),
      new TerserWebpackPlugin()
    ]
  }
  return config
}

const filename = ext => isDev ? `[name].${ext}` : `[name].[hash].${ext}`;

const cssLoaders = extra => {
  const loaders = [
    {
      loader: MiniCssExtractPlugin.loader,
      options: {
        hmr: isDev,
        reloadAll: true
      },
    },
    {
      loader: 'css-loader',
      options: {
        url: false
      }
    }
  ];
  if (extra) {
    loaders.push(extra)
  }
  return loaders
}

const babelOptions = preset => {
  const opts = {
    presets: [
      '@babel/preset-env'
    ],
    plugins: [
      '@babel/plugin-proposal-class-properties'
    ]
  }

  if (preset) {
    opts.presets.push(preset)
  }

  return opts
}

module.exports = {
  mode: 'development',
  entry: [
    '@babel/polyfill',
    './src/js/index.js'
  ],
  output: {
    filename: 'js/' + filename('js'),
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    port: 4250,
    hot: isDev
  },
  devtool: isDev ? 'source-map' : '',
  resolve: {
    //extensions: ['.js', '.json', '.png'],
    alias: {
      '@views': path.resolve(__dirname, 'src/html/views'),
      '@includes': path.resolve(__dirname, 'src/html/includes'),
      '@scss': path.resolve(__dirname, 'src/scss'),
      '@': path.resolve(__dirname, 'src'),
    }
  },
  optimization: optimization(),
  module: {
    rules: [
      {
        test: /\.html$/,
        include: path.resolve(__dirname, 'src/html/includes'),
        loader: 'html-loader',
        options: {
          minimize: false,
        }
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: {
          loader: 'babel-loader',
          options: babelOptions()
        }
      },
      {
        test: /\.css$/,
        use: cssLoaders()
      },
      {
        test: /\.s[ac]ss$/,
        use: cssLoaders('sass-loader')
      },
      {
        test: /\.(png|jpg|svg|gif)$/,
        exclude: path.resolve(__dirname, 'src/fonts'),
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
              outputPath: path.resolve(__dirname, 'dist/img')
            }
          }
        ]
      },
      {
        test: /\.(ttf|otf|svg|woff|woff2|eot)$/,
        exclude: path.resolve(__dirname, 'src/img'),
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
              outputPath: path.resolve(__dirname, 'dist/fonts')
            }
          }
        ]
      },
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: 'css/' + filename('css'),
    }),
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, 'src/favicon'),
        to: path.resolve(__dirname, 'dist')
      },
      {
        from: path.resolve(__dirname, 'src/fonts'),
        to: path.resolve(__dirname, 'dist/fonts')
      },
      {
        from: path.resolve(__dirname, 'src/img'),
        to: path.resolve(__dirname, 'dist/img')
      }
    ])
  ].concat(htmlPlugins)
};