Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack✨ #80

Open
oakland opened this issue May 30, 2018 · 5 comments
Open

webpack✨ #80

oakland opened this issue May 30, 2018 · 5 comments

Comments

@oakland
Copy link
Owner

oakland commented May 30, 2018

https://github.com/webpack/webpack/blob/master/examples/many-pages/README.md
简直发现宝藏了,上面这个例子是官方给的,然后打开 examples 的内容会发现很多 example,都是非常好的 example,特别适合各种场景的研究。

https://github.com/CommanderXL/Biu-blog
这个博客中关于 webpack 的内容都很好,也比较深入。

CommanderXL/Biu-blog#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#usage

To import assets from a node_modules path (include resolve.modules) and for alias, prefix it with a ~:
To import styles from a node_modules path (include resolve.modules) and for alias, prefix it with a ~:

How 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 的回复解释的很好

The url in url-loader has nothing to do with the url in url(...). The url in url-loader means DataUrl. Both can be applied to url(...) and require(...). In fact the css-loader converts url(...) into require(...).

So for your loaders configurations you only need to decide if you want small files inlined as DataUrls (url-loader) or if you want every file as separate request (file-loader).

根据这个说法来看的话,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 中添加

 new BundleAnalyzerPlugin({
   analyzerMode: 'static',
   reportFilename: '../report.html'
 })

static 表示生成的分析文件是一个静态文件,而不是一个服务。reportFilename 表示 webpack 中 output 配置的相对路径文件名,我打算生成在项目的根目录下。同时需要给 .gitignore 添加一个 report.html,忽略这个报告文件。最后说明一下 chunckhash 。最开始 output 配置为

 output: {
   filename: `intelligence/js/[name].[hash].js`,
   chunkFilename: 'intelligence/js/[chunkhash].js',
   path: paths.dist(),
   publicPath: config.compiler_public_path
 }

结果拿到的是不知道哪个 split code 的文件。所以改了一下

 output: {
   filename: `intelligence/js/[name].[${config.compiler_hash_type}].js`,
   chunkFilename: 'intelligence/js/[name].[chunkhash:8].js',
   path: paths.dist(),
   publicPath: config.compiler_public_path
 }

注意这里有个 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

In webpack version < 4 it was common to add vendors as a separate entry point to compile it as a separate file (in combination with the CommonsChunkPlugin).

This is discouraged in webpack 4. Instead, the optimization.splitChunks option takes care of separating vendors and app modules and creating a separate file. Do not create an entry for vendors or other stuff that is not the starting point of execution.

可以看出来现在的这种写法是过时的,不适合升级之后的 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 了。webpack/webpack#360

现在对我来说,我觉得唯一能够比较合理的解释就是 把运行在客户端的代码需要改成 Pitch 的形式,而在编译阶段,实际上就是在 nodejs 环境中运行的代码不需要放在 Pitch 阶段的。这是我能理解的 pitch 出现的原因。

关于 pre-loader, post-loader, normal-loader, pitch-loader 等等的执行顺序可以看这个 so

loader 相关

摘录一些官网的说法,对于开发和理解 loader 很有帮助:

Either return or this.callback can be used to return the transformed content synchronously:

就是说直接 return 也可以,但是更建议用 this.callback 的形式返回,因为里面还会有更多的参考信息可以传递出来。

关于 this.async() 和 this.sync() 在文章里也都有说明。

Loaders were originally designed to work in synchronous loader pipelines, like Node.js (using enhanced-require), and asynchronous pipelines, like in webpack. However, since expensive synchronous computations are a bad idea in a single-threaded environment like Node.js, we advise making your loader asynchronous if possible. Synchronous loaders are ok if the amount of computation is trivial.

这部分内容也很清楚的表明了,对于操作和计算量很大的内容,建议用 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

  57       emit: new AsyncSeriesHook(["compilation"]),

emit 属于 AsyncSeriesHook,也就是说可以注册多个串行的内容,并且他们的参数都是 compilation,而不会互相影响。

再看一个

  47       shouldEmit: new SyncBailHook(["compilation"]),

shouldEmit 是同步的 BailHook,表示可以注册多个,串行执行,但是只要有一个有返回值就不再继续执行后面的注册内容了。这个也符合我们设计的想法,就是一票否决,没必要对后面的进行处理了。

