webpack打包配置解析

257 阅读7分钟

前言:前端模块化发展过程

  • 1:web1.0时代,没有模块化的概念,采用命名空间的形式隔离模块之间的命名冲突,可以理解为一个文件就是一个挂载在window下的对象,其实也无法避免命名冲突
  • 2:接着引入了IIFE自执行匿名函数,每一个模块封装为一个自执行的IIFE,将依赖项作为IIFE的参数传入,避免全局变量的冲突。但是这样还是需要开发人员手动的处理js文件之间的依赖关系,在用script标签引入文件顺序时,需要开发手动的调整。
  • 3:为了解决js文件之间的依赖需要手动维护的问题,引入的AMD的模块化方案,最出名的require.js这个模块加载器 先看下采用AMD模块化方案的使用方式
一:定义一个模块
/**
* 第一个参数为模块的名字:alpha
* 第二个参数为当前依赖的模块集合:"require", "exports", "beta"
* 第三个模块为一个函数,函数的形参与第二个参数的依赖项一一对应。
* return:返回模块暴露的属性
*/
define("moduleA", ["require", "exports", "beta"], function (require, exports, beta) {
    return {
        verb:function(){
           return alpha.verb() +2;
        }
    };
});

二:使用一个模块
/**
* 第一个参数:一个数组存放当前文件所有依赖的模块
*/
require(['moduleA'], function(moduleA) {
    console.log(moduleB);
});

但AMD模式有以下几个问题

  • 问题一:写法上比较繁琐和冗余 每定义一个模块需要声明模块,传入模块依赖项集合,再将模块依赖项作为形参一一对应的传给第三个回调函数,每使用一个模块。也得先require后再作为形参一一对应的传递给第二个回调函数。
  • 问题二:如果模块的颗粒度划分的比较小,就会导致页面有很多的js请求
  • 4:给予AMD存在的问题,最后也就出现了我们现阶段浏览器端的esModules和nodejs端的commonjs方案。(注:当然现在nodejs也支持esModules了,只需我们在运行的时候在package.json中声明以下,或启动nodejs的时候添加对应参数)

基于es2015中esModules的模块化方案,也就推出了现在的工程化打包工具webpack。(当然webpack不止支持esModules)。

webpack是如何分析这些模块的依赖关系的呢?

  • 1:首先我们通过执行webpack-cli -options webpack.dev.config.js。执行webpack.dev.config.js这个文件。为webpack执行的配置文件。
  • 2:首先webpack会从配置文件的entry中指明的入口文件进行匹配,AMD,CMD,esModules,commonjs导入的文件,自动生成文件的依赖图(也就是文件之间的模块依赖图,省略了手工声明依赖关系)。
  • 3:对于webpack来说一切皆模块,但也不是所有的模块都能解析的。所以对于css,scss,less或者是图片,pdf等等。需要通过对应的loader来解析对应的文件,并转化为webpack可识别的模块。通过loader处理的模块被webpack加载最后添加到整个项目的文件依赖图中。 接下来我们分析下怎么处理css
  • 1:首先我们需要使用cssLoader来处理css文件,将css文件转为webpack可识别的css模块,添加的文件的依赖图中
  • 2: cssLoader处理后的模块只是能被webpack加载,但这时还不会有样式上的效果.
  • 3: 要有样式的效果还需要我们引入styleLoader将css添加到style标签中,才会生效
  • 4: 当然这时候也可加入postcssLoader来处理css在不同浏览器上的前缀,做到更充分的兼容
  • 5: 在也可以MiniCssExtractPlugin.loader将css抽取到一个文件 接下来我们以sass为例看下css相关loader的配置
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    entry: './main.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[id].css',
            ignoreOrder: false,
        }),
    ],
    module: {
        rules: [

            {
                test: /\.scss$/,
                // 注意以下use中的normal loader,执行顺序时由下往上执行的。
                // 另外的pitch loader的执行顺序则由上到下,再由下到上。类似DOM的事件捕获和事件冒泡之间的顺序。也可以理解为和洋葱模型的执行顺序很相似。
                use: [
                    MiniCssExtractPlugin.loader,   
                    {
                        loader: 'style-loader',
                        options: {}
                    }
                    {
                        loader: 'css-loader',
                        options: {
                            // 在css中遇到@import语句,就回退两个loader,用sass-loader,postcss-loader继续处理这个import进来的scss文件。
                            // 当然如果能确保@import引入的只有css文件,不会再引入scss文件。那可以将importLoaders: 1,之后在css中遇到@import的文件,只会回退到'postcss-loader'处理就可以了
                            importLoaders: 2, 
                        }
                    }
                    {
                        loader: 'postcss-loader',
                        options: {
                            plugins: [
                                require('autoprefixer') //  需要在项目根目录中创建了 `postcss.config.js`
                            ]
                        }
                    }
                    {
                        loader: 'sass-loader',
                        options: {}
                    }
                ]
            }
        ]
    }

}

