打包工具webpack入门

113 阅读15分钟
工具功能
webpack前端资源模块化管理和打包工具,1、可将很多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源; 2、还可以将按需加载的模块进行代码分割,等到实际需要的时候再异步加载; 3、让它自动实现这些功能,得提前编辑好配置文件
vue-cli脚手架工具,用配置好的模板迅速搭建起一个项目工程来,基于webpack构建,带有合理的默认配置,省去了自己配置webpack配置文件的基本内容调整 webpack 配置最简单的方式就是在 vue.config.js 中的 configureWebpack 选项提供一个对象。

webpack:

webpack1.jpeg

1、webpack打包前后的变化

未使用打包-因为文件里涉及到了lodash,所以要在html文件中先引入lodash:image2022-12-6_19-36-24.png-缺点:脚本的执行需要依赖外部库,如果依赖不存在或者是引入顺序错误,应用程序将无法执行,并且依赖被引入后没有使用,则浏览器被迫下载无用代码
使用打包后-dist文件夹中的index.html是构建过程中经过最小化和优化后产生的输出结果,之前的src/index.html是用于书写和编辑的源代码image2022-12-6_19-39-43.png-此时无需在html文件中引入外部库,直接npm install安装后,通过import方式在使用到的文件引入即可。通过声明模块所需的依赖,webpack 能够利用这些信息去构建依赖图,然后使用图生成一个优化过的 bundle,并且会以正确顺序执行。

2、webpack的相关配置信息

【插播一条📢📢📢】:安装一个package时,此package要打包到生产环境bundle中的话,安装的命令为 npm install --save。如果用于开发环境的bundle,则使用 npm install --save-dev

