阅读 1812

使用webpack4一步步搭建react项目(二)

前面已经实现了一个简单webpack配置,接下来需要在前面的基础上对webpack.config.js进行拆分。

第二章 拆分webpack配置

项目开发时,我们需要用webpack-dev-server启动开发服务器,当我们修改文件时,它能自动重新打包项目并刷新页面。

项目打包上线时,我们希望webpack能进行更多的处理来优化打包后的代码。

针对不同的需求,我们需要将配置文件拆分为webpack.common.js webpack.dev.js webpack.prod.js

项目结构

项目结构v2

代码

项目代码 Github 仓库

配置webpack.common.js

这个文件是共用的配置。

const path = require("path");

module.exports = {
  // 入口文件改为 .jsx文件
  entry: "./src/index.jsx",
  resolve: {
    extensions: [".js", ".json", ".jsx"]
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        // include告诉webpack只对src下的
        // js、jsx文件进行babel转译
        // 加快webpack的打包速度
        include: path.resolve(__dirname, "src"),
        use: "babel-loader"
      }
    ]
  }
};
复制代码

配置webpack.dev.js

我们需要使用webpack-merge将前面配好的webpack.comm.js合并进来。而且需要webpack-dev-server来启动开发服务器。

安装:

  • webpack-merge
  • webpack-dev-server
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const merge = require("webpack-merge");
const common = require("./webpack.common");

// 使用webpack-merge将webpack.common.js合并进来
module.exports = merge(common, {
  // 设置为开发(development)模式
  mode: "development",
  // 设置source map,方便debugger
  devtool: "inline-source-map",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
    publicPath: "/"
  },
  devServer: {
    // 单页应用的前端路由使用history模式时,这个配置很重要
    // webpack-dev-server服务器接受的请求路径没有匹配的资源时
    // 他会返回index.html而不是404页面
    historyApiFallback: true
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      },
      {
        test: /\.(png|jpg|jpeg|svg|gif)$/,
        use: "file-loader"
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/template.html",
      favicon: "./src/assets/favicon-32x32-next.png"
    })
  ]
});

复制代码

配置webpack.prod.js

打包生产环境使用的代码需要非常多的优化和处理,所以这个文件的配置会非常复杂。

配置思路

  • 将css样式从main.js内抽离到单独的.css文件
  • 缓存处理
  • 将第三方库和webpack的runtime从main.js内抽离出来
  • js、css、html代码压缩
  • 使用source-map替代inline-source-map
  • 懒加载(lazy loading)
  • 每次打包前先清空dist目录

抽离css样式

前面的webpack.dev.js只是简单的使用了css-loaderstyle-loader

css-loader将项目导入的css样式转为js模块,打包到main.js内。

style-loadermain.js内提供了一个能将css动态插入到html内的方法。

当用户打开页面时,会先加载html,然后加载main.js,最后运行js脚本将样式插入到style标签内。

这样有多个缺点:

  • css被打包到了main.js内,增加了它的文件大小,而且也不方便对css做缓存
  • css样式得等到main.js脚本运行,并插入到html时才会有效果。这个空档期虽然很短,但它会造成界面闪烁。

优点:

  • 打包速度快

所以style-loadercss-loader的组合仅适用于开发。我们需要使用mini-css-extract插件,并用该插件提供的loader来替代style-loader

// ...
plugins:[
  // 配置mini-css-extract插件
  new MiniCssExtractPlugin({
    // 设置抽取出来的css名字
    filename: "[name].[contentHash].css",
    chunkFilename: "[id].[contentHash].css"
  }),
  // ...
],
// ...
复制代码
// ...
module:{
  rules:[
    {
      test: /\.css$/,
      // 使用MiniCssExtractPlugin.loader替代style-loader
      use: [MiniCssExtractPlugin.loader, "css-loader"]
    },
    // ...
  ]
},
// ...
复制代码

缓存处理

缓存是前端页面性能优化的重点。我们希望浏览器能长久缓存资源,同时又能在第一时间获取更新后的资源。

具体思路是:后端不对index.html做任何缓存处理,对css、js、图片等资源做持久缓存。将output.filename配置为"main.[contentHash].js",这样打包后的main.js中间会加上一段contentHashcontentHash是根据打包文件内容产生的,内容改变它才会发生改变。发布时,由于哈希值不同,服务器能同时保存着不同哈希版本的资源。这样保证了发布过程中,用户仍然能够访问到旧资源,并且新用户会访问到新资源。

