WuShaolin

悟已往之不谏,知来者之可追!

0%

webpack系统学习-基础知识汇总

开篇词 🤗

这是一个系统学习webpack的系列文章,在多年的编程学习之后,我知道:这个系列注定不会一帆风顺🙄,同样我也知道在这些失败的背后隐藏着通往美好未来的秘密,星光不问赶路人,时光不负有心人,出发吧。

友情提示 🤣

本系列开始之前,请调整好心态,我们要做好长(总)期(有)作(报)战(错)的准备😌。经过我的的第二次折腾webpack,得出一个重要结论:

当你依赖多个包时,一定要小心不同版本之间的细微的语法差别。尤其是webpack这种考验配置管理的打包器,这个结论适用于其他所有需要借助配置项、存在多版本交叉依赖的工具使用 🙃

什么是webpack 📦

一言以蔽之,webpack把所有的外部依赖都视为文件,统一处理成web最终能够处理的js css jpg等静态资源

总之,是一个🐄的打包器,可以说是web工程化的道路上的最强利器之一,现代化的web开发必备工具,再多溢美之词详见webpack

起步 ✍️

巧妇难为无米之炊,当然要先安装webpack相关工具链。本系列的webpack版本是基于webpack5.8 webpack-cli 4.2,理论上webpack4以上都可以适用,同时node版本要求10.13.0(LTS)及以上,我选择采用yarn安装,也可以选择npm安装,两者细微的区别:

  • npm i === yarn add
  • npm uninstall === yarn remove
  • 其它的命令基本上把npm 换成 yarn即可

极简项目搭建的初始化脚本如下所示:

mkdir webpack-demo
cd webpack-demo
yarn init -y
yarn add webpack webpack-cli -D

配置文件 📖

项目初始化成功后,我们可以书写es6、sass 、less等其它浏览器不能直接解析的代码了,但是要做到上述功能,我们还需要各一个配置文件webpack.config.js,这是一个最基本的配置文件,后面我会拆分成开发、生产两套配置文件。

//webpack.config.js
"use strict";

const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    //在当前目录下的dist目录
    path: path.join(__dirname, "dist"),
    filename: "bundle.js",
  },
  mode: "production"
};

借助安装的webpack-cli,此时我们已经可以在命令行使用webpack进行打包了,这里为了方便的使用webpack,修改package.jsonscripts

{
  //...
  "script": {
    "build": "webpack"
  },
  //...
}

项目编译查看效果是直接yarn run build,此时会通过在.node_modules/.bin/webpack的软连接编译项目。

核心基础概念

webpack的配置中有很多项,其中有五个最核心的概念:

  • entry 入口,有单入口和多入口。
  • output 输出,根据单入口 多入口有两种写法。
  • mode 模式,三个值 production(默认值) development none
  • loader
  • plugin

entry

这是webpack编译项目的入口文件,通常可以有两种写法:

  1. 单文件入口写法,如上面的基本配置所示。
  2. 多入口文件写法,采用对象的形式,对应output则变为使用占位符[name]
//webpack.config.js
module.exports = {
  //...
  entry: {
    'index': './src/index.js',
    'search': './src/search.js'
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js' //这里的name即上面entry对象的key值
  },
  //...
}

ouput

项目编译构建成功后的输出目录和输出文件的名字,有两种写法,分别对应单入口和多入口写法

