1.webpack之devtool详解
2.å¦ä½å¨WebStorm 2017ä¸è°è¯Vue.js + webpack
3.webpack5loader和plugin原理解析
4.一个关于webpack 自动use strict引起的有趣的业务bug以及背后原理
5.最完备的懒加载错误兜底方案,再也不会白屏了!
6.webpack打包原理 ? 看完这篇你就懂了 !
webpack之devtool详解
Webpack的Devtool详解 Webpack的Devtool是开发者在开发和调试过程中重要的工具之一,它可以帮助开发者更好地理解和调试代码。Devtool提供了多种不同的配置选项,用于生成源代码映射,源码天龙以便于开发者在浏览器控制台中直接查看源代码,从而更有效地定位和解决代码中的问题。 一、什么是Devtool? Webpack的Devtool是一个配置选项,允许开发者在构建过程中生成源代码映射。源代码映射是一种文件,它将编译后的代码映射回原始源代码,以便于开发者调试。Devtool的不同配置选项会生成不同类型的源代码映射,适用于不同的开发场景。 二、Devtool的主要配置选项 1. eval模式:这是最简单的源代码映射形式,它直接在控制台输出源代码。这种模式下,不需要生成额外的文件,但生成的源代码映射相对较小且不持久。 2. source-map模式:在这种模式下,Webpack会生成一个独立的.map文件来存储源代码映射信息。这种模式的源代码映射比较详细,但会增加构建时间。 3. inline-source-map模式:与source-map模式类似,但源代码映射信息会直接嵌入到构建后的文件中。这种模式便于传输和分发构建后的文件,但同样会增加文件大小。 4. cheap-module-eval-source-map模式:适用于模块化的项目,能够提供模块级别的调试信息。它在开发和调试阶段很有用,但生成的源代码映射相对较小。 三、如何选择适合的Devtool配置? 选择Devtool的配置取决于项目的需求和开发阶段。在开发阶段,为了快速迭代和调试代码,可以选择生成更详细的源代码映射;而在生产环境,为了优化性能和文件大小,可以选择生成较小的源代码映射或者不使用源代码映射。常见的做法是在开发阶段使用如eval或cheap-module-eval-source-map等详细的配置选项,而在生产环境使用source-map或inline-source-map等更精简的配置选项。 总之,Webpack的Devtool是开发者在开发和调试过程中的重要工具,通过合理配置Devtool,可以大大提高开发效率和代码质量。å¦ä½å¨WebStorm ä¸è°è¯Vue.js + webpack
æ人è§å¾vue项ç®é¾è°è¯ï¼æ¯å 为ç¨äºwebpackãææ代ç æå¨äºä¸èµ·ï¼è¿å äºå¾å¤æ¡æ¶ä»£ç ï¼æ ¹æ¬ä¸ç¥éæä¹ä¸æãæ以vue+webpackè°è¯è¦ä»webpackå ¥æã
1.æ们å ä»ä¸è¬æ åµå¼å§è¯´ã
-sourcemap
webpacké ç½®æä¾äºdevtoolè¿ä¸ªé项ï¼å¦æ设置为 â#source-mapâï¼åå¯ä»¥çæ.mapæ件ï¼å¨chromeæµè§å¨ä¸è°è¯çæ¶åå¯ä»¥æ¾ç¤ºæºä»£ç ã
devtool: '#source-map'
2.ç¶èè¿ä¸ªè®¾ç½®å®é ä¸æ²¡è¿ä¹ç®åãwebpackå®æ¹ç»åºäº7个é 置项ä¾éæ©:
.devtoolä»ç»
è¿éä¸åçé ç½®æäºä¸åçææï¼æ¯å¦æ¯å¦ä¿ç注éãä¿çè¡ä¿¡æ¯çï¼å ·ä½æ¯ä¸æ¡ä»ä¹ææè¿éä¸è¯¦è§£éï¼æå ´è¶£çç«¥éå¯ä»¥åèè¿ç¯æç«
å®æ¹é»è®¤çæ¯ç¨ â#cheap-module-eval-source-mapâ
devtool: '#cheap-module-eval-source-map'
设置好ä¹åï¼å¨vue项ç®è°è¯çæ¶åï¼ä»£ç éé¢æ 注debuggerçæ¶åå°±è½çå°å¯¹åºç代ç äºï¼é常æ¹ä¾¿ã
.debugger
æè ï¼ç´æ¥æ¾å°å¯¹åºçæ件ãå¨chromeç¨ âctrl(command) + pâï¼è¾å ¥æ件åï¼å¯ä»¥æ¾å°å¯¹åºçæºä»£ç ã
command+p
ææç¹ï¼
æç¹
éè¦æ³¨æçæ¯ï¼è¿éæç¹ä¼æå¨ä¸ä¸è¡ãåæ¶ä¸è¡ä»£ç è¿è¡å¨å®çä¸ä¸è¡æç®æ§è¡ã
.-vue-cli
vue家ç项ç®èææ¶ï¼æ¨è使ç¨ãvue-cliè家å¨è¿é
vue-cliå¯ä»¥å¸®æ们èªå¨æ建项ç®ï¼é¦å npmå ¨å±å®è£
npm install -g vue-cli
ç¶åå建ä¸ä¸ªæ°ç项ç®
vue init webpack my-project
ä¸è·¯å车ï¼æå®ã(æ´å¤é 置项请åèä¸é¢ç»åºçvue-clié¾æ¥)
è¿éä»ç½ä¸ä¸è½½äºä¸ä¸ªå¸¦webpackçvue项ç®(è·ä¹åè®°å¾npm installä¸ä¸)
.vue-cli webpack
ä»bulidæ件夹éé¢å°±å¤§æ¦è½çåºï¼
•webpack.dev.conf: å¼å模å¼ç¨
•webpack.prod.conf: ç产模å¼ç¨
å ¶ä¸ï¼å¼å模å¼æä¾äºdevtool为â#cheap-module-eval-source-mapâï¼ç产模å¼æ ¹æ®configæ件夹ä¸çproductionSourceMapåéæ§å¶æ¯å¦ä½¿ç¨ã
è¥ä¸ºtrueï¼ådevtool为â#source-mapâ
å ¶ä»ä½¿ç¨æ¹æ³ä¸è´ãé常æ¹ä¾¿ã
3.线ä¸è°è¯
å¹³æ¶å¼åçæ¶åï¼æ们ç¨webpackççå è½½ï¼å¯ä»¥çå»æè½½è°è¯çæ¥éª¤ï¼é常æ¹ä¾¿ãä½æ¯åå¸åé¨ç½²å°æå¡å¨ä¸ï¼å°±å¤±å»äºè¿ä¸ªæ¬å°ä¼å¿ã
å¦æ使ç¨æè½½æ件æ¹å¼ä¼æ¯è¾éº»ç¦ãç±äºwebpackæåºæ¥çæ件æçæ¬å·è¿äºä¿¡æ¯ï¼èä¸åå¸ä¸ä¸ªå ç代ç éå¯è½éè¦çå¾ ä¸çï¼è¿ä¸ªæ¹æ¡ä¸å®é ãä½æ¯å¦ææè½½çæ¯çå è½½å°ç«¯å£ä¸çæ件çè¯ï¼è¿ä¸ªé®é¢å°±å¾å¥½åäºã
-çå è½½
å¨æ¤ä¹åï¼å æ¥åæä¸ä¸webpackççå è½½åçã
对项ç®æå å¯ä»¥åç°è¿ä¹ä¸ä¸ªæ件ï¼__webpack_hmr
__webpack_hmr
è¿æ¯webpackçå è½½çæå¡å¨æ¨éäºä»¶ï¼eventsourceç±»åï¼åè½åwebsocketæç¹ç±»ä¼¼ã大è´ä½ç¨æ¯å»ºç«ä¸ä¸ªä¸ä¼åæ¢çstreamæµé¾æ¥ï¼æå¡å¨åéæ´æ°æ°æ®åæ¥appendå°æµçæ«ç«¯ï¼å端读åææ°appendçæ°æ®ï¼ç¶åå¨æçæ´æ°é¡µé¢ä¸çä¸è¥¿ã
æ¥ä¸æ¥æ们è§å¯ä¸ä¸ææå°çæ´æ°æ°æ®æåªäºãé便æ´æ°ä¸ä¸ªæ件ï¼è§¦åçå è½½ï¼åæ个å ï¼åç°æ两个.hot-update.jsonåä¸ä¸ª.hot-update.jsæ件
çå è½½æ´æ°æ件
è¿äºå ·ä½åäºäºå¥æä¸ç¥éï¼è¿éå°±ä¸æ·±ç©¶äºãåºè¯¥æ¯æ ¹æ®jsonéé¢çæ°æ®ï¼è¾¾å°ä¸ä¸ªåç¡®æ´æ°çææã
æ以çæ´æ°å ¶å®å°±æ¯çå¬æå¡å¨ä¸çæ°æ®ï¼æä¿®æ¹çè¯æå¡å¨åéæ°æ®è¿æ¥ï¼å端ææ°æ®æ¿æ¥åæ¿æ¢å°é¡µé¢ä¸è¿ä¹ä¸ä¸ªè¿ç¨ã
-AutoResponder
æ¥ä¸æ¥è°è°çº¿ä¸æè½½æµè¯ï¼è¿éæ¨èä¸æ¬¾è½¯ä»¶ï¼fiddler
fiddleræä¸ä¸ªåè½å«åAutoResponderï¼å®å¯ä»¥å°ä¸ä¸ªå°åæåå¦ä¸ä¸ªå°åãä¹æ以ç¨è¿ä¸ªè½¯ä»¶ï¼æ¯å 为å®è½å¹é æ£åï¼é常æ¹ä¾¿ã
AutoResponder
ä¸ä¸è说å°ï¼webpackçå è½½ç¨å°äºè¿å ç±»æ件
•__webpack_hmr
•xxxxxxxxxxx.hot-update.json
•xxxxxxxxxxx.hot-update.js
webpack5loader和plugin原理解析
大家好,今天为大家解析下loader和plugin一、区别loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
plugin赋予了Webpack各种灵活的功能,例如打包优化、资源管理、环境变量注入等,C代码源码审计目的是解决loader无法实现的其他事从整个运行时机上来看,如下图所示:
可以看到,两者在运行时机上的区别:
loader运行在打包文件之前plugins在整个编译周期都起作用在Webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果
对于loader,实质是一个转换器,将A文件进行编译形成B文件,操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程
下面我们来看看loader和plugin实现的原理
Loader原理loader概念帮助webpack将不同类型的文件转换为webpack可识别的模块。
loader执行顺序分类
pre:前置loader
normal:普通loader
inline:内联loader
post:后置loader
执行顺序
4类loader的执行优级为:pre>normal>inline>post。
相同优先级的loader执行顺序为:从右到左,从下到上。
例如:
//此时loader执行顺序:loader3-loader2-loader1module:{ rules:[{ test:/\.js$/,loader:"loader1",},{ test:/\.js$/,loader:"loader2",},{ test:/\.js$/,loader:"loader3",},],},//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},使用loader的方式
配置方式:在webpack.config.js文件中指定loader。(pre、normal、postloader)
内联方式:在每个import语句中显式指定loader。(inlineloader)
开发一个loader1.最简单的loader//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};它接受要处理的源码作为参数,输出转换后的js代码。
2.loader接受的参数content源文件的内容
mapSourceMap数据
meta数据,可以是任何内容
loader分类1.同步loadermodule.exports=function(content,map,meta){ returncontent;};this.callback方法则更灵活,因为它允许传递多个参数,而不仅仅是content。
module.exports=function(content,map,meta){ //传递map,让source-map不中断//传递meta,让下一个loader接收到其他参数this.callback(null,content,map,meta);return;//当调用callback()函数时,总是返回undefined};2.异步loadermodule.exports=function(content,map,meta){ constcallback=this.async();//进行异步操作setTimeout(()=>{ callback(null,result,map,meta);},);};由于同步计算过于耗时,在Node.js这样的单线程环境下进行此操作并不是好的方案,我们建议尽可能地使你的loader异步化。但如果计算量很小,同步loader也是可以的。
3.RawLoader默认情况下,资源文件会被转化为UTF-8字符串,然后传给loader。通过设置raw为true,loader可以接收原始的Buffer。
module.exports=function(content){ //content是一个Buffer数据returncontent;};module.exports.raw=true;//开启RawLoader4.PitchingLoadermodule.exports=function(content){ returncontent;};module.exports.pitch=function(remainingRequest,precedingRequest,data){ console.log("dosomethings");};webpack会先从左到右执行loader链中的每个loader上的pitch方法(如果有),然后再从右到左执行loader链中的每个loader上的普通loader方法。
在这个过程中如果任何pitch有返回值,则loader链被阻断。webpack会跳过后面所有的的pitch和loader,直接进入上一个loader。
loaderAPI方法名含义用法this.async异步回调loader。返回this.callbackconstcallback=this.async()this.callback可以同步或者异步调用的并返回多个结果的函数this.callback(err,content,sourceMap?,meta?)this.getOptions(schema)获取loader的optionsthis.getOptions(schema)this.emitFile产生一个文件this.emitFile(name,content,sourceMap)this.utils.contextify返回一个相对路径this.utils.contextify(context,request)this.utils.absolutify返回一个绝对路径this.utils.absolutify(context,request)更多文档,请查阅webpack官方loaderapi文档
手写clean-log-loader作用:用来清理js代码中的console.log
//loaders/clean-log-loader.jsmodule.exports=functioncleanLogLoader(content){ //将console.log替换为空returncontent.replace(/console\.log\(.*\);?/g,"");};手写banner-loader作用:给js代码添加文本注释
loaders/banner-loader/index.js
constschema=require("./schema.json");module.exports=function(content){ //获取loader的options,同时对options内容进行校验//schema是options的校验规则(符合JSONschema规则)constoptions=this.getOptions(schema);constprefix=`/**Author:${ options.author}*/`;return`${ prefix}\n${ content}`;};loaders/banner-loader/schema.json
//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},0手写babel-loader作用:编译js代码,将ES6+语法编译成ES5-语法。
下载依赖
//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},1loaders/babel-loader/index.js
//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},2loaders/banner-loader/schema.json
//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},3手写file-loader作用:将文件原封不动输出出去
下载包
//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},4loaders/file-loader.js
//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},5loader配置
//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},6手写style-loader作用:动态创建style标签,插入js中的样式代码,使样式生效。
loaders/style-loader.js
//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},7Plugin原理Plugin的作用通过插件我们可以扩展webpack,加入自定义的构建行为,使webpack可以执行更广泛的任务,拥有更强的构建能力。
Plugin工作原理webpack就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。这条生产线上的flash连线题源码每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack通过Tapable来组织这条复杂的生产线。webpack在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。webpack的事件流机制保证了插件的有序性,使得整个系统扩展性很好。——「深入浅出Webpack」
站在代码逻辑的角度就是:webpack在编译代码过程中,会触发一系列Tapable钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当webpack构建的时候,插件注册的事件就会随着钩子的触发而执行了。
Webpack内部的钩子什么是钩子钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。
TapableTapable为webpack提供了统一的插件接口(钩子)类型定义,它是webpack的核心功能库。webpack中目前有十种hooks,在Tapable源码中可以看到,他们是:
//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},8Tapable还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:
tap:可以注册同步钩子和异步钩子。
tapAsync:回调方式注册异步钩子。
tapPromise:Promise方式注册异步钩子。
Plugin构建对象Compilercompiler对象中保存着完整的Webpack环境配置,每次启动webpack构建时它都是一个独一无二,仅仅会创建一次的对象。
这个对象会在首次启动Webpack时创建,我们可以通过compiler对象上访问到Webapck的主环境配置,比如loader、plugin等等配置信息。
它有以下主要属性:
compiler.options可以访问本次启动webpack时候所有的配置文件,包括但不限于loaders、entry、output、plugin等等完整配置信息。
compiler.inputFileSystem和compiler.outputFileSystem可以进行文件操作,相当于Nodejs中fs。
compiler.hooks可以注册tapable的不同种类Hook,从而可以在compiler生命周期中植入不同的逻辑。
compilerhooks文档
Compilationcompilation对象代表一次资源的构建,compilation实例能够访问所有的模块和它们的依赖。
一个compilation对象会对构建依赖图中所有模块,进行编译。在编译阶段,模块会被加载(load)、封存(seal)、美数dsp源码优化(optimize)、分块(chunk)、哈希(hash)和重新创建(restore)。
它有以下主要属性:
compilation.modules可以访问所有模块,打包的每一个文件都是一个模块。
compilation.chunkschunk即是多个modules组成而来的一个代码块。入口文件引入的资源组成一个chunk,通过代码分割的模块又是另外的chunk。
compilation.assets可以访问本次打包生成所有文件的结果。
compilation.hooks可以注册tapable的不同种类Hook,用于在compilation编译模块阶段进行逻辑添加以及修改。
compilationhooks文档
生命周期简图开发一个插件最简单的插件plugins/test-plugin.js
//此时loader执行顺序:loader1-loader2-loader3module:{ rules:[{ enforce:"pre",test:/\.js$/,loader:"loader1",},{ //没有enforce就是normaltest:/\.js$/,loader:"loader2",},{ enforce:"post",test:/\.js$/,loader:"loader3",},],},9注册hook//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};0启动调试通过调试查看compiler和compilation对象数据情况。
package.json配置指令
//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};1运行指令
//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};2此时控制台输出以下内容:
PSC:\Users\\Desktop\source>//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};2>source@1.0.0debug>node--inspect-brk./node_modules/webpack-cli/bin/cli.jsDebuggerlisteningonws://.0.0.1:/ea-7b--a7-fccForhelp,see:/post/开发思路:
我们需要借助html-webpack-plugin来实现
在html-webpack-plugin输出index.html前将内联runtime注入进去
删除多余的runtime文件
如何操作html-webpack-plugin?官方文档
实现:
//loaders/loader1.jsmodule.exports=functionloader1(content){ console.log("hellofirstloader");returncontent;};7一个关于webpack 自动use strict引起的有趣的业务bug以及背后原理
Webpack自动use strict引起的业务bug分析
在处理业务bug的过程中,我发现它不仅提供了锻炼技巧的机会,还揭示了一些隐藏的知识点。 问题起因于公司管理后台,一个基于webpack多页面应用的项目,其中部分文件夹使用了webpack的特殊处理,而其他未处理的文件夹则仅简单地使用babel进行es5转换。当某个需求需要迁移至已处理的文件夹时,问题出现了。 问题代码中,一个函数没有声明就直接被使用,导致了"params is not defined"的错误。这是因为引入了严格模式,严格模式下未声明就使用会导致编译错误。 解决方法简单,只需为变量声明即可。然而,问题的关键在于严格模式的来源。虽然代码本身并未主动引入,但webpack的打包过程可能在无意中引入了。 在webpack打包产物对比中,我们发现迁移后引入了use strict,这成为问题的根源。进一步的代码分析发现,use strict的出现与import的使用有关,因为webpack会将import转换为浏览器兼容的形式,并将其依赖放在头部执行。 总结来说,问题在于在使用import时,webpack遵循ES6模块的严格模式规范,而es module本身已经是严格模式。因此,webpack会自动添加use strict以适应es module。通过追踪Webpack源码,我们找到了use strict插入的确切位置,这解决了问题的原因。最完备的懒加载错误兜底方案,再也不会白屏了!
优化首屏加载渲染速度,减小首屏包体积,项目中大量代码通过懒加载动态导入(dynamic import)实现。然而,动态导入失败未做处理,导致页面白屏问题在慢网或CDN故障时频繁出现。为解决此问题,本地云免源码需制定一套完善的错误兜底方案。
使用 webpack 时,动态导入常通过返回 promise 对象的函数实现。导入成功时,promise 需 resolve 默认导出 (default export) 的模块,失败情况却未被处理。以 React 为例,通过搭配 React.lazy 动态导入,其实现依赖于返回 promise 的函数。然而,动态导入失败时,lazy 并未隐式处理异常。
因此,需在动态导入外层增加异常捕获及处理逻辑。常见的粗放做法是捕捉错误后执行 location.reload(),使页面刷新。然而,对于因非代码逻辑问题导致错误的网络请求,最佳做法是增加重试机制以提升稳定性。针对慢网和 CDN 故障,动态导入失败时的处理需包括重试功能。
Webpack 懒加载原理在于动态插入 script 标签,在 onload 事件触发时调用 promise 的 resolve 方法加载资源,在 onerror 事件触发时调用 reject 方法。在 script 加载失败时,需避免执行原本的 onerror,同时将原本的 onerror 赋给后续尝试加载的 script。此外,mini-css-extract-plugin 将样式单独提取为 css 文件,加载失败时需采取与 script 类似的处理方式,确保不触发 link 标签的 onerror。
为解决 CDN 故障导致的资源加载失败问题,可以引入一个无侵入式的静态资源自动重试包。该包通过 hook 原生的 document.createElement 和 script.onerror 方法,同时监听 document 的 error 事件,实现 CDN 重试机制。引入此包后,项目可直接实现 CDN 重试功能。
针对资源仍无法加载回的情况,虽然错误未抛出,但页面上未展示资源对应的功能,用户仍能正常使用页面,避免了白屏现象。通过此优化方案,首屏加载速度得到显著提升,提高了程序的鲁棒性,减少了前端白屏率,显著提升了用户体验。
总之,针对业务优化场景中的懒加载失败问题,通过深入分析 webpack 源码,借助 import() 网络重试加载机制,提升了前端工程的稳定性,优化了用户体验,为项目带来了明显收益。
如果你觉得本文对你有所帮助,请在下方点赞支持我,你的「赞」是我创作的动力。
欢迎关注公众号「小李的前端小屋」,我会分享更多前端工作思考与心得,助你成为更好的前端。
webpack打包原理 ? 看完这篇你就懂了 !
深入浅出 webpack
webpack 是一个现代 JavaScript 应用程序的静态模块打包器,其本质是对模块进行递归构建依赖关系图,然后打包成一个或多个 bundle。
理解 webpack 的工作流程,像是理解一条生产线,每一步都有其职责,依赖于前一步骤的结果。插件则像是生产线中的功能模块,根据特定时机对资源进行处理。
webpack 通过 Tapable 组织整个生产流程,它在运行中广播事件,插件只需监听感兴趣的事件,即可加入流程,改变整个系统。事件流机制确保了插件的有序性,提高了系统的扩展性。
webpack 的核心概念包括:入口起点(Entry),定义了构建开始的模块;输出(Output),指定生成的 bundle 存储位置;模块(Module),一切皆模块,所有资源被转换为模块;代码块(Chunk),多个模块组合用于代码优化;loader,处理非 JavaScript 文件,将所有类型转换为可引用的模块;插件,执行更广泛任务。
理解 webpack 的构建流程,从入口文件开始,遍历依赖关系,转换为浏览器可执行代码,并生成最终的 bundle。在实践过程中,定义 Compiler 类,使用 babel 解析源代码,遍历 AST 抽象语法树,找出依赖模块,转换为可执行代码,并构建依赖关系图,重写 require 函数以输出 bundle。
完成 webpack 的理解,需要通过实践,例如构建一个简易版本的 webpack。从定义 Compiler 类开始,解析入口文件,使用 babel 解析内部语法,找出依赖模块,将 AST 转换为代码,递归解析所有依赖项,构建依赖关系图,重写 require 函数以输出 bundle。
通过实际操作,可以深入理解 webpack 的 bundle 实现过程,从入口文件执行开始,利用 eval 执行代码,处理依赖引用,生成最终的 bundle。通过这个实践过程,可以全面掌握 webpack 的工作原理和使用方法。
webpack 4 源码主流程分析(十一):文件的生成
本文深入分析了 Webpack 4 中文件生成的具体流程。在资源写入文件阶段,通过一系列优化和处理,最终返回到 Compiler.js 的 compile 方法,其中 Compiler 的属性 _lastCompilationFileDependencies 和 _lastCompilationContextDependencies 被赋予了 fileDependencies 和 contextDependencies。紧随其后的是创建目标文件夹的过程,该操作通过 outputPath 属性配置,结合 mkdirp 函数完成。
在创建目标文件并写入阶段,通过 asyncLib.forEachLimit 方法并行处理每个文件资源,实现路径拼接、源码转换为 buffer,最后写入真实路径的文件。对于不同类型的 source 实例,如 CachedSource、ConcatSource 和 ReplaceSource,其处理逻辑各不相同,但最终目标都是获取替换后的字符串并合并返回 resultStr。所有文件创建写入完成后,执行回调,触发Compiler.afterEmit:hooks,进一步设置 stats 并打印构建信息。
至此,构建流程全部结束。通过本文的分析,我们可以更直观地了解 Webpack 4 中文件生成的具体实现细节,为深入理解 Webpack 的工作原理和优化提供理论支持。本章小结,下章将解析打包后的文件,敬请期待。
webpack的devtool 配置
devtool配置与代码调试紧密相关,旨在优化调试体验。在前端开发中,源代码经过合并、压缩、转换后,运行的是转换后的代码,这给调试带来了挑战。因此,chrome浏览器率先引入了source map技术,其他浏览器也纷纷跟进,如今几乎所有新版浏览器都支持source map。
source map本质上是一个配置,它不仅记录了所有源码内容,还记录了源码与转换后代码之间的对应关系,便于开发人员直接定位到源代码中的错误。浏览器处理source map的原理在于解析这个映射关系,从而显示正确的源代码位置信息。
最佳实践包括:在开发环境中使用source map作为调试工具,避免在生产环境中使用,以防止额外的网络传输和潜在的代码暴露风险。如果在生产环境中使用source map用于调试,需采取措施确保网络传输和代码安全。
在webpack中,devtool配置可以优化调试体验。使用正确的devtool选项,能够提供不同级别的源码映射,以适应开发与生产环境的需求。例如,"eval"配置选项在开发环境中提供快速构建,同时保留了正确的行号映射,而"cheap"配置在开发环境中提供了较低开销的映射,忽略了列映射信息。
在生产环境中,通常选择不生成source map,以优化性能。然而,如果需要生成source map,可以选择将它作为单独的文件输出,并在bundle中添加引用注释,以便开发工具查找。此外,还存在一些特定场景的配置选项,如仅生成用于错误堆栈跟踪的source map,或不包含sourcesContent的source map,以保护源代码安全。
放弃 console.log 吧!用 Debugger 你能读懂各种源码
很多同学不清楚为什么要使用debugger进行调试,难道console.log不行吗?
即使学会了使用debugger,还是有很多代码看不懂,如何调试复杂的源码呢?
这篇文章将为你讲解为什么要使用这些调试工具:console.log vs Debugger。
相信绝大多数同学都会使用console.log进行调试,将想查看的变量值打印在控制台。
这种方法可以满足基本需求,但遇到对象打印时就无法胜任了。
比如,我想查看webpack源码中的compilation对象的值,我尝试打印了一下:
但你会发现,当对象的值也是对象时,它不会展开,而是打印一个[Object] [Array]这样的字符串。
更严重的是,打印的内容过长会超过缓冲区的大小,在terminal中显示不全:
而使用debugger来运行,在这里设置一个断点查看,就没有这些问题了:
有些同学可能会说,那打印一个简单的值时使用console.log还是很方便的。
比如这样:
真的吗?
那还不如使用logpoint:
代码执行到这里就会打印:
而且没有污染代码,使用console.log的话,调试完成后这个console也不得不删除掉。
而logpoint不需要,它就是一个断点的设置,不在代码中。
当然,最重要的是debugger调试可以看到调用栈和作用域!
首先是调用栈,它就是代码的执行路线。
比如这个App的函数组件,你可以看到渲染这个函数组件会经历workLoop、beginWork、renderWithHooks等流程:
你可以点击调用栈的每一帧,查看都执行了什么逻辑,用到了什么数据。比如可以看到这个函数组件的fiber节点:
再就是作用域,点击每一个栈帧就可以看到每个函数的作用域中的变量:
使用debugger可以看到代码的执行路径,每一步的作用域信息。而你使用console.log呢?
只能看到那个变量的值而已。
得到的信息量差距不是一点半点,调试时间长了,别人会对代码的运行流程越来越清晰,而你使用console.log呢?还是老样子,因为你看不到代码执行路径。
所以,不管是调试库的源码还是业务代码,不管是调试Node.js还是网页,都推荐使用debugger打断点,别再用console.log了,即使想打印日志,也可以使用LogPoint。
而且在排查问题的时候,使用debugger的话可以加一个异常断点,代码跑到抛异常的地方就会断住:
可以看到调用栈来理清出错前都走了哪些代码,可以通过作用域来看到每一个变量的值。
有了这些,排查错误就变得轻松多了!
而你使用console.log呢?
什么也没有,只能自己猜。
Performance
前面说debugger调试可以看到一条代码的执行路径,但是代码的执行路径往往比较曲折。
比如那个React会对每个fiber节点做处理,每个节点都会调用beginWork。处理完之后又会处理下一个节点,再次调用beginWork:
就像你走了一条小路,然后回到大路之后又走了另一条小路,使用debugger只能看到当前这条小路的执行路径,看不到其他小路的路径:
这时候就可以结合Performance工具了,使用Performance工具看到代码执行的全貌,然后用debugger来深入每一条代码执行路径的细节。
SourceMap
sourcemap非常重要,因为我们执行的都是编译打包后的代码,基本是不可读的,调试这种代码也没有什么意义,而sourcemap就可以让我们直接调试最初的源码。
比如vue,关联了sourcemap之后,我们能直接调试ts源码:
nest.js也是:
不使用sourcemap的话,想搞懂源码,但你调试的是编译后的代码,怎么读懂呢?
读懂一行
前面说的debugger、Performance、SourceMap只是调试代码的工具,那会了调试工具,依然读不懂代码怎么办呢?
我觉得这是不可能的。
为什么这么说呢?
就拿react源码来说:
switch case能读懂吧。三目运算符能读懂吧。函数调用能读懂吧。
每一行代码都能读懂,而全部的代码不就是由这一行行代码组成的么?
加上我们可以单步执行来知道代码执行路径。
为啥每行代码都能读懂,连起来就读不懂了呢?
那应该是代码太多了,而你花的时间不够而已。
先要读懂一行,一个函数,读懂一个小功能的实现流程,慢慢积累,之后了解的越来越多之后,你能读懂的代码就会越多。
总结
这篇文章讲了为什么要使用调试工具,如何读懂复杂代码。
console.log的弊端太多了,大对象打印不全,会超过terminal缓冲区,对象属性不能展开等等,不建议大家使用。即使要打印也可以使用LogPoint。
使用debugger可以看到调用栈,也就是代码的执行路径,每个栈帧的作用域,可以知道代码从开始运行到现在都经历了什么,而console.log只能知道某个变量的值。
此外,报错的时候也可以通过异常断点来梳理代码执行路径来排查报错原因。
但debugger只能看到一条执行路径,可以使用Performance录制代码执行的全流程,然后再结合debugger来深入其中一条路径的执行细节。
此外,只有调试最初的源码才有意义,不然调试编译后的代码会少很多信息。可以通过SourceMap来关联到源码,不管是Vue、React的源码还是Nest.js、Babel等的源码。
会了调试之后,就能调试各种代码了,不存在看不懂的源码,因为每一行代码都是基础的语法,都是能看懂的,如果看不懂,只可能是代码太多了,你需要更多的耐心去读一行行代码、一个个函数、理清一个个功能的实现,慢慢积累就好了。
掌握基于debugger、Performance、SourceMap等调试代码之后,各种网页和Node.js代码都能调试,各种源码都能读懂!