webpack系列(二) -- 基础配置

159 阅读13分钟

webpack基础

本章将会介绍

  • loader & plugins
  • 出入口
  • 静态文件数据
  • 热加载

1. 安装

yarn add webpack webpack-cli --dev

一行解决,但有几个要注意的问题

  • 每个项目对 webpack 的版本要求不同,因此不可能使用全局 webpack 进行打包编译
  • 然而当我们默认执行 webpack xxx 打包文件时,实际上跑的 webpack 是全局的,若全局没有安装则会出现找不到的情况
  • 可以使用 npx webpackyarn webpack 指令,会从内到外一层一层 node_modules 中去寻找对应的 webpack
  • 通常我们会发现在老项目的 shell 脚本中,会使用 ./node_modules/.bin/webpack xxx 的指令,实际上就是为了指定使用我们自己项目中安装的webpack

2. 配置文件介绍

webpack.config.js 说是 webpack 的灵魂也不为过。因为对于我们来说可能见到和能操作最多的就是这个文件了

ps: 此处略去命令行的形式,因为实际上用到的并不多,webpack官方也更推荐这种配置文件的形式

3. 打包模式

mode也是耳熟能详的配置项,区分为开发环境development和生产环境production

webpack.config.js

module.exports = {
	mode: 'development' | 'production'
	...
}

为了区分打包模式,常常会分两个 webpack.config.[mode].js 去实现,会在下面介绍。

4. 打包文件出入口

对于entry和output也非常好理解,一个是定义从哪里开始打包,另一个是定义产出

要注意的是,output的路径无法使用相对路径,只能使用绝对路径,因此就需要引入path模块

webpack.config.js

const path = require('path');

module.exports = {
    entry: './index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './dist'),
      	// 静态文件输出路径 和 asset type有关
	      assetModuleFilename: 'assets/[hash][ext][query]',
	      // 每次打包产出默认不会消失(除非文件名相同覆盖),添加后将清空上次打包信息
        clean: true
    }
    ...
}

此时调用 npx webpack 就会默认自动找到当前目录下的 webpack.config.js 文件进行读取打包了~

可是,我们发现每次修改代码后,都需要手动执行:1、执行webpack打包指令 2、页面刷新

不能说相当麻烦,只能说狗都不干!因此我们将使用 webpack-dev-server 去实现修改代码后的 自动重新加载

5. 热加载

官网描述很清楚:webpack-dev-server 为你提供了一个基本的 web server,并且具有 live reloading(实时重新加载) 功能。其实也就是我们常说的热加载

安装

yarn add webpack-dev-server --dev 

webpack.config.js

module.exports = {
		...
    output: {
        filename: 'haha.js',
        path: path.resolve(__dirname, './dist')
    },
    // 也可以不配置,会默认选用output打包出来的路径
		devServer: {
      	static: './dist', // 配置读取打包后的文件
    },
		...
};

运行

npx webpack server --open

当出现下图情况代表成功了,赶紧试试看吧~

image-20220417113554338

此时我们的基本框架搭好了,就该对我们各式各样的代码进行打包了。我们知道除了js外,各种各样的其他文件类型

  • 静态文件图片(png,svg)
  • 文件(csv, execl,json)
  • 还有前端三剑客中的css,和各种框架vue、react,那么对于当前基础的 webpack 来说,实际上只能打包 js 文件而已,那么对于其他的来说我们该怎么办呢?

解决方案

  • 对于第一点,我们可以使用资源模块的加载
  • 对于第二点,此时就要拿出大杀器 loaders了

我们分别来进行介绍~

6. 静态资源模块

在老版本中,我们会看到各种 loader比如 url-loader raw-loader file-loader 去处理静态资源

对于最新版本已经将其内置 asset module type

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

asset module type作用替换之前loader
asset/resource发送一个单独的文件并导出 URLfile-loader
asset/inline导出一个资源的 data URIurl-loader
asset/source导出资源的源代码raw-loader
asset在导出一个 data URI 和发送一个单独的文件之间自动选择url-loader

官方相关文档 写的很清楚,看文档前我们需要带着几个问题去看,

6.1 资源模块使用

实际上很简单匹配对应的rules中,使用上 {type: ModuleType} 对应字段即可。对于原先都是写 {use: xx-loader} 的形式

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist')
    },
   module: {
       rules: [
           {
               test: /\.png/,
               type: 'asset/resource'|'asset/inline'|'asset/source'|'asset'
           }
       ]
   }
};

知道了配置在哪,但何时选用哪种配置呢?让我们一起看看使用结果是啥样的就知道了

6.2 使用结果

  • asset/resource

