likes
comments
collection
share

webpack+babel学习笔记

作者站长头像
站长
· 阅读数 22

首先,来个官方文档对webpack的定义:webpack 是代码编译工具,有入口、出口、loader 和插件。webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。webpack 的核心价值就是前端源码的打包,即将前端源码中每一个文件(无论任何类型)都当做一个 pack ,然后分析依赖,将其最终打包出线上运行的代码。

webpack 的四个核心部分:

  • entry:规定入口文件,一个或者多个。指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
  • output:规定输出文件的位置。告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist
  • loader:让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块
  • plugin:被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。

那么,webpack在日常工作中,是如何配置的呢?

通常情况下,webpack的配置文件在项目内会分为:

  • webpack.common.js
  • webpack.dev.js
  • webpack.prod.js

分别代表公共配置,开发环境配置,线上环境配置。接下来,将按这三个文件来讲解如何配置,部分基础知识和要点会写在注释中,要知道全部配置内容,建议查看官方文档:https://www.webpackjs.com/con...

公共配置webpack.common.js:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const srcPath = path.join(__dirname, '..', 'src')

module.exports = {
    entry: path.join(srcPath, 'index.js'),
           //以该路径下的index.js作为构建的开始进入入口起点后,
           //webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: ['babel-loader'], //使用哪个loader来处理文件
                include: srcPath,
                exclude: /node_modules/
                //test,include,exclude都是拿来匹配哪些文件需要被loader处理的,
                //优先级是exclude > include > test
            },
            {
                test: /\.css$/,
                // loader 的执行顺序是:从后往前
                loader: ['style-loader', 'css-loader', 'postcss-loader'] 
                // 加了 postcss
            },
            {
                test: /\.less$/,
                // 增加 'less-loader' ,注意顺序
                loader: ['style-loader', 'css-loader', 'less-loader']
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html'
        })
    ]
}

开发环境配置webpack.dev.js

const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')//该模块可以将公用配置合并在开发环境配置
const distPath = path.join(__dirname, '..', 'dist')

module.exports = smart(webpackCommonConf, {
    mode: 'development',
    module: {
        rules: [
            // 直接引入图片 url
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            // window.ENV = 'development'
            ENV: JSON.stringify('development')
        })
    ],
    devServer: {
        port: 8080,
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩

        // 设置代理,进行跨域
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }
    }
})

线上环境webpack.prod.js

const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const distPath = path.join(__dirname, '..', 'dist')

module.exports = smart(webpackCommonConf, {
    mode: 'production',
    output: {
        filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        path: distPath,
        // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如
        //cdn 域名),这里暂时用不到
    },
    //以该路径下的index.js作为构建的结束,即出口,
   //webpack 会将模块和库(直接和间接)依赖输出为对应的打包文件
    module: {
        rules: [
            // 图片 - 考虑 base64 编码的情况
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        // 小于 5kb 的图片用 base64 格式产出
                        // 否则,依然延用 file-loader 的形式,产出 url 格式
                        limit: 5 * 1024,

                        // 打包到 img 目录下
                        outputPath: '/img1/',

                        // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那
                        //将作用于所有静态资源)
                        // publicPath: 'http://cdn.abc.com'
                    }
                }
            },
        ]
    },
    plugins: [
        new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('production')
        })
    ]
})

以上为webpack基础配置,以下将介绍其高级配置,如多入口文件,如何压缩抽离css文件,如何抽离公共代码和第三方代码以及如何异步加载js文件

如何配置多入口文件:

之前基础配置的公共配置入口entry只有一个文件,这是项目为单页面的情况。如果项目为多页面或者有需求需要打包出多个页面,则需要配多入口文件。在公共配置webpack.common.js中需要这么修改(仅显示需修改的位置):

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const srcPath = path.join(__dirname, '..', 'src')

