webpack高级篇(二)

442 阅读15分钟

四、webpack高级概念

1. TreeShaking概念详解

用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 importexport。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup

webpack 4 正式版本,扩展了这个检测能力,通过 package.json"sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此可以安全地删除文件中未使用的部分。

  1. TreeShaking只支持importES Module引入方式,不支持requirecommonjs引入方式。因为import的底层是静态引入方式,而commonjs是动态引入方式。
// 开发环境
module.exports =  {
    mode: 'development',
    optimization: {
        usedExports: true // 只打包被使用的模块
    }
}
package.json
不需要TreeShaking的写在package.json中
{
  'sideEffects': ['@babel/polly-fill'], // 特殊要处理的内容
}


{
  'sideEffects': false, // 正常的对所有模块进行TreeShaking,没有特殊要处理的东西
}

{
  'sideEffects': [
  '*.css'
  ],
}
npx webpack
// 上线环境
module.exports =  {
    mode: 'production', // production模式自带TreeShaking这些配置,就不需要optimization了
    // optimization: {
    //     usedExports: true // 只打包被使用的模块
    // }
}

配置TreeShaking非常简单

2. Development和Production模式的区分打包

拆分为三个文件:

  1. webpack.common.js

  2. webpack.dev.js

  3. webpack.prod.js

可以将以上3个文件统一放到build文件夹下。

package.json

"scripts": {
    "dev": "webpack-dev-server --config webpack.dev.js",
    "prod": "webpack --config webpack.prod.js",
}
 webpack.common.js
 
 const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const path = require('path')

const makePlugins = (configs) => {
    const plugins = [
      new CleanWebpackPlugin({
        verbose: true // 运行时打印删除的文件 删除的路径与output的路径相同
      })
    ]

    Object.keys(configs.entry).forEach(item => {
        let obj = {
            template: './src/index.html',
            filename: `${item}/index.html`,
            chunks: [item],
            inject: true
        }
        if (item === 'index') {
            obj.filename = 'index.html'
        }
        plugins.push(
            new HtmlWebpackPlugin(obj)
        )
    })

    return plugins
}


const configs =  {
    entry: {
        index: './src/index.js',
    },
    module: {
        rules: [{
            test: /\.(jsx|js)$/,
            exclude: /node_modules/, // 排除
            loader: 'babel-loader',
            options: {
                presets: [['@babel/preset-env', {
                    targets: {
                      chrome: "67"
                    },
                    useBuiltIns: 'usage' // 只引入用到的polyfill
                }],
                "@babel/preset-react"
            ],
            plugins: ["@babel/plugin-syntax-dynamic-import"]
                // "plugins": [["@babel/plugin-transform-runtime", {
                //         "absoluteRuntime": false,
                //         "corejs": 2,
                //         "helpers": true,
                //         "regenerator": true,
                //         "useESModules": false
                //       }]]
            }
        },{
            test: /\.(jpg|png|gif)$/, //.jpg结尾的文件请求file-loader
            use: {
                loader: 'url-loader',
                options: {
                    // placeholder 占位符
                    name: '[name]_[hash:5].[ext]', // 打包为原始的名字和后缀 看文档placerholder部分
                    outputPath: 'images/', //指定生成的文件目录
                    limit: 20480 // 如果图片超过了204800字节(20kb)打包为base64
                }
            }
        },{
            test: /\.(eot|ttf|svg|woff)$/, 
            use: {
                loader: 'file-loader'
            }
        },{
            test: /\.scss$/, //.jpg结尾的文件请求file-loader
            use: [
                'style-loader',
                { loader: 'css-loader', 
                  options: {
                      importLoaders: 2,
                      modules: true, // 开启模块化打包,就可以用style.avatar的方式引入样式了
                      localIdentName: '[name]__[local]--[hash:base64:5]'
                    } }, 
                {
                    loader: 'postcss-loader',
                    options: {
                        ident: 'postcss',
                        plugins: [
                            require('autoprefixer')
                        ]
                    }
                  },
                'sass-loader'
            ]
        },{
            test: /\.css$/, //.jpg结尾的文件请求file-loader
            use: [
                'style-loader',
                'css-loader', 
                {
                    loader: 'postcss-loader',
                    options: {
                        ident: 'postcss',
                        plugins: [
                            require('autoprefixer')
                        ]
                    }
                  }
            ]
        }]

    },
    optimization: {
        splitChunks: {
            chunks: 'all', // 代码分割配置
            cacheGroups: {
                vendors: false,
                default: false
              }
        }
    },
    output: {
        // publicPath: '/', // 所有打包之前的引用都加一个 / 路径
        // publicPath: 'https://cdn.com.cn',
        filename: '[name].js', // 把打包出的bundle.js注入到html中
        path: path.resolve(__dirname, '../dist')
    }
}

