webpack 简介

112 阅读9分钟

webpack 简介

概念

  • webpack是一个用于现代JavaScript应用程序的静态模块打包工具
  • 当webpack处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图,然后将你项目中所需的每一个模块组合成一个或多个bundles,它们均为静态资源,用于展示你的内容。

模块:前端项目中任何文件。NpmPackange/SCSS/CSS/VUE/IMG/TS/JS

核心概念: image.png

为什么选择webpack?

  1. 支持多种模块标准:包括AMD、CommonJS,以及最新的ES6模块
  2. 依赖自动收集:自动构建并基于你所引用或导出的内容推断出依赖的图谱
  3. 处理各种类型的资源:例如:images,fonts和stylesheets
  4. 关心性能和加载时间:它始终在改进或添加新功能,例如:异步地加载chuk和预取,以便为你的项 目和用户提供最佳体验
  5. 具有完备的代码分割解决方案
  6. 庞大的杜区支持:loader与plugin丰富

webpack 配置

image.png

entry

  1. entry.是配置模块的入口,可抽象成输入,Vebpack执行构建的第一步将从入 口开始搜寻及递归解析出所有入口依赖的模块
  2. entry配置是必填的,若不填则将导致Webpack报错退出。
//1个chunk
module.exports = {
    entry: {
        main: './src/index.js'
    }
}
//1个chunk
module.exports = {
    entry: ['./src/index1.js','./src/index2.js']
}
// 可能输出多个chunk
module.exports = {
    entry: {
        app: './src/index.js',
        adminApp:['./src/admin1.js','./src/admin2']
    }
}

output

  1. 可以通过配置output选项,告知webpack如何向硬盘写入编译文件。注意,即使可以存 在多个entry起点,但只能指定一个output配置。
  2. 包括以下两点:filename用于输出文件的文件名、目标输出目录path的绝对路径。
module.exports = {
    output: {
        filename: 'bundle.js'
    }
}

module.exports = {
    output: {
        filename: '[name].js',
        path: dirname + '/dist',
    },
}

output.filename

  1. 只有一个输出文件,可以写成静态不变的如filename:'bundle.js
  2. 模版和变量可以针对多个Chunk要输出时情况,可以根据Chunk的名称来区分输出的文件 名filename:'[name]js
变量名含义
idChunk的唯一标识,从0开始
nameChunk的名称
hashChunk的唯一标识Hash值
chunkhashChunk内容的Hash值

output.path

output.path配置输出文件存放在本地的目录,必须是string类型的绝对路径。 通常通过Node.js的path模块去获取绝对路径:

//写入到硬盘 ./dist/app.js
module.exports = {
    entry: {
        app: './src/app.js'
    },
    output: {
        filename: '[name].js',
        path: __dirname + '/dist',
    },
}

//是对资源使用CDN和hash的复杂示例
module.exports = {
    //...
    output: {
        path: '/home/proj/cdn/assets/[fullhash]',
        publicPath: "https://cdn.example.com/assets/[fullhash]/"
    }
}

loader

loader用于对模块的源代码进行转换:

  1. loader可以使你在import或"load(加载)"模块时预处理文件。
  2. loader类似于其他构建工具中"任务(task)”,并提供了处理前端构建步骤的得力方式。
  3. loader可以将文件从不同的语言(如TypeScript)转换为JavaScript或将内联图像转换为 data URL
  4. loader甚至允浒你直接在JavaScript模块中import CSS文件!

npm install -S css-loader ts-loader

//通过module.rules来配置
module.exports = {
    //...
    module:{
        rules:[
            {test:/\.css$/,use:'css-loader'},
            {test:/\.ts$/,use:'ts-loader'}
        ]
    }
}

loader的使用

配置方式: 在你的应用程序中,有两种使用loader的方式:

  • 配置方式(推荐):在webpack.config.js文件中指 定loader.
  • 内联方式:在每个import语句中显式指定loader。

注意在webpack v4版本可以通过CLI使用loader, 但是在webpack v5中被弃用。

执行顺序: loader从右到左(或从下到上)地取值(evaluate)/执 行(execute)。在右边的示例中,从sass-loader开始执 行,然后继续执行css-loader,最后以style-loader 为结束。

module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.css$/, use: [
                    {
                        //[style-loader](/loaders/style-loader)
                        loader: 'style-loader'
                    },
                    {
                        //[css-loader](/loaders/css-loader)
                        loader: 'css-loader',
                        options: {
                            module: true
                        }
                    },
                    {
                        // [sass-loader](/loaders/sass-loader)
                        loader: 'sass-loader'
                    },
                ]
            },
        ]
    }
}

