笔记-Webpack

105 阅读5分钟

定义:

将入口文件所引入的静态模块,进行处理解析打包,最后根据依赖关系进行输出到bundle

五个概念:

  1. entry: 入口起点文件,分析构建内部依赖图
  2. output:指示webpack打包后的资源输出到哪里去
  3. loader:处理解析那些非javascript的文件,让webpack可以识别,webpack只能识别执行javascript文件
  4. plugins: 执行范围更广的任务,更强大的功能
  5. mode:指示webpack使用相应的的模式:development/product

webpack.config.js

指示webpack做什么

// resolve 用来拼接绝对路径的方法
const { resolve } = require('path')

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


module.exports = {
    // 输入
    entry: './src/index.js',

    // 输出
    output: {
        filename: 'js/bundle.js',
        // __dirname nodejs的变量,代表当前文件的目录绝对路径
        path: resolve(__dirname, 'build'),
    },

    // loader
    module: {
        rules: [
            // 详细配置
            {
                // 匹配规则
                test: /\.css$/,
                // 使用哪些loader处理
                use: [
                    // 创建style标签,将js中的样式资源插入到head中
                    'style-loader',
                    // 将css文件变成common.js模块加载到js中,里面内容都是样式字符串
                    'css-loader'
                ]
            },

            {
                // 匹配规则
                test: /\.less$/,
                // 使用哪些loader处理
                use: [
                    // 创建style标签,将js中的样式资源插入到head中
                    'style-loader',
                    // 将css文件变成common.js模块加载到js中,里面内容都是样式字符串
                    'css-loader',
                    // 将less编译成css
                    'less-loader'
                ]
            },

            {
                // url-loader 处理不了html中的图片,只能处理样式中的资源
                // 匹配规则
                test: /\.(jpg|png|gif|jpeg)$/,
                // 一个loader不需要使用use,直接loader就可以
                loader: 'url-loader',
                options: {
                    // 图片大小小于8kb,就会被base64编码
                    limit: 8 * 1024,
                    // 给图片重命名
                    // [hash:10]取图片的hash前十位
                    // [ext]取文件原来的扩展名
                    name: '[hash:10].[ext]',
                    // html-loader加载的图片是用commonjs模块解析,url-loader使用es6模块解析
                    // 所以对html-loader加载的图片解析会出问题[Object Module]
                    // 因此,关闭url-loader的es6模块解析,使用commonjs解析
                    esModule: false,
                    // 输出路径
                    outputPath: 'imgs'
                }
            },

            {
                // 专门处理html文件的img图片(负责引入img, 从而被url-loader处理)
                test: /\.html$/,
                loader: 'html-loader'
            },

            {
                // 处理其他资源
                exclude: /\.(css|less|js|html)|jpg|png|gif)$/,
                // url-loader是在file-loader上做了一些优化升级(压缩)
                loader: 'file-loader',
                options: {
                    name: '[hash:10].[ext]',
                    outputPath: 'media'
                }
            }
        ]
    },

    plugins: [
        // 详细plugins

        // 默认创建一个空的html,引入打包输出的所有资源
        new HtmlWebpackPlugin({
            // 复制 './src/index.html'
            template: './src/index.html',
            minify: {
                // 移除空格
                collapseWhitespace: true
                // 移除注释
                removeComments: true
            }
        })
    ],

    mode: 'development', // production

    // 开发服务器 devServer: 自动化编译,刷新浏览器
    // 特点:只在内存中编译打包,不会有任何输出
    // 启动devServer指令为:webpack-dev-server
    devServer: {
        // 构建路径
        contentBase: resolve(__dirname, 'build'),
        // 启动gzip压缩
        compress: true,
        // 本地端口
        port: 3000,
        // 浏览器自动打开
        open: true
    }
}

CSS详细配置

// 提取css
// 压缩css
// css 兼容性处理:postcss -> postcss-loader postcss-preset-env
// postcss找到package.json中browserlist里面的配置,通过配置加载指定的css兼容性样式

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')

// 设置nodejs环境变量
process.env.NODE_ENV = 'development'

module.exports = {

    module: {
        rules: [
            {
                test: /\.css/,
                use: [
                    // 取代style-loader,提取js中的css成为单独的文件
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    // postcss-loader, 上面为简写,下面的为详细配置
                    // 'postcss-loader'
                    // postcss的browerlist默认会找生产环境的配置,所以如果要使用开发环境,那就需要手动配置process.env.NODE_ENV = 'development'
                    {
                        loader: 'postcss-loader',
                        options: {
                            ident: 'postcss',
                            plugins: () => {
                                require('postcss-preset-env')()
                            }
                        }
                    }
                ]
            }
        ]
    },

    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/bundle.css'
        }),
        new OptimizeCssAssetsWebpackPlugin()
    ]
}
// package.json
// 可以从github搜索browerlist搜索详细配置
{
    "borwerlist": {
        "development": [
            "last 1 chorme version",
            "last 1 firefox version",
            "last 1 safari version",
        ],
        "production": [
            ">0.2%",
            "not dead",
            "not op_mini all"
        ]
    }
}