configs.plugins = makePlugins(configs)

module.exports = configs
webpack.dev.js

const webpack = require('webpack')
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
// __dirname 指代webpack.config.js所在的当前目录的路径
const devConfig = {
    mode: 'development', // 打包环境,默认为production
    devtool: 'cheap-module-eval-soure-map',
    devServer: {
        contentBase: './dist', // 访问dist目录
        open: true, // 自动打开浏览器
        port: '8080',
        hot: true,
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    optimization: {
        usedExports: true
    },
}

module.exports = merge(commonConfig, devConfig)
webpack.prod.js

const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

const prodConfig = {
    mode: 'production',
    devtool: 'cheap-module-source-map',
}

module.exports = merge(commonConfig, prodConfig)
npm install webpack-merge -D

3. webpack和Code Splitting之间的关系

Code Splitting就是代码分割

"scripts": {
    "dev-build": "webpack --config ./build/webpack.dev.js"
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "prod": "webpack --config ./build/webpack.prod.js",
}

不用webpack配置实现:

 webpack.common.js
 
 const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const path = require('path')

const makePlugins = (configs) => {
    const plugins = [
      new CleanWebpackPlugin({
        verbose: true // 运行时打印删除的文件 删除的路径与output的路径相同
      })
    ]

    Object.keys(configs.entry).forEach(item => {
        let obj = {
            template: './src/index.html',
            filename: `${item}/index.html`,
            chunks: [item],
            inject: true
        }
        if (item === 'index') {
            obj.filename = 'index.html'
        }
        plugins.push(
            new HtmlWebpackPlugin(obj)
        )
    })

    return plugins
}


const configs =  {
    entry: {
        index: './src/index.js',
    },
    module: {
        rules: [{
            test: /\.(jsx|js)$/,
            exclude: /node_modules/, // 排除
            loader: 'babel-loader',
            options: {
                presets: [['@babel/preset-env', {
                    targets: {
                      chrome: "67"
                    },
                    useBuiltIns: 'usage' // 只引入用到的polyfill
                }],
                "@babel/preset-react"
            ],
            plugins: ["@babel/plugin-syntax-dynamic-import"]
                // "plugins": [["@babel/plugin-transform-runtime", {
                //         "absoluteRuntime": false,
                //         "corejs": 2,
                //         "helpers": true,
                //         "regenerator": true,
                //         "useESModules": false
                //       }]]
            }
        },{
            test: /\.(jpg|png|gif)$/, //.jpg结尾的文件请求file-loader
            use: {
                loader: 'url-loader',
                options: {
                    // placeholder 占位符
                    name: '[name]_[hash:5].[ext]', // 打包为原始的名字和后缀 看文档placerholder部分
                    outputPath: 'images/', //指定生成的文件目录
                    limit: 20480 // 如果图片超过了204800字节(20kb)打包为base64
                }
            }
        },{
            test: /\.(eot|ttf|svg|woff)$/, 
            use: {
                loader: 'file-loader'
            }
        },{
            test: /\.scss$/, //.jpg结尾的文件请求file-loader
            use: [
                'style-loader',
                { loader: 'css-loader', 
                  options: {
                      importLoaders: 2,
                      modules: true, // 开启模块化打包,就可以用style.avatar的方式引入样式了
                      localIdentName: '[name]__[local]--[hash:base64:5]'
                    } }, 
                {
                    loader: 'postcss-loader',
                    options: {
                        ident: 'postcss',
                        plugins: [
                            require('autoprefixer')
                        ]
                    }
                  },
                'sass-loader'
            ]
        },{
            test: /\.css$/, //.jpg结尾的文件请求file-loader
            use: [
                'style-loader',
                'css-loader', 
                {
                    loader: 'postcss-loader',
                    options: {
                        ident: 'postcss',
                        plugins: [
                            require('autoprefixer')
                        ]
                    }
                  }
            ]
        }]

    },
    optimization: {
        splitChunks: {
            chunks: 'all', // 代码分割配置
            cacheGroups: {
                vendors: false,
                default: false
              }
        }
    },
    plugins: [new HtmlWebpackPlugin({
        template: 'src/index.html', // 以src下的index.html作为模板生成html
      }),
      new CleanWebpackPlugin(['dist'], {
          root: path.resolve(__dirname, '../') // 根路径修改为当前文件夹的上一层
      }) // 删除dist文件夹下的内容。只能删除根目录下的,不能删除根目录上级的内容
    ],
    output: {
        // publicPath: '/', // 所有打包之前的引用都加一个 / 路径
        // publicPath: 'https://***.com',
        filename: '[name].js', // 把打包出的bundle.js注入到html中
        path: path.resolve(__dirname, '../dist')
    }
}

configs.plugins = makePlugins(configs)

module.exports = configs

Code Splitting就是代码分割

npm install lodash --save
import _ from 'lodash';

console.log(_.join(['a', 'b', 'c'], '***'))
// 此处省略1000行业务逻辑
console.log(_.join(['d', 'e', 'f'], '***'))

此时打包的结果:lodash也被打包到了main.js中,打包生成的main.js的包就会非常大。 问题:打包文件大,加载时间长。而公共类库则基本不会变化。本地有缓存后,修改代码之后,则需要重新加载。

 webpack.common.js
 
const makePlugins = (configs) => {
  


const configs =  {
    entry: {
        lodash: './src/lodash.js',
        main: './src/main.js',
    },
    optimization: {
        splitChunks: {
            chunks: 'all', // 代码分割配置
            cacheGroups: {
                vendors: false,
                default: false
              }
        }
    },
    plugins: [new HtmlWebpackPlugin({
        template: 'src/index.html', // 以src下的index.html作为模板生成html
      }),
      new CleanWebpackPlugin(['dist'], {
          root: path.resolve(__dirname, '../') // 根路径修改为当前文件夹的上一层
      }) // 删除dist文件夹下的内容。只能删除根目录下的,不能删除根目录上级的内容
    ],
    output: {
        // publicPath: '/', // 所有打包之前的引用都加一个 / 路径
        // publicPath: 'https://***.com',
        filename: '[name].js', // 把打包出的bundle.js注入到html中
        path: path.resolve(__dirname, '../dist')
    }
}

当业务逻辑发生变化时,只需要加载main.js即可。

以上的做法不够智能!

用webpack配置来实现:

module.exports = {
  optimization: {
      splitChunks: {
          chunks: 'all' // 帮我们做代码分割,遇到公用类库的时候自动打包生成文件
      }
  }
}

打包后的目录结构:生成了两个js,自动拆分了公共类库。

main.js
vendor~main.js // 

通过合理的代码分割,让我们的项目运行效率更高。

另一种方法: 异步模块的引入。对异步加载的代码也会进行分割,会把库单独放到一个文件中。

function getComponent() {
    return import('lodash').then((default: _ ) => {
       let element = document.createElement('div')
       element.innerHTML = _.join(['a', 'b'], '_')
       return element
    })
}
// 返回element
getComponent().then(element => {
    document.body.appendChild(element)
})

上边的语法需要我们用babel来做转换

npm install babel-plugin-dynamic-import-webpack --save-dev
.babelrc

{
    "presets": [
        ['@babel/preset-env', {
            "targets": {
              "edge": "17",
              "firefox": "60",
              "chrome": "67",
              "safari": "11.1",
            },
            useBuiltIns: 'usage'
          }
        ],
        "@babel/preset-react"
    ],
    "plugins": ["dynamic-import-webpack"]
}

总结:

  1. 代码分割和webpack无关,是单独的一个概念,来提高代码的性能。
  2. webpack有两种实现代码分割的方式:同步、异步
  3. 同步代码:只需要在webpack.common.js中做optimization的配置即可
  4. 异步代码:无需做任何配置,会自动进行代码分割,放置到新的文件中

4. splitChunks配置参数详解

主要作用:将所有的第三方模块(node_modules)中的文件都打包到vendor.js文件中。

移除 babel-plugin-dynamic-import-webpack 插件,不支持魔法注释的写法。"plugins": ["dynamic-import-webpack"]也移除。 使用官方提供的插件。babeljs.io/docs/en/bab…

plugin-syntax-dynamic-import插件作用插件语法动态导入。webpack4默认是允许import动态导入的,但是需要babel的插件支持,目前babel的插件包为:@babel/plugin-syntax-dynamic-import,动态加载的最大好处就是实现了懒加载,用到哪个模块才会加载那个模块,可以提高SPA应用程序的首屏加载速度。

npm install --save-dev @babel/plugin-syntax-dynamic-import
修改.babelrc

{
   "presets": [
       ['@babel/preset-env', {
           "targets": {
             "edge": "17",
             "firefox": "60",
             "chrome": "67",
             "safari": "11.1",
           },
           useBuiltIns: 'usage'
         }
       ],
       "@babel/preset-react"
   ],
   "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

此时就可以使用魔法注释了/* webpackChunkName:"lodash" */,参考文档:Magic Comments

function getComponent() {
   // 调用 import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会被分离到一个单独的chunk中。
    return import(/* webpackChunkName:"lodash" */ 'lodash').then((default: _ ) => { // 魔法注释
       let element = document.createElement('div')
       element.innerHTML = _.join(['a', 'b'], '_')
       return element
    })
}
// 返回element
getComponent().then(element => {
    document.body.appendChild(element)
})

打包出的文件名变为:vendors~lodash.js

webpackChunkName:新 chunk 的名称。从 webpack 2.6.0 开始,[index] and [request] 占位符,分别支持赋予一个递增的数字(1、2、3...)和实际解析的文件名。 因此也可以用/* webpackChunkName: "[request]" */这种占位符的方式来写。

参考文档:webpack.docschina.org/plugins/spl…

cacheGroups:

module.exports = {
  optimization: {
      splitChunks: {
          chunks: 'all',
          cacheGroups: {
            vendors: false,
            default: false
        }
      }
  }
}

splitChunks的默认配置: This configuration object represents the default behavior of the SplitChunksPlugin.

// splitChunks的默认配置
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 只对异步代码的代码分割生效,对同步引入的代码库就不生效,参数: async、initial(同步代码分割)、all,all的时候需要配置cacheGroups参数
      minSize: 30000, // 引入的模块、包只有大于3000个字节(30KB)才会做代码分割
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/, // 检测引入的库是否是在node_modules中,如果是,则代码分割,就会打包到vendors这个组里,生成vendors~***.js,vendors代表符合这个组vendors的要求,***.js代表分割出的代码实际的入口文件名,如:main.js则为vendors~main.js
          priority: -10,
          filename: 'vendors.js', // 这样配置就全部打包生成到了vendors.js中,不再分多个vendors
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

打包同步的代码,不仅仅会走chunks: 'initial'这个配置,还要走cacheGroups这个配置规则

同步引入import,异步引入promise

// splitChunks的默认配置
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 只对异步代码的代码分割生效,对同步引入的代码库就不生效,参数: async、initial(同步代码分割)、all,all的时候需要配置cacheGroups参数
      minSize: 30000, // 引入的模块、包只有大于3000个字节(30KB)才会做代码分割
      maxSize: 50000, // 50KB, lodash 1MB,使用较少
      minChunks: 2, // 当一个模块至少使用了n次之后就会进行代码分割,例如:为2时,则不会进行代码分割
      maxAsyncRequests: 5, // 同时加载的模块数,最多分割5个,大于5个时就不会分割
      maxInitialRequests: 3, // 整个网站首页加载时,入口文件引入其他js文件或库, 入口文件,按默认配置即可
      automaticNameDelimiter: '~', // 组和文件之间的链接符vendors~main.js
      name: true, // 打包生成的名字在cacheGroups中生效
      cacheGroups: { // 缓存组,把所有符合的组都先缓存,再分析
        vendors: {
          test: /[\\/]node_modules[\\/]/, // 检测引入的库是否是在node_modules中,如果是,则代码分割,就会打包到vendors这个组里,生成vendors~***.js,vendors代表符合这个组vendors的要求,***.js代表分割出的代码实际的入口文件名,如:main.js则为vendors~main.js
          priority: -10, // 判断vendors和default放到哪个组中,如果两个组都符合条件,值越大优先级就越高,就放到哪个组中,你可以思考一下为什么要有这个参数
          filename: 'vendors.js', // 这样配置就全部打包生成到了vendors.js中,不再分多个vendors
        },
        default: { // 默认存放位置
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true, // 看之前代码是否已经被引入过打包过,则会忽略这个模块,直接使用之前已经打包过的模块
          filename: 'common.js'
        }
      }
    }
  }
}