加上contentHash后打包文件名变成main.xxxxxx.js,其中xxxxxx代表一串很长的哈希值。

但是,现在我们的业务代码、引用的第三方库,还有webpack生成的runtime都被捆绑打包到了main.xxxxxx.js内。第三方库和webpack的runtime变动的频率非常低,所以我们不希望每次业务代码的改动导致用户得连同它们一起重新下载一遍。因此我们需要将它们从main.xxxxxx.js内抽离出来。

还有一点需要注意,webpack以前的版本有个小小的问题。在打包文件内容没发生变化的情况下contentHash任然会发生改变。此时需要使用webpack.HashedModuleIdsPlugin插件来替代默认的哈希生成。虽然webpack4修复了这个问题,但是官方文档还是推荐我们使用webpack.HashedModuleIdsPlugin插件。

抽离第三方库与webpack runtime

前面已经说明了为什么要抽离他们。

以前的版本使用commons-chunk-plugin插件来抽离第三方库,webpack 4通过配置optimization.splitChunks来抽取。内部其实使用了split-chunks-plugin插件。

optimization.runtimeChunk选项配置为single,可以将webpack runtime抽离到单文件中。

// ...
optimization: {
  // 抽离webpack runtime到单文件
  runtimeChunk: "single",
  splitChunks: {
    chunks: "all",
    // 最大初始请求数量
    maxInitialRequests: Infinity,
    // 抽离体积大于80kb的chunk
    minSize: 80 * 1024,
    // 抽离被多个入口引用次数大于等于1的chunk
    minChunks: 1,
    cacheGroups: {
      // 抽离node_modules下面的第三方库
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        // 从模块的路径地址中获得库的名称
        name: function(module, chunks, chacheGroupKey) {
          const packageName = module.context.match(
            /[\\/]node_modules[\\/](.*?)([\\/]|$)/
          )[1];
          return `vendor_${packageName.replace("@", "")}`;
        }
      }
    }
  },
  // ...
},
// ...
复制代码

js、css、html代码压缩

打包后的js、css、html会有注释、空格、换行,开启代码压缩可以大幅度减少资源的体积。

mode设为"production"时,会默认使用terser-webpack-plugin插件对js进行压缩。我们还要开启html与css的压缩,所以要重写optimization.minimizer选项。

// ...
optimization: {
  minimizer: [
    // 压缩css
    new OptimizeCssAssetsWebpackPlugin(),
    // 压缩js,记得sourceMap设为true
    new TerserWebpackPlugin({ sourceMap: true }),
    // 该插件还能对html进行压缩
    new HtmlWebpackPlugin({
      template: "./src/template.html",
      favicon: "./src/assets/favicon-32x32-next.png",
      minify: {
        // 折叠空白符(去除换行符和空格)
        collapseWhitespace: true,
        // 移除注释
        removeComments: true,
        // 移除属性上不必要的引号
        removeAttributeQuotes: true
      }
    })
  ],
  // ...
}
// ...
复制代码

使用source-map替代inline-source-map

发生bug时,我们很难通过打包后的代码找出错误的源头。所以我们需要source map将代码映射为原来我们手写时候的样子。

前面的webpack.dev.js内使用的是inline-source-map。它的缺点是将map内敛到了代码内,这样用户会连同资源将map一起下载。

所以我们使用source-map,它会给打包后的每个js单独生成.map文件。

懒加载(lazy loading)

懒加载也叫按需加载。我们当前打包的所有js会在页面加载过程中被加载运行。但是大多数情况下,用户并不会访问应用的所有页面与功能。我们可以将每个页面的代码或一些不常使用的功能模块做成按需加载,这样可以大大减小用户初次访问时所要加载的资源大小。

懒加载是webpack4默认支持的,不需要任何配置。前端人员需要在开发时使用dynamic import按需引入模块。webpack会自动将dynamic import引入的模块单独打包为一个chunk(注意:dynamic import语法上需要babel插件的支持,会在下一章节提到该插件)。

webpack官网提供的例子:

// print.js

console.log('The print.js module has loaded! See the network tab in dev tools...');

export default () => {
  console.log('Button Clicked: Here\'s "some text"!');
};
复制代码
// src/index.js

// ...
button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
  var print = module.default;

  print();
});
// ...
复制代码

通过点击按钮,触发加载print模块。/* webpackChunkName: "print" */这个注释告诉webpack该模块打包成的chunk名字叫print

清空dist目录

使用clean-webpack-plugin在打包前清空dist目录。

webpack.prod.js的完整配置