可见 tapable 虽然是一种发布-订阅模式,但是总的来说还是是一种流程控制的机制。如何对整个流程做控制,根本上说是这个意思。tapable 根本上说其实就是一种流程控制的设计思路,流程控制机。

https://www.programmersought.com/article/82515250027/
上面这篇文章里给出了一些 hook 的示意图。

然后又看到一篇文章也很好:
https://www.digitalocean.com/community/tutorials/js-create-custom-webpack-plugin
文章里把 webpack 的各个内容也都做了解释:

  • Compiler: it has the top-level API and provides hooks for them for controlling the execution of webpack.
  • Compilation or dependency graph: returned by the compiler and it starts creating the dependency graph.
  • Resolver: creates an absolute path for the entry path provided and return details like results, request, path, context, etc.
  • Parser: it creates the AST (abstract syntax tree) and then looks for interesting thing like requires and imports and then creates the dependency object.
  • Module Factories: These objects are passed to the moduleFactory function and creates the module.
  • Templates: it does the data binding of the module object and create the code in the bundled file.

其实这种模式也就导致了一个问题,就是订阅可以在 webpack.config 的 plugins 中集中,但是发布却是分布在各个地方,并不是集中的。

https://github.com/alligatorio/bundlesize-webpack-plugin/tree/aligator-post
上面这个 plugin 里面有很多打印相关的信息,可以作为 plugin 中打印信息的参考。

https://webpack.js.org/api/plugins/#custom-hooks
上面这个链接告诉我们可以自定义 hook

The class exposes tap, tapAsync, and tapPromise methods which plugins can use to inject custom build steps that will be fired throughout a compilation...An understanding of the three tap methods, as well as the hooks that provide them, is crucial.
The objects that extend Tapable (e.g. the compiler)...

这里说的 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
Executed right before emitting assets to output dir.
Callback Parameters: compilation

这个就写了 emit 是 AsyncSeriesHook,执行的时机是在生成 assets 到 output 目录之前,callback 的参数是 comilation。
那么这些在 webpack 源码中对应的又是那些内容呢,执行 grep 命令来看下