//webpack.config.js
module.exports = {
  //...
  output: {
    //单入口文件对应的写法
    path: path.join(__dirname, "dist"),
    filename: "bundle.js",
    //or 多入口文件对应的写法
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  //...
}

可以看出来,entryoutput这两个概念是配合着使用。

mode

启用不同的mode,会开启不同的内置函数进行优化,并没有特别需要注意的要点。

development 会将 DefinePluginprocess.env.NODE_ENV 的值设置为 development. 为模块和 chunk 启用有效的名。
production 会将 DefinePluginprocess.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPluginTerserPlugin
none 不使用任何默认优化选项

对于剩下的两个概念,极其重要,需要仔细研究一下🤓

Loaders

loader这个概念是对依赖的文件进行预处理,对于需要打包的资源选用不同的loader进行处理,会起到事半功倍的效果😋。

通常来说,工程开发中(目前我暂时涉猎的)用到以下几种的loader:

loader的简单分类

文件相关的loader:

  • url-loader,可以用来预处理图片、文字等静态资源。
  • file-loader,基本上与url-loader作用一样,但是可以通过options配置项启用base64编码来处理上述静态资源。

语法转换相关的:

  • babel-loader,大名鼎鼎的babel转换,把es6等高阶语法降级成基础js语法,提高老版本项目的兼容性。

样式相关的:

  • style-loader,将导出的css文件插入到style标签里 再插入到head标签内,实现html的加载css代码

  • css-loader,加载其它css预处理器处理后的css文件并解析import的css文件

  • less-loader or sass-loader or stylus-loader,加载并编译less sass文件

框架相关的:

  • vue-loader,加载并编译vue单文件组件。

上面是几种常用的loader的分类,下面就使用一下它们。

babel-loader的使用

在使用babel-loder的时候需要配合另外的几个依赖,一起食用才会更加美味😋。

yarn add @babel/core @babel/preset-env babel-loader -D ,安装成功后修改配置文件

//webpack.config.js
module.exports = {
  //...
  //4、loader的使用
  module: {
    rules: [
      {
        //4.1 解析js文件,需要安装@babel/core @babel/preset-env babel-loader 配置.babelrc
        test: /.js$/,
        use: 'babel-loader'
      }
    ]
  }
}

loader的写法稍微有一点点小麻烦,同时babel-loader还需要配置.babelrc

//项目根目录下新建.babelrc
{
  "presets": [
    "@babel/preset-env", //es6的预设
  ]
}

css相关loader的使用

本文使用lesscss预处理器,所以配合使用的依赖有四个:

yarn add less less-loader css-loader style-loader -D

对应的配置文件修改如下:

//webpack.config.js
module.exports = {
  //...
  //4、loader的使用
  module: {
    rules: [
      {
        //4.1 解析js文件,需要安装@babel/core @babel/preset-env babel-loader 配置.babelrc
        test: /.js$/,
        use: 'babel-loader'
      },
      {
        //4.2 解析.css文件,需要style-loader css-loader,链式调用,从右往左,必须先解析.css文件
        test: /.css$/,
        use: [
          'style-loader', //把css放到style标签里面插进head标签里面
          'css-loader', //加载css文件,并转换成commonjs对象
        ]
      },
      {
        //4.3 解析.css文件,需要style-loader css-loader less-loader, 链式调用,从右往左,必须先解析.less文件
        test: /.less$/,
        use: [
          'style-loader', //把css放到style标签里面插进head标签里面
          'css-loader',
          'less-loader', //把less转换成css文件
        ]
      },
    ]
  }
}

多个loader加载时注意顺序:链式调用多个loader,从右往左,所以一定要注意加载的先后顺序。


图片、字体等静态资源相关loader的使用

对于图片、字体这些依赖来说,虽然我们看来不是一种类型,但是在打包器看来都是静态资源,直接打包就完事了🤓

上面的简单分类里面说过,一般有两种loader-file-loaderurl-loader,对于具体采用哪种,视实际情况而定。

先安装yarn add file-loader url-loader -D,同样的,修改配置文件。

//webpack.config.js
module.exports = {
    //...
    //4、loader的使用
  module: {
    rules: [
      {
        //4.1 解析js文件,需要安装@babel/core @babel/preset-env babel-loader 配置.babelrc
        test: /.js$/,
        use: 'babel-loader'
      },
      {
        //4.2 解析.css文件,需要style-loader css-loader,链式调用,从右往左,必须先解析.css文件
        test: /.css$/,
        use: [
          'style-loader', //把css放到style标签里面插进head标签里面
          'css-loader', //加载css文件,并转换成commonjs对象
        ]
      },
      {
        //4.3 解析.css文件,需要style-loader css-loader less-loader, 链式调用,从右往左,必须先解析.less文件
        test: /.less$/,
        use: [
          'style-loader', //把css放到style标签里面插进head标签里面
          'css-loader',
          'less-loader', //把less转换成css文件
        ]
      },
      {
        //4.4 解析图片文件,需要file-loader
        test: /.(png|img|jpg|jpeg|gif)$/,
        use: "file-loader",
       },
      {
         //4.5 解析字体文件,需要file-loader
        test: /.(woff|woff2|eot|ttf|otf)$/,
        use: "file-loader",
      },
      {
        //4.6 解析图片、字体文件,也可以url-loader
        //TODO 字体文件22m打包有问题,需要解决
        test: /.(png|img|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 1024 * 10, 
              //小于这个字节数会直接base64处理,不生成单独的图片字体文件,直接打进对应的js文件中,对应的那个js文件会变大
            },
          },
        ],
      },
    ]
  }
}