官网介绍: 所有文件都将被发送到输出目录,并且其路径将被注入到 bundle 中。

实际上指的是,将所有匹配上的资源,copy一份并按命名规则改名到我们的output产出中。并在打包出来的bundle.js中,引用资源的地方会直接引用刚刚复制的文件。看下例子:

image-20220417181413707

ps: 注意需要在生产模式下打包才可以看到,因为都知道热加载模式为了更快改动是将打包的文件放在内存中的

  • asset/inline

官网介绍: 所有文件都将作为 data URI 注入到 bundle 中。

这也很好理解,就将文件不在作为源文件的形式,换成dataURL的形式,那么什么是dataURL呢?→ 看文档

由此可知,所有的文件都可以被解析为 data: 前缀开头的字符形式。我们常见的 base64 格式就是其中之一。

  • asset/source

官网介绍: 所有文件将原样注入到 bundle 中。

其实这个描述不准确,是将文件内容以文本形式注入。对于读取文本数据/md文件这个是最适合的。然而对于图像,则将会无法正常读取。因为就相当于直接将图像用文本文档打开,这是完全不可读的,对于html也是一样。

  • asset

官网介绍: 自动地在 resourceinline 之间进行选择:小于 8kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型。

换而言之,就是会根据文件大小进行判断选用不同的形式引入。因为我们看到当文件转换为dataURL将会变得很大而且不可读!!这是一个很好的中和情况。

默认是8KB作为中间值,但可以使用 parser.dataUrlCondition.maxSize 去自定义限定这个中间值,这里同样给出一个例子:

image-20220417205802301

6.3 自定义存储文件路径

我们往往需要控制将静态文件都集合在打包出来的某个文件夹下,不会混乱。

显然对于静态文件存储只有在我们设置type为 assetasset/resource 时才会生效,因为这两者才会将文件进行copy,直接打包到我们的产出文件夹中。

1、配置 output.assetModuleFilename 即可

  • [hash] - 按文件内容生成hash值,若文件不变则hash不变
  • [ext] - 扩展名
  • [query] - loader后的参数 例如 babel-loader?a=1 其中 a=1 会被匹配

image-20220417214215027

2、在对应 rules.generator.filename / rules.generator.publicPath里面配置对应