webpack搭建工程化架构,在打包时如何将js和css自动兼容到对应版本的浏览器。

  • 1:在安装webpack时会默认安装browsersList,browsersList会依据项目中 .browserslistrc文件(或者是package.json中browsersList字段)中的配置。browsersList会到caniuse上,自动获取需要支持的浏览器列表。(至于如何获取,可以理解为browsersList向caniuse发送了一个请求获取支持的浏览器列表)

  • 2:如何配置.browserslistrc呢?一般我们只需要在项目跟路径创建一个.browserslistrc文件。

last 2 version # 兼容最近的两个版本
> 1% # 兼容市场占有率大于1%的浏览器
maintained node versions
not dead # 支持市场还在持续维护的浏览器

若是不配置的话则采用以下默认配置

0.5% # 市场占有率大于0.5%的浏览器
last 2 versions # 兼容最近两个版本的浏览器
Firefox ESR
not dead # 兼容在持续维护中的浏览器

使用命令行执行npx browserslist就可以看到到.browserslistrc配置所有需要支持的浏览器列表了。

image.png

上面聊了browserslist获取到需要支持的浏览器的版本,那webpack搭建的工程是如何自动将css兼容到对应浏览器版本的呢。

  • 这里就需要postcss来帮我们处理了,通过browserslist获取需要支持的浏览器,根据浏览器自动添加css前缀。
  • 这时候就需要我们引入postcss来处理所有的css。需要我们通过postcss-cli手动的调用命令行行编译制定css文件添加前缀。
  • postcss本身并不会帮我们添加前缀,需要我们引入autoprefixer这样一个postcss插件,自动添加需要兼容浏览器的css前缀。

如果每次都需要我们通过postcss去处理每一个css文件这样就很繁琐了。

所以也就引入了postcss-loader来自动处理所有的css文件,并自动添加需要兼容的前缀

  • postcss-loader内部使用的是postcss,postcss是使用autoprefixer插件来自动添加前缀的。
  • 所以需要我们配置postcss-loader(注:postcss-loader配置在css-loader的下面或右边)来处理,并在options的plugins里,引入autoprefixer
{
    loader: 'postcss-loader',
    options: {
        plugins: [
            require('autoprefixer'), //  需要在项目根目录中创建了 `postcss.config.js`
             require('postcss-preset-env'), // postcss-preset-env为postcss预设的插件集合。与babel-preset-env类似。
        ]
    }
}

// 因为`postcss-preset-env`依据预设了postcss的很多插件
// 我们可以拿来即用,不用关注内部需要使用那些插件, 所以我们可以改写为以下方式
{
    loader: 'postcss-loader',
    options: {
        plugins: [
             require('postcss-preset-env'), // postcss-preset-env为postcss预设的插件集合。与babel-preset-env类似。
        ]
    }
}

以上的这些postcss-loader的配置,都可以放到postcss.config.js中配置。这样postcss-loader会默认从项目的根目录读取postcss.config.js的配置。便于项目中配置的复用

// postcss.config.js
module.export = {
    plugins: [
        require('postcss-preset-env')
    ]
}

使用postcss.config.js作为项目全局的postcss-loader配置使用

// postcss.config.js写好后我们只需要在项目中,按下面这样配置我们的postcss-loader即可。
// postcss-loader会默认去读取postcss.config.js中的配置
{
    loader: 'postcss-loader'
}