配置含义解释
入口entry指示webpack从哪个模块开始,构建内部的依赖图,进入起点后,会找到入口起点依赖的模块和库默认值./src/index.js,可通过配置config.js中的entry属性,来指定一个或多个不同的入口起点1、单个入口,拓展性不高; 2、对象语法,最可拓展的方式(推荐)在其中传入不同类型的文件,可以实现将css和js或者其他文件分离在不同的bundle image2023-2-13_17-22-30.png用来描述入口的对象有以下属性:image2022-12-6_14-58-10.png
输出output指示webpack从哪里输出所创建的bundle,如何向硬盘写入编译文件主要输出默认./dist/main.js,其余生成的文件默认在./dist文件夹中。也可通过配置output字段,只能指定一个output配置项。设置为一个对象,输出文件名可配置在output.filename中如果有多个入口起点,可使用占位符确保每个文件具有唯一的名称filename: '[name].[fullhash].js'image2022-12-6_18-51-37.pngpath:path.resolve(_dirname,'dist') 表示生成的bundle文件路径clean:true 表示每次构建前清理/dist文件夹,这样只会生成用到的文件
loaderwebpack只能处理js和JSON文件,这个能让webpack处理其他类型的文件的源代码,将它们转换为有效的模块,供应用程序使用,以及添加到依赖图中loader的两个属性:test:识别出哪些文件会被转换,use:定义在进行转换时,用哪个loader注:1、定义在module.rules中;2、使用正则表达式匹配文件时,单引号和双引号意义不一样,单引号匹配任何满足这个表达式的文件,后者匹配具有绝对路径的满足这个表达式的单个文件1、config.js文件中(推荐)可在module.rules中指定多个loader,loader取值、执行的顺序:从下往上,或者是从右到左;2、在import语句显式指定!将资源中的loader分开!前缀表示禁用所有已配置的normal loader;!!前缀表示禁用所有已配置的loader; -!前缀表示禁用所有已配置的preloader和loader;3、链式调用,第一个loader将转换结果传递给下一个loader,直到最后一个loader返回webpack所要的JS
插件plugin支柱功能,用于执行范围更广的任务,比如打包优化、资源管理、注入环境变量是一个具有apply方法的JS对象,apply会被webpack compiler调用,在整个编译生命周期都可访问compiler对象可以在这里暴露一个动态的Public Path场景:在my-host.com/app/* 上有一个host应用,并且在foo-app.com 上有一个子应用。子应用程序挂载在host域上,可以通过my-host.com/app/foo-app 访问到子应用,且my-host.com/app/foo-app…* 可以通过代理重定向到foo-app.com/* 【HtmlWebpackPlugin插件】默认生成自己的index.html文件,替换原有的index.html文件,所有的bundle会自动添加到html中
targetwebpack 能够为多种环境或 target 构建编译,target设置为node时,表示webpack将在Node.js环境编译代码
模式mode告知 webpack 使用相应模式的内置优化   www.webpackjs.com/configurati…
devServerwebpack-dev-server是一个封装好的webpack开发服务器,默认集成一些第三方插件并可供配置放在devServer对象中image2023-3-3_16-11-3.png
devtooldevtool: 'inline-source-map'  (可用于开发环境、测试环境,但不要用于生产环境,否则会增加bundle体积大小与降低整体性能)devtool: 'source-map' (可用于生产环境)当webpack打包源代码的时候,将好几个js文件打包到一个bundle文件中输入,出错的时候无法定位到某个js文件上。所以提供了一inline-source-map选项(不要用于生产环境),帮助定位错误在哪个js文件中的哪一行

3、webpack的相关分析

  • 【chunk】

不同于以下配置,在webpack中指的是一个代码块。在webpack打包过程中,一堆module的集合,从入口模块开始,入口模块引用其他模块,其他模块再引用模块,这条打包路径就形成一个chunk。如果有多个入口文件,可能会产生多条打包路径,就会形成多个chunk。这是main chunk。还有一种chunk是可以延迟加载的块,可能会出现在使用动态导入或SplitChunksPlugin 时。

  • 【配置configuration】

mode、entry、output等选项属于基本配置,把 webpack 配置以 npm 包的形式来管理,是对今后项目构建升级的维护考虑

  • 【依赖图】

从入口开始,webpack会递归的构建一个依赖关系图,这个图包含了应用程序中所需的每个模块,然后将所有的模块打包为少量的bundle由浏览器加载

<对于 HTTP/1.1 的应用程序来说,由 webpack 构建的 bundle 非常强大。当浏览器发起请求时,它能最大程度的减少应用的等待时间。而对于 HTTP/2 来说,你还可以使用代码分割进行进一步优化>

  • 【runtime】

在浏览器运行过程中,webpack用来连接模块化应用程序所需的所有代码,包括:在模块交互时,连接模块所需的加载和解析逻辑。以及尚未加载模块的延迟加载逻辑。

  • 【manifest】

出现的原因:在经过打包、压缩、延迟加载而拆分为细小的chunk,原先/src目录的文件结构不复存在,一旦应用在浏览器中以index.html打开时,webpack要如何管理一些bundle和应用所需要的各种资源模块之间的交互呢。

解决:当compiler开始执行、解析和映射应用程序时,manifest作为数据集合,保留了所有模块的详细要点。当完成打包并发送到浏览器时,runtime会通过manifest来解析和加载模块

  • 【webpack模块】

能以各种方式表达它们的依赖关系

image2022-12-6_15-42-58.png

  • 【模块解析module resolution】

resolver是一个帮助寻找模块绝对路径的库,从每个require、import语句中找到需要引入到bundle中的模块代码。使用enhanced-resolve能解析出绝对路径、相对路径、模块路径

  • 【webpack5模块联合】

wepack构建产生的模块都存储在本地,直接被当前应用所使用,webpack5中,运行时把当前构建的应用作为容器应用,异步加载远程模块(不属于当前构建)。

如下图,容器应用之间可以相互的依赖和相互加载、使用。

image2022-12-6_16-17-42.png

区别:1、模块共享:微前端需要把该模块独立出去,并以合理调用方式被其他微应用远程加载;模块联合中每个应用允许暴露多个接口,其他应用可以动态远程加载该应用后,直接使用其接口。2、模块加载:模块联合中可像使用普通npm包一样引用一个远程模块。3、模块切换:微前端中微应用的切换通常由路由状态改变,模块联合中远程模块与路由没有关联,加载的契机由host应用自己决定。

Webpack 5 通过ModuleFederationPlugin来实现模块接口暴露和远程模块声明的工作。ModuleFederationPlugin插件组合了ContainerPlugin和ContainerReferencePlugin这两个插件的功能,ContainerPlugin使用指定的公开模块来创建一个额外的容器入口,ContainerReferencePlugin允许使用import方式使用远程模块,需提前声明远程模块。ModuleFederationPlugin构建一个运行时独立模块

name表示的是容器的名称,filename为容器入口文件,构建后会在dist目录里产生ui.js的额外容器入口文件;exposes暴露任何想要分享出去的模块;shared用来指定公共模块异步模块加载使用

image2022-12-6_16-34-52.png

  • 【缓存】

如图展示的是webpack的工作流程图。浏览器的缓存可以使网站加载速度更快,但同时由于缓存的存在,获取文件更新的代码会有些许棘手

流程图-导出.png

解决:通过必要的配置,确保webpack编译生成的文件能够被客户端缓存,在文件内容变化后,能够请求到新的文件。

通过将output.filename设置为'[name].[contenthash].js'这种可替换模板字符串的方式,其中[contenthash]将根据资源内容创建出唯一hash,资源内容变,[contenthash]也会变

  • 【模块热替换HMR】

在应用程序运行过程中,替换、添加或删除模块时,无需重新加载整个页面。(1、保留在完全重新加载页面期间丢失的应用程序状态;2、只更新变更的内容;3、在源代码中修改CSS/JS时,会立刻在浏览器中进行更新)

流程图-导出 (1).png

必须使用module.hot API处理传入的HMR请求,告诉webpack支持HMR

// 检查是否支持HMR接口 
if(module.hot) { 
// 支持热更新 
module.hot.accept('xxx',function(){}); 
}

在xxx中写上对应依赖的文件,function中添加模块更新后的处理函数。

用于处理新的模块代码替换老的模块后,业务代码仍绑定在旧函数的事件处理程序中。所以面对较为复杂的应用程序中实现HMR较为棘手。但可以通过借助一些loader来实现HMR。

例子:

index.js文件中:

import printMe from "./print.js"; 
async function getComponent() { 
   const element = document.createElement("button"); 
   // 使用default的原因是因为webpack4在导入CommonJS模块时,将不再解析为module.exports的值 
   // 而是为CommonJS模块创建一个artificial namespace对象 
   const { default: _ } = await import("lodash"); 
   element.innerHTML = _.join(["Hello", "webpack!"], ""); 
   element.onclick = printMe; 
   return element; 
} 

getComponent().then((component) => { 
   let element = component; // 存储element,在print.js修改时重新渲染
   document.body.appendChild(element); 
}); 
   
if (module.hot) { 
   module.hot.accept("./print.js", function () { 
   console.log("Accepting the updated printMe module!"); 
   printMe(); 
   // printMe()可替换为以下 
   // document.body.removeChild(element); 
   // element = component(); 
   // 重新渲染“component”,以便更新click事件处理函数 
   // document.body.appendChild(element); 
   }); 
}

print.js文件中:

image2022-12-28_16-1-24.png 若更新print.js代码中的console.log("Updating print...”)

image2022-12-28_16-3-34.png 虽然打印出来的是更新后的模块代码,但是点击按钮是打印的还是之前的代码,所以需要在accept函数中进行更新业务代码

image2022-12-28_16-7-10.png

  • 【tree shaking】

用于移除js上下文中的未引用代码,依赖于静态的ES6模块化语法,所有导入文件都会受到tree shaking的影响

满足条件:

  1. 使用ES module规范编写代码 
  2. 在webpack.config.js中配置optimization.usedExports:true,启动标记功能 ;
  3. 在生产环境中启动代码压缩:mode:"production"  optimization.minimize="true"

<所有的import标记为 /* harmony import*/; 所有被使用过的export标记为 /* harmony export([type]) /;  没有被使用过的export标记为 / unused harmony export [FuncName] */>