如果是自己写的模块,由于不在node_modules文件夹下,则走default规范,生成的文件为default~main.js

最难的模块: cacheGroups上边的代码有效则不会走到cacheGroups分组中。

参考文档:webpack.docschina.org/plugins/spl… 写一些相关split-chunks-plugin的分享,使用心得

5. Lazy Loading懒加载,Chunk是什么?

Lazy Loading是什么

参考文档:webpack.js.org/guides/lazy…

写法1:

function getComponent() {
    return import(/* webpackChunkName:"lodash" */ 'lodash').then((default: _ ) => { // 魔法注释
       let element = document.createElement('div')
       element.innerHTML = _.join(['a', 'b'], '_')
       return element
    })
}
// 放进click事件中,点击页面的任何地方,loadsh才会被加载
document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})

通过import方式可以对某些模块进行懒加载Lazy Loading,在访问页面的时候不会加载,在执行某个事件的时候,才会被加载。懒加载并不是webpack的概念,webpack只不过是能识别import的语法,对其引入的模块进行代码分割。import返回的实际是一个promise语法,因此需要babel-polyfill。新版本babel内置babel-polyfill。 官方说明:https://babeljs.io/docs/en/babel-polyfill

As of Babel 7.4.0, this package has been deprecated in favor of directly including core-js/stable (to polyfill ECMAScript features) and regenerator-runtime/runtime (needed to use transpiled generator functions)

