# webpack **Repository Path**: zhangBo11/webpack ## Basic Information - **Project Name**: webpack - **Description**: webpack学习 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2021-07-20 - **Last Updated**: 2025-03-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: webpack源码学习 ## README ##### 模式(mode) 在 packages.json 中配置 webpack 启动时命令时加入参数 - --mode 模块内可通过 process.env.NODE_ENV 获取当前环境变量,在 webpack 配置文件中无法获取 ``` // 跟直接在配置文件中设置mode参数效果一样 "scripts": { "build": "webpack --mode=production", }, ``` - --env 模块内不能通过 process.env.NODE_ENV 获取,webpack 配置中可通过函数参数获取环境变量 - 内置插件 DefinePlugin 模块内可通过配置对象的 k 访问对应的 val,在 webpack 配置文件中无法获取 ``` plugins:[ new webpack.DefinePlugin({ 'process.env.NODE_ENV':JSON.stringify('development'), 'NODE_ENV':JSON.stringify('production'), }) ] ``` - cross-env 插件 跨系统设置环境变量,为了使脚本在 windows,mac 和 linux 都能执行 - 在模块内和配置文件中都可通过 process.env.NODE_ENV 访问到配置的环境变量 - 配置命令:"build":"cross-env NODE_ENV=production webpack" 没有&,下面的 windows 和 mac 有& - windows 通过 set 可配置环境变量参数, 在模块内和配置文件中都可通过 process.env.NODE_ENV 访问到配置的环境变量 ``` "scripts": { "build": "set NODE_ENV=production&webpack", }, ``` - mac 通过 export 配置环境变量参数 ``` "scripts": { "build": "export NODE_ENV=production&webpack", }, ``` ##### babel 编译 - babel-loader 使用 Babel 和 webpack 转译 JavaScript 文件 - @babel/core Babel 编译的核心包,babel的内部经历了【解析-转换-生成】三个步骤,@babel/core负责解析 - @babel-preset-env 和@babel-preset-react es6 和 react 的 Babel 预设 - @babel/plugin-proposal-decorators 把类和对象装饰器译 es5 - @babel/plugin-proposal-class-properties 转换静态类性及使用属性初始值化语法声明的属性 ##### eslint - eslint 简单体验 ``` npm i eslint babel-eslint eslint-loader -D // webpack.config.js module:{ // 添加新的rule rules:[ { test: /\.js$/, loader: 'eslint-loader', // 进行代码风格检查 enforce: 'pre', // 给loader进行分类pre=>normal=>inline=>post options: { fix: true }, // 如果发现不合要求,会动修复 exclude: /node_modules/, // 排除node_modules文件 } ] } // 新建.eslintrc.js // 简单配置 module.exports = { root: true, //配置文件是可有继承关系的 parser: 'babel-eslint', // 把源代码转成ast语法树工具, parserOptions: { sourceType: 'module', ecmaVersion: 2015, }, // 指定脚本的运行环境 env: { browser: true, }, rules: { indent: 'off', quotes: 'off', 'no-console': 'error', }, }; ``` - airbnb 已经配置了,可根据项目扩展修改规则和配置 - 参考文档[eslint-config-airbnb](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb) ``` .eslintrc.js module.exports = { // root: true, //配置文件是可有继承关系的 extends: 'airbnb', // 是继承或者说扩展自airbnb的配置 parser: 'babel-eslint', // 把源代码转成ast语法树的工具, parserOptions: { sourceType: 'module', ecmaVersion: 2015, }, // 指定脚本的运行环境 env: { browser: true, }, rules: { // indent: 'off', // quotes: 'off', // 'no-console': 'error', 'no-console': 'off', 'linebreak-style': 'off', // 关闭换行所使用的字符验证/r/n=>windows,/r=>mac,/n=>linux indent: ['error', 2], }, }; ``` - 自动修复 - 安装 vscode 的 eslint 插件 - 配置自动修复参数 ``` .vscode/settings.json { "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact", ], "editor.codeActionsOnSave": { "source.fixAll.eslint": true } } ``` ##### devtool 配置项, 认识下面关键字的含义 - eval 使用eval包括代码块 - source-map 生成map文件 - cheap 不包括列信息,也不包含loader的sourcemap - module 包含loader的sourcemap,否则不能识别源文件 - inline - 开发环境折中方案 devtool:'eval-source-map' - 线上 devtool:hidden-source-map - 测试环境 通过 filemanager-webpack-plugin 和 webpack 自带的 SourceMapDevToolPlugin 插件 - 具体思路,在打包时候通过 webpck.SourceMapDevToolPlugin 代替配置项 devtool 生成 map 文件,并通过 append 在打包生成的 main.js 文件中(原始资源)追加一个,之后 map 文件可访问的 url 地址 - 通过 filemanager-webpack-plugin 将 map 文件移动到 append 追加的地址下。在自己的测试中可进行源码映射 ##### 打包第三方模块 - 以 loadsh 和 jquery 为例,详解一下三种方式的区别 - ProvidePlugin 配置的变量被定义到上下文中,不用在模块内引入可直接使用,但是未定义到全局变量上 - expose-loader 可将配置的变量添加到全局变量上 - externals 前两种配置,在打包时都会对该模块进行打包,如果我们不想在打包时对该模块进行打包,可用本方式 key 是模块名,value 是模块导出到全局的变量名,但需要通过srcipt标签引入这个模块 ##### watch 配置 - 作用:监听 npm run build 后文件变化,如果符合配置条件重新打包 ##### css 提取 通过 link 标签插入,图片打包到 images 目录下的相关配置 - 将 css 从 js 文件中提取出来,让 html 加载时候,link 和 srcipt 是并行加载,提高效率。 - 图片被打包后出现两个,导致引入不显示的问题,webpack5 的资源模块废弃了 file-loader,url-loader,raw-loader, 使用时跟新的 asset 产生冲突导致 ``` // 解决办法1:使用新的asset模块 // 解决方按2:添加一个配置项type:'javascript/auto' { test: /\.(png|jgp|gif|bmp|svg)$/, use: [ { loader: 'url-loader', options: { limit: 6 * 1024, esModule: false, // emitFile: false, name: '[hash:10].[ext]', outputPath: 'images',// 将图片打包到images目录下 publicPath: '/images'// 将源文件打包后,图片引入会加上该路径 }, }, ], ++++ type: 'javascript/auto' // 解决webpack5新的资源系统和url-loader,file-loader打包出两个图片问题 }, ``` ##### hash 和 chunkhash 和 contenthash 的认识和区别 - hash 每次 webpack 构建时生成的一个唯一 hash 值 - chunkhash 根据 chunk(代码块,一个入口文件及文件中的依赖构成一个代码块(chunk))生成 hash 值,来源于同一个 chunk,则 hash 值一样 - contenthash 根据内容生成的 hash 值,文件内容相同 hash 值就行同 - 性能:上面三个从上往下更精细,但是更耗性能,所以要根据实际情况灵活使用。 - 用法:为了保证每次打包时,内容未改动的地方不会重新生成新的文件(js 和 css),能使用缓存,提高性能。 - js 文件经常改变,css 文件不长改变,通过 link 引入 css 代码时,css 文件生成通过 contenthash - 多出口时,jquery 和 loadsh 不经常变动的,可单独配置出口文件,通过 chunkhash 定义出口文件名 - 多出口时,业务代码 js 部分,经常变动可用 chunkhash 定义出口文件名 ##### 同步加载 - 4 种方式 - commonjs-commonjs ``` var modules = { './src/title.js':(module,exports,require)=>{ module.exports = 'title' } } var cache = {} function require(moduleId){ var cacheModule = cache[moduleId]; if(cacheModule !=underfined){ return cacheModule } var module = cache[moduleId] = { exports:{} } modules[moduleId](module,module.exports,require) return module.exports; } var exports = {} let title = require('./src/title.js') console.log(title) ``` - commonjs-esmodule ``` //1.module导出的内容 var modules = { './src/index.js':(module,exports,require)=>{ "use strict" //执行r函数,r函数给exports设置toStringTag为Module,添加属性_esModule:true require.r(exports); //3.执行d函数 的函数给exports设置属性,d函数调用o函数判断属性只存在definition不存在exports中 require.d(exports,{ "default":()=>__WEBPACK_DEFAULT_EXPORT__ }) const __WEBPACK_DEFAULT_EXPORT__ = 'name-title'; } } ``` - esmodule-commomjs - esmodule-esmodule ``` // commonjs导出 commonjs-commonjs esModule-commonjs //导出内容 module.exports= {} 模块处理方式为 exports = 导出内容 //esModule导出方式 commonjs-esModule esModule-esModule // 导出内容 export const name = 'name' export const content = 'content' 模块处理方式 将导出的模块 { name:()=>'name', content:()=>'content' } 遍历添加到module.exports中 ``` - 异步加载 import() ``` //1.require.e ``` ##### ast(抽象语法树) - 原理:代码转为 ast,遍历处理 ast 生成新的 ast,将新的 ast 转换成代码 - ast 定义了代码的结构,通过操纵这棵树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作 - babel 将 es5+转为 es5,通过操纵 ast 语法树 - 通过列表 babel 原理,实现箭头函数转换普通函数的插件 - 实现 es6 class 转换为 es5 构造函数的插件 - webpack tree shaking - tree shaking 就是通常用于描述移除 JavaScript 上下文中的未引用代码.它依赖于 es2015 模块语法的 静态结构特性,例如 import 和 export - 原理:也是通过将源码解析为 ast 语法树,遍历处理 ast,在生成优化后的代码,但是需要对应的包支持 tree shaking,eg:antd,lodash ##### webpack 源码调试 - 打包和编译的区别 ``` 编译--是打包的一个环节 打包--指个是整个任务:找入口文件,进行编译,再找入口依赖的模块,再进入 递归编译。 ``` - webpack 编译主要步骤 - 1 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象 - 2 用上一步得到的参数初始化 Compiler 对象 - 3 加载所有配置的插件 - 4 执行对象的 run 方法开始执行编译 - 5 根据配置中的 entry 找出入口文件 - 6 从入口文件出发,调用所有配置的 Loader 对模块进行编译 - 7 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖- 的文件都经过了本步骤的处理 - 8 根据入口和模块之间的依赖关系,组装成一个个包含多个模- 块的 Chunk - 9 再把每个 Chunk 转换成一个单独的文件加入到输出列表 - 10 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统 ##### loader - 用处:打包、解析入口文件及其依赖的文件,主要是对文件内容进行处理,处理为webpack能够有效处理的模块。es6 转换 es5,scss 转为 css 然后导出 css 内容,style-loader,将 css 内容,通过 js 创建 style 标签或 link 标签插入页面 - 基本概念 - loader 函数的 pitch 属性 - loader 通过 enforce 定义的类型,执行顺序:post - inline - normal- pre,loader 本身无差别,通过 enforce 控制的 - loader 特殊配置 ! ,-!,!!可配置启用的 loader 类型 - loader 手写 - babel-loader 获取 js 源码,通过@babel/core 解析为 ast,在编译成 es5 代码,返回 - file-loader 将图片内容,通过 loader-runner 的方法,生成到要打包到的目录中,返回打包后的图片名称 - url-loader 如果 limit 大于设置的,采用 file-loader,如果小于,将 content 转为 base64 格式,拼凑后返回 base64 内容 - less-loader 将源码 less,通过 less 模块编译成 css,返回 css - style-loader 创建 style 标签,将 css 代码插入,然后将 style 标签插入 head 标签中,返回空。 - loader-runner 分析 - loader-runner 执行位置,在 webpack 进行编译时,在分析入口文件调用所有配置的 loader 对模块进行编译,此时调用 loader-runner - loader-runner 执行分析 - 1.runner 过程 匹配符合要编译文件的 loader,通过 loader 配置的 enforce 将 loder 分类,通过特殊配置符号筛选符合的 loader,按照 enforce 给 loader 进行排序,生成 loaders 数组 - 2.执行 runLoaders 函数,传入基本参数和回调函数,该函数主要定义各种重要的属性,定义完成后执行下一步 - 3.执行 iteratePitchLoaders 函数,该函数递归执行 loaders 中的 loader 的 pitch 方法,pitch 函数可同步可异步,并对边界情况进行相应处理,执行到最后一个 loder 后,进行下一步 - 4.执行 processResource 函数,读取编译文件,执行下一步 - 5.执行 inerateNormalLoaders 函数,该函数递归执行每个 loader 函数,normal 函数可同步可以不,并对边界情况进行处理,执行到 loaders 中的第一个后,执行回调函数,返回结果 ##### tapable 事件监听触发器 - 事件注册发布 - 注册事件 tap,tapAsync,tapPromise - 发布事件 call,callAsync,promise - 拦截器 interceptor,拦截器中的 call,tap,register 钩子 - call 注册函数执行前执行,多个事件函数,call 只执行一次 - tap 每个事件函数执行前执行,多个事件函数执行多次 - register 每次注册事件时执行,下一个拦截器的 register 函数得到的参数 取决于上一个 register 返回的值 - HookMap - 基本使用 ``` //map是用来批量创建钩子的 key是字符串 值是一个钩子hook map.for('key1').tap('plugin1',(name)=>console.log(1,name)); map.for('key1').tap('plugin2',(name)=>console.log(2,name)); // map.get('key1').call('zhufeng'); map.for('key2').tap('plugin3',(age)=>console.log(3,age)); map.for('key2').tap('plugin4',(age)=>console.log(4,age)); map.get('key2').call(12); // console.log //3 12 //4 12 ``` - stage 为 options 的配置项,源码中通过一个算法,将每次 tap 时的 tapInfo 根据 stage 排序,执行顺序跟注册顺序无关,跟 stage 值大小有关,越小先执行。 - before 为 options 中的配置项,值为一个字符串或者数组,为其他 tap 事件中 opions 中的 name,通过算法将其排到 taps 的对应位置,如果 name 不存在,看代码分析会插入到 taps[0]中,第一个执行。 ##### plugin 插件 - 用处:在 webpack 编译阶段,根据生命周期钩子函数及提供参数,对编译内容进行各种功能的扩展,比如在编译阶段,获取编译内容,将内容打为压缩包。 - 为什么需要插件 - webpack 基础配置无法满足需求 - 插件几乎能够任意更改 webpack 编译结果 - webpack 内部也是通过大量插件实现了 ##### optimize(优化项) - resolve.extensions,指定extensions之后可以不用在require或import的时候添加文件扩展名,会一次根据添加的扩展名进行匹配。 - resolve.alias,配置别名可以增加webpack查找模块的速度。 - resolve.modules,如果可以确定项目内所有的第三方依赖模块都是在项目根目录下的 node_modules 中的话,modules: [path.resolve(__dirname, 'node_modules')]. - resolve.mainFields,当从npm包中导入模块时,此选项将决定在package.json中使用那个字段导入模块. - resolve.mainFiles,解析目录时要使用的文件名 - resolveLoader这组选项与上面的resolve对象的属性集合相同,但仅用于解析webpack的loader包。 - 。。。还有一些可以看webpack.config.js中的注释。 - tree shaking,webpack默认支持,原理是利用esmodule的模块特点,现只支持esmodule,在.babelrc里设置module:false(作用:babel不转换import,export等esmodule方式,webpack识别模块后,进行tree-shaking优化,之后自己转换) - 可以在package.json中设置sideEffects,也可以在module.rules中设置 - sideEffects:false,所有代码都设置为没有副作用(都可以进行tree-shaking),即webpack可以安全删除未用到的export - sideEffects,可以接收数组参数,sideEffects:["*.css"],表明文件夹下的内容有副作用,webpack不会进行tree-shaking. ``` // index.js import { sum1 } from './title' console.log(sum1(1, 2)); //title.js export function sum1(a, b) { return a + b } export function reduce1(a, b) { return a - b } //编译后的代码 (()=>{"use strict";console.log(3)})(); // 浏览器可识别 ``` - tree-shaking 作用场景 ``` //1.没有导入或导入没有使用 //2.代码不会被执行,不可到达 //3.代码执行结果不会被用到 //4.代码中只写不读的变量 ``` - 代码分离,对于大的web应用,将所有的代码放到一个文件中显然不够有效,特别是当某些代码在特殊的时候才会用到。 - 1.多入口打包文件,缺点:各chunk之间包含重复的模块,重复模块都会被引入到各个bundle中,不够灵活不能将核心应用程序逻辑进行动态拆分代码。 - 2.动态导入,动态导入推荐选择es提案的import()语法实现,打包可分离出一个单独的bundle - 懒加载(按需加载)在代码分离---动态导入中,产生一个单独bundle的基础上,通过交互动态加载这个包,这就是懒加载(按需加载)eg:添加点击事件,点击时加载。 - preload和prefetch ``` // 概念: 1.preload chunk 会在父 chunk 加载时,以并行方式加载,prefetch chunk 会在父 chunk 加载结束后开始加载。 2.preload chunk 具有中等优先级,并立即下载,prefetch chunk在浏览器闲置时下载 3.preload chunk 会在父chunk中立即请求,用于当下时刻。prefetch chunk会用于未来的某个时刻。 4.浏览器支持程度不同 5.preload 加载优先级high(慎用),prefetch 加载优先级lowest //用法:通过给import()添加魔法注释 eg: // 不需要插件搭配 import(/*webpackPrefetch:true*/'./title.js').then(xxx) // 注意preload需要搭配插件使用:@vue/preload-webpack-plugin,作用给html插入link标签as:script import(/*webpackPreLoad:true*/'./title.js').then(xxx) ``` - splitChunks ``` // 配置参数的作用 optimization: { splitChunks: { chunks: 'all',//代码分割应用于哪些情况 initial/async/all=async+initial minSize: 0,//分割出去的代码块最小的尺寸多大,默认是30K 0就是不限制 ,只要符合分割的要求就会分割 minChunks: 2,//如果一个模块被多少个入口引入了才会分割. maxAsyncRequests:3,//限制异步模块内部并行加载的请最大请求数,说白了就是每个import()里面最大的请求数 maxInitialRequests:5,//限制同步模块内部并行加载的请最大请求数, //name: false,//打包后的名称,默认规则是 分割名称~ automaticNameDelimiter: '~', //分割符 cacheGroups: { //设置缓存组用来抽取满足不同规则 的chunk vendors: {//lodash既然在node_modules里,肯定是属于vendor缓存组 chunks: 'all', test: /node_modules/,//如果这个模块request路径里面包含node_modules priority: -10 // 优先级,当模块属于多个模块时,优先选择数值大的 }, default: { chunks: 'all', minSize: 0, minChunks: 2, //如果一个模块被至少2个入口引用了,那么就属于这个组 priority: -20 } //有些模块,比如说jquery按上面的配置,它会同时属于vendors和default两个缓存组 } } }, ``` - oneOf:每个文件对于rules中的所有规则都会遍历一遍,如果使用oneOf就可以解决该问题,只要能匹配一个即可退出。(注意:在oneOf中不能两个配置处理同一种类型文件) ``` module.exports = { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, //优先执行 enforce: 'pre', loader: 'eslint-loader', options: { fix: true } }, { // 以下 loader 只会匹配一个 oneOf: [ ..., {}, {} ] } ] } } ``` - 利用缓存 - webpack5缓存配置项 ``` cache: { // 默认开启,设置false关闭 type: 'memory', //'memory(内存,默认)' | 'filesystem(硬盘)' // 缓存类型 cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), // 存储缓存信息 }, ``` - babel-loader:Babel在转义js文件过程中消耗性能较高,将babel-loader执行的结果缓存起来,当重新打包构建时会尝试读取缓存,从而提高打包构建速度、降低消耗 ``` { test: /\.js$/, exclude: /node_modules/, use: [{ loader: "babel-loader", options: { cacheDirectory: true } }] }, ``` - cache-loader - 在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里 - 存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader ``` npm i cache-loader -D ``` ``` const loaders = ['babel-loader']; module.exports = { module: { rules: [ { test: /\.js$/, use: [ 'cache-loader', ...loaders // 解构其他loader ], include: path.resolve('src') } ] } } ``` ##### webpack5新特性 - 持久化缓存 - 资源模块 - moduleIds和chunkIds - 移除node.js的polyfill - 更强大的tree-shaking - sideEffects(副作用),配合tree-shaking使用 - 模块联邦 ##### polyfill - 三个概念 - 最新的ES语法:比如,箭头函数 - 最新的ES API:比如,Promise - 最新的ES实例方法:比如,String.prototype.includes - Babel默认只转换新的javascript语法,而不转换新的API,比如 Iterator, Generator, Set, Maps, Proxy, Reflect,Symbol,Promise 等全局对象。以及一些在全局对象上的方法(比如 Object.assign)都不会转码。 - 比如说,ES6在Array对象上新增了Array.form方法,Babel就不会转码这个方法,如果想让这个方法运行,必须使用 babel-polyfill来转换等 - babel-polyfill 它是通过向全局对象和内置对象的prototype上添加方法来实现的。比如运行环境中不支持Array.prototype.find方法,引入polyfill, 我们就可以使用es6方法来编写了,但是缺点就是会造成全局空间污染 - @babel/preset-env默认支持语法转化,需要开启useBuiltIns配置才能转化API和实例方法 - useBuiltIns可选值包括:"usage" | "entry" | false, 默认为 false,表示不对 polyfills 处理,这个配置是引入 polyfills 的关键 - babel-runtime - Babel为了解决全局空间污染的问题,提供了单独的包babel-runtime用以提供编译模块的工具函数 - 简单说 babel-runtime 更像是一种按需加载的实现,比如你哪里需要使用 Promise,只要在这个文件头部import Promise from 'babel-runtime/core-js/promise'就行了 - babel-plugin-transform-runtime - @babel/plugin-transform-runtime插件是为了解决,多个文件重复引用相同helpers(帮助函数)-> 提取运行时 - 新API方法全局污染 -> 局部引入 - 最佳实践 - babel-runtime适合在组件和类库项目中使用,而babel-polyfill适合在业务项目中使用。 ##### vite 基本用法 - http://www.zhufengpeixun.com/strong/html/103.16.vite.basic.html