安装:

  • mini-css-extract-plugin
  • clean-webpack-plugin
  • terser-webpack-plugin optimize-css-assets-webpack-plugin
  • url-loader
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const merge = require("webpack-merge");
const common = require("./webpack.common");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
const webpack = require("webpack");

module.exports = merge(common, {
  // 设置为生产(production)模式
  mode: "production",
  // 在生产环境中使用"source-map"而不是"inline-source-map"
  devtool: "source-map",
  output: {
    // 这里添加contentHash
    // 由于我们的entry中没有配置入口的名称
    // webpack会默认取名为main
    // 因此这里的配置会生成"main.xxxxxx.js"
    filename: "[name].[contentHash].js",
    // 通过splitChunks抽离的js文件名格式
    chunkFilename: "[name].[contentHash].chunk.js",
    path: path.resolve(__dirname, "dist"),
    publicPath: "/"
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        // 这里使用MiniCssExtractPlugin.loader替代style-loader
        use: [MiniCssExtractPlugin.loader, "css-loader"]
      },
      {
        test: /\.(png|jpg|jpeg|svg|gif)$/,
        use: {
          // 这里使用url-loader替代file-loader
          loader: "url-loader",
          options: {
            // 当图片小于8kb时,url-loader会将图片转为base64
            // 这样可以减少http请求的数量
            // 如果大于8kb的话,url-loader会将图片交给file-loader处理
            // 所以url-loader需要依赖file-loader
            limit: 1024 * 8,
            name: "img/[name].[hash:8].[ext]"
          }
        }
      }
    ]
  },
  optimization: {
    // 抽离webpack runtime到单文件
    runtimeChunk: "single",
    // 压缩器
    minimizer: [
      // 压缩css
      new OptimizeCssAssetsWebpackPlugin(),
      // 压缩js,记得将sourceMap设为true
      // 否则会无法生成source map
      new TerserWebpackPlugin({ sourceMap: true }),
      // 该插件还能压缩html
      new HtmlWebpackPlugin({
        template: "./src/template.html",
        favicon: "./src/assets/favicon-32x32-next.png",
        minify: {
          // 折叠空白符
          collapseWhitespace: true,
          // 移除注释
          removeComments: true,
          // 移除属性多余的引号
          removeAttributeQuotes: true
        }
      })
    ],
    splitChunks: {
      chunks: "all",
      // 最大初始请求数
      maxInitialRequests: Infinity,
      // 80kb以上的chunk抽离为单独的js文件
      // 配合上面的 maxInitialRequests: Infinity
      // 小于80kb的所有chunk会被打包一起
      // 这样可以减少初始请求数
      // 大家可以根据自己的情况设置
      minSize: 80 * 1024,
      // 抽离多入口引用次数1以上的chunk
      minChunks: 1,
      cacheGroups: {
        // 抽离node_modules内的第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          // 根据路径获得第三方库的名称
          // 并将抽离的chunk以"vendor_thirdPartyLibrary"格式命名
          name: function(module, chunks, chacheGroupKey) {
            const packageName = module.context.match(
              /[\\/]node_modules[\\/](.*?)([\\/]|$)/
            )[1];
            return `vendor_${packageName.replace("@", "")}`;
          }
        }
      }
    }
  },
  plugins: [
    // 每次打包前,先清除输出目录
    new CleanWebpackPlugin(),
    // 抽离css
    new MiniCssExtractPlugin({
      filename: "[name].[contentHash].css",
      chunkFilename: "[id].[contentHash].css"
    }),
    // 确保在文件没发生改变时,contentHash也不会变化
    new webpack.HashedModuleIdsPlugin()
  ]
});

复制代码

配置npm脚本

// ...
"scripts": {
  "start": "webpack-dev-server --config webpack.dev.js",
  "build": "webpack --config webpack.prod.js"
},
// ...
复制代码

结尾

基本配置完成,可以安装react-router redux进行单页应用开发了

npm i react-router-dom redux react-redux redux-thunk
复制代码

下章节内容:添加babel插件支持decorator、类属性与dynamic import;添加sass预处理;添加postcss Autoprefixer自动补充浏览器厂商前缀;使用.browserslistrc配置需要兼容的浏览器范围;添加Prettier ESLint来规范与格式化代码;

其他章节

参考

Learn Webpack

Webpack 的 Bundle Split 和 Code Split 区别和应用

webpack guides

learn Webpack step by step

webpack 持久化缓存实践

大公司里怎样开发和部署前端代码?

文章分类
前端
文章标签