优点:可以让我们的页面加载更快。

异步函数,可以省略掉promise比较复杂的写法,改为async await。 写法2:

async function getComponent() {
    const {default: _} = await  import(/* webpackChunkName:"lodash" */ 'lodash')
    const element = document.createElement('div')
    element.innerHTML = _.join(['a', 'b'], '_')
    return element
}
// 放进click事件中,点击页面的任何地方,loadsh才会被加载
document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})
Chunk是什么

对网站功能进行划分,每一类一个chunk。 在webpack中,生成了几个文件,就是几个Chunk,也就是打包片段。如下图:

minChunks参数:当一个模块至少使用了n次之后就会进行代码分割,例如:为2时,则不会进行代码分割,默认为1.

6. 打包分析,preloading,prefetching

打包分析:在用webpack进行代码打包之后,借助打包分析工具,对我们打包生成的问题件进行分析,进而查看打包是否合理。 参考网站:github.com/webpack/ana…

// package.json

"scripts": {
    "dev-build": "webpack --profile --json > stats.json --config ./build/webpack.prod.js"
}

npm run dev-build // 进行打包,生成了`stats.json`文件

webpack.github.io/analyse/ 打开后,上传stats.json,可进行不同模块分析。 还有其他很多分析工具。如:

  1. webpack-chart
  2. webpack-visualize
  3. webpack-bundle-analyzer 较常用
  4. webpack bundle optimize helper