grep -rni 'hooks.emit.call' node_modules/webpack/lib
node_modules/webpack/lib/Compiler.js:491:		this.hooks.emit.callAsync(compilation, err => {

可以看到 emit 这个 hook 的内容在 Compiler.js 的第 491 行,打开文件看下内容:

 491     this.hooks.emit.callAsync(compilation, err => {
 492       if (err) return callback(err);
 493       outputPath = compilation.getPath(this.outputPath);
 494       this.outputFileSystem.mkdirp(outputPath, emitFiles);
 495     });

可以看到 emit 的作用果然就是上面文档里的说明,代码写的真好,代码就是文档。
然后我们在当前文件中向上翻,到 class Compiler extends Tapable 这里,看到有下面这样的内容:

  45     this.hooks = {
  46       /** @type {SyncBailHook<Compilation>} */
  47       shouldEmit: new SyncBailHook(["compilation"]),
  48       /** @type {AsyncSeriesHook<Stats>} */
  49       done: new AsyncSeriesHook(["stats"]),
  50       /** @type {AsyncSeriesHook<>} */
  51       additionalPass: new AsyncSeriesHook([]),
  52       /** @type {AsyncSeriesHook<Compiler>} */
  53       beforeRun: new AsyncSeriesHook(["compiler"]),
  54       /** @type {AsyncSeriesHook<Compiler>} */
  55       run: new AsyncSeriesHook(["compiler"]),
  56       /** @type {AsyncSeriesHook<Compilation>} */
  57       emit: new AsyncSeriesHook(["compilation"]),
  58       /** @type {AsyncSeriesHook<string, Buffer>} */
  59       assetEmitted: new AsyncSeriesHook(["file", "content"]),
  60       /** @type {AsyncSeriesHook<Compilation>} */
  61       afterEmit: new AsyncSeriesHook(["compilation"]),
  62
  63       /** @type {SyncHook<Compilation, CompilationParams>} */
  64       thisCompilation: new SyncHook(["compilation", "params"]),
  65       /** @type {SyncHook<Compilation, CompilationParams>} */
  66       compilation: new SyncHook(["compilation", "params"]),
  67       /** @type {SyncHook<NormalModuleFactory>} */
  68       normalModuleFactory: new SyncHook(["normalModuleFactory"]),
  69       /** @type {SyncHook<ContextModuleFactory>}  */
  70       contextModuleFactory: new SyncHook(["contextModulefactory"]),
  71
  72       /** @type {AsyncSeriesHook<CompilationParams>} */
  73       beforeCompile: new AsyncSeriesHook(["params"]),
  74       /** @type {SyncHook<CompilationParams>} */
  75       compile: new SyncHook(["params"]),
  76       /** @type {AsyncParallelHook<Compilation>} */
  77       make: new AsyncParallelHook(["compilation"]),
  78       /** @type {AsyncSeriesHook<Compilation>} */
  79       afterCompile: new AsyncSeriesHook(["compilation"]),
  80
  81       /** @type {AsyncSeriesHook<Compiler>} */
  82       watchRun: new AsyncSeriesHook(["compiler"]),
  83       /** @type {SyncHook<Error>} */
  84       failed: new SyncHook(["error"]),
  85       /** @type {SyncHook<string, string>} */
  86       invalid: new SyncHook(["filename", "changeTime"]),
  87       /** @type {SyncHook} */
  88       watchClose: new SyncHook([]),
  89
  90       /** @type {SyncBailHook<string, string, any[]>} */
  91       infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
  92
  93       // TODO the following hooks are weirdly located here
  94       // TODO move them for webpack 5
  95       /** @type {SyncHook} */
  96       environment: new SyncHook([]),
  97       /** @type {SyncHook} */
  98       afterEnvironment: new SyncHook([]),
  99       /** @type {SyncHook<Compiler>} */
 100       afterPlugins: new SyncHook(["compiler"]),
 101       /** @type {SyncHook<Compiler>} */
 102       afterResolvers: new SyncHook(["compiler"]),
 103       /** @type {SyncBailHook<string, Entry>} */
 104       entryOption: new SyncBailHook(["context", "entry"])
 105     };

看第 57 行,可以看到

  57       emit: new AsyncSeriesHook(["compilation"]),

就可以知道 emit 是 AsyncSeriesHook 的类型,然后 callback 中的参数是 compilation。我们在来验证下我们的想法,选 49 行的 done hook 和 68 行的 normalModuleFactory hook 来看下:

  49       done: new AsyncSeriesHook(["stats"]),
  ...
  68       normalModuleFactory: new SyncHook(["normalModuleFactory"]),

应该表示 done hook 是 AsyncSeriesHook 的类型,然后 callback 是 stats;而 normalModuleFactory 是 SyncHook 的类型,然后 callback 的参数是 normalModuleFactory。去上面的链接中看下,就是文档中看下:

done
AsyncSeriesHook
Executed when the compilation has completed.
Callback Parameters: stats
...
normalModuleFactory
SyncHook
Called after a NormalModuleFactory is created.
Callback Parameters: 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 方法:

  45   tapAsync(options, fn) {
  46     if (typeof options === "string") options = { name: options };
  47     if (typeof options !== "object" || options === null)
  48       throw new Error(
  49         "Invalid arguments to tapAsync(options: Object, fn: function)"
  50       );
  51     options = Object.assign({ type: "async", fn: fn }, options);
  52     if (typeof options.name !== "string" || options.name === "")
  53       throw new Error("Missing name for tapAsync");
  54     options = this._runRegisterInterceptors(options);
  55     this._insert(options);
  56   }

第 51 行给了 type: async,所以才能在 HookCodeFactory.js 中的 create 和 apply 方法才能对 type 进行判断,就是这里添加的 type。

然后尝试自己写了个 plugin,感觉很有意思:

class MyFirstPlugin {
  apply (compiler) {
    compiler.hooks.shouldEmit.tap('MyFirstPlugin', compilation => {
      console.log('should i emit?');
      return false;
    })
    compiler.hooks.emit.tapAsync('MyFirstPlugin', (compilation, callback) => {
      console.log('Have I reached here?');
      callback();
    })
  }
}

module.exports = MyFirstPlugin;

打印出来了 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 的过程中。

image

5-Parser — A parser takes a string of source code and converts it into AST( Abstract Syntax Tree ) ( https://astexplorer.net/ ). Webpack has a parser class that uses acron parser, which takes the Module Object ( containing the source code ) created by Module Factory and creates an AST out of it. Webpack traverses through entire AST. It also finds all the require and import from AST statements and creates a dependency graph. It attaches those dependencies to the modules. It’s important to note that if webpack finds any rules for loaders, the loaders convert those codes( e.g. css files ) into JavaScript, and then Parser parses them into AST.

也就是说先 loader 处理,然后再 parser 解析。

对 tapable instance 的理解,就是所有可以被 plugin into 的东西。看 https://www.youtube.com/watch?v=xse6JKcfbzs&ab_channel=CodingTech
image
image
image
image
image
image
image
image
image
image

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

这就是经典的事件注册和触发机制啊。实际使用的时候,声明事件和触发事件的代码通常在一个类中,注册事件的代码在另一个类(我们的插件)中。

// Car.js
import { SyncHook } from 'tapable';

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();

钩子的使用基本就是这个意思,Car中只负责声明和调用钩子,真正的执行逻辑,不再Car中,而是在注册它的index.js之中,是在Car之外。这样就做到了很好的解耦。

对于Car而言,通过这种注册插件的方式,丰富自己的功能。

《javascript 设计模式与开发实践》

当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外, 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联 系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一 起的时候,要跟踪一个 bug 不是件轻松的事情。

这也是 webpack 的 tapable 给我的感受,虽然对象解耦了,但是逻辑却隐藏的更深了。定位 bug 更难了,因为逻辑分布在各个地方。

require.ensue

require.ensure

require.ensure() is specific to webpack and superseded by import().

也就是说 require.ensure 正在被 import 替代,属于过时的写法了。

code-splitting/#dynamic-imports 里也提到了:

Two similar techniques are supported by webpack when it comes to dynamic code splitting. The first and recommended approach is to use the import() syntax that conforms to the ECMAScript proposal for dynamic imports. The legacy, webpack-specific approach is to use require.ensure. Let's try using the first of these two approaches...

require.ensure 的用法如下:

require.ensure(
  dependencies: String[],
  callback: function(require),
  errorCallback: function(error),
  chunkName: String
)

Split out the given dependencies to a separate bundle that will be loaded asynchronously. When using CommonJS module syntax, this is the only way to dynamically load dependencies. Meaning, this code can be run within execution, only loading the dependencies if certain conditions are met.

var a = require('normal-dep');

if ( module.hot ) {
  require.ensure(['b'], function(require) {
    var c = require('c');

    // Do something special...
  });
}

The following parameters are supported in the order specified above:
dependencies: An array of strings declaring all modules required for the code in the callback to execute.
callback: A function that webpack will execute once the dependencies are loaded. An implementation of the require function is sent as a parameter to this function. The function body can use this to further require() modules it needs for execution.
errorCallback: A function that is executed when webpack fails to load the dependencies.
chunkName: A name given to the chunk created by this particular require.ensure(). By passing the same chunkName to various require.ensure() calls, we can combine their code into a single chunk, resulting in only one bundle that the browser must load.

Although the implementation of require is passed as an argument to the callback function, using an arbitrary name e.g. require.ensure([], function(request) { request('someModule'); }) isn't handled by webpack's static parser. Use require instead, e.g. require.ensure([], function(require) { require('someModule'); }).

我原样把文档中的内容搬进来了,就是因为我觉得文档实在解释的太好了,比我说什么都更清楚。

关于 require.ensure 中的第一个 array 参数,文档里没有解释,我找到了一个 so 的问答,解释的很清楚。

在看上面的内容中,提到了 dynamic import,然后看了一眼 dynamic import 的文档,发现关于 dynamic-imports 的内容还要再看,里面有很多细节没有研究清楚。包括为什么要在引入 commonjs 的库时添加 default 属性,还有 dynamic import 的时候的 magic comment 等等。

stats

打印少量的内容,配置 stats 为如下:

entry: '',
...
stats: 'minimal'

一个小巧的 webpack plugin,可以用来分析 webpack plugin 的写法

https://github.com/yangmingshan/remove-source-webpack-plugin

compilation object

compilation 有很多 api,可以看这个:https://webpack.js.org/api/compilation-object/#root

plugin-patterns

这个应该是常见 plugin 的写法。
https://webpack.js.org/contribute/plugin-patterns/

everything is plugin

https://medium.com/webpack/the-contributors-guide-to-webpack-part-2-9fd5e658e08c,这篇文章里也有各个模块的说明。

context

The entry object is where webpack looks to start building the bundle. The context is an absolute string to the directory that contains the entry files.

我们把 compiler 打印出来可以看到 context 长这个样式:

2    context: '/fakePath/test/webpackTest',

outputFileSystem

https://webpack.js.org/api/node/#custom-file-systems

By default, webpack reads files and writes files to disk using a normal file system. However, it is possible to change the input or output behavior using a different kind of file system (memory, webDAV, etc). To accomplish this, one can change the inputFileSystem or outputFileSystem. For example, you can replace the default outputFileSystem with memfs to write files to memory instead of to disk:

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('...');
});

Note that this is what webpack-dev-middleware, used by webpack-dev-server and many other packages, uses to mysteriously hide your files but continue serving them up to the browser!

从上面的内容中,我们可以获得两个信息,第一就是 webpack-dev-middleware 和 webpack-dev-server 使用了这种方式来让输出写入到内存中,而不是磁盘中。第二是我们从 compilation 和 compiler 中可以获得很多功能。

看一下 compilation 和 compiler

可以去我的 gist 看到这两个文件。

resolver

关于 resolver 的内容看下面,其实 resolver 就是查找文件路径的一个方式。例如 alias 就是在 resolver 中配置的。
https://webpack.js.org/api/resolvers/
https://github.com/webpack/enhanced-resolve
https://webpack.js.org/configuration/resolve/
https://webpack.js.org/concepts/module-resolution

下面这个虽然是个收费课程,但是里面还是有一些文字描述的,也可以看看。除了 resolver 还写了 module factory 的一些内容。也可以看。
https://frontendmasters.com/courses/webpack-plugins/resolver-module-factories/

template

https://stackoverflow.com/questions/62262585/modify-webpacks-maintemplate

可以看到 template 是干什么用的,就是最后生成代码的时候,用的模板。
也可以看看 plugin 的强大,以及各种 plugin 的使用效果。

其实,到这里,我已经对于完全了解所有的 compiler hooks 和 compilation hooks 逐渐失去了兴趣,不是因为我已经全部掌握了,而是因为我发现 hooks 实在太多了,而通过这么长时间的学习,我对于 plugin 的原理已经基本掌握了,无非是我需要根据自己的需求,找到合适的 tap 机会,然后做自己想做的事情。所以没有必要完全去了解每个 tap 的机会都是什么时候,都用来干什么,真的当需求来了的时候再去找就行了。 webpack 提供了这么多的 hooks,基本上可以说肯定有你想要的 hook 的时机。

这个博客下面有很多不错的文章,推荐阅读!
https://lihautan.com/webpack-plugin-main-template/

plugin 能拿到所有的一切

有句话怎么说来着?大饼卷一切,那么在 webpack 的世界里这句话就是 plugin 卷一切。
plugin 能拿到什么?看看下面这段代码

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...
          });
        });

        // Explore each asset filename generated by the chunk:
        chunk.files.forEach(filename => {
          // Get the asset source for each file generated by the chunk:
          var source = compilation.assets[filename].source();
        });
      });

      callback();
    });
  }
}
module.exports = MyPlugin;

