webpack是一个模块打包工具,可以把各种资源如JavaScript、CSS、图片等都视为模块,使得工程中的各种资源能够被打包成一个整体的bundle.js文件。Webpack具有很高的可配置性和灵活性,可以通过插件和loader来扩展webpack的功能,适用于大型、复杂的项目。
webpack通过一种叫做loader的机制来处理非JavaScript类型的文件,并且可以把这些文件打包成合适的格式供浏览器使用(它可以处理多种不同类型的文件(如js、css、图片等),并根据需求进行转换、压缩和打包。)。除此之外,webpack还具有代码拆分、优化、模块热替换等强大功能。
安装webpack&webpack-cli(建议安装本地开发依赖):npm i webpack webpack-cli -D。
webpack原理
当webpack处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。webpack的主要原理可以概括为以下几个步骤:
- 入口(Entry):webpack根据配置文件指定的入口点开始分析,找出所有依赖的模块。
- 依赖分析(Dependency Analysis):webpack通过代码中的require、import等语句分析出模块间的依赖关系。
- 加载器(Loader):webpack使用加载器处理非JavaScript文件(如CSS、图片等),并将它们转换为有效的模块即浏览器可以理解的格式,以便webpack能够处理它们。
- 插件(Plugins):webpack允许使用范围广泛的插件来扩展其功能,插件可以执行范围广泛的任务,如打包优化、资源管理和环境变量注入等。
- 输出(Output):webpack将处理后的模块合并成少量的bundle,通常是一个或多个js文件,能够在浏览器中加载这些文件。
打包流程
读取配置文件 —》 找到入口文件 —》 递归解析项目中的所有依赖模块(包括js文件、css、图片等)—》 使用相应的loader编译这些模块,例如转译ES6、压缩代码、提起公共模块等 —》 合并模块(webpack将所有模块合并成一个或多个包bundle,根据配置规则将模块分组打包)—》 输出最终的包到指定目录下,以便在浏览器中运行。
webpack配置
五个核心概念:
-
Entry:入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
-
Output:output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。
-
Loader:webpack通过loaders去支持其他文件类型并把他们转成有效模块,并且添加到依赖图中。loader本身是一个函数,接收源文件作为参数,返回转换的结果。(webpack 自身只能解析 JavaScript和json两种文件类型)。
常见loader:
- babel-loader:转换ES6、ES7等js新语法;
- css-loader:支持.css文件的加载和解析,把它当成一个模块;
- style-loader:将css加到style标签内,否则打包后的样式不生效;
- less-loader:把less文件转换成css;
- ts-loader:将TS转换成JS;
- file-loader:进行图片、字体的打包;
- raw-loader:将文件以字符串的形式导入;
- thread-loader:多进程打包js和css。
注:file-loader、raw-loader、url-loader等在webpck5中已经被资源模块类型(asset module type)替换掉了,
-
Plugins:插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量等。
常见的插件:
- HtmlWebpackPlugin:创建html标签,去承载输出的bundle;
- CleanWebpackPlugin:清理构建目录(dist目录);
- CommonsChunkPlugin:将chunks相同的模块代码提取成公共的js;
- ExtractTextWebpackPlugin:将css从bundle文件里提取成一个独立的css文件
- uglifyjsWebpackPlugin:压缩JS。
-
Mode:模式,有生产模式production和开发模式development。设置模式可以让webpack自动调起相应的内置优化,设置了 mode 之后,webpack4 会同步配置 process.env.NODE_ENV 为 development 或 production。不同模式,优化内容也有不同:
production 模式下有更好的用户体验:
- 较小的输出包体积
- 浏览器中更快的代码执行速度
- 忽略开发中的代码
- 不公开源代码或文件路径
- 易于使用的输出资产
development 模式会提供最好的开发体验:
- 浏览器调试工具
- 快速增量编译可加快开发周期
- 运行时提供有用的错误消息
none:不使用任何默认优化选项。
// webpack.config.js
// 插件需要手动引入,loader会自动加载
const HtmlWebpackPlugin = require('html-webpack-plugin')
moudule.exports = {
// 单入口 entry:'./index.js'
// 多入口 key:构建包名称,即 [name] ,在这里为index; value:入口路径
// 入口决定 webapck 从哪个模块开始生成依赖关系图(构建包),每一个入口文件都对应着一个依赖关系图。
entry: {
"index": `./index.js`,
},
output: {
// 打包输出文件路径 path 必须为绝对路径
path: path.resolve(__dirname, '../../dist/build'),
publicPath: "www.xxx.xx.com", // 如果打包出来的资源要放到cdn上,可以配置pbulicPath加上域名
filename: "[name].bundle.js", // 包名称
// 或使用函数返回名(不常用)
// filename: (chunkData) => {
// return chunkData.chunk.name === 'main' ? '[name].js': '[name]/[name].js';
// },
chunkFilename: '[name].[chunkhash].bundle.js', // 块名,公共块名(非入口)
// 打包生成的 index.html 文件里面引用资源的前缀
// 也为发布到线上资源的 URL 前缀 使用的是相对路径,默认为 ''
publicPath: '/',
// 打包成库 使用 webapck 构建一个可以被其它模块引用的库
// 一旦设置后,该 bundle 将被处理为 library。
library: 'webpackNumbers',
// export 的 library 的规范,有支持 var, this, commonjs,commonjs2,amd,umd
libraryTarget: 'umd',
},
// 环境 可以是 none、development、production;默认为 production
mode: 'production',
// mode 也可以在script命令行里配置
// "build:prod": "webpack --config config/webpack.prod.config.js --mode production"
// source map 通过source map 定位到源代码
// 开发模式设置为eval-source-map可以直接定位到具体的错误行;
// 生产环境下关闭,将devtool 设置为 nosources-source-map,防止源码泄漏,提高网站的安全性
devtool: 'eval-source-map',
module: {
rules: [
{ // js 语法检查 npm install eslint-loader eslint --save-dev
test: /.js$/, //只检测js文件
exclude: /node_modules/, // 排除node_modules文件夹
enforce: "pre", //提前加载使用
use: { //使用eslint-loader解析
loader: "eslint-loader"
}
},
{ // 打包less资源 npm install css-loader style-loader less-loader less --save-dev
test: /.less$/, // 检查文件是否以.less结尾(检查是否是less文件)
use: [ // 数组中loader执行是从下到上,从右到左顺序执行
'style-loader', // 创建style标签,添加上js中的css代码
'css-loader', // 将css以commonjs方式整合到js文件中
'less-loader' // 将less文件解析成css文件
]
},
{ // js 语法转换 将浏览器不能识别的新语法转换成原来识别的旧语法,做浏览器兼容性处理
// npm install babel-loader @babel/core @babel/preset-env --save-dev
test: /.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ['@babel/preset-env']
// @babel/preset-env是一个智能预设,允许使用最新的JavaScript,而无需微观管理目标环境需要哪些语法转换(以及可选的浏览器polyfill)。
}
}
},
{ // webpack4 打包样式文件中的图片资源 npm install file-loader url-loader --save-dev
test: /.(png|jpg|gif)$/,
use: {
// url-loader是对象file-loader的上层封装,使用时需配合file-loader使用。
loader: 'url-loader',
options: {
limit: 8192, // 8kb --> 8kb以下的图片会base64处理
outputPath: 'images', // 决定文件本地输出路径
publicPath: '../build/images', // 决定图片的url路径
name: '[hash:8].[ext]' // 修改文件名称 [hash:8] hash值取8位 [ext] 文件扩展名
}
}
},
{
// webpack5 配置方式
test: /.(png | svg | jpg | gif)$ /,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024 // 10kb 如果小于10kb直接转成base64 加到页面里面
}
}
},
{
test: /.(woff|woff2|eot|ttf|otf)$/, //字体
type: 'asset/resource'
}
]
},
plugins: [
new HtmlWebpackPlugin({
// 以当前文件为模板创建新的HtML(1. 结构和原来一样 2. 会自动引入打包的资源)
template: './src/index.html',
}),
],
devServer: {
contentBase: resolve(__dirname, 'build'), // 运行项目的目录
hot: true, // 开启热更新
open: true, // 自动打开浏览器
compress: true, // 启动gzip压缩
port: 3000, // 端口号
},
// 配置webpack的优化参数
optimization: {
},
resolve: { // 通常配置一些简便开发的内容
// 配置别名 提供路径的简写
alias: {
"@css": "/css"
},
// 扩展名省略,定义可以省略的扩展名
extensions: [".js", ".css", ".json"]
}
}
资源内联
页面框架的的初始化脚本、上报相关打点、css内联避免页面闪动、小图片或者字体内联(可以减少http请求次数)等都需要把代码放在打包后的代码前面先加载。在Webpack中内联html和javaScript代码可以通过raw-loader这个插件来完成,在引入JS插件时,需要先利用babel-loader进行转换,所以babel-loader也需要一起引入
<!--
html与js内联的方式
首先拆分需要内联的HTML片段 放在meta.html中
利用插件内联HTML片段与JS插件
-->
<head>
<!-- 引入meta.html片段-->
${ require('raw-loader!./meta.html')}
<title>Webpack内联文件</title>
<!-- 将外部JS插件进行内联 -->
<script>
${ require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js')}
</script>
</head>
// css内联在webpack.config.js中配置style-loader即可
// 也可以使用html-inline-css-webpack-plugin插件
热更新
当我们对代码修改并保存后,webpack会对修改的代码块进行重新打包,并将新的模块发送至浏览器端,浏览器用新的模块代替旧的模块,从而实现了在不刷新浏览器的前提下更新页面。webpack4及之前需要安装热更新插件:npm i webpack-dev-server;webapck5之后只需要在配置中新增参数devServer配置即可。
热更新工作原理
- 启动Webpack Dev Server:在Webpack配置中,设置
devServer.hot为true,以启用热模块替换功能。 - 建立WebSocket连接:启动Webpack Dev Server时,它会创建一个Socket服务器,用于与浏览器建立WebSocket连接。在浏览器中访问应用程序时,Webpack Dev Server会将一个运行时脚本(runtime script)注入到页面中。这个脚本会建立与Webpack Dev Server的 WebSocket连接,以便实时接收来自服务器的更新通知。
- 文件监控与编译:Webpack会监控所有入口文件及其依赖的文件。当修改了源代码并保存时,Webpack会监听文件系统的变化,并调用webpack-complier编译修改后的模块。
- 推送更新:当编译完成后,Webpack会将更新的模块信息发送给Webpack Dev Server。Webpack Dev Server会通过WebSocket连接将更新的模块信息推送给浏览器,并带上构建时的 hash,让浏览器与上一次的资源进行对比。
- 客户端处理更新:浏览器接收到更新的模块信息后,会使用Webpack的HMR Runtime(热模块替换运行时)来处理这些更新。HMR Runtime会根据模块的更新信息,将新的模块代码插入到应用程序中,而无需重新加载整个页面。
webpack优化
-
多页面打包:优化SEO,加快首屏渲染。
-
基础库分离:将一些基础包分离,不打入bundle,单独打包,也可以引入CDN。对于echarts等大型包,也可以通过test检测单出打包,做更细致的优化。参考下面代码分割。
-
tree shaking:production时默认开启。
-
动态import:即按需加载模块,减少文件体积,适用于单入口文件打包。通过import函数导入的模块都被打包成独立的js文件,在实际使用中,会通过触发某些事件,再去执行import函数,这时就会在html文档上插入一个script标签,加载我们需要的模块。需要安装插件@babel/plugin-syntax-dynamic-import。
- 单文件入口:容易出现打包文件体积过大问题,可以使用动态引入来分割代码,且如果这个模块又大又靠后使用,则使用动态引入。
//代码引入模块 // js const loadimport = ()=>{ // webpackChunkName可以指定打包后的文件名称 import(/*webpackChunkName: a*/'./utilsA.js').then( res =>{ console.log('utilsA导出的内容为res.default', res.default) // 其他操作 }) /* 另一种写法 require.ensure(['导入文件需要的额依赖如jquery'], (jquery)=>{ let res = require("./utilsA.js") console.log(res.default) }, '打包文件的别名') */ } // html <button onClick={loadimport}>hello</button> // webpack配置 { "plugins": ["@babel/plugin-syntax-dynamic-import"] } // 很少会使用import函数来动态导入一个模块。因为在所有导入该模块的地方,如果有一处忘记使用import函数,它就会被打包到最终的bundle里面,这样优化的目的就达不到了。此外,还需要判断这个模块是否真的需要分割出来,因为建立一个http请求也需要一定的开销。import函数使用不当不仅达不到性能优化的目的,可能还会降低页面的性能。 -
代码分割:配置多入口文件时问题主要是重复加载同一段逻辑代码,可以使用代码分割,将重复的部分单独打包,并利用浏览器缓存功能,第一个入口文件加载时,就会缓存此重复代码,下次再需要的时候就可以直接取缓存。
// 配置多文件入口
module.exports = {
// 如果两个入口文件都引入了app3.js,那么两个出口文件中都会把app3.js打包进去
entry: {
app: './app.js',
app2: './app2.js'
},
output: {
path: __dirname + "/dist",
filename: "[name].[hash:4].bundle.js",
chunkFileName: "[name].js"
},
// 优化相关配置 一般设置好模式,就会又默认的优化配置,但是代码分割还是需要单独配置
optimization: {
splitChunks: {
// all:不管同步或异步都会拆分; async:至拆分异步即动态import部分; initial:只拆分同步;
chunks: "all",
minChunks: 2, // 一个模块被重复引用几次才会被拆分成独立的文件
// 1000bety 大于minSize的chunk才会进行拆分,避免拆分过多, 增加http请求次数
minSize: 1000,
name: "a",// 指定文件名 所有重复的内容,都会打包进同一个文件包括第三方库
// 针对上面的基础分割规则,如果有特殊分割需求的时候可以配置 cacheGroups
cacheGroups: {
// 例如:如果要把第三方库单独打包,需要再单独配置
vendor: {
test: /[\/]node_modules[\/]/, // 第三方库从node_modules中找
filename: "vendor.[hash:4].js",
chunks: 'all',
minChunks: 1,
},
common: { // 把业务公用代码单独打包再一起为common.js 上面基础配置可以去掉了
filename: "common.[hash:4].js",
chunks: 'all',
minChunks: 2,
minSize: 1000,
}
}
},
// 大多项目通用的、单入口或多入口都会把以下两种模块单独打包:
// 第三方库单独打包 ...vendor.js
// runtime运行代码(webpack用于组织模块运行的代码)...runtime.js
// 单独配置 runtimeChunk
runtimeChunk: {
name: 'runtime'
}
}
}
-
多线程打包:thread-loader。
-
dll优化打包速度:提前打包不变的包(vendor),通知到正式打包,然后Dll处理过的就不再处理。 新建一个webpack.dll.config.js配置文件:
const webpack = require("webpack")
module.exports = {
mode: "production",
entry: {
vendor: [
"axios",
"lod
]
},
output: {
path: __dirname + "/dist/dll",
filename: "[name].[chunkhash:4].dll.js",
library: '[name_library]', // 通过library去定位哪些东西是已经打包的
},
plugins: [
new webpack.DllPlugin({
// DllPlugin插件会输出一个json,里面的东西是通知正式打包已经打包了内容,path为json的路径
path: __dirname + "/[name]-manifest.json",
name: '[name_library]', // 必须与library同名,标识
context: __dirname, // 当前文件夹
})
]
}
/*
// 新增script脚本 并执行 生成打包文件vendor.dll.js,以及vendor-manifest.json文件
"dll": "webpack --config ./webpack.dll.config.js"
// 通知正式打包 在webpack.proconfig.js 生产环境的配置文件中添加配置,把dll输出的json文件给它
plugins: [
new webpack.DllReferencePlugin({
manifest: require(__dirname + "/vendor-manifest.json")
})
]
// 注:但是,这样有个问题:生成的vendor.dll.js文件不会自动引入到html里面,还需要手动引入才能生效。
// 解决:需要在入口文件index.html文件中手动加入:
<head>
//...
<script src="vendor.dll.js"></script>
</head>
*/
-
优化打包速度:webpack内置的stats,分析plugin打包速度插件:speed-measure-webpack-plugin;
-
bundle分析:使用插件webpack-bundle-analyzer
-
方法一,在打包脚本后面加上--json,把生成的json文件传到官网上解析
'getJson' : "webpack --config ./webpack.proconfig.js--json>stats.json" -
方法二、引入webpack-bundle-analyzer插件
-
// 在webpack.baseconfig.js中引入插件
const bundleanalyzer = require("webpack-bundle-analyzer").BundleAnalyzerPlugin
module.exports= {
// ...
plugins:[
new bundleanalyzer(),
]
}
webpack打包后文件带的hash值的意义:
浏览器加载了一个资源后会缓存资源,但是如果名字改了就会重新请求。所以内容改变后,hash值就会改变,浏览器就会重新请求最新的资源。配置文件名时通过[hash:4]添加hash值,也可以取8位,但是这样配置存在一个问题,一旦有一个模块改动,所有打包的文件都会重新生成hash值,没有修改的模块缓存就失效了。因此,想要精准控制各个模块的hash不相互影响,可以使用[chunkhash:4],可以最大程度利用缓存。
require.context("文件目录",boolean是否包含内层子文件,正则匹配规则):批量引入某个文件下的符合条件的所有文件。避免单独一个一个引入。
/*
>mode
num1.js
num2.js
num3.js
*/
// 同时引入./mode文件夹下的所有js文件,不包含内层子文件。 返回引入的文件内容
const res = require.context("./mode", false, /.js/)
// res.keys()返回数组["num1.js","num2.js", "num3.js"]
let _all;
res.keys().forEach((item)=>{
console.log(res(item).default)
_all += res(item).default
})
webpack实战
区分环境,指导vite在不同环境下做不同的事情:
生产模式:需要代码压缩、tree-shaking;不需要详细的source map,不需要开启开发模式;
开发模式:需要详细的额source map,开启开发模式,不需要压缩、代码混淆等。
根据不同环境进行不同打包,一般在process.env中设置,有的时候需要在js代码中获取环境,我们可以借助插件完成。
新建不同环境下的配置文件:
- 基础配置:webpack.baseconfig.js
- 开发环境配置:webpack.devconfig.js
- 生产环境配置:webpack.proconfig.js
// webpack.baseconfig.js
const htmlWebpackPlugin = require("html-wepack-plugin");
const minicss = require("mini-css-extract-plugin");
let pluginArr = [
new htmlWebpackPlugin({
filename: "index.html",
template: "./index.html"
})
]
function hasMiniCss() {
if (process.env.NODE_ENV === "production") {
pluginArr.push(new minicss({
filename: "test.bundle.css"
}))
}
}
hasMiniCss(); // 编程式配置
module.exports = {
entry: {
app: "./app.js"
},
output: {
path: __dirname + "/dist",
filename: "[name].[chunkhash:4].bundle.js"
},
module: {
rules: [
{
test: /.css$/,
use: [
process.env.NODE_ENV === "production" ? minicss.loader : "style-loader",
"css-loader"
]
}
]
},
/*
plugins: [
new htmlWebpackPlugin({
filename: "index.html",
template: "./index.html"
})
]
*/
}
// webpack.devconfig.js
const base = require("./webpack.baseconfig.js");
// webpack提供了合并配置对象的merge方法, 后面配置的会覆盖前面的。
const merge = require("webpack-merge").merge;
module.exports = merge(base, {
mode: "development",
devtool: "eval-cheap-source-map",
devServer: {
port: 1000,
hot: true,
proxy: {
"/": {
target: "http://loaclhost:3000",
pathRewrite: {
"^/num1": "/api/getNum1",
"^/num2": "/api/getNum2",
},
headers: {},
}
}
},
})
// webpack.proconfig.js
const base = require("./webpack.baseconfig.js");
const merge = require("webpack-merge").merge; // webpack提供了合并配置对象的merge方法
module.exports = merge(base, {
mode: "production",
})
/*
// cross-env是一个库,帮助脚本可以跨平台运行 需要安装 npm i cross-env -save-dev
// 在package.json文件中配置打包脚本 运行脚本中指定当前环境cross-env,可用于配置文件中判断。
"dev": "cross-env NODE_ENV=development webpack-dev-server --config ./webpack.devconfig.js",
// 脚本中执行build是 利用本地安装的webpack打包。
"build": "cross-env NODE_ENV=production webpack --config ./webpack.proconfig.js"
// 指定环境方法二、 脚本中指定 --env
"dev": "webpack --config ./webpack.proconfig.js --env production",
*/
// 接收env参数需要改造以下配置文件,将配置对象改为函数方式接收env参数
const base = require("./webpack.baseconfig.js");
const merge = require("webpack-merge").merge;
module.exports = function (env) {
return merge(base(env), {
mode: "production",
})
}
// webpack.baseconfig.js也需要改造成方法接收
module.exports = function (env) {
console.log(env)
return {
// ...
}
}
在业务代码中如何获取环境变量:
// 在不同环境的配置文件中引入webpack,在插件中配置注册变量,会成为全局变量,在业务代码中可以全局引用
const webpack = require("webpack");
module.exports = merge(base, {
mode: "production",
plugins:[
new webpack.DefinePlugin({
baseURL: "www.xxx.com" // baseURL就可以全局使用
})
]
})
// 在development环境可以指定baseURL:"www.yyy.com"
// ./app.js
console.log(baseURL); // www.xxx.com