module.exports = {
    entry: {
        index: path.join(srcPath, 'index.js'),
        other: path.join(srcPath, 'other.js')
    },
    //多文件时入口entry为对象形式,key对应的是引入的模块的重新命名,value为引入的模块
    //的文件位置(在webpack中任何文件都是模块)
    plugins: [
        // new HtmlWebpackPlugin({
        //     template: path.join(srcPath, 'index.html'),
        //     filename: 'index.html'
        // })

        // 多入口 - 生成 index.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html',
            // chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全
            //部引用
            chunks: ['index']  // 只引用 index.js
        }),
        // 多入口 - 生成 other.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'other.html'),
            filename: 'other.html',
            chunks: ['other']  // 只引用 other.js
        })
    ]
    //针对每一个入口文件都要建一个HtmlWebpackPlugin的插件,即new一个
    //HtmlWebpackPlugin实例。针对chunks,如果不写上的话,每一个html文件都会index.js
    //和other.js
}

由于修改了多入口,所以webpack.prod.js也需要修改其出口:

output: {
        // filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
        path: distPath,
        // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如
        //cdn 域名),这里暂时用不到
    },

如何压缩抽离css文件,抽离公共代码和第三方代码

const path = require('path')
const webpack = require('webpack')
const { smart } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HappyPack = require('happypack')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { srcPath, distPath } = require('./paths')

module.exports = smart(webpackCommonConf, {
    mode: 'production',
    module: {
        rules: [
            // 抽离 css
            {
                test: /\.css$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-
                                                  //loader
                    'css-loader',
                    'postcss-loader'
                ]
            },
            // 抽离 less
            {
                test: /\.less$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-
                                                  //loader
                    'css-loader',
                    'less-loader',
                    'postcss-loader'
                ]
            }
        ]
    },
    plugins: [
        //每抽离一个新的文件都需要在plugins里new一个新的实例来导出对应的模块(文件)
        new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('production')
        }),

        // 抽离 css 文件
        new MiniCssExtractPlugin({
            filename: 'css/main.[contentHash:8].css'
        }),

        // 忽略 moment 下的 /locale 目录
        new webpack.IgnorePlugin(/\.\/locale/, /moment/),

        // happyPack 开启多进程打包
        new HappyPack({
            // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
            id: 'babel',
            // 如何处理 .js 文件,用法和 Loader 配置中一样
            loaders: ['babel-loader?cacheDirectory']
        }),

        // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
        new ParallelUglifyPlugin({
            // 传递给 UglifyJS 的参数
            // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
            uglifyJS: {
                output: {
                    beautify: false, // 最紧凑的输出
                    comments: false, // 删除所有的注释
                },
                compress: {
                    // 删除所有的 `console` 语句,可以兼容ie浏览器
                    drop_console: true,
                    // 内嵌定义了但是只用到一次的变量
                    collapse_vars: true,
                    // 提取出出现多次但是没有定义成变量去引用的静态值
                    reduce_vars: true,
                }
            }
        })
    ],

    optimization: {
        // 压缩 css
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],

        // 分割代码块
        splitChunks: {
            chunks: 'all',
            /**
             * initial 入口chunk,对于异步导入的文件不处理
                async 异步chunk,只对异步导入的文件处理
                all 全部chunk
             */

            // 缓存分组
            cacheGroups: {
                // 第三方模块
                vendor: {
                    name: 'vendor', // chunk 名称
                    priority: 1, // 权限更高,优先抽离,重要!!!
                    test: /node_modules/,
                    minSize: 0,  // 大小限制,大于等于该限制的才会被抽离成新的模块
                    minChunks: 1  // 最少复用过几次,即被引用的次数大于等于该次数
                                  //才会被抽离成新的模块
                },

                // 公共的模块
                common: {
                    name: 'common', // chunk 名称
                    priority: 0, // 优先级
                    minSize: 0,  // 公共模块的大小限制
                    minChunks: 2  // 公共模块最少复用过几次
                }
            }
        }
    }
})

因为多抽离出了文件,为了保证一一对应且不引入多余的模块,需要对webpack.common.js的plugins的chunks重新赋值,即:

plugins: [
        // new HtmlWebpackPlugin({
        //     template: path.join(srcPath, 'index.html'),
        //     filename: 'index.html'
        // })

        // 多入口 - 生成 index.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html',
            // chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),
            //默认全部引用
            chunks: ['index', 'vendor', 'common']  // 增加vendor.js,common.js
        }),
        // 多入口 - 生成 other.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'other.html'),
            filename: 'other.html',
            chunks: ['other', 'common']  // 增加common.js
        })
    ]

如何异步加载js文件

在需要异步加载JS文件的位置写上setTimeout等异步任务即可。

那么,以上内容出现的module,chunk和bundle有什么区别呢?

  • module-各个源码文件,webpack中一切皆为模块。
  • chunk-多模块合并成的,如entry,import()和splitChunk
  • bundle-最终输出的文件

以上概念的包含关系为: module < chunk < bundle

webpack如何优化构建速度(性能优化)?

  1. 优化babel-loader:即开启babel-loader的缓存,并且使用include或者exclude明确打包范围。
  2. 使用ignorePlugin,即不引入某些不需要的模块,缩小产出文件的体积;使用noParse引入模块,但是不打包。
  3. 使用happyPack开启多进程打包。
  4. 使用ParalleUglifyPlugin开启多进程进行代码压缩。
  5. 使用自动刷新或者热更新。
  6. 使用DllPlugin,将一些大的库事先打包成dll文件来引用,不用每次都去打包。
module: {
    noParse: [react\.min\.js$/],
    rules: [
        {
            test: /\.js$/,
            loader: ['babel-loader?cacheDirectory'],
            //cacheDirectory用于开启缓存
            include: srcPath,
            // exclude: /node_modules/
        } 
    ],
    plugins: [
         // 忽略 moment 下的 /locale 目录
        new webpack.IgnorePlugin(/\.\/locale/, /moment/),
        // happyPack 开启多进程打包
        new HappyPack({
            // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
            id: 'babel',
            // 如何处理 .js 文件,用法和 Loader 配置中一样
            loaders: ['babel-loader?cacheDirectory']
        }),
        // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
        new ParallelUglifyPlugin({
            // 传递给 UglifyJS 的参数
            // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
            uglifyJS: {
                output: {
                    beautify: false, // 最紧凑的输出
                    comments: false, // 删除所有的注释
                },
                compress: {
                    // 删除所有的 `console` 语句,可以兼容ie浏览器
                    drop_console: true,
                    // 内嵌定义了但是只用到一次的变量
                    collapse_vars: true,
                    // 提取出出现多次但是没有定义成变量去引用的静态值
                    reduce_vars: true,
                }
            }
        })
    ]
]

什么是babel?

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。简而言之就对超过ES5的js代码(如ES6),编译为ES5代码,承担的是编译转换的工作。

babel-polyfill与babel-runtime由于babel只是简单地将符合JS语法的代码转换为ES5代码,对于如ES6无能为力,如转换abc.includes()时,判断出该代码符合JS语法便进行转换,并不考虑.includes()在ES5下是否可运行。所以需要使用babel-polyfill对.includes()进行扩展,简单来说就是babel-polyfill相当于打补丁,通过引入第三方 polyfill 模块,例如 core-js,会包含诸如.includes()的扩展,使得编译转换后的代码能正常运行。另外,babel-polyfill支持按需引入以减小打包后的代码大小。babel-polyfill的缺点就是这些扩展是在全局定义的,会污染全局变量(全局环境),因此在这种情况下,babel-runtime孕育而生,babel-runtime是通过对babel-polyfill提供的扩展方法进行重命名,如includes变成_includes来杜绝对原本includes方法的污染。

这里只是简单讲解其中由来,具体配置细节可以参考以下文章:关于Babel你只需要知道三个插件