这里有一个问题,字体文件太大了,需要压缩,现在直接打包,比较费劲😶,同时此时编译后的静态文件并没有单独的文件名称,这个问题需要用到后续的优化插件

开启webpack的监听功能

在开发中,经常需要更新页面,而我们如果不想每次重新编译,则可以开启这个选项,修改后的配置文件如下:

//webpack.config.js
module.exports = {
    //...
    //附加:开启监听
  watch: true, //开启监听 等同于在package.json中增加 webpack --watch命令
  // 只有开启监听后下面配置才有用
  watchOptions: {
    //使用正则去忽略某些文件,能提高监听的性能,默认为空
    ignored: /node_modules/, 
    //聚合等待时间,不会立刻执行
    aggregateTimeout: 200,
    //轮询时间,默认一秒轮询1000次
    poll: 1000
  },
}

这个功能通常来说并不是特别重要,因为后面有其它操作来替换-也就是热更新服务器。

Plugins

有一些功能loader不方便完成,所以此时需要一种补充功能的东西,这些东西称之为Pluginwebpack变得更加灵活了。

上一节结尾处提到了一种热更新机制-开发环境时,本地代码更新后需要即时编译结果,我们需要借助插件来完成,但是这个热更新插件(专业名词叫HMR-HotModuleReplacementPlugin)自己还不能完成全部热更新机制,我们还需要一个热更新服务器(WDS-webpack-dev-server,它的后台使用了WDM-web-server-middleware)去把项目直接在浏览器里起起来。

热更新

首先要明确的一点是,热更新机制只有在开发环境下才有意义。

安装依赖,yarn add webpack-dev-server -D,修改配置文件,这次改动较大,要细心👀

//webpack.config.js
const webpack = require("webpack")
module.exports = {
    //...
    mode: "development",
    //5、plugin的使用。
  // 以webpack内置的HMR-HotModuleReplacementPlugin(热模块替换,只替换更新的)为例 
  // WDS-webpack-dev-server的热更新服务器需要这个插件配合使用。
  //同样实现热更新的还有WDM-webpack-dev-middleware,它将webpack输出文件传输给服务器,更加灵活
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
  ],
  //开启内置的webpack-dev-server热更新的配置哦
  //注意:老版本的直接在package.json的scripts使用webpack-dev-server --open,新版本后使用webpack serve
  devServer: {
    contentBase: path.join(__dirname, 'dist'), //基础路径
    hot: true
  }
}

webpack-dev-server3版本的命令变了,package.json的scripts中要用webpack serve命令,如果你是老版本的话,继续使用webpack-dev-server --open即可。


版本迭代的一点小优化

随着我们一直在改良项目的编译流程,项目的版本自然而然也会越来越多,多版本必然带来一些问题,每次更新版本后生产环境里需要替换哪些内容呢,全替换还是有针对性的替换新的,如果只替换修改后的,那么如何保证找到更新的文件呢?🤔,上面的一系列文件带来了一个新的理念-文件指纹。

简而言之,文件指纹就是给文件打上标签,用以区分不同版本。