JS详细配置

// 代码格式规则检测
// 代码兼容性处理 es6 babel
module.exports = {
    module: {
        rules: [
            // 语法检查 eslint-loader eslint
            // 只检查源代码,第三方库不检查
            // 设置检查规则,在package.json中的eslintconfig
            // aribnb -> eslint-config-aribnb-base eslint eslint-plugin-import
            {
                test: /\.js$/,
                exclude: /node_modules/,
                enforce: 'prev', // 优先执行
                loader: 'eslint-loader',
                options: {
                    // 自动修复eslint的错误
                    fix: true
                }
            },
            // 兼容性处理 babel-loader @babel-core
            // 1. 基本兼容性处理:@babel/preset-env, 这个必须配置,不管是按需还是全部
            // 2. 全部的js兼容处理: @babel/polyfill 直接在接口require插件即可 但会导致代码体积变大
            // 3. 按需加载:core-js
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                options: {
                    // 指示babel进行怎么样的兼容性处理
                    presets: ['@babel/preset-env'], // 基本的转换

                    // 按需加载
                    presets: [
                        [
                            '@babel/preset-env',
                            {
                                // 按需加载
                                useBuiltIns: 'usage',
                                // core-js版本
                                corejs: {
                                    version: 3
                                },
                                // 支持的浏览器版本
                                target: {
                                    chorme: '60',
                                    firefox: '60',
                                    ie: '9',
                                    safari: '10',
                                    edge: '17'
                                }
                            }
                        ]
                    ] // 基本的转换
                }
            }
        ]
    }
}
// package.json
{
    "eslintConfig": {
        "extends": "aribnb-base"
    }
}

性能优化

  1. 开发环境:
    • 优化打包构建速度:HMR
    • 优化代码调试: sourceMap
    • oneOf
    • 缓存
  2. 生产环境:
    • 优化打包速度
    • 优化代码性能

HMR 热加载

改变一个模块,只重新打包这一个模块

module.exports = {
    devServer: {
        hot: true
    }
}

需要注意的是:

  1. 使用style-loader,而且style-loader支持hmr,不能使用其他压缩抽离的loader
  2. js使用hmr,监听js时候变化,只能处理非入口js,
  3. html使用hmr,没有必要,因为只要html变化了,肯定所有的多要重新加载,

sourceMap

提供源代码到构建后的代码之间映射的技术,可以通过映射追踪到出错的源代码 [inline-|-hidden-|eval-][nosources-][cheap-[module-]]source-map

  1. source-map
    外部,错误代码准确信息和错误位置
  2. inline-source-map
    内联,速度更快,只生成一个sourcemap, 错误代码准确信息和错误位置
  3. hidden-source-map
    外部,有错误信息,但没有错误位置,之隐藏源代码
  4. eval-source-map
    内联,错误代码准确信息和错误位置
  5. nosources-source-map 外部,有错误信息,但没有错误位置,全部隐藏
  6. cheap-srouce-map
    外部,错误代码准确信息和错误位置,但是只能精确到行
  7. cheap-module-source-map 外部,错误代码准确信息和错误位置

最快
eval-cheap-source-map,
eval-source-map
最友好 source-map cheap-module-source-map cheap-source-map


module.exports = {
    devtool: 'source-map'
}

oneOf

提升构建速度,让rules里面的loader只会匹配一次

缓存

  1. babel缓存 第二次打包构建速度更快
{
    test: /\.js$/,
    exclude: /node_modules/,
    loader: 'babel-loader',
    options: {
        // 开启babel缓存
        // 第二次构建时,会读取之前的缓存
        cacheDirectory: true
    }
}
  1. 文件资源缓存
    • hash 每次webpack构建时会生成一个唯一的hash值
      问题:因为js和css同时使用一个hash值,如果重新打包,会导致所有缓存失效(可能我只是改动了一个文件)
    • chunkhash 根据chunk生成的hash值,如果打包来源同一个chunk,那么chunk值就一样
      问题:js和css的hash值还是一样的,因为css是在js被引入的,所以同属于一个chunk
    • contenthash 根据文件的内容生成的hash值,不同文件hash值一定不一样

tree shaking

去除在应用程序中没有使用的代码,让代码体积更小

  1. 必须使用es6模块化
  2. 开启production

下面可对tree shaking进行配置,不是必要的,但是最好配置

