Webpack5
Webpack是一款模块打包工具,就是把多个文件打包成一个或几个文件,它不仅能打包JS文件, 还能打包css, image等静态资源。当然,默认情况下,只能打包JS文件和JSON文件。mkdir webpack-demo && cd webpack-demo && npm init -y,创建项目。npm i webpack webpack-cli -D,安装webpack。webpack4把webpack命令抽离出来,形成了一个单独的包webpack-cli ,安装它才能在命令行中使用webpack命令。为什么要单独形成一个包呢?因为webpack-cli 还提供了两个其它的功能,init 和 migrate,快速创建webpack配置和提供升级迁移。mkdir src && touch src/index.js && touch src/component.js,component.js 文件
export default (text = 'hello world') => {
const element = document.createElement('div');
element.innerHTML = text;
return element;
}
index.js 文件
import component from './component';
document.body.appendChild(component());
npx webpack 生成了dist 目录,打包成功了。npm5之后,npx可以执行命令。没有配置文件,webpack怎么打包?webpack4提供零配置,打包时如果没有配置文件,默认入口是src/index.js,默认输出到dist/main.js。但有一个WARNING,
没有设置mode,webpack4 增加了一个mode 配置项 ,用什么模式进行打包,development 是开发模式,优化开发体验,production是生产模式,用于部署生产环境。npx webpack --mode development 或npx webpack --mode produciton,生产模式下,打包后代码压缩了。 指令越长,手动输入就越麻烦,可以用npm run 命令,在package.json的scripts的字段中
"scripts": {
"dev": "webpack --mode development",
"build": "webpack --mode production"
}
npm run build或npm run dev进行打包。验证一下打包后的文件,手动建一个html 文件,script 引入dist/main.js?有了webpack后,能自动化就自动化。webpack有一个插件html-webpack-plugin,它会创建html文件,并自动引入打包后文件。npm i html-webpack-plugin -D,不过,这要写webpack配置文件了。webpack有production和development两种mode,不同的环境有不同的配置。怎么处理?可以一个配置文件,根据不同的环境变量,返回不同的配置。webpack默认的配置文件名是webpack.config.js,webpack打包时,会自动找webpack.config.js文件,它要exports 出一个函数,接受环境变量,返回配置对象
module.exports = (env) => {
if (env === 'production') {
return ({ // 生产环境配置英})
} else {
return ({// 开发环境配置英})
}
}
执行webpack命令时,--env提供环境变量,webpack --env production。也可以多个配置文件,一个环境一个配置文件,执行webapck命令时,用--config指定使用哪一个配置文件。这里使用多个配置文件。根目录新建webpack.dev.config.js,配置文件有几个重要的概念,entry, output, module, plugins。
entry:打包的入口文件,webpack打包时,从哪个文件开始。webpack有一个context的概念,寻找入口文件时,从哪个目录开始寻找?默认是从执行webpack命令的目录,也就是根目录。context 就是配置从哪个目录开始查找,一般不用配置。
output:打包后的文件放到什么地方,以及文件名是什么
module:处理哪些模块,用什么规则处理,规则就是指定loaders。loaders就是告诉webpack怎么处理不认识的文件,把它转化成JS,因为webpack只认识js文件, 只有转化成JS,webpack才能进行打包
plugins:打包过程其它的事情,比如压缩,生成html文件,扩展了webpack的功能
由于有零配置,webpack提供了默认的entry和output, 如果觉得ok,配置文件中可以只写module 和plugins,mode也可以在配置文件中配置,npm run 命令中的 --mode去掉,webpack.dev.config.js如下
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: 'development',
plugins: [
new HtmlWebpackPlugin(),
],
};
如果觉得不ok 的话,可以写entry 和output, 把它覆盖掉。webpack.dev.config.js如下
const path = require('path'); const htmlWebpackPlugin = require('html-webpack-plugin'); // 引入插件 module.exports = { mode: 'development', entry: path.join(__dirname, 'src/index.js'), output: { path: path.join(__dirname, 'dist'), filename: 'main.js' }, plugins: [ new htmlWebpackPlugin() ] }
output的path要使用绝对路径,因此使用了Node.js内置path模块。plugins是个数组,每一个用到的插件都是数组中的一项。具体到每一个插件呢?插件都会暴露出构造函数,通过new 调用,就可以使用,如果插件还有配置项,就给构造函数传递一个配置对象作为参数。dev命令改成
"dev": "webpack --config webpack.dev.config.js",
npm run dev 打包成功,查看一下,没有问题。
但前端不止JS,还有CSS,图片等。想要把页面更美观一点,写一点CSS,在src下新建style.css
body {
background: cornsilk;
}
并index.js引入,打包,发现wepack 不认识CSS,要配置loader。处理CSS两个基本loader,css-loader和style-loader, npm i css-loader style-loader -D
module.exports = {
mode: 'development',
module: {
rules: [
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
],
},
plugins: [
new HtmlWebpackPlugin(),
],
};
当使用多个loader来处理同一个文件时,要注意loader的执行顺序,它是按照从右到左,从下到上的顺序执行。css-loader 先执行,执行结果给到style-loader。css-loader读取css文件,将css代码输出到打包后的JS文件中(把样式都转换成字符串)。style-loader是动态创建<style>标签(style是window对象属性),然后把css代码注入到style标签中。打包后,只有一个main.js,浏览器只执行main.js,所以动态创建style标签,用JS去创建style标签。less, sass, stylus 等预处理器也是一样,安装loader,放到最后边,比如npm install less less-loader -D,use: ["style-loader", "css-loader", "less-loader"]。
处理图片,webpack5内置Asset Modules(资源模块),代替了url-loader, file-loader。它有四种类型: asset/resource,asset/inline,asset,asset/source。asset/resoure: 图片会被拷贝到输出目录,同时为图片生成url。在JS中import image或在CSS中url(image),实际上是import的图片url。在src目录下放一张图片,比如logo.png
import Logo from "./Logo.png"; export default (text = "Hello world") => { const element = document.createElement("div"); element.innerHTML = text; const img = document.createElement('img'); img.src = Logo; img.alt = "Logo" element.append(img) return element; };
webpack配置文件rules新增
{ test: /\.(png|jpg)$/, type: "asset/resource", }
执行打包命令,dist目录多了一个.png文件。打开浏览器控制台,img的src指向了dist目录下的png文件,打包同时生成url。asset/inline: 图片会被转换成base64编码,内联到打包生成的文件中,把上面的type改成asset/inline, 重新打包,dist目录下并没有生成一个新的文件,打开浏览器控制台,img的src是data:... base64编码。asset: 它是asset/resource和asset/inline的组合,webpack会根据资源的大小动态选择使用哪种类型。默认情况下是8kb,如果资源大于8kb,就用asset/resourece, 如果资源小于8kb,就用asset/inline。把type改成 "assest",重新打包,没什么变化,因为logo.png的大小只有2.5kb。通过parse配置改变默认的8kb的大小。
{
test: /\.(png|jpg)$/,
type: "asset",
parser: { dataUrlCondition: { maxSize: 1024 } },
}
再进行打包,生成一个图片。如果想给图片改名字,或放到不同的目录下,配置generate,parser下面
generator: { filename: 'static/[hash][ext][query]' }
asset/source: 导出资源的源代码,并将其作为文本字符串注入到打包后的文件中。比如import txt文本,文本文件中的内容(源代码)会被当做字符串。新建altText.txt,内容是Logo From file,然后在compoent.js
import alt from './altText.txt' img.alt = alt;
在webpack的配置中添加rules
{ test: /\.txt$/, type: "asset/source" }
字体的处理和图片一样,也是使用内置的资源模块
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
}
eslint的配置,要用插件了,不是loader了,npm install eslint-webpack-plugin eslint -D
const ESLintPlugin = require('eslint-webpack-plugin'); module.exports = { // ... plugins: [new ESLintPlugin({ // 检测哪些文件 context: path.join(__dirname, 'src'),
cache: true, // 默认缓存到node_modules 的.cache下面 })], // ... };
根目录下写eslint的配置文件,ESLint 8 和ESLint 9 的配置文件写法不同。ESLint 8 的配置文件名是.eslintrc.js,假设定义如下配置
module.exports = { "env": { "browser": true, "es2021": true }, "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, rules: { "no-var": "error" } }
ESLint 9的配置文件是eslint.config.mjs,因为配置文件中使用了ES模块,所以后缀名成了.mjs
import js from "@eslint/js"; import globals from "globals"; export default [ js.configs.recommended, { languageOptions: { globals: { ...globals.browser,
...globals.node } }, rules: { "no-unused-vars": "error" } } ];
webpack怎么选择配置文件?webpack ESLint plugin的configType配置项有两个选项,'flat' | 'eslintrc'。'eslintrc' 是默认选项,就是使用ESLint 8的配置。也就是说,在根目录下创建.eslintrc.js文件,webpack就能对代码进行检测。如果要使用ESLint 9 的配置,那就要配置flat,webpack官网说是实验性的功能,所以还要配置eslintPath,所以,在webpack 中使用ESLint 9 的配置,除了在根目录下配置eslint.config.mjs, 还要配置eslint-webpack-plugin
plugins: [new ESLintPlugin({ // 检测哪些文件 context: path.join(__dirname, 'src'), configType: 'flat', eslintPath: 'eslint/use-at-your-own-risk' })],
ESLint 配置好了,如果VS Code 安装了ESLint 插件,它也会根据配置文件对代码进行检测。但你会发现dist目录它也会检测,我们告诉webpack检测哪些代码文件,但没有告诉ESLint插件检测哪些文件,这要配置ESlint ignore 文件,在项目根目录下 .eslintignore, 内容 dist
配置好后,修改代码还是比较麻烦了,修改一下,就要npm run dev一下,怎么自动打包,并刷新浏览器页面呢?webpack-dev-server,npm i webpack-dev-server -D,script 命令改一下。
"dev": "webpack server --config webpack.dev.config.js"
npm run dev, 浏览器localhost:8080, 打开页面,改变样式,页面实时刷新。webpack-dev-sever是webpack自带的一个小型服务器,它会监测每个文件的变动,每当有文件改动时,就会重新打包,然后浏览器自动刷新页面,能时时看到代码的变动,这是所谓的liveload 或hotload(热更新)。webpack-dev-server 打包后文件是放到内存中的,而不像npm run build 把文件打包到硬盘上。想要访问服务器的文件,路径是http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename],host默认是localhost, port是8080, publicPath默认是/, filename就是打包后文件。所以http://localhost:8080/就可以访问服务器的内容。如果想要配置webpack-dev-sever,配置文件提供了devServer 配置项,比如把端口改为9000
devServer: {
port: 9000, // 设置端口号
stats: 'errors-only', // 只有产生错误的时候,才显示错误信息,日志的输出等级是error.
}
npm run dev,重启服务器,这时看到项目启动在9000端口下。
webpack-dev-server 还有两个配置项需要注意一下:
static: webpack-dev-server 会把所有的静态文件(css, js, img 等)进行打包,放到服务器根目录下,供我们访问。但如果访问的资源不是由webpack-dev-server 打包生成的,它就找不到了,此时就要指定这些静态资源,比如index.html 文件,所在的位置,也就是static,否则就会显示404. 如果不使用webpack-html-plugin, webpack 是不会打包生成index.html的, 需要手动创建index.html, 这时index.html 就不是webpack-dev-server 打包生成的资源,就要指定它的位置。在浏览器中输入localhost:8080, 实际是向webpack-dev-server 请求index.html文件,如果webpack-dev-server 并没有生成这个文件,它就会static指定的目录去找。如果static没有配置,它会到项目根目录去找。由于都使用webpack-html-plugin,static也基本用不到。
proxy: 代理,解决前端跨域问题,如axios.post(‘/api/login’), /api 就是标识,然后我们再在proxy 配置项里面给这个标识配置一个代理到的真实路径,如 ‘/api’ : ‘http://102.03.34.58/api’, 那么当我们调用接口的时候,实际上变成了http://102.03.34.58/api/login, 代理配置中的真实路径,就是会替换到请求中的标识
module.exports = { devServer: { proxy: { '/api': 'http://102.03.34.58/api' }, }, }
但有时候,接口没有那么规范,可能有的以api 开始,有的没有api, '/api' 标识还可以是一个对象
proxy: { '/api': { target: 'http://102.03.34.58', pathRewrite: { '^/api': '' } } }
target 是请求的服务器地址,后面没有api, 使用这种方式配置以后,代理会在前端请求中的/api前面加上target, 相当于还是请求了 http://102.03.34.58/api/login,所以这里增加了pathRewrite 路径重写,所有将/api/xxx 变成 /xxx, 去掉/api,所以最后真实的请求路径中 http://102.03.34.58/login. pathRewrite 中的属性是正则表达式,^以什么开始, 值 呢?就是匹配到的路径重写成会什么。
proxy 中的属性'/api', 是前端发送请求时,请求接口中的url要加的参数,当真正发送请求时,webpack 服务中配置的代理碰到api 就会拦截,然后把它变成我们配置的真实的路径
historyApiFallback, 当使用React-router的时候,webpack dev server 刷新页面的时候,显示not get,设置historyApiFallback: true, 刷新页面dev server会重定向到index.html,再执行js,执行前端路由,正确显示页面。
开发中有代码报错,怎么找错呢?使用source maps,webpack打包之后只生成一个js文件。程序运行后有问题,浏览器开发工具,只会把问题定位到打包后的文件中,而不是打包之前的源代码,你也不知道具体哪个文件出错了。使用source maps后就不一样了,问题会通过map文件,直接定位到源代码出错的文件中,因为source maps文件就是源代码和打包后的代码之间的映射文件(每一行每一列的映射关系)。配置soure maps,也很简单,devtools: ‘eval-source-map’。source maps有很多类型,挑选适合自己的。
devtool: "eval-source-map"
假设在index.js中,console.log 写成了conso, 报错如下
点击第二行的eval()就可以定位到源代码。
HMR:hot module replacement, 哪个模块有变化,就替换哪个模块,不用刷新整个页面。webpack默认开启了hot模式,尝试使用hmr进行更新,如果不行,就刷新整个页面,所以更改css不会刷新页面,style-loader实现了热替换,更改js就会刷新页面,需要手动实现js的hmr。在最外层的index.js,写
if(module.hot) { module.hot.accept('./component'); // 相应监听哪个文件变化,就要把哪个文件放到accept里面 // 如果还有其他文件,就一一列出 // module.hot.accept('a.js') // module.hot.accept('b.js') }
accept,还可以接受第二个函数参数,当js文件发生变化,就会调用函数。JS的hmr 还是比较复杂的,不过,各大框架都提供了hmr的loader,比如react-loader,vue-loader
oneOf: 当webpack遇到一个文件时,它会遍历rules,找到合适的loader,但它遍历有点奇怪,找到了loader后,它还有把所有的loader遍历完,相当于switch 语句没有写break; oneOf 就相当于break;找到一个合适的loader,就退出遍历了。module: {rules: [{oneOf: [各种test的loader的配置] }]}
开发环境的配置基本差不多了,那就再配置一下生产环境。新建webpack.prod.config.js,最基本的配置mode: production,和 html-webapck-plugin.
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: 'production',
plugins: [
new HtmlWebpackPlugin(),
]
};
script 的build命令改成
"build": "webpack --config webpack.prod.config.js"
抽离css到单独的文件。因为如果CSS在JS会造成页面的闪动。使用mini-css-extract-plugin 。它能把多个css文件打包成一个,因此,它有一个loader来处理抽取的工作。插件拿到loader生成的内容,输出一个单独的css文件。npm i mini-css-extract-plugin -D
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin(),
new MiniCssExtractPlugin({
filename: 'style.css'
})
]
};
css-loader 把 css文件按照commonJS 规范输出为JS,MiniCssExtractPlugin.loader 再把它抽取出来,最后MiniCssExtractPlugin把它输出为一个单独的CSS文件,命名为style.css。由于CSS中有背景图,还要处理一下
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.(png|jpg)$/,
type: "asset",
parser: { dataUrlCondition: { maxSize: 8000 } },
}
]
},
npm run build, 可以看到css输出到一个单独的文件中了。但CSS 并没有压缩,使用css-minimizer-webpack-plugin 进行压缩,npm i css-minimizer-webpack-plugin -D
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
mode: 'production',
module: {...},
plugins: [...],
optimization: {
minimizer: [
`...`,
new CssMinimizerPlugin(),
],
}
};
module 和 plugins没有变化,这里就用...代替了。配置css压缩时,注意`...` 放到前面,因为在webpack5中,生产模式下,它自动会开启一些优化功能,比如JS的压缩。如要直接写css的压缩插件,就会把原有的优化功能覆盖掉。`...` 表示继承存在的压缩功能(extend existing minimizers)。
处理JS代码。在开发环境下,并没有处理JS文件,因为自己开发用的浏览器一般都是最新的,绝大数JS功能都实现了,但生产环境可能要处理一下。因为用户使用什么浏览器,控制不了,有可能是旧版本的浏览器。处理JS文件用的是babel,npm i babel-loader, @babel/core, @babel/preset-env 等。babel 要开启缓存
{
test: /\.js$/,
exclude: /node_modules)/,
use: "babel-loader",、
options: {
cacheDirectory:true,
CacheCompression: false // 不进行压缩
}
}
创建babel的配置文件 babel.config.json
presets: [
[
"@babel/preset-env",
{
"modules": false
}
]
]
babel的配置有点复杂,我有一篇专门的文章来介绍Babel 7
如果想要线上环境debug,可以配置生产模式下source map
devtool: "source-map"
代码分割
动态导入:使用import()语法,
import(/* webpackChunkName: "optional-name" */ "./module").then(
module => {...}
).catch(
error => {...}
);
在src下面新建lazy.js
export default "Hello from lazy";
然后index.js
export default (text = "Hello world") => {
const element = document.createElement("div");
element.innerHTML = text;
element.onclick = () =>
import("./lazy")
.then((lazy) => {
element.textContent = lazy.default;
})
.catch((err) => console.error(err));
return element;
};
注意使用lazy.default。当lazy.js用commonJS书写时,也要使用default。npm run build, 打包文件中多了958.js
如要想要改变动态导入的模块输出的文件名,可以
import(/* webpackChunkName: "lazy" */ "./lazy").then()
还要配置output.chunkFilename, 这个以后再说。
打包文件分离
多入口,提取公共模块,多入口就是配置文件中的entry可以是一个对象 {index: ‘./src/index’, main: ‘./src/mian’}, 有几个入口文件,就输出几个bundle 文件。output filename: [name].js, 抽取css时,css的name也要改成[name].css, 每一个入口中所包含的css都会打包成一个单获的css文件。新建一个print.js
import './style.css'
export default function print() {
console.log('print');
}
配置文件如下
const path = require('path');
module.exports = {
mode: 'production',
entry: {
main: './src/index.js',
print: './src/print.js'
},
output: {
filename: "[name].js", // 双引号
path: path.join(__dirname, "dist")
},
module: {...},
plugins: [
new HtmlWebpackPlugin(),
new MiniCssExtractPlugin({
filename: "[name].css" // 双引号
})
],
......
};
npm run build, dist 目录有点混乱了,output现在有一个clean配置,打包之前,它会先清理dist目录。
output: {
filename: "[name].js", // 双引号
path: path.join(__dirname, "dist"),
clean: true
},
但这种简单粗暴的多入口打包有个问题, 如果每一个入口中都引入机同的包,比如lodash, 那每一个入口对应的bundle都包含lodash,体积太大。npm i lodash, 并在index.js 和print.js 中引入,npm run build
SplitChunksPlugin, 提取公共模块
optimization: {
minimizer: [
`...`,
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all',
}
},
抽取出来的模块名是webapck默认的。 也可以手动控制打包分离
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
chunks: "all",
},
},
},
cache缓存
文件名使用[contenthash], [name]-[contenthash].js. contenthash就是文件内容变,hash值就会变。output下的filename改为
chunkFilename: "[name].[contenthash].js",
filename: "[name].[contenthash].js",
assetModuleFilename: "[name].[contenthash][ext][query]"
chunkFilename,就是没有entry中定义,而生成的chunk的文件名,比如异步加载的文件,splitChunks生成的文件。filename 就是entry中定义的入口。assetModuleFilename: 就是图片等用assetmodule处理的文件。同时,css文件名
new MiniCssExtractPlugin({
filename: "[name].[contenthash].css",
}),
optimization: {
runtimeChunk: 'single',
}
此时如果添加一个模块,比如a.js,然后引入index.js,再打包,你会发现,所有的contenthash都发生变化,为什么呢?理论上,vendor不应该发生变化啊?这是因为module.ids增加了,
optimization.moduleIds 设为 'deterministic'
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
}
整个optimization
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
minimizer: [
`...`,
new CssMinimizerPlugin(),
],
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
chunks: "all",
},
},
},
},
在一个典型的webpack打包的应用中,有三种代码,你写的代码,第三方库,webpack运行时runtime和manifest 。代码在浏览器中运行时,wepack 需要runtime 和manifest来链接模块,它们包含加载和解析模块的逻辑。当模块进行解析时,它来连接这些模块。已经下载到浏器中的模块,需要连接在一起,它才能执行。懒加载的模块需要解析。
打包之后,整个应用变成了一个压缩文件,也有可能分为几个文件,webpack是怎么管理程序运行需要的模块之间的交互的呢?这就是menifest data的作用。当webpack进入,解析,打包你的应用时,它会记录所有module 详细的信息,这些信息的集合就是manifest。当打包后的文件时进入浏览器运行时,webpack runtime 就是依据这些信息来解析和加载模块。不论你使用什么模块语法,import 或require 都会变成__webpack_require__, __webpack_require__ 指向是模块的标识符。使用manifiest data,runtime能够知道向哪里去找标识符后面的module。
resolve解析
当webpack去查找一个模块时,如果是相对路径引入,它会转化成绝对路径,寻找模块的位置。如果是引入的第三方模块(模块路径),它会先到配置文件中的resolve.modules定义的目录去查找。默认情况下,resolve.module指的就是node_modules,就像下面的配置一样
resolve: {
modules: ['node_modules']
}
如果想让webpack去其它目录找第三言模块,则要把其它目录的路径配置到resolve.modules中,并且放在node_modules前面。.
resolve: {
modules: [path.resolve(__dirname, 'src/downloaded_libs'), 'node_modules']
},
使用resolve.alias可以创建alias路径来代替原始的模块路径
resolve: {
alias: {
react: path.join(__dirname, 'node_modules/react'),
'react-dom': path.join(__dirname, 'node_modules/react-dom')
}
}
指定react和react-dom的别名路径,当webpack遇到react和react-dom时,它只会到当前项目的node_modules里面去找,可以解决npm link时, react 和react-dom 从不同的项目中获取的问题。alias也能创建文件别名路径
resolve: { alias: { CssFolder: path.resolve(__dirname, 'src/stylesheets/') } }
import application from "../../../stylesheets/application.scss" 变成了 import application from "CssFolder/application.scss"
通过alias和modules的配置,如果成功解析到路径,webpack就要看这个路径指向的是文件还是目录。如果是文件,并且有扩展名,文件就能找到了,如果没有扩展名,那就要找resolve.extensions中配置的扩展名。
resolve: {
extensions: ['.js', '.jsx']
}
如果路径指向的是一个目录,webpack先找package.json, 如果有,它会找resolve.mainFields定义的字段,
resolve: {
mainFields: ['browser', 'module', 'main'],
},
然后拿着这些字段到package.json去匹配,看package.json中有没有定义这些字段,第一个匹配成功的就是要找的文件的路径。如果没有pacakge.json或resolve.mainFields返回的是无效的路径,就要查找
webpack有一个入口文件,入口文件是一个module,它有可能通过import指向另外的module,另外的module又有可能指向另外的module,层层import,因此webpack在打包的的过程中,它也会根据这种import关系,遍历整个import, 构建出整个项目的依赖树,整个项目的依赖树构建完成,也就意味着,项目中所需要的代码都包含进来了,包含的过程中也对代码进行处理,生成打包文件。代码处理的依据就是配置文件。
当import 一个文件,webpack会到文件目录中找这个模块,使用的是resolve 配置,resovle 就是调整模块解析算法的。 如果解析失败,会报错,如果解析成功,就会找对应的loader进行处理,loader处理完成后,就会把处理完后的代码放到打包文件中。在整个打包构建的过程中,webpack 会暴露一些event出来,webpack插件可以拦截这些event,做一些事情,比如抽离css到一个单独的文件。
每一个模块都解析完之后,webpack就会输出打包文件。打包文件就是在浏览器中执行的webpack的运行时,还包括manifest,它列出了将要加载的打包文件。
当然在打包过程,你还可以设置分离点,生成单独的包。
在内部,webpack通过chunks 管理打包过程,chunks是一小段代码,这些代码包含在打包文件中,能够在webpack的输出中看到,输出的文件称为chunks。
注意点
webpack每打包一次就会生成一个[hash], 项目中只要有一个文件发生变化,webpack就需要重新打包,生成一个新的[hash], [hash]值就会发生变化,所有打包文件名字中[hash]值就会发生变化。
[chunkhash],就是每一个chunk有一个hash. 比如有两个入口文件,一个是index.js,它import index.css, 一个admin.js, 它import admin.css. 如果进行打包, index.js和index.css是一个chunkhash, admin.js 和admin.css是一个chunkhash, 但这两个chunkhash是不同的hash。如果此时改掉了index.css,那么重新打包后的index.js 和index.css 这两个文件都会有新的chunkhash, 文件名发生变化。
contenthash, 为每一个文件单独计算hash值,哪个文件内容发生变化,打包时,哪个文件的对应的hash值就会发生变化。
webpack打包速度提升
1,安装分析插件:基于时间的分析工具speed-measure-webpack-plugin, 基于产物内容的分析工具webpack-bundler-analyzer
2, 提升编译阶段的效率:a, 减小编译的模块,使用IgnorePlugin, 最典型的是moment包,它配置了多语言,实际上只用一个语言。
module.exports = { // ... plugins: [ // ... new webpack.IgnorePlugin(/^\.\/(?!zh-tw|en-gb)/, /moment[\/\\]locale$/) ] }
安装引用类库的模块,比如引入loadash/es
使用DLLPlugin:将项目所依赖的框架模块单独构建打包,与普通的模块打包区分开
b, 提升单个模块的构建速度。include 和exclude,babel-loader。需要注意,优先使用exclude, 在include 和exclude 基础上noParse. ts编译,去掉检查
c,并行构建:中小型项目,并行构建可以并不适应,因为多线程之间的通信,大型项目可以使用thread-loader, parallel-webapck,只对特别耗时的开启多线程
{ test: /\.js$/, use: [ {loader: 'thread-loader', options: {works: 8}} ] }
eslint 也能开启多线程。
3,提高打包过程中的效率: splunk chunk 和tree shaking: 以default模式引用的模块并不能tree shaking。引入单个对象的方式,无论是import * as ... 还是 import {...}, 都可以tree shaking. side effect ,babel 7 之后,module 选项是auto, 就是不会再把ESM转化成commonJS 了
4,开启缓存,babel-loader缓存,使用cache-loader
5 webpack5 持久化缓存:
webpack 5 会跟踪每一个模块的依赖项,fileDependencies, ContextDependienices, missingDependencies, 当模块本身或其依赖项发生变化时,webpack 会找到所受影响的模块,重新进行构建。还可以配置失效。webpack5 会忽略插件的缓存设置,由引擎自身提供构建各环节的缓存的读写逻辑
loader,帮助webpack将不同类型的文件转换为webpack可识别的模块。loader 本身是一个函数,第一个参数就是要转换的文件的内容,比如图片,css文件的内容,输出的是webpack能识别的内容,第二个参数是source map,第三个参数是meta,就是其他loader传递过来的数据,比如css-loader 处理好了,给style-loader。
同步loader:在loader函数中,对content 转换完成后,调用this.callback(null, content, map, meta) 表示处理完成,或者return 也表示处理完成。异步loader,调用this.async() 赋值给一个callback变量,异步处理完成后,调用callback,把内容输出,表示异步处理完成。 raw loader, 它接受到的conent参数是二进制, 还要给loader赋值.raw =true , 用于处理图片,字体等资源。pitch loader: 要给loader,赋值个 pitch方法,它是一个函数,在loader 之前执行,如果它有return,那loader 就不执行了。简单实现一个babel-loader
const babel = require('@babel/core') module.exports = function (content) { const callback = this.async(); // 异步loader const options = this.getOptions({ type: 'object', properties: { // loader option 接受哪些属性 presets: { type: "array" } }, additionalProperties: true // 用户可以添加自己的属性 }) // 使用babel进行编译转换 babel.transform(content, options, (err, result) => { if(err) callback(err); else callback(null, result.code ) }) }