关于指纹的分类,一般有3种,都是md5值:

  • hash ,图片、字体使用8位的普通hash并加上后缀,使之可以单独编译成原来格式的文件,不丢失后缀。
  • chunkhash,js文件使用8位的chunkhash做指纹
  • contenthash,css文件一般用8位的contenthash

拆分webpack.config.js

既然我们已经涉及到了版本管理,同时也要区分开发环境和生产环境了,陪伴我们很久的老朋友webpack.config.js也要离开历史的舞台了,它的两个孩子webpack.prod.jswebpack.dev.js将会代替他陪我们走完剩下的路👬

至此,package.json的脚本也可以告一段落了:

{
  "scripts": {
    "build": "webpack --config webpack.prod.js",
    "watch": "webpack --watch",
    "dev": "webpack serve --config webpack.dev.js"
  },
}

拆分后的开发环境配置文件 webpack.dev.js更新如下:

"use strict";

const path = require("path");
const webpack = require("webpack")

module.exports = {
  //1、单入口文件写法如下:
  // entry: "./src/index.js",
  // output: {
  //   path: path.join(__dirname, "dist"),
  //   filename: "bundle.js",
  // },
  //2、多入口文件写法如下:entry 使用对象的写法,output只能使用占位符[name]
  entry: {
    index: "./src/index.js",
    search: "./src/search.js",
  },
  output: {
    path: path.join(__dirname, "dist"),
    filename: "[name].js",
  },
  //3、mode有三个值:production(默认值)、development、none,开启之后会启动相应的默认配置函数
  mode: "development",
  //附加:开启监听
  watch: true, //开启监听 等同于在package.json中增加 webpack --watch命令
  // 只有开启监听后下面配置才有用
  watchOptions: {
    //使用正则去忽略某些文件,能提高监听的性能,默认为空。它是会输出到硬盘中。
    ignored: /node_modules/, 
    //聚合等待时间,不会立刻执行
    aggregateTimeout: 200,
    //轮询时间,默认一秒轮询1000次
    poll: 1000
  },
  //4、loader的使用
  module: {
    rules: [
      {
        //4.1 解析js文件,需要安装@babel/core @babel/preset-env babel-loader 配置.babelrc
        test: /.js$/,
        use: "babel-loader",
      },
      {
        //4.2 解析.css文件,需要style-loader css-loader,链式调用,从右往左,必须先解析.css文件
        test: /.css$/,
        use: [
          "style-loader", //把css放到style标签里面插进head标签里面
          "css-loader", //加载css文件,并转换成commonjs对象
        ],
      },
      {
        //4.3 解析.css文件,需要style-loader css-loader less-loader, 链式调用,从右往左,必须先解析.less文件
        test: /.less$/,
        use: [
          "style-loader", //把css放到style标签里面插进head标签里面
          "css-loader",
          "less-loader", //把less转换成css文件
        ],
      },
      // {
      //   //4.4 解析图片文件,需要file-loader
      //   test: /.(png|img|jpg|jpeg|gif)$/,
      //   use: "file-loader",
      // },
      // {
      //   //4.5 解析字体文件,需要file-loader
      //   test: /.(woff|woff2|eot|ttf|otf)$/,
      //   use: "file-loader",
      // },
      {
        //4.6 解析图片、字体文件,也可以url-loader
        //TODO 字体文件22m打包有问题,需要解决
        test: /.(png|img|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 1024 * 10, //小于这个字节数会直接base64处理,不生成单独的图片字体文件,直接打进对应的js文件中,对应的那个js文件会变大
            },
          },
        ],
      },
    ],
  },
  //5、plugin的使用。
  // 以webpack内置的HMR-HotModuleReplacementPlugin(热模块替换,只替换更新的)为例 
  // WDS-webpack-dev-server的热更新服务器需要这个插件配合使用。
  //同样实现热更新的还有WDM-webpack-dev-middleware,它将webpack输出文件传输给服务器,更加灵活
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
  ],
  //开启内置的webpack-dev-server热更新的配置哦
  //注意:老版本的直接在package.json的scripts使用webpack-dev-server --open,新版本后使用webpack serve
  devServer: {
    contentBase: path.join(__dirname, 'dist'), //基础路径
    hot: true
  }
};