loader 特性

  1. loader支持链式调用。链中的每个loader会将转换应用在已处理过的资源上。一组链式的loader将按照 相反的顺序执行。链中的第一个loader将其结果(也就是应用过转换后的资源)传递给下一个loader,依 此类推。最后,链中的最后一个loader,返▣webpack所期望的JavaScript
  2. loader可以是同步的,也可以是异步的。
  3. loader运行在Node.js中,并且能够执行任何操作。
  4. loader可以通过options对象配置(仍然支持使用query参数来设置选项,但是这种方式已被废弃)。
  5. 除了常见的通过package.json的main来将一个npm模块导出为loader,还可以在module.ules中使 用loader字段直接引用一个模块。
  6. 插件plugin)可以为loader带来更多特性。
  7. loader能够产生额外的任意文件。

loader 开发

  • loader本质上是导出为函数的JavaScript模块。loader runner会调用此函数,然后将上一个
  • loader产生的结果或者资源文件传入进去。函数中的this作为上下文会被webpack填充,并且
  • loader runner中包含一些实用的方法,比如可以使loader调用方式变为异步,或者获取query 参数。
/**
 * @param {string|Buffer} content源文件的内容 
 * @param {object} [map] 可以被https://github.com/mozilla/source-map使用的SourceMap数据
 * @param {any} [meta] meta数据,可以是任何内容。
 */
function webpackLoader(content, map, meta) {
    // your wepack loader code
}

同步loader

//如果是单个处理结果,可以在同步模式中直接返回。如果有多个处理结果,看下面this.callback()的示例。
module.exports = function (content, map, meta) {
    return someSyncOperation(content);
}
// this.callback方法更灵活,它允许传递多个参数,而不只是content
module.exports = function (content, map, meta) {
    this.callback(null, someSyncOperation(content), map, meta);
    return;
}

异步loader

// 对于异步loader,使用this.async()来获取callback函数
module.exports = function (content, map, meta) {
    let callback = this.asycn()
    someAsyncOperation(content, function (err, res) {
        if (err) return callback(err)
        callback(null, res, map, meta)
    })
}

//多个返回结果
module.exports = function (content, map, meta) {
    let callback = this.asycn()
    someAsyncOperation(content, function (err, res, sourceMaps, meta) {
        if (err) return callback(err)
        callback(null, res, sourceMaps, meta)
    })
}

resolve

image.png

resolve.alias

创建import或require的别名,来确保引入模块变得简单。

resolve:{
    alias:{
        @components: './src/components/'
    }
}

import Button from './src/components/button'
//                ↓
import Button from '@components/button' 

resolve.extensions

在导入语句没带文件后缀时,Webpack会自动带上后缀后去尝试访问文件是否存在。
resolve.extensions用于配置在尝试过程中用到的后缀列表

extensions:['.js','.json']

遇到import dafa from'/data'这样的导入语句时,Webpack会先去寻找/data.js文件,如果 该文件不存在就去寻找/data.json文件,如果还是找不到就报错

module.export = {
    //...
    resolve: {
        extensions: ['.js', '.json', '.wasm']
    }
}

resolve.modules

配置Webpack去哪些目录下寻找第三方模块,默认是只会去node_modules目录下寻找。 有时你的项目里会有一些模块会大量被其它模块依赖和导入,由于其它模块的位置分布不定, 针对不同的文件都要去计算被导入模块文件的相对路径,这个路径有时候会很长,就像这样import'../../../components/button'这时你可以利用modules配置项优化,假如那些被大 量导入的模块都在./src/components目录下,把modules配置成

const path = require('path')

module.export = {
    //...
    resolve: {
        modules: [path.resolve(__dirname, 'src'), 'node_modules']
    }
}

import '../../../components/button'
//            ↓
import 'button'

resolve.enforceExtension