问题:当有些副作用的文件不应该被移除掉,可以通过sideEffect属性来处理副作用。

解决:在package.json文件中指定“sideEffects":false提示指定包以及依赖项没有副作用(副作用指:一个函数修改了它自己范围之外的东西),也可以sideEffects:["./xx./xx"]指定哪些文件/模块是有副作用的

流程图-导出 (3).png

4、对比webpack提供代码发生变化后自动编译代码的几种方式

方式含义使用
watch mode通过watch,可以监听到依赖图中所有文件的更改,一旦有一个文件被更新,代码将被重新编译,从而不必手动运行构建运行命令:npm run watch1、在package.json文件的npm scripts中增加image2022-12-12_10-6-50.png2、通过npm run watch 命令运行,运行一次后并没有退出命令行,而是接着在终端监听文件变化,根据文件的变化会重新编译修改后的模块;3、缺点就是:浏览器的变化并不能自动更新,而是需要我们进行手动刷新
webpack-dev-server提供了一个基本的Web server,具有实时重新加载的功能。在内部使用了webpack-dev-middleware这个封装器。编译后不会写入到任何输出文件,而是将bundle文件保留在内存中运行命令:npm start1、安装依赖npm install --save-dev webpack-dev-server 2、在webpack.config文件中增加:(第一部分是将dist目录下的文件作为localhost:8080的可访问文件,文件的访问路径为:http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename];第二部分是因为若单个HTML页面中具有多个入口,将要添加该配置,否则要遇到这个问题)image2022-12-12_10-19-14.png3、在package.json的script中添加:"start": "webpack serve --open" 这样就可以通过npm start运行吗浏览器便可自动加载页面
webpack-dev-middleware是一个封装器,可以把webpack处理过的文件发送到一个server,可作为一个单独的package来使用运行命令:npm run server1、安装依赖npm install --save-dev express webpack-dev-middleware;2、在webpack.config.js中的output属性中添加:publicPath:'/',添加server.js文件;3、在package.json的script中添加:"server":"node server.js",方便运行server