拆分后的生产环境的配置文件 webpack.prod.js加上指纹后的更新如下:

"use strict";

const path = require("path");

module.exports = {
  //1、单入口文件写法如下:
  // entry: "./src/index.js",
  // output: {
  //   path: path.join(__dirname, "dist"),
  //   filename: "bundle.js",
  // },
  //2、多入口文件写法如下:entry 使用对象的写法,output只能使用占位符[name]
  entry: {
    index: "./src/index.js",
    search: "./src/search.js",
  },
  output: {
    path: path.join(__dirname, "dist"),
    filename: "[name]_[chunkhash:8].js", //js文件使用8位的chunkhash做指纹
  },
  //3、mode有三个值:production(默认值)、development、none,开启之后会启动相应的默认配置函数
  mode: "production",
  //4、loader的使用
  module: {
    rules: [
      {
        //4.1 解析js文件,需要安装@babel/core @babel/preset-env babel-loader 配置.babelrc
        test: /.js$/,
        use: "babel-loader",
      },
      {
        //4.2 解析.css文件,需要style-loader css-loader,链式调用,从右往左,必须先解析.css文件
        test: /.css$/,
        use: [
          "style-loader", //把css放到style标签里面插进head标签里面
          "css-loader", //加载css文件,并转换成commonjs对象
        ],
      },
      {
        //4.3 解析.css文件,需要style-loader css-loader less-loader, 链式调用,从右往左,必须先解析.less文件
        test: /.less$/,
        use: [
          "style-loader", //把css放到style标签里面插进head标签里面
          "css-loader",
          "less-loader", //把less转换成css文件
        ],
      },
      {
        //4.4 解析图片文件,需要file-loader
        test: /.(png|img|jpg|jpeg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name]_[hash:8].[ext]'//图片使用8位的普通hash并加上后缀
            }
          }
        ],
      },
      {
        //4.5 解析字体文件,需要file-loader
        test: /.(woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name]_[hash:8].[ext]'
            }
          }
        ],
      },
      // {
      //   //4.6 解析图片、字体文件,也可以url-loader
      //   //TODO 字体文件22m打包有问题,需要解决
      //   test: /.(png|img|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/,
      //   use: [
      //     {
      //       loader: "url-loader",
      //       options: {
      //         limit: 1024 * 10, //小于这个字节数会直接base64处理,不生成单独的图片字体文件,直接打进对应的js文件中,对应的那个js文件会变大
      //       },
      //     },
      //   ],
      // },
    ],
  },
};

抽离css并加指纹

前面我们的编译流程结果,细心地你会发现并没有单独的css文件,这是因为它被打到了一起,并动态插入到了HTML的head标签的style中,我们需要借助一个插件-mini-css-extract-plugin把它们抽离出来。

yarn add mini-css-extract-plugin -D,有了这个插件之后我们就不需要style-loader,借助这个插件自带的loader即可。

更新后的开发环境的配置文件webpack.prod.js如下:

//webpack.prod.js
"use strict";

const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

module.exports = {
  module: {
      rules: [
      {  
                //4.2 解析.css文件,需要style-loader css-loader,链式调用,从右往左,必须先解析.css文件
        test: /.css$/,
        use: [

          MiniCssExtractPlugin.loader, //提取成带指纹的单个css文件,不能与style-loader共存
          "css-loader", //加载css文件,并转换成commonjs对象
        ],
      },
      {
          //4.3 解析.css文件,需要style-loader css-loader less-loader, 链式调用,从右往左,必须先解析.less文件
          test: /.less$/,
          use: [

            MiniCssExtractPlugin.loader,
            "css-loader",
            "less-loader", //把less转换成css文件
          ],
             }
      ]

  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    })
  ]
};