// package.json
{
    "slideEffects": false // 所有代码都没有副作用(都能进行tree shaking),但可能会把css / @babel/polyfill文件干掉
    "slideEffects": ["*.css"] // 保证这些文件不会被tree shaking
}

代码分割

  1. 根据入口文件进行分割
    module.exports = {
        entry: [
            a: './src/a.js',
            b: '.src/b.js'
        ]
    }
    
  2. optimization 可以将node_modules中的代码单独打包一个chunk
    module.exports = {
        optimization: {
            splitChunks: {
                chunks: 'all'
            }
        }
    }
    
  3. 通过import动态导入,针对某个文件,单独打包
    import('./test').then(() => {
        console.log('success')
    }).catch(() => {
        console.log('fail')
    })
    // 下方可以固定chunk名字
    import(/* webpackChunkName: 'test' */'./test').then(() => {
        console.log('success')
    }).catch(() => {
        console.log('fail')
    })
    

懒加载、预加载

// 懒加载
import(/* webpackChunkName: 'test' */'./test').then(() => {
    console.log('success')
}).catch(() => {
    console.log('fail')
})
// 预加载
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(() => {
    console.log('success')
}).catch(() => {
    console.log('fail')
})

PWA

渐进式网络开发应用程序(离线可访问) workbox -> workbox-webpack-plugin

module.exports = {
    plugins: [
        new WorkboxWebpackPlugin.GenerateSW({
            // 帮助serviceworker快速启动
            // 删除旧的serviceworker

            // 生成一个serviceworker配置文件

            clientsClaim: true,
            skipWaiting: true
        })
    ]
}
// 入口文件  index.js
// eslint不认识window, navigator全局变量,所以需要修改eslintConfig配置
// serviceworker必须运行在服务器上
if('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWor ker.register('./service-worker.js').then(() => {
            console.log('success')
        }).catch(() => {
            console.log('fail')
        })
    })
}
// package.json
{
    "eslintConfig": {
        "extends": "airbnb-base",
        "env": {
            "brower": true // 支付浏览器全局变量
        }
    }
}

多进程打包

npm i thread-loader
开启多进程打包大概需要600ms, 进程通信也有开销,只有工作消耗时间较长,才需要多进程打包。一般给babel-loader用,

{
    use: [
        // 'thread-laoder',
        {
            loader: 'thread-loader',
            options: {
                workers: 2 // 进程为2
            }
        },
        {
            loader: 'babel-loader',
            options: []
        }
    ]
}

externals

忽略指定的包被打包

module.exports = {
    extrernals: {
        jquery: 'jQuery' // jquery将不会被打包,手动引入cdn
    }
}

dll

用dll可以对node_modules分别单独打包

// webpack.dll.js
const { resolve } = require('path')
const webpack = require('webpack')
module.exports = {
    entry: {
        jquery: ['jquery']
    },
    output: {
        filename: '[name].js',
        path: resolve(__dirname, 'dll'),
        library: '[name]_[hash]', // 打包的库里面向外暴露出去的内容叫什么名字
    },
    plugins: [
        // 打包生成一个mainfest.json 提供和jquery映射
        new webpack.DllPlugin({
            name: '[name]_[hash]', // 映射库的暴露的内容名称
            path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径

        })
    ]
}
npm install add-asset-html-webpack-plugin -D
// webpack.config.js
const webpack = require('webpack')
const AddAssetHtmlWepbackPlugin = require('add-asset-html-webpack-plugin')
module.exports = {
    plugins: [
        new webpack.DllReferencePlugin({
            mainfest: resolve(__dirname, 'dll/mainfest.json')
        }),
        new AddAssetHtmlWepbackPlugin({
            filepath: require(__dirname, 'dll/jquery.js')
        })
    ]
}

webpack性能优化

  • 开发环境性能优化
  • 生产环境性能优化

开发环境性能优化

  • 优化打包速度
    • HMR
  • 优化代码调试
    • source-map

生产环境性能优化

  • 优化打包构建速度
    • oneOf
    • babel缓存
    • 多进程
    • externals
    • dll
  • 优化代码运行的性能
    • 缓存(hash-chunkhash-contenthash)
    • tree shaking
    • code split
    • 懒加载/预加载
    • pwa

整理

// 打包成一个chunk, 输出一个bundle文件
entry: './src/index.js',
// 所有的入口文件,只会形成一个chunk, 输出只有一个bundle
entry: [
    '.src/index.js',
    '.src/add.js'
],
// 有几个入口文件,就有几个chunk,输出几个boundle
entry: {
    index: ['.src/index.js', '.src/add.js'],
    index2: '.src/index2.js'
}


