你需要知道的webpack高级概念

517 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

一. tree shaking

1.1 概念

在打包过程中,用来删除没有被使用到的文件,减小bundle.js的体积

1.2 使用

  • 在生产环境下

    • tree shaking默认已经配置好了,只需要添加如下配置即可
    • sideEffects配置的作用是,用来排除打包的时候,不需要使用 tree shaking的文件,
    • sideEffects设置为false的时候,是告诉webpack,所有文件都使用tree shaking
      //package.json
     //...
     //sideEffects: ["*.css","./src/some.js"]
     "sideEffects":false
    
  • 在开发环境下

    • 需要我们手动配置 tree shaking
     // webpack.config.js
     module.exports = {
         // ...
         optimization: {
             usedExports: true
        }
     }
    
     //package.json
     "sideEffects": false
    

详情参考webpack官网:tree-shaking

二. 开发和生产模式下的打包模式区分

可以分别配置开发和生产环境各自独立的配置文件

在打包的时候,通过package.json的scripts分别配置生产环境和开发环境各自的打包命令

 // webpack.common.js
 ​
 const path = require('path')
 const htmlWebpackPlugin = require('html-webpack-plugin')
 const { CleanWebpackPlugin } = require('clean-webpack-plugin')
 ​
 module.exports = {
     entry: path.resolve(__dirname,'./src/index.js'),
     output:{
         path: path.resolve(__dirname,'./dist'),
         filename'bundle.js'
    },
     plugins: [
         new CleanWebpackPlugin(),
         new htmlWebpackPlugin({
             title'prouction'
        })
    ]
 }
 // webpack.dev.js
 const common require('./webpack.common')
 const merge require('webpack-merge')
 ​
 module.exports = merge(common,{
     mode'development',
     devtool'inline-source-map',
     devServer: {
         contentBase'./dist'
    }
 })
 // webpack.prod.js
 const merge = require('webpack-merge')
 const common = require('./webpack.common')
 const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
 const webpack = require('webpack')
 ​
 module.exports = merge(common,{
     mode'production',
     plugins: [
         new  UglifyJSPlugin(),
         new webpack.DefinePlugin({
             'process.env.NODE_env'JSON.stringify('production')
        })
    ]
 })
 // package.json
 ​
 scripts:{
     "dev": "webpack-dev-server --config webpack.dev.js",
     "prod": "webpack --config webpack.prod.js"
 }

详情参考Webpack官网:development

三. code splitting

作用: 代码分割,通过合理的代码分割,可以使代码性能更高

核心: 提取不会更改的核心内库,避免修改后重复加载,影响页面响应性能

示例:

 // webpack.config.js
 module.exports = {
     optimization: {
         splitChunks:{
             chunks: 'all'
        }
    }
 }  

详情参考webpack官网:code splitting

四. Caching

问题描述: 一般情况下,为了使页面在每次版本更新之后,都会主动加载最新的代码,我们在打包文件的时候,一般会给文件加上特定的版本号,这样虽然解决了加载旧缓存的问题,但是带来了一个新问题,由于文件名发生了变化,每次修改代码都会重新加载整个资源文件,导致网页性能变差。

思考: 资源文件中包含很多通用的代码,在打包的时候,提取运行时的文件(即通用的),将修改后需要重新加载的部分单独打包到一个文件,每次修改之后的更新,提取出来的运行时文件不变,这样可以使浏览器加载该文件的时候,直接从浏览器获取,提升页面性能

使用方法:

  1. 修改输出文件名,为每一个打包之后的输出文件加上特有的hash值
 module.exports = {
     output:{
         pathpath.resolve(__dirname,'./dist'),
         filename: '[name][contenthash].js'
    },
 }
  1. 将bundle.js中的运行时文件拆分为单独的文件(每次打包的时候,提取的运行时文件不发生变化,直接从缓存中取即可,不用重复加载)
 module.exports = {
      optimization: {
        runtimeChunk: 'single'
    }
 }
  1. 结合代码分割一起使用,提升界面性能
 module.exprots = {
     optimization: {
        runtimeChunk: 'single',
        splitChunks: {
            cacheGroups: {
                verdor: {
                    test: /[\/]node_modules[\/]/,
                    name: 'vendors',
                    chunks: 'all'
                }
            }
        }
    }
 }

