使用 webpack 打包多页面应用(Multiple-Page Application)

多页面网站同样可以用 webpack 来打包,以便使用 npm 包,import()code splitting 等好处。

MPA 意味着并没不是一个单一的 html 入口和 js 入口,而是每个页面对应一个 html 和多个 js。那么我们可以把项目结构设计为:

  1. ├── dist
  2. ├── package.json
  3. ├── node_modules
  4. ├── src
  5. ├── components
  6. ├── shared
  7. | ├── favicon.png
  8. └── pages 页面放这里
  9. | ├── foo 编译后生成 http://localhost:8080/foo.html
  10. | | ├── index.html
  11. | | ├── index.js
  12. | | ├── style.css
  13. | | └── pic.png
  14. | └── bar http://localhost:8080/bar.html
  15. | ├── index.html
  16. | ├── index.js
  17. | ├── style.css
  18. | └── baz http://localhost:8080/bar/baz.html
  19. | ├── index.html
  20. | ├── index.js
  21. | └── style.css
  22. └── webpack.config.js

这里每个页面的 index.html 是个完整的从 <!DOCTYPE html> 开头到 </html> 结束的页面,这些文件都要用 html-webpack-plugin 处理。index.js 是每个页面的业务逻辑,作为每个页面的入口 js 配置到 entry 中。这里我们需要用 glob 库来把这些文件都筛选出来批量操作。为了使用 webpack 4 的 optimization.splitChunksoptimization.runtimeChunk 功能,我写了 html-webpack-include-sibling-chunks-plugin 插件来配合使用。还要装几个插件把 css 压缩并放到 <head> 中。

  1. npm install glob html-webpack-include-sibling-chunks-plugin uglifyjs-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin --save-dev

webpack.config.js 修改的地方:

  1. // ...
  2. const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
  3. const MiniCssExtractPlugin = require('mini-css-extract-plugin')
  4. const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
  5. const HtmlWebpackIncludeSiblingChunksPlugin = require('html-webpack-include-sibling-chunks-plugin')
  6. const glob = require('glob')
  7. const dev = Boolean(process.env.WEBPACK_SERVE)
  8. const config = require('./config/' + (process.env.npm_config_config || 'default'))
  9. const entries = glob.sync('./src/**/index.js')
  10. const entry = {}
  11. const htmlPlugins = []
  12. for (const path of entries) {
  13. const template = path.replace('index.js', 'index.html')
  14. const chunkName = path.slice('./src/pages/'.length, -'/index.js'.length)
  15. entry[chunkName] = dev ? [path, template] : path
  16. htmlPlugins.push(new HtmlWebpackPlugin({
  17. template,
  18. filename: chunkName + '.html',
  19. chunksSortMode: 'none',
  20. chunks: [chunkName]
  21. }))
  22. }
  23. module.exports = {
  24. entry,
  25. output: {
  26. path: resolve(__dirname, 'dist'),
  27. // 我们不定义 publicPath,否则访问 html 时需要带上 publicPath 前缀
  28. filename: dev ? '[name].js' : '[chunkhash].js',
  29. chunkFilename: '[chunkhash].js'
  30. },
  31. optimization: {
  32. runtimeChunk: true,
  33. splitChunks: {
  34. chunks: 'all'
  35. },
  36. minimizer: dev ? [] : [
  37. new UglifyJsPlugin({
  38. cache: true,
  39. parallel: true,
  40. sourceMap: true
  41. }),
  42. new OptimizeCSSAssetsPlugin()
  43. ]
  44. },
  45. module: {
  46. rules: [
  47. // ...
  48. {
  49. test: /\.css$/,
  50. use: [dev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
  51. },
  52. // ...
  53. ]
  54. },
  55. plugins: [
  56. // ...
  57. /*
  58. 这里不使用 [chunkhash]
  59. 因为从同一个 chunk 抽离出来的 css 共享同一个 [chunkhash]
  60. [contenthash] 你可以简单理解为 moduleId + content 生成的 hash
  61. 因此一个 chunk 中的多个 module 有自己的 [contenthash]
  62. */
  63. new MiniCssExtractPlugin({
  64. filename: '[contenthash].css',
  65. chunkFilename: '[contenthash].css'
  66. }),
  67. // 必须放在html-webpack-plugin前面
  68. new HtmlWebpackIncludeSiblingChunksPlugin(),
  69. ...htmlPlugins
  70. ],
  71. // ...
  72. }

entryhtmlPlugins 会通过遍历 pages 目录生成,比如:

entry:

  1. {
  2. 'bar/baz': './src/pages/bar/baz/index.js',
  3. bar: './src/pages/bar/index.js',
  4. foo: './src/pages/foo/index.js'
  5. }

在开发环境中,为了能够修改 html 文件后网页能够自动刷新,我们还需要把 html 文件也加入 entry 中,比如:

  1. {
  2. foo: ['./src/pages/foo/index.js', './src/pages/foo/index.html']
  3. }

这样,当 foo 页面的 index.js 或 index.html 文件改动时,都会触发浏览器刷新该页面。虽然把 html 加入 entry 很奇怪,但放心,不会导致错误。记得不要在生产环境这么做,不然导致 chunk 文件包含了无用的 html 片段。

htmlPlugins:

  1. [
  2. new HtmlWebpackPlugin({
  3. template: './src/pages/bar/baz/index.html',
  4. filename: 'bar/baz.html',
  5. chunksSortMode: 'none',
  6. chunks: ['bar/baz']
  7. },
  8. new HtmlWebpackPlugin({
  9. template: './src/pages/bar/index.html',
  10. filename: 'bar.html',
  11. chunksSortMode: 'none',
  12. chunks: ['bar']
  13. },
  14. new HtmlWebpackPlugin({
  15. template: './src/pages/foo/index.html',
  16. filename: 'foo.html',
  17. chunksSortMode: 'none',
  18. chunks: ['foo']
  19. }
  20. ]

代码在 examples/mpa 目录。