参考文档:webpack.docschina.org/guides/code…

preloading,prefetching: webpack.docschina.org/guides/code…

optimization: {
    splitChunks: {
        chunks: 'async', // 默认值,默认只对异步代码进行分割
    }
}

首次访问页面,加载速度就是最快的。

在浏览器控制台,command+shift+p,输入coverage,选择show coverage,点击录制按钮,可以查看页面代码的利用率,进而对代码进行分析,改进。重点考虑的并不是代码缓存的问题,而是代码的使用率。把首次进入页面不执行的代码进行拆分。

从上图可以看出,此页面加载的代码使用率只有70%,584KB的代码只有409KB被使用。理论上还有100多KB可以被优化,从而提高首屏加载的时间,速度就会更快。因此,webpack希望我们用更多的异步加载代码,同步代码打包成vendor.js意义并不是很大。只有更多的异步代码,才能让网站的性能得到提升。因此chunks的默认值为async。同步代码只能增加缓存,对性能的提升非常有限

在空闲时间加载不是第一次就要执行的代码,比如模态框等。就满足了首页加载快的问题,也满足了再使用时再加载反应慢的问题。这个解决方案就依赖preloading,prefetching

使用magic commont语法,放到import语句中,/* webpackPrefetch: true */表示主要js加载完成之后,网络带宽有空闲的时候,才会加载此js。类似于<script async ></script>async属性。