对特定规则创建不同的文件夹,比如我希望图片放在 static/img/* 下,txt文件放在 static/txt/* 下的情况,就需要使用单独配置了。

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
		...
    output: {
        filename: "haha.js",
        path: path.resolve(__dirname, "./dist"),
        assetModuleFilename: 'assets/[hash][ext][query]',
        clean: true,
    },
    module: {
        rules: [
            {
                test: /\.png/,
                type: "asset",
                generator: {
                    // filename: 'static/[hash][ext]',	// 文件名和output.assetModuleFilename一样
                    publicPath: 'assets/',	// 静态文件目录
                },
            }
        ]
    }
		...
};

优先级比较 rules.generator.filename > rules.generator.publicPath > output.assetModuleFilename

6.4 使用场景总结

我们都知道 webpack是默认能识别打包 jsjson 文件的,因此这两个不需要我们的静态资源类型指定就直接可以使用。

  • 对于 csv 格式的静态数据,我们只能使用读取内容的形式即 asset/source 的形式去获取内容文本。

  • 对于 svg png 等图片形式、字体比如 ttf 类型文件,其实可以通过 asset 自动判断使用 inlineresource 去将文件导入。(实际上 ttf 只能用 resource 但大小肯定大于限定值因此统一到此)

此外其实还有些其他格式,而且对于 csv 格式我们仅仅只能读取文本内容,再对文本进行拆分等,其实往往是希望能转换成我们可操作的 json 形式,对于 css 族类的样式文件也无法直接操作,很显然之前的方法都不在适合了,有什么办法呢?此时就要引出我们的 loader 了!!

7. loaders和plugins介绍

各式各样的 pluginsloaders 可以让 webpack 按我们设想的方式打包出东西

image-20220417085711332

7.1 loaders

Webpack 支持使用 loader 对文件进行预处理。你可以构建包括 JavaScript 在内的任何静态资源。并且可以使用 Node.js 轻松编写自己的 loader。推荐loaders

我们都知道对于 webpack 来说,jsonjs 类型的文件都是内置可读的。但对于其他文件形式,我们除了上述的 [静态资源模块](#2.4 静态资源模块) 加载方式外,还有很多形式是无法去直接读取的,cssless 等。此时就要使用 loaders

对于loader来说有两种配置方式,应该都很常见

  • config(推荐)

这是最常见的一种配置方式,在 webpack.config.js 文件中指定 loader。去设定针对那些资源使用哪些 loader

配置静态资源模块时是配置 module.rules.type ,此时要配置 module.rules.use 去设定针对那些资源使用哪些 loader

webpack.config.js

module.exports = {
  module: {
    rules: [
      { test: /\.css$/, use: 'css-loader' },
      { test: /\.ts$/, use: 'ts-loader' },
    ],
  },
};
  • 内联形式

在每个 import 语句中显式指定 loader。

例: import Styles from 'style-loader!css-loader?modules!./styles.css';相信大家在一些老项目都见到过这种形式,在import的时候指定当前所需的loader和loader的匹配规则。

老样子先来看看官网描述:通过为内联 import 语句添加前缀,可以覆盖 配置 中的所有 loader, preLoader 和 postLoader:

  • 使用 ! 前缀,将禁用所有已配置的 normal loader(普通 loader)

    import Styles from '!style-loader!css-loader?modules!./styles.css';
    
  • 使用 !! 前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader)

    import Styles from '!!style-loader!css-loader?modules!./styles.css';
    
  • 使用 -! 前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders

    import Styles from '-!style-loader!css-loader?modules!./styles.css';
    

选项可以传递查询参数,例如 ?key=value&foo=bar,或者一个 JSON 对象,例如 ?{"key":"value","foo":"bar"}

相信大家对 preLoaderpostLoader 是什么感到疑惑。我们都知道loader都是从右向左进行加载的,在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。其实看下下面的例子就知道和 redux 的洋葱圈很像

对于以下 use 配置:

module.exports = {
  //...
  module: {
    rules: [
      {
        //...
        use: ['a-loader', 'b-loader', 'c-loader'],
      },
    ],
  },
};

将会发生这些步骤:

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

先别深究,之后在写手写 loader 时会再详细说。

7.2 Plugins

插件 是 webpack 的主要功能。插件目的在于解决 loader 无法实现的其他事。Webpack 提供很多开箱即用的 插件

插件注册方法大同小异:

  • 安装后 require 导入插件
  • plugins 中注册 插件实例 (new 一个)
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 访问内置的插件
const path = require('path');

module.exports = {
    entry: './path/to/my/entry/file.js',
    output: {
        filename: 'my-first-webpack.bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                use: 'babel-loader',
            },
        ],
    },
    plugins: [
        new webpack.ProgressPlugin(),
        new HtmlWebpackPlugin({ template: './src/index.html' }),
    ],
};

下面我们来对一下常见的 loadersplugins 进行介绍~

8. 常用loaders

8.1 css族

就静态资源方式我们无法直接操作 css 族 - css、less、sass ,那么我如何使用 loader 配置呢?

下面将以less为例:

  • 首先我们知道 loader 生效都是从右向左的
  • less文件先编译为css再转换成style标签引入
  • 因此从左向右需要 style-loader css-loader less-loader 三个loader进行转化

webpack.config.js

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(css|less)$/,
                use: ['style-loader', 'css-loader', 'less-loader']
            }
        ]
    },
		...
};

可能有人会问: 为什么还要 style-loader 按道理 css-loader 不就已经实现了转成 css 浏览器能认识的语言,为什么还要再进行转换?我们来看下面两张对比图

  • 实际上css-loader对于文件来说确实已经转化好了我们的样式写入js中(可以打包后在产出的js中查看)
  • 但并没有去引用,因此并不会生效,在此处我们使用了 style-loader 去通过style的形式去引入

此外,虽然 style 形式引入确实可以用了,但我们往往不会去关心这个文件,因此希望对其进行压缩再将 css 通过link方式抽离出去。应该怎么办呢?这就需要请出我们的耳熟能详的压缩css插件 MiniCssExtractPlugincss-minimizer-webpack-plugin

安装

yarn add css-minimizer-webpack-plugin mini-css-extract-plugin --dev

webpack.config.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /.s?css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
      },
    ],
  },
  optimization: {
    minimizer: [
      // 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`)
      // 这是一个内置插件,可以极大压缩js文件
      `...`,
      new CssMinimizerPlugin(),	// 压缩css文件
    ],
  },
  plugins: [new MiniCssExtractPlugin()],	// 注册分离css插件
};

我们看到原先 style-loader 的地方被替换为了 MiniCssExtractPlugin.loader 用作分离loader

Ps:这将仅在生产环境开启 CSS 优化。如果还想在开发环境下启用 CSS 优化,请将 optimization.minimize 设置为 true:

webpack.config.js

// [...]
module.exports = {
  optimization: {
    // [...]
    minimize: true,
  },
};

至此对于 css 族的文件读取与优化都在此介绍了,

8.2 数据文件族

我们知道 jsjson 文件都能直接被webpack读取,但其他数据文件比如 csvxml tsvyamltomljson5 等格式的文件webpack就无法帮我们了,需要使用对应的 loader 。或者我们可以使用对应的 parser 进行解析,下面将会举两个例子说明。

  • 直接使用 文件-loader

安装

yarn add csv-loader xml-loader --dev

webpack.config.js

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(tsv|csv)$/,
                use: ['csv-loader']
            },
            {
                test: /\.xml$/,
                use: ['xml-loader']
            }
        ]
    },
		...
};

在文件中使用import引入,最终对于 csvtsv 将会被转换为 Arrayxml 文件将会被转换为 Object 格式。

  • 使用 文件库提供的 parser

安装

yarn add toml --dev

webpack.config.js

const toml = require('toml');

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.toml$/,
              	type: 'json',
                parser: {
	                  parse: toml.parse
                }
            }
        ]
    },
		...
};

总结

至此我们学会了所有文件导入方式,实际上优先级(方便程度)也能看出 asset > loader > parser。目前学习到的知识,对于所有文件都能进行解析打包成 js 了,但对于浏览器就一定认识我们的 js 吗? 我相信大部分新同学肯定没有感受过兼容ie的苦,干啥啥不行的感觉。因为另一大神器 babel 已经帮我们解决了这个问题~

8.3 babel

对于低版本浏览器/IE这种毒瘤,我们新版的js语法可能还未支持,react、vue常见框架的jsx浏览器就更认不得了

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const htmlWebpackPlugin = new HtmlWebpackPlugin({
    template: path.join(__dirname, '../example/src/index.html'),
    filename: './index.html'
});

module.exports = {
		...
    module: {
        rules: [{
            test: /\.(jsx?|tsx?)$/,
            use: ['babel-loader'],
            exclude: /node_modules/
        }]
    }
  	...
};

babel.config.js

module.exports = {
    presets: [
        [
            '@babel/preset-env',
            {
                useBuiltIns: 'entry',
                corejs: '3',
                loose: true,
            },
        ],
        [
            '@babel/preset-react',
            {
                runtime: 'automatic',
            },
        ],
        '@babel/preset-typescript',
    ]
};

多的babel相关就不多描述,可以自己查阅 babel文档 或者可以找我出一期相关介绍~

8.4 Typescript

typescript可以说已经是我们前端开发必不可少的东西了,对于ts文件的解析,那肯定也跑不掉需要对应的loader啦~

截止目前,有两兄弟一直在打架 ts-loader 和 上面的 babel-loader 。两边都想成为打倒另一个成为 ts 专属,但最终目前最好的方案就是两者共存。

我们先来分析一下两者优缺点:

优点
缺点
ts-loader正常转化效率快但无法将 js 转换为低版本兼容。换句话说无法识别 babel 配置进行相应转换
babel-loader + @babel/preset-typescript会进行相应 babel 转译但打包出的包不会保留任何 ts 信息,只剩下 js 。对于我们的工具类函数/组件库来说这就没了意义,除非你专门去手写一个 @type/xxx 。速度慢于 ts-loader

因此最终总结下来最高效的就是:

1、先用 ts-loader 转出 js文件

2、对转出的 js 文件,进行 babel 转译低版本

9. 常用plugins

所有规范的 plugins 都被收录在 传送门 中,数量不多大致通读一遍有个印象后,开发需要/看见能及时反应查找。

下面将会介绍常见plugin

9.1 HtmlWebpackPlugin

这个可以说是最常见的插件了,基本所有的项目中都会遇到。

作用:

在我们每次编译脚本/代码时,会动态生成一个 html 页面,并将我们打包好的js文件注入到这个页面中

使用场景:

  • 很显然对于目前前端框架SPA来说,都是为了展示页面
  • 对于一些工具类函数等,想要测试在浏览器运行情况时,也会使用这个,无需手动测试了

使用方法:

安装yarn add html-webpack-plugin --dev

很好理解,所有打包过程都是开发环境下就执行完的,所以无需作为生产依赖安装 (指的是dependencies不是指 mode)

webpack.config.js:

// 项目中常见配置
// 给定template 会按照我们的模板 index_template.html 进行生成注入,否则会自动生成最简洁的html文件
// 默认注入在 head 标签中,使用 defer 延迟加载,默认生成文件名 index.html
// 更多配置项可看 https://github.com/jantimon/html-webpack-plugin#options
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
		...
    plugins: [
      new HtmlWebpackPlugin({
        template: './index_template.html'
      })
    ]
    ...
};