上面这段代码来自官网,https://webpack.js.org/contribute/plugin-patterns/

这篇Webpack插件机制之Tapable-源码解析文章写的好,从 hook 的分类开始,到 tapable 的源码都有很好的解释,不仅说了这种方式的好,而且也吐槽了这种方式的缺点,对于理解 tapable 有很好的帮助。

compilation object

compilation object 的属性和方法很丰富,仅仅在官方文档中写的就是很多,见 compilation-object,但是实际上还有很多内容是没有写出来的,我们也可以用的。
例如官方示例中给出的 Plugin 就是添加一个 filelist.md 文件到 compilation 中,用到了 compilation.assets 的属性。其实我们还可以拿到 compilation.modules 和 compilation.chunks。例如:

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;

childCompiler

关于 childCompiler 的内容其实很少,这是仅有的一篇,作为想要了解 webpack 全貌的我,不会放过任何一篇相关的文章。所以这篇也会收入囊中。
https://medium.com/@prateekbh/my-experience-writing-a-webpacks-child-compiler-plugin-a1237c175947

要分析的 plugin

简单一点的 plugin 比如官方给出的 EntryOptionPlugin 就已经定义在官方的 webpack 中了。
webpack.IgnorePlugin 也可以看一下,里面有关于 normalModuleFactory 的 beforeResolve hook。

