Open oakland opened 6 years ago
上面的文章太长了,打算重新开一个 comment 来写其他相关内容
how-to-fully-optimize-webpack-4-tree-shaking 这篇文章是不错的。
要知道 tree shaking 和 babel 之间的关系,看下 babel 的一些配置,尤其是模块方面的配置会如何影响 tree shaking。
要对打包文件进行优化,可以通过 stats.json 文件进行分析 https://webpack.js.org/guides/code-splitting 里面最后提供了一个工具:https://webpack.jakoblind.no/optimize/,可以使用一下看看。
diff --git a/build/webpack-compiler.js b/build/webpack-compiler.js
index a7465b71..6b2f1e3d 100644
--- a/build/webpack-compiler.js
+++ b/build/webpack-compiler.js
@@ -1,3 +1,4 @@
+const fs = require('fs');
const webpack = require('webpack')
const debug = require('debug')('app:build:webpack-compiler')
const config = require('../config')
@@ -14,6 +15,14 @@ function webpackCompiler (webpackConfig, statsFormat) {
}
const jsonStats = stats.toJson()
+
+ fs.writeFile('./stats.json', JSON.stringify(jsonStats), (err) => {
+ if (err) {
+ return console.log(err)
+ }
+ console.log('File was saved.')
+ });
+
debug('Webpack compile completed.')
debug(stats.toString(statsFormat))
webpack 5: module federation
升级 css-loader 之后,打包报错:Cannot read property 'split' of undefined ... mini-css-extract-plugin,查了很多资料都不行。最后发现是在 scss 中写了 :global 的原因,给 css-loader 添加了一个 exportGlobals: true 的配置就好了。
// 因为有如下的规则
:global {
.title {...}
}
https://github.com/webpack/webpack/blob/master/examples/many-pages/README.md 简直发现宝藏了,上面这个例子是官方给的,然后打开 examples 的内容会发现很多 example,都是非常好的 example,特别适合各种场景的研究。
https://github.com/CommanderXL/Biu-blog 这个博客中关于 webpack 的内容都很好,也比较深入。
https://github.com/CommanderXL/Biu-blog/issues/31
小程序工程化实践(上篇)-- 手把手教你撸一个小程序 webpack 插件 上面这篇文章也是非常不错的,写的很好,很全面。而且自定义的 webpack Plugin 也是比较有深度也有实际意义的,很多文章中的 plugin 都是一个很简单的 demo 。
从 bundle.js 源码学习 Webpack
http://webpack.wuhaolin.cn/
在控制台 console.log(window) 可以看到很多内容,包括 webpack 的一些东西都挂载在上面。
react-router-and-webpack-v4-code-splitting-using-splitchunksplugin 这篇文章不错,里面介绍了什么是 chunk 什么是 vendor 等等。
这篇文章非常全面,而且对新手很友好
react-router相关: https://medium.com/@francoisz/react-router-v4-unofficial-migration-guide-5a370b8905a https://medium.com/@Sawtaytoes/async-react-router-v4-components-c18792e6f331 immutable相关: https://juejin.im/post/5948985ea0bb9f006bed7472 Babel相关: http://guoyongfeng.github.io/my-gitbook/05/toc-react.html http://www.ruanyifeng.com/blog/2016/01/babel.html https://github.com/thejameskyle/babel-handbook/blob/master/translations/zh-Hans/README.md Webpack相关: Webpack-The Confusing Parts Webpack & Hot Module Replacement [HMR] (under-the-hood) Webpack’s HMR And React-Hot-Loader — The Missing Manual Koa2相关: https://chenshenhai.github.io/koa2-note/
code splitting
code splitting,可以看看这篇文章。里面有很多关于配置的小技巧。
如何在 css 中使用 webpack 的 alias 配置
css 中有时候会要用到 background: url(../../path/to/image),如果 url 路径很长,希望用 alias 来简写,但是发现直接改成 url(@image/path/to/image) 是不行的,显示 cannot resolve。查了一下发现需要在前面添加
~
来实现。就是url(~@image/path/to/image)
,这样就可以了。 css-loader-issue-49 css-loader#usageHow Webpack works
Understand how webpack works,这篇文章讲的很好,基本把 loader, plugin 以及 webpack 是如何工作的讲的很清楚。尽管讲的是 webpack2.x ,但是还是值得看的。配置的写法已经过时了,但是工作原理没有变化。 webpack, the confusing parts,这篇文章也不错,把配置上的很多东西讲解的很清楚。 另外,这篇文章的作者在 medium 上的很多内容都是高质量的,值得都看一看 比如 entry 可以是 String / Array / Object 三个类型,每个类型都应该是在什么情况下用。
Array 就是表示数组里的每个元素,也就是 js 文件路径所代表的 js 文件,各个之间没有依赖关系,相对独立。举例也很明显,比如 googleAnalytics.js 文件是一个很独立的文件,那么就可以放在 Array 里,只是相当于把这个文件打包到整个文件里而已,并没有和其他 js 文件有相互关系。
而当应用不是 SPA 的时候,是多个 html 文件,需要每个引用不同 js 文件的时候,就用 Object 的格式。
url-loader VS file-loader
这个 wbepack 的 issue 的回复解释的很好
根据这个说法来看的话,css-loader 会把 css 文件中的 url(...) 格式转变成 require(...) 的格式。至于 url-loader 还是 file-loader 的话,两者都可以,只是 url-loader 会把小文件转成行内 DataUrl 的形式,而 file-loader 则是一个独立的文件。 关于什么是 Dataurl ,这篇文章 和 这篇文章不错,推荐看。
Webpack 4 Tutorial: from 0 Conf to Production Mode
最近在看 这篇文章,是关于 webpack4 的。很简单,但是很有启发性。
这里面说了一个词叫 tree-shaking,不知道是什么意思,后来查了一下,觉得很形象,就是引入的时候把依赖中没有用到的代码去掉。就像我们很多时候收割果实的时候,会使劲摇一下树,然后把树上的果实或者叶子摇下来一样。这个过程就是把代码中没有用到的部分摇掉,减少无用代码。
这里面说了,现在把所有的 css 文件提取成一个单独的文件用 mini-css-extract-plugin 这个插件,而不是用原来的 extract-text-webpack-plugin 这个插件。
这里再顺便说一下 plugin 和 loader 之间的区别,其实重点是对 plugin 这个词的理解。 plugin 本身是插拔的意思,中文译为“插件”,其实就是说这个东西是在一个主要的东西上面附加的内容,插上就可以用的。那么显然,不用这个东西不影响主要的内容,只是说有了这个东西会增加一个功能,是这个意思。从这个英文的角度去理解 plugin 会更好理解。而 loader 显然是 webpack 的主要功能。
顺便学一个单词,stay tuned,表示“敬请期待”。 Stay tuned! More coming soon...
css-loader
css-loader#modules,css-loader 可以配置 modules 选项,来支持是否是 css module,如果是 false,则不会有对应的 mapping 映射,就是单纯的引入 css 文件,如果是 modules: true,那么就是会对 class 做对应的映射。
webpack-bundle-analyzer
最近引入了这个 插件,引入方法很简单,看 npmjs 中的 api 就可以了,在 webpack.prod.config.js 中
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
在 plugins 中添加static 表示生成的分析文件是一个静态文件,而不是一个服务。reportFilename 表示 webpack 中 output 配置的相对路径文件名,我打算生成在项目的根目录下。同时需要给 .gitignore 添加一个 report.html,忽略这个报告文件。最后说明一下 chunckhash 。最开始 output 配置为
结果拿到的是不知道哪个 split code 的文件。所以改了一下
注意这里有个 chunckhash, chunck, hash, contenthash, name 这几个参数都有不同的意义。同时还可以通过 hash:8 的方式限制 hash 的长度。
可以看一下 webpack-bundle-analyzer 的源码,了解一下运行机制以及 webpack 的依赖原理。
如何提升 webpack 的打包速度
去掘金搜一搜,有不少的文章 如何让webpack打包的速度提升50%? keep-webpack-fast-a-field-guide-for-better-build-performance 这篇文章获得 7k 多的赞,应该还是不错的,可以看下。
seperate app and vendor entries
可以看出来现在的这种写法是过时的,不适合升级之后的 webpack。
sass-loader
sass-loader environment variables 这个文章中给出了 prependData 的方式,但是我用了之后不行,后来查了一下发现用 data: xxx 的方式可以使用。应该是 sass-loader 的版本支持不同。 subvertallchris
dynamic import vs static import
这篇文章不错,详细的说明了常见的 dynamic import 的方式和常见使用方法。
热更新原理
https://juejin.im/post/5de0cfe46fb9a071665d3df0 打开一个有热更新功能的项目,通过浏览器的 network 面板可以看到 __webpack_hmr 的请求,而且类型(type)是 eventsource,之前看高程三的时候就看过 eventsource,一直没看到过实例,这些看到了。可以再回顾一下,复习一遍。
normal loader VS pitch loader
最近在看 style-loader 的源码,发现返回的是一个带 pitch 方法的模块,而且 pitch 最后 return 的是一个 reqire 的字符串。什么是 pitch,为什么要有 pitch,pitch 到底干了什么,其实都不知道,找了些资料,整理在这里。 https://stackoverflow.com/questions/55789849/how-style-loader-works-with-css-loader
关于 pitch loader 的使用场景我其实一直不是很理解,就是说为什么会有 pitch loader 这个特殊的 loader 形式?这个 issue 里很多人也有类似的疑惑。尽管这个是很久之前的 issue 了。https://github.com/webpack/webpack/issues/360
现在对我来说,我觉得唯一能够比较合理的解释就是 把运行在客户端的代码需要改成 Pitch 的形式,而在编译阶段,实际上就是在 nodejs 环境中运行的代码不需要放在 Pitch 阶段的。这是我能理解的 pitch 出现的原因。
关于 pre-loader, post-loader, normal-loader, pitch-loader 等等的执行顺序可以看这个 so
loader 相关
摘录一些官网的说法,对于开发和理解 loader 很有帮助:
就是说直接 return 也可以,但是更建议用 this.callback 的形式返回,因为里面还会有更多的参考信息可以传递出来。
关于 this.async() 和 this.sync() 在文章里也都有说明。
这部分内容也很清楚的表明了,对于操作和计算量很大的内容,建议用 async loader 的方式,防止线程阻塞。
这篇文章写的好,https://juejin.im/post/6844904054393405453,而且还提供了很多 loader 的写法,包括对 pitch 的解释也很到位。
下面这篇 loader 十问写的很好,有很多深入的思考内容:https://juejin.im/post/6844903693070909447
./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/styles.scss( ./node_modules/css-loader/dist/runtime/api.js ) ./node_modules/css-loader/dist/runtime/api.js ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js ./src/index.js
./src/styles.scss( ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js, ./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/styles.scss )
测试 loader 前缀
!!
,!
,-!
plugins
tapable, tap
关于 tapable 最好的解释还是这篇文章,https://www.programmersought.com/article/1459649892/,里面解释了所有 tapable 的类型用法和原则。 同样,这篇 文章 也给了很好的说明。
BailHook 表示:一旦有返回值了就停止执行下面的注册内容。 WaterfallHook 表示:上一个注册内容的返回值给传递给下一个注册内容作为参数。 ParallelHook 表示:并行执行,最终执行时间以耗时最长的注册内容为准。 ParallelBailHook 表示:并行执行,且并行执行的任一注册内容有返回值,就停止执行最后注册的 callback,注意,这里的最后注册的 callback,并不是并行执行的内容中耗时最长的那个 callback,而是在 callAsync(...args, cb) 中的这个 cb。 SeriesHook 表示:串行执行,最终执行时间为所有注册内容耗时之和。 SeriesBailHook 表示:串行执行,且串行执行的任一注册内容有返回值,就停止执行。 *SeriesWaterfallHook 表示:串行执行,并且上一个注册内容的返回值作为参数传递给下一个注册内容。
然后我们看看 emit 属于什么类型的 hook
emit 属于 AsyncSeriesHook,也就是说可以注册多个串行的内容,并且他们的参数都是 compilation,而不会互相影响。
再看一个
shouldEmit 是同步的 BailHook,表示可以注册多个,串行执行,但是只要有一个有返回值就不再继续执行后面的注册内容了。这个也符合我们设计的想法,就是一票否决,没必要对后面的进行处理了。
可见 tapable 虽然是一种发布-订阅模式,但是总的来说还是是一种流程控制的机制。如何对整个流程做控制,根本上说是这个意思。tapable 根本上说其实就是一种流程控制的设计思路,流程控制机。
https://www.programmersought.com/article/82515250027/ 上面这篇文章里给出了一些 hook 的示意图。
然后又看到一篇文章也很好: https://www.digitalocean.com/community/tutorials/js-create-custom-webpack-plugin 文章里把 webpack 的各个内容也都做了解释:
其实这种模式也就导致了一个问题,就是订阅可以在 webpack.config 的 plugins 中集中,但是发布却是分布在各个地方,并不是集中的。
https://github.com/alligatorio/bundlesize-webpack-plugin/tree/aligator-post 上面这个 plugin 里面有很多打印相关的信息,可以作为 plugin 中打印信息的参考。
https://webpack.js.org/api/plugins/#custom-hooks 上面这个链接告诉我们可以自定义 hook
这里说的 three tap methods 就是指
tap, tapAsync, tapPromise
,看来理解这三个方法很重要。而且可以知道 compiler 其实是 Tapable 的扩展。https://juejin.im/post/6844904004435050503,这篇文章对于 tapable 的解释很简单明确,值得一看。
hook
https://webpack.js.org/api/compiler-hooks 上面这个网址列出了所有的 hook,这些 hook 对应的 tap 的 callback 都有参数,比如说
这个就写了 emit 是 AsyncSeriesHook,执行的时机是在生成 assets 到 output 目录之前,callback 的参数是 comilation。 那么这些在 webpack 源码中对应的又是那些内容呢,执行 grep 命令来看下
可以看到 emit 这个 hook 的内容在 Compiler.js 的第 491 行,打开文件看下内容:
可以看到 emit 的作用果然就是上面文档里的说明,代码写的真好,代码就是文档。 然后我们在当前文件中向上翻,到 class Compiler extends Tapable 这里,看到有下面这样的内容:
看第 57 行,可以看到
就可以知道 emit 是 AsyncSeriesHook 的类型,然后 callback 中的参数是 compilation。我们在来验证下我们的想法,选 49 行的 done hook 和 68 行的 normalModuleFactory hook 来看下:
应该表示 done hook 是 AsyncSeriesHook 的类型,然后 callback 是 stats;而 normalModuleFactory 是 SyncHook 的类型,然后 callback 的参数是 normalModuleFactory。去上面的链接中看下,就是文档中看下:
果然,没有错。这个对于理解 webpack 又进了一步。
tapable
在 "node_modules/tapable/lib/HookCodeFactory.js" 中我看到了两次 switch case,一次是
create(options) {}
,一次是callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {}
,猜测这个就是创建和触发 plugin 的 hook 机制。这样理解下来,对于 tapable 的理解就更深刻了。在
node_modules/tapable/lib/AsyncSeriesHook.js
中,我们看到 AsyncSeriesHook 是 extends 了 Hook,打开 "node_modules/tapable/lib/Hook.js" 我们可以看到有 tapAsync 方法:第 51 行给了 type: async,所以才能在 HookCodeFactory.js 中的 create 和 apply 方法才能对 type 进行判断,就是这里添加的 type。
然后尝试自己写了个 plugin,感觉很有意思:
打印出来了
should i emit?
,然后把 shouldEmit 的 return false 改成 return true,就会同时打印出should i emit? Have I reached here?
大概能理解 plugin 的构造和过程了吧?
然后去看看 Compilation 的 hook 文档,node_modules/webpack/lib/Compilation.js。同样打开
node_modules/webpack/lib/Compilation.js
也可以看到所有 hook 在代码中的定义。我是看了下面这篇文章才去看到源码,对于理解源码有很大的帮助。 https://medium.com/@imranhsayed/webpack-behind-the-scenes-85333a23c0f6 上面这篇文章非常抽象,但是非常有用,多看几篇很有帮助。 parser 的解释很到位,parser 其实就是解析 module 中的 require 和 import 部分,然后把 dependency 加入到继续 compile 的过程中。
也就是说先 loader 处理,然后再 parser 解析。
对 tapable instance 的理解,就是所有可以被 plugin into 的东西。看 https://www.youtube.com/watch?v=xse6JKcfbzs&ab_channel=CodingTech
https://github.com/TheLarkInn/artsy-webpack-tour
这个是 webpack 的作者自己的视频,不过英文说得实在是听不懂……https://www.youtube.com/watch?v=CA-upQKYjYc&ab_channel=WebTechTalks
下面这篇文章也不错,关于 pre post 和 normal loader,还有一些其他 tips https://survivejs.com/webpack/loading/loader-definitions/
https://juejin.im/post/6844903895584473096
export default class Car { constructor() { this.startHook = new SyncHook(); }
start() { this.startHook.call(); } }
// index.js import Car from './Car';
const car = new Car(); car.startHook.tap('startPlugin', () => console.log('我系一下安全带')); car.start();
require.ensure( dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String )
var a = require('normal-dep');
if ( module.hot ) { require.ensure(['b'], function(require) { var c = require('c');
}); }
entry: '', ... stats: 'minimal'
2 context: '/fakePath/test/webpackTest',
const { createFsFromVolume, Volume } = require('memfs'); const webpack = require('webpack');
const fs = createFsFromVolume(new Volume()); const compiler = webpack({ / options / });
compiler.outputFileSystem = fs; compiler.run((err, stats) => { // Read the output later: const content = fs.readFileSync('...'); });
class MyPlugin { apply(compiler) { compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => { // Explore each chunk (build output): compilation.chunks.forEach(chunk => { // Explore each module within the chunk (built inputs): chunk.getModules().forEach(module => { // Explore each source file path that was included into the module: module.buildInfo && module.buildInfo.fileDependencies && module.buildInfo.fileDependencies.forEach(filepath => { // we've learned a lot about the source structure now... }); });
} } module.exports = MyPlugin;
class MyFirstPlugin { apply (compiler) { compiler.hooks.emit.tapAsync('MyFirstPlugin', (compilation, callback) => { const filelist = compilation.modules.map(m => m.id).join('\n'); // compilation.modules 可以获取所有的 modules // 通过下面这种方式可以给最终的编译(打包)结果添加文件 compilation.assets['filelist.md'] = { source: function() { return filelist; }, size: function() { return filelist.length; } }; callback(); }) } }
module.exports = MyFirstPlugin;
180 const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!"); 181 const noAutoLoaders = 182 noPreAutoLoaders || requestWithoutMatchResource.startsWith("!"); 183 const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!"); 184 let elements = requestWithoutMatchResource 185 .replace(/^-?!+/, "") 186 .replace(/!!+/g, "!") 187 .split("!"); 188 let resource = elements.pop(); 189 elements = elements.map(identToLoaderRequest);