详情参考webpack官网:Caching

五. webpack打包分析

  • webpack打包分析官网
  • 在官网中有介绍到,要使用webpack打包分析,首先得生成一个JSON文件,可通过如下命令打包生成
 webpack --profile --json > stats.json

使用该命令打包之后,会在项目目录下面生成一个stats.json的文件,该文件包含打包的一些信息

打开官网中推荐的链接,选择刚刚打包生成的 stats.json文件,即可查看webpack的打包分析

其它打包工具推荐

  • Webpack官网-----》 Guides ------》 Code Splitting ------》 BundleAnalysis
  • 该目录下推荐了好几种Webpack打包分析,使用方法跟上述方法一样,选择stats.json文件即可
  • 推荐:webpack-bundle-analyzer

六. Prefetching和Preloading

缓存解决的问题是: 非首次加载代码的时候,浏览器直接从缓存中获取文件,从而减少请求,提升效率

webpack的优化并不满足于此:Webpack希望第一次访问界面的时候,它的速度就是最快的(提升页面的代码使用率)

缓存带来的效率提升有限

Prefetching和Preloading的作用

  • 首次加载的时候,只需要加载核心代码
  • 在页面展示出来之后,即网络空闲的时候,加载后续界面调用需要用到的代码

区别:

  • prefetching:在界面界面核心代码加载完成之后,空闲的时候加载文件
  • preloading:和核心代码块一起加载

使用:

  • webpack推荐使用异步加载的方法,更利于网页的优化
  • import的文件路径前面加上 /* webpackPrefetch: true */即可使用prefetch
  • 前提是使用了 babel模块支持ES6语法
 // index.js
 //...
 document.addEventListener('click', () =>{
 import(/* webpackPrefetch: true */ './click.js').then(({default: func}) => {
 func();
 })
 });
 // click.js
 function handleClick() {
 const element = document.createElement('div');
 element.innerHTML = 'Dell Lee';
 document.body.appendChild(element);
 }
 ​
 export default handleClick;

七. Css 代码分割

默认情况下,webpack打包的时候,会将CSS文件打包到js文件中

MiniCssExtractPlugin插件: 用来分割CSS代码,常用于线上打包环境

详细使用见webpack官网:mini-css-extract-plugin

八. Shimming

解决webpack打包过程中的兼容性问题

详见webpack官网:shimming

九. 环境变量的使用

在构建的命令行中传入 --env.production参数

在 webpack.common.js中根据参数判断打包类型

使用:

 // package.json
 ​
  "scripts": {
     "dev-build": "webpack --config ./build/webpack.common.js",
     "prod-build": "webpack --env.production --config ./build/webpack.common.js"
  },
 // webpack.common.js
 const merge = require('webpack-merge');
 const devConfig = require('./webpack.dev.js');
 const prodConfig = require('./webpack.prod.js');
 const commonConfig = {
 //...
 }
 ​
 module.exports = (env) => {
 if(env && env.production) {
 return merge(commonConfig, prodConfig);
 }else {
 return merge(commonConfig, devConfig);
 }
 }
 ​

十. 编写一个Loader

10.1 同步loader

  1. 新建文件夹,my-loader

  2. 初始化文件夹:npm init -y

  3. 在my-loader文件夹下新建loaders文件夹,存放我们的loader文件

    • 我们创建一个Loader,功能是替换掉我们js文件中的某个字符串
    • 新建 replaceLoader,文件内容如下
     // replaceLoader.js
     // 
     module.exports = function(source){
         return source.replace('myname','world')
     ​
     }
    
  4. 新建src文件夹

    • 新建index.js文件,随便写点代码,例如
     // index.js
     ​
     console.log('hello myname')
    
  5. 在项目目录下面新建 webpack.config.js 文件

    • 指定 mode以及打包入门和出口
    • 为js文件配置我们自己写好的loader
     const path = require('path')
     ​
     module.exports = {
         mode: 'development',
         entry: {
             main'./src/index.js'
        },
         output: {
             path: path.resolve(__dirname,'./dist'),
             filename: '[name].js'
        },
         module: {
             rules: [
                        {
                             loader: path.resolve(__dirname,'./loaders/replaceLoader.js')
                        }
                    ]
                }
            ]
        }
     }
    
  6. 最后打包文件,发现dist目录下的main.js文件中最终打印的事 hello world,即替换成功