module VS chunk VS module

https://stackoverflow.com/questions/42523436/what-are-module-chunk-and-bundle-in-webpack
chunk
module
bundle
我的理解是 bundle > chunk > module,就是 module 组成了 chunk,所有的 chunk 是 bundle。

下面两个对 chunk 的理解也有帮助
webpack/webpack.js.org#970
https://webpack.js.org/concepts/under-the-hood/#chunks

运行的过程

真正运行的是 node_modules/webpack/bin/webpack.js

webpack 可以理解为一种基于事件流的编程范例,一系列的插件运行。

webpack 运行流程图,摘自极客时间
image

webpack options apply plugin 的工作内容
image

不同的 module factory 对应的类型
image

webpack 打包之后的内容
image

preloader / postloader / normal loader

rule.enforce 可以控制 loader 的类型,https://webpack.js.org/configuration/module/#ruleenforce
而且文章中有句话

Inline loaders and ! prefixes should not be used as they are non-standard. They may be use by loader generated code.
也就说这种 inline 引入 loader 的方式不是标准模式,不建议通过这种方式引入,还是应该通过配置文件的方式来引入。这种引入方式通常是在 loader 生成的 code 中会采用。

通过 import 的方式引入的时候可以对 loader 配置进行覆盖,https://webpack.js.org/concepts/loaders/#inline