outpath: {
    filename: 'js/bundle.js',
    // __dirname nodejs的变量,代表当前文件的目录绝对路径
    path: resolve(__dirname, 'build'),
    // 所有资源引入公共路径前缀 --> imgs/a.jpg --> '/imgs/a.jpg'
    publicPath: '/',
    // 非入口chunk的名称
    chunkFilename: '[name]_chunk.js',
    // 整个库向外暴露的变量名
    library: '[name]',
    // 变量名添加到哪个环境上  browser, node
    libraryTarget: 'window',
    libraryTarget: 'global',
    libraryTarget: 'commonjs'
},

module: {
    rules: [
        // loader的配置
        {
            test: /\.css$/,
            // 多个loader用use
            user: [
                'style-loader',
                'css-loader'
            ]
        },
        {
            test: /\.js$/,
            // 排除 node_modules 下的js文件
            exclude: /node_modules/,
            // 只检查 src 下的js 文件
            include: resolve(__dirname, 'src'),
            // 优先执行
            enforce: 'pre',
            // 延后执行
            // enforce: 'post',
            // 单个loader直接用loader
            loader: 'eslint-loader',
            // 单个loader使用,需要添加options来配置选项
            options: {}
        },
        {
            // 以下配置只会生效一个
            oneOf: []
        }
    ]
},

// 解析模块的规则
resolve: {
    // 配置解析模块的路径别名: 简写路径,缺点路径没有提示
    alias: {
        $css: resolve(__dirname, 'src/css')
    },
    // 配合省略文件路径的后缀名
    extensions: ['.js', '.json', '.jsx', '.css'],
    // 告诉 webpack 解析模块是去找哪个目录
    modules: [resolve(__dirname, '../../node_modules'), 'node_modules']
},


devServer: {
    // 运行代码的目录
    contentBase: resolve(__dirname, 'build')
    // 监视 contentBase 目录下的所有文件,一旦文件变化就会 reload
    watchContentBase: true,
    wiatchOptions: {
        // 忽略文件
        ignored: /node_modules/
    },
    // 启动gzip压缩
    compress: true,
    // 端口号
    port: 5000,
    // 域名
    host: 'localhost',
    // 自动打开浏览器
    open: true,
    // 开启HMR功能
    hot: true,
    // 不要显示启动服务器的日志信息
    clientLogLevel: 'none',
    // 除了一些基本的启动信息以外,其他内容都不要显示
    quiet: true,
    // 如果出错了,不要全屏提示
    overlay: false,
    // 服务器代理 --> 解决开发环境跨域问题
    proxy: {
        // 一旦 devServer(5000) 服务器接收到 /api/xxx 的请求,就会把请求转发到另一个服务器(3000)
        '/api': {
            target: 'http://localhost:3000',
            // 发送请求是,请求路径重写:将 /api/xxx --> /xxx (去掉 /api)
            pathRewrite: {
                '^/api': ''
            }
        }
    }
},

optimization: {
    splitChunks: {
        chunks: 'all',
        // 分割的chunk最小为30kb
        minSize: 30 * 1024,
        // 最大没有限制
        maxSize: 0,
        // 要提取的chunk最少被引用1次
        minChunks: 1,
        // 按需加载时并行加载的文件的最大数量
        maxAsyncRequest: 5,
        // 入口js文件最大并行请求数量
        maxInitialRequests: 3,
        // 名称连接符
        automaticNameDelimiter: '~',
        // 可以使用命名规则
        name: true,
        // 分割chunk的组
        chcheGroups: {
            // node_modules 文件会被打包到 vendors 组的chunk中, --> vendors~xxx.js
            // 满足上面的公共规则
            vendors: {
                test: /[\\/]node_modules[\\/]/,
                // 优先级
                priority: -10
            },
            default: {
                // 要提取的chunk最少被引用2次
                minChunks: 2,
                // 优先级
                priority: -20,
                // 如果当前要打包的模块,和之前已经被提取的模块是同一个,就会复用,而不是重新打包模块
                reuseExistingChunk: true
            }
        }
    },
    // 将当前模块的记录其他模块的hash单独打包为一个文件 runtime
    // 解决:修改a文件导致b文件的contenthash 变化
    runtimeChunk: {
        name: entrypoint => 'runtime-${entrypoint.name}'
    },
    minimizer: {
        // 需要引入 terser-webpack-plugin 包
        // 配置生产环境的压缩方案:js和css
        new TerserWebpackPlugin({
            // 开启缓存
            cache: true,
            // 开启多进程打包
            parallel: true,
            // 启动source-map
            sourceMap: true
        })
    }
}

总结:

  1. 只能处理js/json
  2. dev比pro少了一个压缩
  3. 能对es6模块化编译成能识别的模块化
  4. 开发环境不需要配置太多东西,不然开发环境会很慢,将一些必要的但是开发环境不必要的,放在生产环境中