10.2 异步loader

  1. 在loader文件夹下创建 asyncReplaceLoad

    • 异步的loader文件入口
     // asyncReplaceLoader
     ​
     const loaderutils = require('loader-utils')
     ​
     module.exports = function(source){
     ​
         const options = loaderutils.getOptions(this)
         const callback = this.async()
     ​
         setTimeout(()=>{
             const result = source.replace('Kobe',options.name)
             callback(null,result)
        },1000)
     ​
     }
    
  2. 在 webpack.config.js文件中增加loader

     const path = require('path')
     ​
     module.exports = {
         mode: 'development',
         entry: {
             main'./src/index.js'
        },
         output: {
             path: path.resolve(__dirname,'./dist'),
             filename: '[name].js'
        },
         module: {
             rules: [
                {
                     test:/.js$/,
                     use: [
                        {
                             loader: path.resolve(__dirname,'./loaders/asyncReplaceLoader.js'),
                             options: {
                                 name: 'world'
                            }
                        },
                        {
                             loader: path.resolve(__dirname,'./loaders/replaceLoader.js'),
                             options: {
                                 name: 'Kobe'
                            }
                        }
                    ]
                }
            ]
        }
     }
    

十一. 编写一个Plugin

在本章,我们将介绍如何实现一个简单的plugin,让大家知道基本的实现的基本思路

插件功能: 在打包文件的时候,帮我们生成一个版本文件,记录我们的版本相关信息

1. 创建文件夹,my-plugin,使用 npm init -y 命令初始化文件夹,并安装webpack和webpack-cli模块

2. 创建webpack.config.js,配置基本的webpack打包配置

 // webpack.config.js
 const path = require('path')
 ​
 module.exports = {
     mode: 'development',
     entry: {
         main: path.resolve(__dirname,'./src/index.js')
    },
     output: {
         pathpath.resolve(__dirname,'./dist'),
         filename: '[name].js'
    }
 }

3. 项目根目录创建 src文件夹,并在src文件夹下面创建index.js文件,用于测试

 // index.js
 console.log('hello world')

4. 在项目根目录创建plugings文件夹,用来存储plugin文件

5. 在plugins文件夹下创建一个类(注意:这里不同于loader,loader是function,而plugin是一个类)

 // copyright-webpack-plugin.js
 ​
 class CopyrightWebpackPlugin{
     // 如下为构造函数,可以通过 options参数获取plugin中传递过来的options参数,同时,在plugin被new的时候,会调用,在这里非必要,如果我们不显示声明,会有默认的构造函数
     // constructor(options){
     //     console.log(options)
     //     console.log('插件被使用了...')
     // }
 
     // 插件发生作用调用的方法,这里存在很多类似于生命周期的钩子函数,具体清参考webpack官网,documentation下的,API目录下的Plugins中的Compiler Hooks
     //这里实现的功能是,在打包完成之前,通过向compilation.assets对象中添加一个属性,来实现创建我们版本文件的目的
     apply(compiler){
         compiler.hooks.emit.tapAsync('Copyright-webpack-plugin',(compilation,callback)=>{
             compilation.assets['copyright.txt']={
                 sourcefunction(){
                     return 'copyright by mapengfei'
                },
                 sizefunction(){
                     return 22
                   
                }
            }
             callback()
        })
    }
 }
 ​
 module.exports = CopyrightWebpackPlugin

6. 使用plugin

 // webpack.config.js
 ​
 const copyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin')
 ​
 module.exports = {
     //...
     plugins: [
         new copyrightWebpackPlugin()
    ]
 }