// prefetching
import(/* webpackPrefetch: true */ 'LoginModal');
// preloading
import(/* webpackPreload: true */ 'ChartingLibrary');

prefetching与preloading的区别:prefetching是主流程加载完之后才会加载,preloading是和主业务文件一起加载。

最优编码方式:缓存带来的代码性能提升非常有限,重点考虑如何让页面加载的js文件代码的利用率最高。交互之后才用到的代码可以写到异步组件中,通过懒加载把代码逻辑加载进来,这样才会提升页面性能。因为懒加载导致的反应慢的问题,可以用prefetching、preloading来解决。

前端代码性能优化,要把重点放在code coverage代码覆盖率上,缓存并不是最重要的点

7. css文件的代码分割

webpack.common.js

output: {
  filename: '[name].js', // 入口文件
  chunkFilename: '[name].chunk.js', // 不是入口文件则走这个chunkFilename
}

生成的js文件

main.js
vendors~lodash.chunk.js

参考文档:webpack.docschina.org/configurati…

react-china.org/t/webpack-o…

css文件的代码分割

参考文档: webpack.docschina.org/plugins/min…

默认css会直接打包到js中,借助mini-css-extract-plugin可单独打包为css文件,此包暂不支持热更新。因此此插件一般用在线上环境打包时使用。

// webpack.prod.js

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',
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader,
              options: {
                publicPath: '../'
             }
            },
            "css-loader"
          ]
        }
      ]
    },
    plugin: [
      new MiniCssExtractPlugin({
          filename: '[name].css',  // 被页面直接引用,走这个名字
          chunkFilename: '[name].chunk.css',
      }),
      new OptimizeCssAssetsPlugin({
        assetNameRegExp: /\.optimize\.css$/g,
        cssProcessor: require('cssnano'),
        cssProcessorPluginOptions: {
          preset: ['default', { discardComments: { removeAll: true } }],
        },
        canPrint: true
      })
    ]
}

module.exports = merge(commonConfig, prodConfig)

要使用这个插件,还需要对loader进行配置。之前是通过style-loader将css样式挂载到页面上,现在要改为使用此插件来单独生成一个文件。

参考文档:github.com/NMFR/optimi… 压缩css

MiniCssExtractPlugin中也借助splitChunks

知识点:

  1. filename和chunkFilename要搞清楚
  2. mini-css-extract-plugin这个插件只能用在线上环境,因为不支持HMR

8. webpack与浏览器缓存(caching)

contenthash

// webpack.prod.js

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

webpack版本较老时:

optimization: {
  runtimeChunk: {
    name: entrypoint => `runtime~${entrypoint.name}`
  }
},

main.js放业务逻辑,vendors.js放引入的库。业务逻辑和库之间也是有关联的,关联都放在了,manifest内置了包和包之间的关系,runtime.js抽离出来中放对应manifest相关的关系。

9. Shimming(垫片)的作用

babel-polyfill的作用:在低版本浏览器上的兼容问题。垫片。

webpack是基于模块打包的,webpack的这些变量只能在模块这一个文件中被使用。在另一个文件中就无法使用这个模块的变量。

// webpack.common.js

const webpack = require('webpack')
plugins: [
    new webpack.ProvidePlugin({
        $: 'jquery'  // 如果在代码的模块中使用了$这个关键字,则会自动引入jquery,把模块的名字命名为$。 import $ from 'jquery',在配置里帮我们解决了
        _: 'lodash', // 也可以写成这样: _join: ['lodash', 'join']
    })
],

代码如下依旧可以正常编译:

export function ui() {
    $('body').css('background', _.join(['blue'], ''))
}

export function ui() {
    $('body').css('background', _join(['blue'], ''))
}
// this指向问题
console.log(this)
console.log(this === window) // 一个模块里的this默认指向模块自身,而不是window
npm install imports-loader --save-dev // 修改webpack默认行为,就是shimming

使用多个loader

module: {
  rules: [
    {
        test: /\.js$/,
     	exclude: /node_modules/,
     	use: [{
     	   loader: 'babel-loader' // 2.在用babel-loader做文件编译
     	}, {
     	   loader: 'imports-loader?this=>window' // 1.先 this一直指向window
     	}] ,
    }
  ]
}

shimming这个概念很宽泛,遇到不同场景,找对应的解决方法。

参考文档: webpack.docschina.org/guides/shim…

10. 环境变量的使用

只需要一个common.js文件通过在package.json中传递不同的参数,区分是开发环境还是生产环境。

这种写法并不常用,只是为了让我们理解webpack中全局变量的概念

//  package.json

{
  "name": "***",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev-build": "webpack --config ./build/webpack.common.js",
    "dev": "webpack-dev-server --config ./build/webpack.common.js",
    "build": "webpack --env.production --config ./build/webpack.common.js" //通过--env.production,把环境变量传进去
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.2.0",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@babel/plugin-transform-runtime": "^7.2.0",
    "@babel/preset-env": "^7.2.0",
    "@babel/preset-react": "^7.0.0",
    "autoprefixer": "^9.3.1",
    "babel-loader": "^8.0.4",
    "clean-webpack-plugin": "^1.0.0",
    "css-loader": "^1.0.1",
    "express": "^4.16.4",
    "file-loader": "^2.0.0",
    "html-webpack-plugin": "^3.2.0",
    "imports-loader": "^0.8.0",
    "mini-css-extract-plugin": "^0.5.0",
    "node-sass": "^4.10.0",
    "optimize-css-assets-webpack-plugin": "^5.0.1",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "url-loader": "^1.1.2",
    "webpack-cli": "^3.1.2",
    "webpack-dev-middleware": "^3.4.0",
    "webpack-dev-server": "^3.1.10",
    "webpack-merge": "^4.1.5"
  },
  "dependencies": {
    "@babel/polyfill": "^7.0.0",
    "@babel/runtime": "^7.2.0",
    "@babel/runtime-corejs2": "^7.2.0",
    "jquery": "^3.3.1",
    "lodash": "^4.17.11",
    "react": "^16.6.3",
    "react-dom": "^16.6.3",
    "webpack": "^4.25.1"
  }
}
//  webpack.common.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const merge = require('webpack-merge');
const devConfig = require('./webpack.dev.js');
const prodConfig = require('./webpack.prod.js');
const commonConfig = {
	entry: {
		main: './src/index.js',
	},
	module: {
		rules: [{ 
			test: /\.js$/, 
			exclude: /node_modules/,
			use: [{
				loader: 'babel-loader'
			}, {
				loader: 'imports-loader?this=>window'
			}]
		}, {
			test: /\.(jpg|png|gif)$/,
			use: {
				loader: 'url-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
					limit: 10240
				}
			} 
		}, {
			test: /\.(eot|ttf|svg)$/,
			use: {
				loader: 'file-loader'
			} 
		}]
	},
	plugins: [
		new HtmlWebpackPlugin({
			template: 'src/index.html'
		}), 
		new CleanWebpackPlugin(['dist'], {
			root: path.resolve(__dirname, '../')
		}),
		new webpack.ProvidePlugin({
			$: 'jquery',
			_join: ['lodash', 'join']
		}),
	],
	optimization: {
		runtimeChunk: {
			name: 'runtime'
		},
		usedExports: true,
		splitChunks: {
      chunks: 'all',
      cacheGroups: {
      	vendors: {
      		test: /[\\/]node_modules[\\/]/,
      		priority: -10,
      		name: 'vendors',
      	}
      }
    }
	},
	performance: false,
	output: {
		path: path.resolve(__dirname, '../dist')
	}
}

module.exports = (env) => {
	if(env && env.production) {//线上环境
		return merge(commonConfig, prodConfig);
	}else {//开发环境
		return merge(commonConfig, devConfig);
	}
}
// webpack.dev.js

const webpack = require('webpack');

const devConfig = {
	mode: 'development',
	devtool: 'cheap-module-eval-source-map',
	devServer: {
		contentBase: './dist',
		open: true,
		port: 8080,
		hot: true
	},
	module: {
		rules: [{
			test: /\.scss$/,
			use: [
				'style-loader', 
				{
					loader: 'css-loader',
					options: {
						importLoaders: 2
					}
				},
				'postcss-loader',
				'sass-loader',
				
			]
		}, {
			test: /\.css$/,
			use: [
				'style-loader',
				'css-loader',
				'postcss-loader'
			]
		}]
	},
	plugins: [
		new webpack.HotModuleReplacementPlugin()
	],
	output: {
		filename: '[name].js',
		chunkFilename: '[name].js',
	}
}

module.exports = devConfig;
// webpack.prod.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

const prodConfig = {
	mode: 'production',
	devtool: 'cheap-module-source-map',
	module: {
		rules:[{
			test: /\.scss$/,
			use: [
				MiniCssExtractPlugin.loader, 
				{
					loader: 'css-loader',
					options: {
						importLoaders: 2
					}
				},
				'postcss-loader',
				'sass-loader',
				
			]
		}, {
			test: /\.css$/,
			use: [
				MiniCssExtractPlugin.loader,
				'css-loader',
				'postcss-loader'
			]
		}]
	},
	optimization: {
		minimizer: [new OptimizeCSSAssetsPlugin({})]
	},
	plugins: [
		new MiniCssExtractPlugin({
			filename: '[name].css',
			chunkFilename: '[name].chunk.css'
		})
	],
	output: {
		filename: '[name].[contenthash].js',
		chunkFilename: '[name].[contenthash].js'
	}
}

module.exports = prodConfig;

--env.production改为--env.production===abc,则使用方法如下:

module.exports = (env) => {
	if(env && env.production === 'abc') {//线上环境
		return merge(commonConfig, prodConfig);
	}else {//开发环境
		return merge(commonConfig, devConfig);
	}
}

11. 使用EnvironmentPlugin来设置环境变量

参考文档:www.webpackjs.com/plugins/env…

webpack.js.org/plugins/env…

EnvironmentPlugin 是一个通过 DefinePlugin 来设置 process.env 环境变量的快捷方式。

使用示例: 当我们需要在项目中添加埋点,需要将相应的版本号信息上报的时候,就可以这么做:

// 引入项目版本号
const { version } = require('../package.json')
const { EnvironmentPlugin } = require('webpack')
new EnvironmentPlugin({
  PKG_VERSION: version,
})

这样就将 PKG_VERSION注入到了环境变量中

使用:

// xxx.js
release_version: process.env.PKG_VERSION

webpack编译后的效果为(假设package.json中版本号为0.1.0):

// xxx.js
release_version: '0.1.0'

这样就将环境变量引入到了项目文件中!