那么问题来了,因为postcss-loader遇到css中的@import语句并不会处理@import所引入的文件,在css中如果我们遇到了@import语句后怎么办呢?

需要importLoaders来处理

 {
    loader: 'css-loader',
    options: {
        importLoaders: 2, // importLoaders的value为数字num,表示在css中遇到@import语句,就从之前的num个loader开始,再次解析这个@import进来的文件。
        // css里background:url();url引入的文件不以esModule的方式导入,直接导入文件本身
        // 因为在css-loader中遇到url引入的图片,会替换为require()引入图片,所以读取的时候只能是require().default获取图片,显然css中不能这样写。所以设置esModule: false,。
        // 在css中不以esModule的方式导入,直接获取图片文件。
        esModule: false, 
    }
}

webpack搭建工程化架构,在打包时如何处理图片,pdf或字体等二进制文件呢?

  • 1 需要引入file-loader来处理这些模块(注:file-loader虽然能处理图片,但图片的处理我们一般用url-loader,因为url-loader能将小文件转为base64编码的图片内嵌到我们的css或html中,以达到减少网页请求数量的目的)
// file-loader的配置
// 匹配pdf
{
    test: /\.pdf$/,
    use: {
        loader: 'file-loader',
        options: {
            // 引入的模块不转为esModule,直接引入文件本身
            esModule: false, 
            outputPath: 'dist/asset/', // 打包指定资源的输出目录
        }
    }
}
// 匹配字体
{
    test: /\.(ttf|woff2?)$/,
    use: {
        loader: 'file-loader',
        options: {
            // 引入的模块不转为esModule,直接引入文件本身
            esModule: false, 
            outputPath: 'dist/asset/', // 打包指定资源的输出目录
        }
    }
}
  • 2 针对图片模块化加载,使用url-loader(注:url-loader默认会将所有的图片都转为base64编码的图片内联到html中)处理。
// url-loader的配置
/* url默认会将所有的图片转为base64,所以在options里可以限制limit,
* 体积在limit内的图片转为base64编码的图片。而大于limit限制的图片url-loader内部
* 会调用file-loader来拷贝这个文件,将文件的地址放到html的内联属性上
*/
{
    test: /\.(png|svg|gif|jpe?g)$/,
    use: {
        loader: 'url-loader',
        options: {
            name: '[name].[hash:6].[ext]', // 打包输出图片的名称
            limit: 2 * 1024,
            outputPath: 'dist/img/', // 打包后img的输出路径
        }
    }
}

因为webpack默认只识别js文件,所以使用loader用来转换特定类型的文件,帮助webpack识别和加载这些文件,形成打包的依赖图。

上面聊了loader,那webpack plugin主要处理什么呢?

因为loader的执行是在模块加载时执行的,那我们知道其实webpack打包时时有自己的一些自定义事件的,plugin基于webpack的事件机制,能让我们的plugin在webpack打包流水线上的任意时机执行。比如

  • 可以在打包开始的时候执行plugin。
  • 可以在打包过程中的某个事件执行。
  • 可以在文件落地磁盘前执行某些操作。 因为loader只在模块加载时执行,无法参与到这些过程中,所以引入了webpack plugin来处理的这些时机的文件。

接下来聊聊webpack常见的插件配置

  • 1:clean-webpack-plugin,在每次打包时都会情清空dist目录内的文件
// clean-webpack-plugin配置
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

plugin: [
    // 也可以传入配置参数,清空dist下指定类型的文件。配置的文档地址:[https://www.npmjs.com/package/clean-webpack-plugin](url)
    new CleanWebpackPlugin();
]
  • 2:html-webpack-plugin,在每次打包时帮我们自动注入数据到html中,避免每次打包都需要手动修改html。
const HtmlWebpackPlugin = require('html-webpack-plugin');

// HTML模板使用ejs语法,写上占位符。在HtmlWebpackPlugin中传入数据可以进行插入对应占位符。
plugin: [
    new HtmlWebpackPlugin({
        title: 'webpack app',
        template:'src/public/index.html',
    });
]
  • 3:webpack.DefinePlugin,在每次打包时注入全局的常量
const { DefinePlugin } = require('webpack');

// HTML模板使用ejs语法,写上占位符。在HtmlWebpackPlugin中传入数据可以进行插入对应占位符。
plugin: [
    new DefinePlugin({
        BASE_URL: '"./"'
        envConfig: JSON.stringify({ mode: 'development' })
    });
]
  • 3:copy-webpack-plugin,每次打包拷贝静态资源到目标dist目录
const CopyWebpackPlugin = require('copy-webpack-plugin');

plugin: [
    new CopyWebpackPlugin({
        patterns: [
            {
                from: 'src/public/',
                to: 'dist/css/', // to不写的话,则默认拷贝到dist目录下
                globOptions: {
                    ignore: ['**/index.html'], // 忽略public下的index.html文件,这个文件不拷贝
                }
            }
        ],
    });
]

上面的loader介绍也好,plugin的介绍也好,本质上都是对js之外的文件资源进行处理。

webpack如何处理js的呢?

首先想到的肯定是babel,babel依赖的@babel/core来解析我们的代码。但如果只通过@babel/core解析es6+的代码。那么编译后的代码是没有变化的。@babel/core依赖的是babel语法转换插件帮助我们转换语法

  • 因为babel使用的是插件机制,每种需要转换的语法都需要我们提前安装对应的语法转换插件,如@babel/plugin-transform-arrow-fucntions插件来转换箭头函数。但是通过开发人员手动安装所有的语法转换插件太过繁琐。
  • 所以官方已经将大部分的es6+语法转换插件预设到@babel/preset-env中。我们开发的时候只需要安装和引入@babel/preset-env就可以使用es6+语法了。-->(这个其实和postcss的postcss-preset-env很像,postcss也是把常用的插件预设到postcss-preset-env中了,我们开发的时候只要引入postcss-preset-env即可)。
  • 但对于很对还在实验性阶段的es语法,还是不会预设到@babel/preset-env中的,所以想要使用最新的实验性es语法,还是需要手动安装引入对应语法的babel转换插件

那在webpack中我们可以引入babel-loader来处理js。

// babel-loader的使用
{
    test: /\.js$/,
    use:[{
        loader: 'babel-loader',
        exclude: '/node_modules/'
        options: {
            preset: [
                 // 引入babel插件集合,但这样使用的话。万一项目中其他地方还要配置babel-loader,还需要将这个options拷贝过去,所以我们可以和postcss类似。在项目根目录新建一个babel.config.js,之后项目中所有的babel-loader都从babel.config.js中读取配置项
               [
                   '@babel/perset-env',
                  // 声明babel转换后的代码需要兼容到哪些浏览器,如果没有配置target。babel则默认会兼容到browsersList中的浏览器版本。所以项目中配置的browsersList,在babel中我们不需要配置target这个参数。所以注释掉这个配置
                  // {
                  //     target: 'chrome 80'
                  // }
               ]
            ]
        }
    }]
}

创建babel.config.js作为项目全局的babel-loader配置使用.

// 当前文件:babel.config.js,新建在项目根目录

module.export = {
    presets: {
        [ '@babel/presets-env' ]
    }
}

// babel.config.js写好后我们只需要在项目中,按下面这样配置我们的babel-loader即可。
// babel-loader会默认去读取babel.config.js中的配置。
// 此时的babel-loader配置如下
{
    loader: 'babel-loader'
}

从postcss-loader和babel-loader的配置可以看出什么共同点吗?

  • 1:都已browserslist作为兼容判断条件的条件
  • 2:都是插件机制,需要对应的兼容转换时都需要引入和使用对应的插件,这样做都比较繁琐和冗余
  • 3:于是postcss和@babel官方都各自抽取了postcss-preset-env和@babel-presets-env的插件集合,支持大部分标准的css和js的兼容。
  • 4:但是在每个postcss-loader或@babel-loader中配置对应的preset-env,代码结构又嵌套比较深,也不好复用。于是postcss和@babel官方各自定义了postcss.config.js和babel.config.js。只要在项目根目录建了这两个文件,之后postcss-loader或@babel-loader都会到对应的配置文件中读取配置。达到整个项目中的postcss和@babel的配置复用
  • 注意:如果在根目录之下的其他路径也新建了babel.config.js。在这个文件目录之下,@babel就会以这个新建的babel.config.js的配置为依据编译代码,可以将babel.config.js理解为是一个具有作用域的配置文件

上面我们聊了babel如何转换es6+语法的配置,那es6+的api该如何转换呢?

@babel7之前我们可以直接通过引入@babel/polyfill来兼容所有已成为标准的es6+新特性。但这样直接引入太过粗暴,很多我们没有用到的特性对应的ployfill也被引入了,导致代码体积变大。

所以在@babel7之后就支持按需引入@babel/polyfill了,那在@babel7之后我们怎么按需引入polyfill了呢?

  • 1:需要我们安装@core-js3,(官方不建议继续使用@core-js2,因为@core-js2将要停止维护了)
  • 2:需要我们安装regenerator-runtime,来支持Promise,async await等语法。

那如何使用呢?

// 当前是babel.config.js文件
module.exports = {
    presets: [
        [
            '@babel/presets-env',
            {
                // 有3个值false(不对当前js文件做polyfill填充),usege(根据我们代码中用到的es6+特性及需要兼容browserslist的版本按需的进行polyfill填充), entry(只根据需要兼容browserslist版本的浏览器进行polyfill,不管源代码中有没有用到es6+新特性),默认false。
                useBuiltIns: "usage",
                corejs: 3, // 这里必须指定corejs为3,否则会默认寻找corejs2,这样打包的时候就会报错。corejs3支持的es6+特性也更多
            }
        ]
    ]
}

注意:如果选择了useBuiltIns: "entry";就需要在项目的入口js文件中引入"core-js/stable"和"regenerator-runtime/runtime"

// 当前为入口js文件
import "core-js/stable";
import "regenerator-runtime/runtime";

一般在工作中我们使用的也就是useBuiltIns: "usage", corejs: 3,来进行@babel/polyfill的按需加载,减少我们代码打包后的体积。

到了这里在webpack中babel的配置也就结束了。接下来我们介绍下webpack提供的其他的功能

如何引入webpack的热更新功能

  • 1:webpack热更新功能依赖于webpack-dev-server,在server中配置hot为true。webpack的配置如下
module.export = {
    mode: 'development',
    devtool: false,
    entry: './src/main.js',
    output: {
        filename: 'js/main.js',
        path: path.resolve(__dirname, 'dist),
    },
    target: 'web', // 开发阶段需要不需要考虑兼容浏览器,target为web,不依赖browserslist
    devServer: {
        hot: true,
        publicPath: '×××', // 静态资源打包后的cdn域名和前缀path
    }
}
  • 2:代码的入口文件配置
// 入口文件main.js

if(module.hot) {
    // app.js下所有的改动都支持热更新
    module.hot.accept(['./app.js'], () => {
        console.log('热更新了')
    })
}

webpack-dev-server与热更新引入后,如何让webpack支持react呢?

  • 1:首先需要babel-loader处理jsx模块,
  • 2:babel-loader使用babel插件@babel/preaet-react来处理jsx模块文件。 这样我们的webapck就支持react的jsx模块文件了。
  • 3:但是react的热更新还依赖于其他的babel插件react-hot-loader@hot-loader/react-dom
//  babel-loader配置
{
    test: /\.jsx?$/,
    exclude: /(node_modules)/,
    use: [
        loader: 'babel-loader',
    ]
}
// babel.config.js配置
module.export = {
    presets: [
        ['@babel/presets-env', {
            useBuiltIns: usage,
            corejs: 3,
        }],
        ['@babel/preset-react'],
        ['react-hot-loader/babel']
    ]
}
// webpack.config.js
module.exports = {
    alias: {
        'react-dom': '@hot-loader/react-dom',
    }
}
// App.jsx
import { hot } from 'react-hot-loader/root';

function App = () => {
    return (<div>
        ×××
    </div>)
}


export default hot(App);
// mian.js
import APP from './app.jsx';
// 模块热替换的 API
if (module.hot) {
    module.hot.accept();
}

基于上面的配置已经支持了react的jsx,在补充上react相关的热更新配置,那么一个基础的react配置就完成了。

结尾:今天先写到这了,之后再补充