webpack 知识点

1.webpack 模块化实现

项目里面引入了moment

import moment from 'moment'

在非debug状态下,尝试输入moment是拿不到moment这个模块的,但是debug代码的时候不能直接拿到moment这个模块,而是通过__WEBPACK_IMPORTED_MODULE_18_moment___default可以拿到
按理说moment()调用一次就拿到了moment对象,然而并不可以

moment

__WEBPACK_IMPORTED_MODULE_18_moment___default()()调用了两次才拿到

  onTimeChange = val => {
    const mon = moment //这边通过
    console.info(val)
  }
debug

从上图可知 debug 状态还是无法再控制台输出moment(),而赋值给变量mon就可以执行mon()拿到moment对象,而__WEBPACK_IMPORTED_MODULE_18_moment___default()()还是要执行两次才能拿到moment对象

webpack务虚扫盲
从 Bundle 文件看 Webpack 模块机制
webpack模块加载原理

2. require.context

官网的解释:

require.context(directory:String, includeSubdirs:Boolean /* optional, default true */, filter:RegExp /* optional */)

Specify a whole group of dependencies using a path to the directory
, an option to includeSubdirs
, and a filter
for more fine grained control of the modules included. These can then be easily resolved later on:

var context = require.context('components', true, /\.html$/);//获取components目录下所有以.html结尾的文件(包括其子目录)
var componentA = context.resolve('componentA');

使用webpack的require.context实现路由“去中心化”管理
webpack require context 说明

3. 使用ejs作为HtmlWebpackPlugin的模板

通常情况下我们使用HTML作为单页面的模板,

  new HtmlWebpackPlugin({  // Also generate a test.html
      filename: 'test.html',
      template: 'src/assets/test.html'
    })

但是有时候我们需要在页面用到变量或者遍历数组,那么就需要使用模板引擎,比如ejs,

  <script src="<%= htmlWebpackPlugin.options.devServer %>/webpack-dev-server.js" type="text/javascript"></script>

上面的htmlWebpackPlugin.options.devServer变量 来自于webpack.config.js

 new HtmlWebpackPlugin({
      inject: false,
      template: '../index.ejs',
      appMountId: 'app',
      devServer: 'http://localhost:3001', // 看这里
      mobile: true,
      lang: 'en-US',
      title: 'My App',
      window: {
        env: {
          apiHost: 'http://myapi.com/api/v1'
        }
      }
    })
  ]

具体配置参考jaketrent/html-webpack-template
小白学react之EJS模版实战

4. vscode debug webpack

我们在启动项目的时候都是使用npm script启动的,那么怎么用vscode启动项目并debug webpack的打包呢?

首先配置vscode launch.json

    {
      "type": "node",
      "request": "launch",
      "name": "Launch via NPM with dll",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run-script", "dll"],
      "port": 9229
    },

一开始做的尝试node --inspect-brk ./node_modules/.bin/webpack --config config/webpack.config.dll.js但是报错

basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")           ^^^^^^^  SyntaxError: missing ) after argument list

看了这篇issue node --inspect-brk ./node_modules/.bin/webpack --config config/webpack.config.dll.js 报错解决办法 修改为node --inspect-brk ./node_modules/webpack/bin/webpack --config config/webpack.config.dll.js
这样就可以调试了,哈哈哈

报错的原因好像是操作系统的问题,.bin目录下是shell和cmd文件,直接node --inspect-brk ./node_modules/.bin/webpack就执行shell,而在Windows上执行node --inspect-brk ./node_modules/.bin/webpack.cmd也会报错

SyntaxError: missing ) after argument list

我的项目是用create-react-app创建的,并且做了eject,然后webpack的配置全部暴露出来了,启动项目"start": "node scripts/start.js", 尝试上面的方法配置webpack,debug会报错,然后重新做了配置"start:debug": "node --nolazy --inspect-brk=9229 scripts/start.js",,vscode launch.json加上如下代码,

    {
      "type": "node",
      "request": "launch",
      "name": "Launch via NPM with start",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run-script", "start:debug"],
      "port": 9229
    },

这样不但start.js webpack.config.dev.js断点能进入,webpack的loader也可以断点进入,比如css-loader,但是去掉所有的断点,然后启动,要过很久才能加载出页面。

5. style-resources-loader@1.2.1导致cra创建的typescript项目编译很卡

我用cra新建的项目的webpack的版本是4.39.1,style-resources-loader@1.2.1依赖的webpack的版本是^4.16.5,改变一行代码,编译下来要20s,升级style-resources-loader到1.3.3之后,编译只要5秒的样子,提升了几倍。style-resources-loader@1.3.3 依赖webpack@4.41.3,我项目里安装的是webpack@4.39.1,不晓得都是webpack@4.x为啥编译速度差别这么大.
刨根问底,在style-resources-loaderissue找到了答案,在我的mac上编译是不卡的,但是在windows上很卡,Failed to cache .less files on Win 10 with Webpack 4 #17是因为webpack升级到4有些代码改了,而该库没有做出对不同系统文件系统路径兼容的处理,导致代码编译的样式没有被缓存,是的每次rebuild都会去重新编译样式文件。

6. 关于webpack是怎么将css 跟js是怎么插入到dom中的

cra 新建的项目dev环境默认有三个文件,webpack@4.42.0


dev环境的三个js文件
html-webpack-plugin flow

由图可以看出来,script标签插入HTML字符串是在afterTemplateExecutionbeforeEmit之间完成的,做了代码测试看到的结果也是如此,扒开源码看看

文件名 作用 解释
bundle.js webpack runtime代码 包括webpack-dev-server
0.chunk.js 第三方库代码 react react-dom等
main.chunk.js 自己写的代码 包括自己写的js跟css
  • webpack 怎样把样式代码插入到js中的

  • webpack怎样实现对样式文件的缓存的

  • 是style-loader还是html-webpack-plugin将style标签插入到dom中的

style-loader@1.0.0源码各种拼接字符串还是很难读的,找到一篇不错的源码解析文章webpack loader 从上手到理解系列 style-loader

{loaderUtils.stringifyRequest(
        this,
        `!!${request}`
      )}

!!从未见过,在js里是将其他类型转为boolean,官方文档显示,有三种使用 loader 的方式

  • 配置(推荐):在 webpack.config.js 文件中指定 loader。
  • 内联:在每个 import 语句中显式指定 loader。
  • CLI:在 shell 命令中指定它们。

内联(inline)

可以在 import 语句或任何 等同于 "import" 的方法 中指定 loader。使用 ! 将资源中的 loader 分开。每个部分都会相对于当前目录解析。

import Styles from 'style-loader!css-loader?modules!./styles.css';

使用 ! 或者!或者-!为整个规则添加前缀,可以覆盖webpack.config.js配置中的所有 loader 定义。具体规则如下:

  • Prefixing with ! will disable all configured normal loaders
import Styles from '!style-loader!css-loader?modules!./styles.css';
  • Prefixing with !! will disable all configured loaders (preLoaders, loaders, postLoaders)
import Styles from '!!style-loader!css-loader?modules!./styles.css';
  • Prefixing with -! will disable all configured preLoaders and loaders but not postLoaders
import Styles from '-!style-loader!css-loader?modules!./styles.css';

这边提到了preLoaders ,postLoaders ,在webpack 1.0是有这个配置的,v2就拿掉了,但是概念还是在的,变成了enforce配置,如下:

  module: {
-   preLoaders: [
+   rules: [
      {
        test: /\.js$/,
+       enforce: "pre",
        loader: "eslint-loader"
      }
    ]
  }

pre将该loader设置到最前面,post将该loader设置到最后面。但是loader的执行是有两个阶段的。

  1. pitching 阶段:loader 上的 pitch 方法,按照 后置(post)、行内(normal)、普通(inline)、前置(pre) 的顺序调用。更多详细信息,请查看 越过 loader(pitching loader)

  2. normal阶段:loader 上的 常规方法,按照 前置(pre)、行内(normal)、普通(inline)、后置(post) 的顺序调用。模块源码的转换,发生在这个阶段。

官方文档举了两个阶段的例子

module.exports = {
  //...
  module: {
    rules: [
      {
        //...
        use: [
          'a-loader',
          'b-loader',
          'c-loader'
        ]
      }
    ]
  }
};

将会发生这些步骤:

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

这也就解释了style-loader为什么代码都写在module.exports.pitch中而且module.exports返回的是空函数。

  • webpack怎样将js插入到dom里的
    html-webpack-plugin的index.js -> postProcessHtml函数 -> injectAssetsIntoHtml函数 -> 赋值给webpack的compilation.assets
    整个流程HTML都是字符串,然后HTML字符串交给了webpack的Compiler.js处理

  • html字符串是怎样写入到文件里的?

content = Buffer.from(bufferOrString, "utf8")//将字符串转为Buffer
this.outputFileSystem.writeFile(targetPath, content, err => {})//outputFileSystem为fs或者memory-fs,content为Buffer

就这两个重要步骤

html-webpacl-plugin apply方法,官方注释已经写得很明确

        const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
          // Allow plugins to change the html before assets are injected
          .then(([assetTags, html]) => {
            const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: self, outputName: childCompilationOutputName };
            return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
          })
          .then(({ html, headTags, bodyTags }) => {
            return self.postProcessHtml(html, assets, { headTags, bodyTags });//将<srcipt src="xxxx">标签插入HTML字符串
          });

        const emitHtmlPromise = injectedHtmlPromise
          // Allow plugins to change the html after assets are injected
          .then((html) => {
            const pluginArgs = { html, plugin: self, outputName: childCompilationOutputName };
            return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
              .then(result => result.html);
          })
          .catch(err => {
            // In case anything went wrong the promise is resolved
            // with the error message and an error is logged
            compilation.errors.push(prettyError(err, compiler.context).toString());
            // Prevent caching
            self.hash = null;
            return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
          })
          .then(html => {
            // Allow to use [templatehash] as placeholder for the html-webpack-plugin name
            // See also https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
            // From https://github.com/webpack-contrib/extract-text-webpack-plugin/blob/8de6558e33487e7606e7cd7cb2adc2cccafef272/src/index.js#L212-L214
            const finalOutputName = childCompilationOutputName.replace(/\[(?:(\w+):)?templatehash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, (_, hashType, digestType, maxLength) => {
              return loaderUtils.getHashDigest(Buffer.from(html, 'utf8'), hashType, digestType, parseInt(maxLength, 10));
            });
              // Add the evaluated html code to the webpack assets
            compilation.assets[finalOutputName] = {
              source: () => html,//此处的HTML任然为字符串
              size: () => html.length
            };
            return finalOutputName;
          })
          .then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
            outputName: finalOutputName,
            plugin: self
          }).catch(err => {
            console.error(err);
            return null;
          }).then(() => null));

再次验证script标签是在afterTemplateExecution跟beforeEmit插入到HTML字符串中的,但是没有写入到文件中,只是将拼接好的HTML字符串,交给了webpack

compilation.assets

compilation.assets是个对象,存了文件名跟对应文件内容的字符串。
HtmlWebpackPlugin index.js

        emitHtmlPromise.then(() => {
          callback();//出发了callAsync的回调
        });

Compiler.js emitAssets方法


image.png

这里调用了memory-fs的mkdirp创建目录,然后根据文件分别新建对应的文件

// Write the file to output file system
this.outputFileSystem.writeFile(targetPath, content, err => {})

而memory-fswriteFileSync就是将文件用二进制的形式存在了current对象里

writeFileSync

字符串转为二进制对象content = Buffer.from(bufferOrString, "utf8");至于怎样将这个对象显示在chrome的source面板中的,有空再探索。

webpacl Compilerthis.outputFileSystem的实例属性默认为null,start.js新建devServer const devServer = new WebpackDevServer(compiler, serverConfig);传递webpack compiler实例到dev server ,然后dev server再将compiler传递到dev middleware ,webpack-dev-middleware@3.7.2 fs.js setFs函数中给outputFileSystem赋值

const MemoryFileSystem = require('memory-fs');

      fileSystem = new MemoryFileSystem();

      // eslint-disable-next-line no-param-reassign
      compiler.outputFileSystem = fileSystem;

同样我们如果在webpack-dev-server配置writeToDisk:true,fs.js文件的toDisk函数调用fs.writeFile将文件写入磁盘

const fs = require('fs');

              return fs.writeFile(targetPath, content, (writeFileError) => {
                   return callback();
              });

在构建的时候没有dev server是怎样将文件写入磁盘的?

const webpack = (options, callback) => {
    let compiler;
           if (typeof options === "object") {
        compiler = new Compiler(options.context);
        compiler.options = options;
        new NodeEnvironmentPlugin({
            infrastructureLogging: options.infrastructureLogging
        }).apply(compiler);
        if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);//调用plugin的apply函数
                }
            }
        }
    } 
    return compiler;
};

NodeEnvironmentPlugin.js

class NodeEnvironmentPlugin {
    constructor(options) {
        this.options = options || {};
    }

    apply(compiler) {
        ...
        compiler.inputFileSystem = new CachedInputFileSystem(
            new NodeJsInputFileSystem(),
            60000
        );
        const inputFileSystem = compiler.inputFileSystem;
        compiler.outputFileSystem = new NodeOutputFileSystem();//给outputFileSystem赋值
        compiler.watchFileSystem = new NodeWatchFileSystem(
            compiler.inputFileSystem
        );
        ...
    }
}
module.exports = NodeEnvironmentPlugin;

NodeOutputFileSystem.js

const fs = require("fs");
const path = require("path");
const mkdirp = require("mkdirp");

class NodeOutputFileSystem {
    constructor() {
        this.mkdirp = mkdirp;
        this.mkdir = fs.mkdir.bind(fs);
        this.rmdir = fs.rmdir.bind(fs);
        this.unlink = fs.unlink.bind(fs);
        this.writeFile = fs.writeFile.bind(fs);//其实就是调用node原生的fs
        this.join = path.join.bind(path);
    }
}

module.exports = NodeOutputFileSystem;

最后在Compiler.js line 434,调用node的 fs.writeFile函数写入文件到磁盘

// Write the file to output file system
this.outputFileSystem.writeFile(targetPath, content, err => {})

仓库地址 webpack-speed-measure

webpack是怎样监测文件的变化的?

loader 在什么时候执行的?
loader-runner负责执行loader

你真的掌握了loader么?- loader十问
从零实现一个 Webpack Loader

plugin跟loader是并行执行的吗?有什么先后顺序吗?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,332评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,930评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,204评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,348评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,356评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,447评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,862评论 3 394
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,516评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,710评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,518评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,582评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,295评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,848评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,881评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,121评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,737评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,280评论 2 341

推荐阅读更多精彩内容