这部分的源码在 node_modules/webpack/lib/NormalModuleFactory.js 里:

 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);

一直有点纠结 pre-loader / post-loader / normal-loader 存在的意义,看了半天发现大部分的配置中都没有相关的内容。唯一一个用了 rule.enforce: 'pre' 的就是 eslint-loader,然后 eslint-loader 还被 deprecated 了,现在直接用 eslint-webpack-plugin 就行了,所以感觉这个出现的意义并不是很强,也没有必要钻牛角尖去深入研究。只需要知道有这么个可能性就行了,至于什么时候用,等真的有使用场景了再来看就好了。大部分情况下就是用的 !! 来禁用所有 loader 配置,然后通过个性化的 loader 来实现引入某个特殊的内容。

可视化展示webpack内部插件与钩子关系 这篇文章好强啊,作者很厉害。

关于 webpack_public_path

https://webpack.js.org/guides/public-path/#on-the-fly

关于 webpack hot reload 的不同方案对比

https://stackoverflow.com/questions/42294827/webpack-vs-webpack-dev-server-vs-webpack-dev-middleware-vs-webpack-hot-middlewar

this.addDependency

You must pass an absolute path to addDependency

loader 的揭秘文章

下面这篇挺好的
https://champyin.com/2020/01/28/%E6%8F%AD%E7%A7%98webpack-loader/

html-webpack-plugin

做 polyfill shimming 的时候,用了 html-webpack-plugin 的 excludeChunks,但是发现不生效,怎么试都不行,最后发现 exclueChunks 的值应该是对应的 entry,而不 output.filename。

module concanation and scope hoisting

module-concatenation-plugin
optimizationconcatenatemodules

Tells webpack to find segments of the module graph which can be safely concatenated into a single module. Depends on optimization.providedExports and optimization.usedExports. By default optimization.concatenateModules is enabled in production mode and disabled elsewise.

This concatenation behavior is called “scope hoisting.”
Scope hoisting is specifically a feature made possible by ECMAScript Module syntax. Because of this webpack may fallback to normal bundling based on what kind of modules you are using, and other conditions.

关于这部分的内容没有仔细看,今天在做 tree shaking 的时候偶然发现了其中的奥秘。就是 webpack 会合并一些 module,提高 js 的运行效率。