如果是true,将不允许无扩展名文件。默认如果./foo有.js扩展,require(./foo)可以正 常运行。但如果启用此选项,只有require('./foo.js)能够正常工作。

plugin

插件是webpack的支柱功能。插件目的在于 解决loader无法实现的其他事。Nebpack提 供很多开箱即用的插件。

用法:
由于插件可以携带参数/选项,你必须在 webpack配置中,向plugins属性传入一个 new实例.

ProgressPlugin用于自定义编译过程中的进度 报告,HtmlWebpackPlugin将生成一个 HTML文件,并在其中使用script引入一个名 为my-first-webpack.bundle.js的JS文件。

const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');//访问内置的插件
const path = require('path');
module.exports = {
    entry: './path/to/my/entry/file.js',
    output: {
        filename: 'my-first-webpack.bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                use: 'babel-loader',
            }
        ]
    },
    plugins: [
        new webpack.ProgressPlugin(),
        new HtmlWebpackPlugin({ template: './src/index.html' }),
    ]
}

plugin 创建

  1. 一个JavaScript命名函数或JavaScript类。
  2. 在插件函数的prototype上定义一个apply方法。
  3. 指定一个绑定到webpack自身的事件钩子。
  4. 处理webpack内部实例的特定数据。
  5. 功能完成后调用webpack提供的回调。
//一个 JavaScript 类
class MyExamplewebpackPlugin {
    //在插件函数的prototype上定义一个`apply`方法,以compiler为参数。
    apply(compiler) {
        //指定一个挂载到webpack自身的事件钩子。
        compiler.hooks.emit.tapAsync(
            'MyExamplewebpackPlugin',
            (compilation, callback) => {
                console.log("这里表示了资源的单次构建的`compilation`对象:", compilation);
                //用webpack提供的插件API处理构建过程
                compilation.addModule(/*...*/);
                callback();
            })
    }
}

devServer

image.png

devServer.hot

启动webpack的热模块替换特性:

module.exports = {
    //...
    devServer: {
        hot: true
    }
}
// 通过命令行使用:
npx webpack serve --hot
// 如果需要禁用
npx webpack serve --no-hot

启用热模块替换功能,在构建失败时不刷新页面 作为回退,使用hot:'only'或者命令行 --host only

devServer.compress

启用gzip compression

module.exports = {
    //...
    devServer: {
        compress: true
    }
}
// 命令行启用
npx webpack serve -compress
// 禁用
npx webpack serve -compress

devServer.historyApiFallback

  1. 单页面应用,服务器在针对任何命中的路由时都返回一个对应的HTML文件
module.exports = {
    //...
    devServer: {
        historyApiFallback: true
    }
}
  1. 多个单页面组成,不同请求匹配不同的页面
module.exports = {
    //...
    devServer: {
        historyApiFallback: {
            rewrites: [
                { from: /^\/$/, to: '/views/landing.html' },
                { from: /^\/subpage/, to: '/views/subpage.html' },
                { from: /./, to: '/views/404.html' },
            ]
        }
    }
}

devServer.headers

为所有的响应添加headers:

module.exports = {
    //...
    devServer: {
        headers: {
            'x-Custom-Foo': 'bar'
        }
    }
}
//or 一个数组
module.exports = {
    //...
    devServer: {
        headers: [
            {
                key: 'X-Custom',
                value: 'foo'
            },
            {
                key: 'X-XXXXX',
                value: 'xxx'
            }
        ]
    }
}

devServer.allowedHosts

允许访问开发服务器的服务列入白名单

当设置为allowedHosts:'auto'时,此配置项总是允许localhost、 host和client.webSocketURLhostname:

module.exports = {
    //...
    devServer: {
        allowedHosts: ['xxx.com', 'xxx.host.com']
    }
}

devServer.https

默认情况下,开发服务器将通过HTTP提供服务。可以选择使用HTTPS提供服务

module.exports = {
    //...
    devServer: {
        https: true
    }
}

但是你也可以提供自己的证书:

module.exports = {
    //...
    devServer: {
        https: {
            ca: './path/to/server.pem',
            pfx: './path/to/server.pfx',
            key: './path/to/server.key',
            cert: './path/to/server.crt',
            passphrase: 'webpack-dev-server',
            requestCert: true,
        }
    }
}

devServer.open

告诉dev-server在服务器已经启动后打开浏览器。设置其为true来打开你的默认浏览器。

module.exports = {
    //...
    devServer: {
        open: true
    }
}
//在浏览器中打开指定页面
module.exports = {
    //...
    devServer: {
        open: ['/my-page']
    }
}

devServer.proxy

当拥有单独的API后端开发服务器并且希望在同一域上发送API请求时,代理某些URL可能会很有用

//对/api/users的请求会将请求代理到
http://localhost:3000/api/users

module.exports = {
    //...
    devServer: {
        proxy: {
            '/api': {
                target: 'http://localhost:3000',
                // 如果不希望传递'/api',则需要自己重写路径
                pathRewrite: { '^/api': '' },
                //默认情况下,将不接受在HTTPS上运行且证书无效的后端服务器。如果需要,可以这样修改配置
                secure: false,
                //有时不想代理所有内容。可以基于函数的返回值绕过代理。在该功能中,可以访问请球,响应和代理选项。
                //1. 返回null或undefined以继续使用代理处理情球。
                //2. 返回false会为请球产生404错误。
                //3. 返回提供服务的路径,而不是继续代理请求。
                bypass: function (req, res, proxyOptions) {
                    if (req.headers.accpet.indexOf('html') !== -1) {
                        console.log('Skipping proxy for browser request');
                        return './index.html'
                    }
                }
            }
        }
    }
}

mode

提供mode配置选项,告知webpack使用相应模式的内置优化。

选项描述
development会将DefinePlugin中process.env.NODE_ENV的值设置为development.为模块和chunk.启用有效的名。
production会将DefinePlugin中process.env.NODE_ENV的值设置为production。为模块和chunk.启用确定性的混淆名称,FlagDependencyUsagePlugin, FlagIncludedChunksPlugin,ModuleConcatenationplugin, NoEmitonErrorsPluginTerserPlugin。
none不使用任何默认优化选项

webpck原理

基本概念

  1. Entry:指示webpack应该使用哪个模块,来作为构建其内部依赖图(dependency graph) 的开始。进入入口起点后,webpack会找出有哪些模块和库是入口起点(直接和间接)依赖 的。
  2. Module:模块,在Webpack里一切皆模块,一个模块对应着一个文件。Webpack会从配 置的Ent女y开始递劃归找出所有依赖的模块。
  3. Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
  4. Loader:模块转换器,用于把模块原内容按照需求转换成渐内容。
  5. Plugin:扩展插件,在Webpack构建流程中的特定时机会广播出对应的事件,插件可以监 听这些事件的发生,在特定时机做对应的事情。

流程概况

  1. 初始化参数:从配置文件和Shel语句中读取与合并参数,得出最终的参数:
  2. 开始编译:用上一步得到的参数初始化Compiler对像,加载所有配置的插件,执行对像的 un方法开始执行编泽;
  3. 确定入口:根据配置中的entry找出所有的入口文件:
  4. 编译块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖 的模块,再递归本步骤直到所有入口依赖的文件都经过了本步的处理:
  5. 完城模块编译:在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的 最终内容以及它们之间的依赖关系:
  6. 输出资源:根据入口和模块之间的依赖关系,组装一个个包含多个模块的Chuk,再把 每个Chuk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出院城:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文 件系统

image.png

webpack 优化

image.png

缩小文件搜索范围

  1. Loader对文件的转换操作很耗时,需要让尽可能少的文件被Loader处理。
  2. 通过test、include、exclude三个配置项来命中Loader要应用规则的文件。 为了尽可能少的让文件被Loader处理
  3. 优化resolve可以减少默认尝试
module.exports = {
    module: {
        rules: [
            {
                // 如果项目源码中只有js文件就不要写成/\.jsx?$/,提升正则表达式性能
                test: /\.js$ /,
                //babel-loader支持缓存转换出的结果,通过cacheDirectory选项开启
                use: ["babel-loader?cacheDirectory"],
                //只对项目根目录下的5rc目录中的文件采用babel-loader
                include: path.resolve(__dirname, "src"),
            }
        ],
        resolve: {
            //尽可能的减少后缀尝试的可能性
            extensions: ["js"],
        }
    }
}

区分环境

  • 在开发过程中,切忌在开发环境使用生产环境才会用到的工具,如在开发环境下,应该排除 [fullhash]/[chunkhash]/[contenthash]等工具。
  • 在生产环境,也应该避免使用开发环境才会用到的工具,如webpack-dev-server等插件。

外部拓展(Externals)

externals配置选项提供了「从输出的bundle中排除依赖」的方法。相反,所创建的bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。此功能通常对library开发 人员来说是最有用的,然而也会有各种各样的应用程序用到它。

例如,从CDN引入jQuery,而不是把它打包

module.exports = {
    extrenals: {
        jquery: 'jQuery'
    }
}

分割代码按需加载

在给单页应用做按需加载优化时,一般采用以下原则:

  1. 把整个网站划分成一个个小功能,再按照每个功能的相关程度把它们分成几类。
  2. 把每一类合并为一个Chunk,按需加载对应的Chunk。
  3. 对于用户首次打开你的网站时需要看到的画面所对应的功能,不要对它们做按需 加载,而是放到执行入口所在的Chunk中,以降低用户能感知的网页加载时间。
  4. 对于个别依赖大量代码的功能点,例如依赖Chart,.js去画图表、依赖flv,js去播 放视频的功能点,可再对其进行按需加载

例如路由选择()=>import('')加载方式,当页面跳转相应页面再加载

缓存cache

通过配置webpack持久化缓存cache:filesystem,来缓存生成的webpack模块和chunk,改 善构建速度。

通过cache:filesystem可以将构建过程的webpack模板进行缓存,大幅提升二次构建速度、打 包速度,当构建突然中断,二次进行构建时,可以直接从缓存中拉取,可提速90%左右。

module.exports = {
    caches: {
        type: 'filesystem'
    }
}

多进程

通过thread-loader将耗时的loader放在 一个独立的worker池中运行,加快loader 构建速度。

我们应该仅在非常耗时的loader前引入 thread-loader。


module.exports = {
    rules: {
        test: '...',
        use: [
            {
                //...
            },
            {
                loader: 'thread-loader',
                options: {
                    workerParallelJobs: 2,
                },
            }
        ]
    }
}

Webpack5 新增关键特性

内置静态资源构建能力一Asset Modules

在webpack5之前,通常使用:

  1. raw-loader将文件导入为字符串
  2. url-loader将文件作为data URI内联到bundle中
  3. file-loader将文件发送到输出目录

资源模块类型(asset module type),通过添加4种新的模块类型,来替换所有这些loader:

  1. asset/resource发送一个单独的文件并导出URL。之前通过使用file-loader实现。
  2. asset/inline导出一个资源的data URI。之前通过使用url-loader实现。
  3. asset/source导出资源的源代码。之前通过使用raw-loader实现。
  4. asset在导出一个data URI和发送一个单独的文件之间自动选择。之前通过使用url-loader,并且配置资 源体积限制实现。

文件缓存

在webpack4中,我们会使用cache-loader缓存一些性能开销较大的loader,或者是使用hard- source.-webpack-plugin为模块提供一些中间缓存。在Webpack5之后,默认就为我们集成了一 种自带的缓存能力(对module和chunks进行缓存)。通过如下配置,即可在二次构建时提速

module.exports = {
    caches: {
        //默认缓存到node modules,/.cache,/webpack中
        //也可以自定义缓存目录,cache.cacheDirectory选项仅当cache.type被设置成filesystem才可用。
        //cacheDirectory:path.resolve(dirname,'node_modules/.cac/webpack'),
        type: 'filesystem',
        buildDependencies: {
            //2.将您的配置添加为buildDependency以使配置更政时缓存失效
            config: [__filename]
            //3.如架您有其他构建所依赖的东西你可以在这里添加它们
            //清注意,webpack、加载器和从你的配置中引用的所有摸块都会自动添加
        }
    }
}

更好的tree-shaking

  • webpack4没有分析模块的导出和导入之间的依赖关系。webpack5有一个新选项 optimization.innerGraph,它在生产模式下默认启用,它对模块中的符号运行分析以找出从导出 到导入的依赖关系。
  • webpack5添加了对某些CommonJs结构的支持,允许消除未使用的CommonJs导出并跟踪 require0调用中引用的导出名称。

常用loader

  • babel-loader: 把ES6转换成ES5
  • css-loader: 加载CSS,支持模块化、压缩、文件导入等特性
  • style-loader: 把CSS代码注入到JavaScript中,通过DOM操作去加载CSS
  • sass-loader: 把SCSS/SASS代码转换成CSS
  • postcss-loader: 扩展CSS语法,使用下代CSS
  • less-loader: 把Less代码转换成CSS代码
  • eslint-loader: 通过ESLint检查JavaScript代码
  • vue-loader: 加载Vue.js单文件组件
  • define-plugin: 定义环境变量
  • ignore-plugin: 用于忽略部分文件
  • commons-.chunk-plugin: 提取公共代码
  • Web-webpack-plugin: 方便的为单页应用输出HTML
  • hot-module-replacement-plugin: 开启模块热替换功能
  • uglifyjs-webpack-plugin: 通过UglifyES压缩ES6代码