5、对比webpack提供的代码分离特性的几种方式

代码分离:将代码分离到不同的bundle中,按需加载或并行加载这些文件。可控制资源加载的优先级,以及减小加载时间

含义使用备注
入口起点使用entry配置手动分离优点:最简单直观;缺点: 手动配置较多,存在一些隐患。如果入口chunk之间包含一些重复的模块,重复的模块都会被引入到各个bundle中,bundle文件体积将会增大1.png
防止重复使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk1、第一种:配置dependOn选项,可以在多个chunk之间共享模块2.png 📢:一个HTML页面上使用多个入口时么,还需设置optimization: { runtimeChunk: 'single'} ; 2、第二种:使用splitChunksPlugin插件,可将公共的依赖模块提取到已有的入口chunk中,或者是提取到一个新生成的chunk 3.png
动态导入通过模块的内联函数调用来分离代码 1、ES提案的import()语法实现动态导入;2、webpack特定的require.ensure;意义:延迟或“按需”加载是优化站点或应用程序的好方法。这种做法实质上涉及在逻辑断点处拆分代码,然后在用户完成需要或将需要新代码块的操作后加载它。这加快了应用程序的初始加载速度并减轻了它的整体重量,因为有些块甚至可能永远不会加载。)1、import()调用会在内部用到Promise 11.png 通过import(/* webpackChunkName:'xxx' / "./math.js")可以修改打包后的bundle名称xxx.bundle.js;2、可以采用预获取、预加载模块,通过使用内置指令可以告知浏览器prefetch(预获取): 将来某些导航下可能需要的资源  (父组件加载结束后开始加载,在浏览器闲置时下载)preload(预加载):当前导航下可能需要资源 (父组件加载时,并行方式开始加载,具有中等优先级,会立即下载)import(/ webpackPrefetch: true */  '.xx/xx');会生成这个link追加到页面head头部举例:33.png点击前:未请求该文件44.png点击后:请求该文件,节省了网络流量 55.png

6、webpack版本区别(可持续补充中......)

1、vector.js文件的作用是存入一些文件或library,打包成为单独的chunk,浏览器可以独立的缓存它们,减少加载时间

在webpack<4的版本中,通常将vector作为一个单独的入口起点添加在entry中,之后可编译为一个单独的文件

但是在webpack4版本中,不推荐这么做,而是使用optimization.splitChunks选项,将vector和app模块分开,并为其创建一个单独的文件

2、在webpack4版本中可以通过cli使用loader,但是在webpack5版本中已弃用

3、webpack5具有资源模块,允许使用资源文件而无需配置额外的loader

webpack5之前webpack5
raw-loader:将文件导入为字符串asset/source:导出资源的源代码
url-loader:将文件作为data URL内联到bundle中asset/inline:导出一个资源的data URL   asset:在导出一个data URL和发送一个单独的文件之前自动选择
file-loader:  将文件发送到输出目录asset/resource:发送一个单独的文件并导出URL

4、从webpack5开始,可以是使用Web Worker代替 worker-loader  (只适用于ES模块,不可用于CommonJS)

new Worker(new URL('./worker.js', import.meta.url)); 这种语法为了实现不使用bundler就可以运行代码,可以在浏览器中的原生ES模块中使用

参考文档:

1、www.lumin.tech/articles/we…

2、www.webpackjs.com/concepts/mo…