webpack 和 jsonp

为什么很多 webpack 打包后的内容里有 jsonp 的内容。
webpack 和 jsonp 到底什么关系?好像是和动态加载有关系。

https://juejin.im/post/6872354325553741838#heading-4
上面这篇文章里有关于 exports 和 webpack_exports 的区别,好像是 commonjs 和 es modules 导致的。

cacheGroup

在 webpack 的配置中,有 optimization.splitChunksPlugin.cacheGroup 的字段配置,一直没有看明白什么是 cacheGroup。
react-router-and-webpack-v4-code-splitting-using-splitchunksplugin 这篇文章中,grouping chunks 这个部分是专门讲 cacheGroup 的。

cacheGroups tells SplitChunksPlugin to create chunks based on some conditions, for example, create a separate chunk file for all the code being imported from node_modules.

cacheGroups is a plain object with key being the name of chunk and value being some configuration of that chunk. By default, Webpack ships with vendors and default cacheGroups but let’s turn those off by setting their value to false, else it will just confuse you to understand code splitting.

这篇文章的后面都是 splitChunksPlugin 的介绍,介绍的还是挺不错的。值得一看。

这里面有几个字段可以深入看下,例如 reuseExistingChunk, priority, enforce

reuseExistingChunk tells SplitChunksPlugin to use existing chunk if available instead of creating a new one. For example, if any module imported inside common chunk code is part of another chunk already, then instead of creating a new chunk for it, the older one is reused instead.
If a module falls under many chunk groups, then the module will be a part of a chunk group with higher priority. For example, if common chunk has no test cases and chunks set to all, that means any imported module (statically or dynamically) will be part of common chunk as well, even npm module. But since vendor chunk has higher priority, npm modules are more likely to be a part of vendor chunk and reset will stay inside common chunk.
enforce value is set to true to force SplitChunksPlugin to form this chunk irrespective of the size of the chunk. SplitChunksPlugin by default only form chunks if the resulting chunk size is greater than 30kb. In our case, common chunk contains only HelloComponent code which is obviously less than 30kb. enforce is necessary here or set value of splitChunks.minSize as minimum as possible.

这里的 priority 针对的其实是各个 cacheGroup,如果某个 module 同时满足多个 cacheGroup 的 test 字段,谁的 priority 大,就放在谁的 chunk 里。

enforce is necessary here or set value of splitChunks.minSize as minimum as possible.
enforce 字段表示强制提取一个 chunk,而不是通过 minSize 字段来判断,或者说 enforce 其实相当于设置 minSize 为 0。

Since we have 3 async chunks sharing hello.component.js module, we should see a common.js chunk file loaded synchronously. BTW, how a chunk file is loaded (whether synchronously or asynchronously) is decided by Webpack and you can’t force it to do otherwise.

这里说了一个现象,就是 hello.component.js 尽管是 async component 的引入的,但是它本身的载入方式是 sync,并且作者说这是 webpack 决定的,而不是由配置可以决定的。

rip commonsChunkPlugin 这篇文章是 sokra 写的,也写了 priority 等字段,而且举了几个例子。

@oakland oakland changed the title webpack webpack✨ Sep 19, 2019
@oakland
Copy link
Owner Author

oakland commented Nov 10, 2020

上面的文章太长了,打算重新开一个 comment 来写其他相关内容

tree shaking

how-to-fully-optimize-webpack-4-tree-shaking 这篇文章是不错的。

要知道 tree shaking 和 babel 之间的关系,看下 babel 的一些配置,尤其是模块方面的配置会如何影响 tree shaking。

@oakland
Copy link
Owner Author

oakland commented Dec 4, 2020

要对打包文件进行优化,可以通过 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))

image

@oakland
Copy link
Owner Author

oakland commented Dec 4, 2020

webpack 5: module federation

webpack/webpack#10352

@oakland
Copy link
Owner Author

oakland commented Jan 26, 2021

升级 css-loader 之后,打包报错:Cannot read property 'split' of undefined ... mini-css-extract-plugin,查了很多资料都不行。最后发现是在 scss 中写了 :global 的原因,给 css-loader 添加了一个 exportGlobals: true 的配置就好了。

// 因为有如下的规则
:global {
  .title {...}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant