webpack学习笔记(二)

192 阅读10分钟

一、webpack高级概念

1. tree shaking

可以将应用程序想象成一棵树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
tree shaking依赖于ES2015模块系统中的静态结构特性,例如 import 和 export。很多时候,我们只需要引入库的某个部分,而非全部,利用它会去除其他部分,将减小打包后的体积。mode为production的时候,optimization: { usedExports: true}已经是配置好的,不需要再配置。

#webpack.config.js
//开发模式
optimization:{
    usedExports:true
}

由于tree shaking会将未使用或者未导入的模块全部删除,这里的"sideEffects有很大的用途,比如我们在使用@babel/polyfill的时候,他的内部并没有使用export导出任何模块,他只是通过类似windows.Promise这样给全局T添加一些函数,但是我们使用Tree Shaking这种去打包的时候,他会发现这个模块我们并没有通过import引入任何模块,他会以为,我们并没有使用这个模块,不会对他进行打包,这时候,需要配置:"sideEffects": ["@babel/polyfill"],打包的时候不会对这个模块进行摇树。 如果引入代码无副作用,则设置为sideEffects:false;

#package.json
"sideEffects": false

2. production和development模式的区分打包

开发环境和生产环境的构建目标差异很大。在开发环境中,需要具有实时重新加载或热模块替换能力、source map和localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack配置。
新建webpack.dev.js、webpack.prod.js和webpack.common.js。将通用配置放到webpack.common.js中,借助webpack-merge插件将代码合并。

//安装merge
npm install --save-dev webpack-merge
  • common.js
const path = require("path");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
    entry:{
        main:"./src/index.js",
        other:'./src/other.js'
    },
    output:{
        filename:"[name].[hash].js",
        path:path.resolve(__dirname,"../dist")
    },
    module:{
        rules:[
            {
                test:/\.css$/,
                use:[
                    "style-loader",
                    {   
                        loader:"css-loader",
                        options:{
                            importLoaders:1
                        }
                    },
                    "postcss-loader"
                ],
                
            },
            {
                test:/\.less$/,
                use:["style-loader","css-loader","less-loader"]
            },
            {
                test:/\.js$/,
                exclude:/node_modules/,
                use: {
                    loader: "babel-loader"
                }
            },
            {
                test:/\.(png|jpg|gif)$/,
                use:[{
                    loader:"url-loader",
                    options:{
                        name:"[name]_[hash].[ext]",
                        outputPath:"images/",
                        limit:3000
                    }
                }]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: './src/index.html'
        }),
    ],
    optimization:{
        usedExports:true,
        splitChunks:{
            chunks:"all",
            minSize: 3
        }
    }
}
  • webpack.dev.js
const merge = require("webpack-merge");
const webpack = require('webpack');
const commonConfig = require('./webpack.common')
module.exports = merge(commonConfig,{
    mode:"development",
    devtool:"cheap-module-eval-source-map",
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
    ],
    devServer:{
        port:"8081",
        open: true,
        // 打包后访问的html所在路径
        contentBase:"./dist",
        hot:true
    }
})
  • webpack.prod.js 安装uglifyjs-webpack-plugin压缩代码,由于这个插件会影响webpack编译速度,一般在生产环境使用。
const merge = require("webpack-merge");
const commonConfig = require('./webpack.common');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = merge(commonConfig,{
    mode:"production",
    devtool:"source-map",
    plugins: [
      new UglifyJSPlugin()
    ]
})

!注意:由于tree-shaking会将import的css和less文件去除。修改package.json:

"sideEffects": ["*.css","*.less"]

3.代码分离

代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。 有三种常用的代码分离方法:

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:splitChunks提取被重复引入的文件,单独生成一个或多个文件,这样避免在多入口重复打包文件。是webpack内置插件,无需安装。
#webpack.common.js      
    optimization: {
      splitChunks: {
        chunks: 'async',//all:同步和异步;initial:同步
        minSize: 30000,//文件在压缩前的最小尺寸
        maxSize: 0,//设置输出文件的最大体积,如果需要打包的模块超过这个大小,他会进行分割成多个文件进行打包输出
        minChunks: 1,//当一个模块被用了至少多少次的时候,才进行分割。
        maxAsyncRequests: 5,//同时加载的模块数。如果页面引用的模块超过五个,不会对超过的模块进行代码分割
        maxInitialRequests: 3,//入口文件进行加载引入的模块最多数,如果入口文件引入模块超过三个,超过的就不会进行代码分割
        automaticNameDelimiter: '~',//打包输出文件的连接符,例如vendors~main.js;vendors是组名,后面就是连接符;vendors~main.js意思是vendors组的入口文件是main.js
        name: true,
        cacheGroups: {
            // 如果引入的包是node_modules里面的内容,会进入到这里的配置
          vendors: {
            test: /[\\/]node_modules[\\/]/,//检测引入的第三方库是不是node_modules里面的内容
            priority: -10,
            filename: 'vendors.js' //如果是node_modules里面的内容,会打包到这个文件里面
          },
          // 如果引入的包不是node_modules里面的内容,会进入到这里的配置
          default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true,
            filename: 'common.js'
          }
        }
      }
   }
  • 动态导入:通过模块的内联函数调用来分离代码。

4.Lazy Loading 懒加载

懒加载即按需加载,在页面初始化的时候,不加载那些初始化不需要的包文件,只在需要包的函数中,进行异步加载包文件。异步代码(import):无需做任何配置,会自动进行代码分割。

function getComponent () {
  return import(/* webpackChunkName:"lodash"*/'lodash').then(({default: _}) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['a','b','c'],'***')
    return element
  })
}
// 点击页面才会执行
document.addEventListener('click', () => {
  getComponent ().then((element) => {
    document.body.appendChild(element)
  })
})

async await改写

async function getComponent () {
  const {default: _} = await import(/* webpackChunkName:"lodash"*/'lodash');
  const element = document.createElement('div')
  element.innerHTML = _.join(['a','b','c'],'***')
  return element
}
// 点击页面才会执行
document.addEventListener('click', () => {
  getComponent ().then((element) => {
    document.body.appendChild(element)
  })
})

5.CSS 文件的代码分割

  • 使用MiniCssExtractPlugin 插件进行css代码分割
    MiniCssExtractPlugin插件把css样式从js文件中提取到单独的css文件中。需要注意:这个插件不支持热更新,一般在线上环境使用这个插件。
npm install --save-dev mini-css-extract-plugin

在开发环境的配置中添加下面的配置,由于是将css单独抽出来,注意去除style-loader。

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const prodConfig = {
    // 配置打包模式
    mode: 'production',
    devtool: 'cheap-module-source-map',
    plugins: [
      new MiniCssExtractPlugin({})
    ],
    module: {
      rules: [
        {
            test: /\.css$/,
            use: [
                MiniCssExtractPlugin.loader,
                'css-loader',
                'postcss-loader'
            ]
        }
      ]
    }
}
// 模块导出的是两个文件的合并
module.exports = merge(commonConfig, prodConfig)

当打包的文件直接引入到页面的时候他的命名规则会走filename的配置项,如果是间接引入到页面,就会走下面的chunkFilename的配置项。

    plugins: [
      new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[name].chunk.css',
      })
    ]
  • 对打包输出的css文件进行压缩,在webpack.prod.js里面引入这个插件,进行配置如下:
npm install --save-dev optimize-css-assets-webpack-plugin
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const prodConfig = {
......
    optimization: {
      minimizer: [new OptimizeCssAssetsPlugin({})]
    },
  }
  • 多个js入口文件引入的css文件打包输出为一个文件
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true,//忽略其他打包css的默认配置
        },
      },
    },
  • 根据配置入口js文件不同,对其中引入的css文件进行单独打包
    如下面配置:入口文件有foo跟bar,分别进行打包输出为不同的文件:
    splitChunks: {
      cacheGroups: {
        fooStyles: {
          name: 'foo',
          test: (m, c, entry = 'foo') =>
            m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true,
        },
        barStyles: {
          name: 'bar',
          test: (m, c, entry = 'bar') =>
            m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true,
        },
      },
    },

6.浏览器缓存

contenthash指内容更改,hash值才会改变,生产环境中配置如下:

    output: {
      filename: '[name].[contenthash].js',
      chunkFilename: '[name].[contenthash].js'
    }

对于新版本的webpack4.x打包之后如果文件没有更改,会保持不变,但是老版本的webpack4.x货值之前的版本,有可能会发生变化。这个时候需要在optimization选项中添加下面的配置:

    optimization: {
      runtimeChunk: {
          name: 'runtime'
      },
    }

7.Shimming的作用

  • Shimming的使用
    webpack编译器能够识别遵循ES2015模块语法、CommonJS或 AMD规范编写的模块,Jquery这些库会创建一些需要被导出的全局变量。由于每个模块都是独立的,入口模块引入了jquery,其他模块也是无法使用的,如果每个模块都引入jquery文件,也不可行。可以利用shimming解决问题,在代码中检测到$,就引入jquery。
    join: ['lodash', 'join']意思就是,当我们打包的时候遇到_join,就去引入lodash,将lodash中的join方法,赋值。
#webpack.common.js
const webpack = require('webpack')
module.exports = {
    plugins: [
        .....
        new webpack.ProvidePlugin({
            $: 'jquery',
            _join: ['lodash', 'join']
        })
    ],
}
  • 利用Shimming改变模块中this指向window
    由于模块的this并不是指向全局,借助imports-loader改变模块中this的指向
npm install imports-loader --save-dev

当我们打包js文件的时候,会走下面的规则,然后将我们每个模块中的this指向我们的window对象。

    module: {
        rules: [{ 
            test: /\.js$/,
            exclude: /node_modules/,
            use: ["babel-loader","imports-loader?this=>window"]
        }]
       }

8.Webpack环境变量的使用方法

npm install cross-env --save-dev
#package.json
  {
    //注意在打包之前设置NODE_ENV
    "build": "cross-env NODE_ENV=production webpack --config ./build/webpack.common.js"
  }
}
#webpack.common.js
const env = process.env.NODE_ENV
module.exports = ()=>{
    if (env && env=="production") {
        return merge (commonConfig, prodConfig)
    } else {
        return merge (commonConfig, devConfig)
    }
}

二、webpack实战配置

1.PWA(Progressive Web Application)的打包配置

首先安装http-server,让打包输出的dist的文件夹中启动一个服务器。服务器挂了,可以利用PWA,把之前访问过的页面显示出来。

npm install http-server -D

输入命令npm install workbox-webpack-plugin --save-dev进行安装,由于只需要在线上环境,使用pwa技术,让用户体验更好,所以,我们只需要修改webapck.prod.js的配置文件。

const { GenerateSW } = require('workbox-webpack-plugin')
const prodConfig = {
    plugins: [
        new GenerateSW()
    ]
}

npm run build后,会生成service-worker.js。接下来注册Service worker,在入口文件添加以下代码:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js').then(registration => {
      console.log('SW registered: ', registration);
    }).catch(registrationError => {
      console.log('SW registration failed: ', registrationError);
    });
  });
}

2. 使用 WebpackDevServer 实现请求转发

当请求'/react/api',接口会转发到'www.dell-lee.com'; 访问http://localhost:8081/react/api/header.json相当于访问https://www.dell-lee.com/header.json;通过pathRewrite重写请求路径。
有时候我们请求的地址是https协议的网址,需要加一个配置项:secure: false,

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        '/react/api': {
          target: 'https://www.dell-lee.com',
          secure: false,
          pathRewrite: {
            'header.json': 'demo.json'
          }
        },
      },
      hot: true,
    }

在这里也可以进行拦截,比如下面的代码配置:bypass里面的配置项就是说当发送请求要接手的是html页面数据的时候,也就是说请求是一个html的地址的时候,直接跳转到index.html页面。

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        '/react/api': {
          target: 'https://www.dell-lee.com',
          secure: false,
          bypass: function(req, res, proxyOptions) {
            if (req.headers.accept.indexOf('html') !== -1) {
              console.log('Skipping proxy for browser request.');
              return '/index.html';
            }
          },
          pathRewrite: {
            'header.json': 'demo.json'
          }
        },
      },
      hot: true,
    },

下面代码表示遇到'/auth', '/api'这两个地址,都是转发到https://www.dell-lee.com这个服务器。

    devServer: {
      proxy: {
      context: ['/auth', '/api'],
      target: 'https://www.dell-lee.com',
      },
      hot: true,
    }

如果我们想做一个根目录的路径的转发,也就是'/',我们需要将配置项的index设置为false或者为''空字符串,如下配置:

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        index: '',
        '/': {
          target: 'https://www.dell-lee.com',
          changeOrigin: true
 ...

3. 使用DllPlugin提高打包速度

引入第三方模块的时候,要去使用打包输出的第三方模块的文件进行引入。如果引入了过多的第三方库文件,会使打包的速度降低,如果能够实现在第一次打包的时候就去分析第三方模块的代码,然后再次打包的时候,就根据前面分析的结果进行打包,不用再次分析,提高打包效率。
新建webpack.dll.js。将引入的第三方模块打包到dll文件夹中,library: '[name]',将打包后的第三方模块通过变量的形式暴露到全局中;变量的名字叫vender。
然后利用使用DllPlugin进行分析,name: '[name]',是指要分析的库名,将将分析的结构放在../dll/[name].manifest.json。manifest.json会映射到相关的依赖。

#webpack.dll.js
const path = require('path')
const webpack = require('webpack')
nodule.exports = {
    mode: 'production',
    entry: {
        vendors: ['react', 'react-dom', 'lodash']
    },
    output: {
        filename: '[name].dll.js',
        path: path.resolve(__dirname, '../dll'),
        library: '[name]'
    },
    plugins: [
        new webpack.DllPlugin({
            name: '[name]',
            path: path.resolve(__dirname, '../dll/[name].manifest.json')
        })
    ]
}

修改package.json

"scripts":{
    "build:dll": "webpack --config ./build/webpack.dll.js",
}

接下来需要安装dd-asset-html-webpack-plugin,将打包后的文件进行挂载到我们打包输出的页面中,作用就是往html页面中去增加静态资源。使用DllReferencePlugin读取vendor-manifest.json文件,查看是否有第三方库,如果有就不需要再引入了。

npm install add-asset-html-webpack-plugin --save
#webpack.common.js
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
......
module.exports = {
......
    plugins: [
        new AddAssetHtmlWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
        }),
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
        })
    ]
.......
}

执行npm build:dll命令后,在打包,会发现打包速度提高了很多。
我们还可以对第三方库进行拆分。

const plugins = [
    new CleanWebpackPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpakcPlugin({
        template: './src/index.html'
    })
]
const fs = require('fs')
const files = fs.readdirSync(path.resolve(__dirname, './dll'))
files.forEach(file => {
    if(/.*\.dll.js/.test(file)) {
        plugins.push(
          new AddAssetHtmlWebpackPlugin({
              filepath: path.resolve(__dirname, '../dll', file)
          }))
    }
    if(/.*\.manifest.json/.test(file)) {
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, '../dll', file)
        })
    }
})

const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
......
module.exports = {
......
    plugins: plugins
.......
}