webpack 知識點

1.webpack 模塊化實現(xiàn)

項目里面引入了moment

import moment from 'moment'

在非debug狀態(tài)下,嘗試輸入moment是拿不到moment這個模塊的,但是debug代碼的時候不能直接拿到moment這個模塊,而是通過__WEBPACK_IMPORTED_MODULE_18_moment___default可以拿到
按理說moment()調(diào)用一次就拿到了moment對象,然而并不可以

moment

__WEBPACK_IMPORTED_MODULE_18_moment___default()()調(diào)用了兩次才拿到

  onTimeChange = val => {
    const mon = moment //這邊通過
    console.info(val)
  }
debug

從上圖可知 debug 狀態(tài)還是無法再控制臺輸出moment(),而賦值給變量mon就可以執(zhí)行mon()拿到moment對象,而__WEBPACK_IMPORTED_MODULE_18_moment___default()()還是要執(zhí)行兩次才能拿到moment對象

webpack務虛掃盲
從 Bundle 文件看 Webpack 模塊機制
webpack模塊加載原理

2. require.context

官網(wǎng)的解釋:

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結(jié)尾的文件(包括其子目錄)
var componentA = context.resolve('componentA');

使用webpack的require.context實現(xiàn)路由“去中心化”管理
webpack require context 說明

3. 使用ejs作為HtmlWebpackPlugin的模板

通常情況下我們使用HTML作為單頁面的模板,

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

但是有時候我們需要在頁面用到變量或者遍歷數(shù)組,那么就需要使用模板引擎,比如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模版實戰(zhàn)

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
這樣就可以調(diào)試了,哈哈哈

報錯的原因好像是操作系統(tǒng)的問題,.bin目錄下是shell和cmd文件,直接node --inspect-brk ./node_modules/.bin/webpack就執(zhí)行shell,而在Windows上執(zhí)行node --inspect-brk ./node_modules/.bin/webpack.cmd也會報錯

SyntaxError: missing ) after argument list

我的項目是用create-react-app創(chuàng)建的,并且做了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創(chuàng)建的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有些代碼改了,而該庫沒有做出對不同系統(tǒng)文件系統(tǒng)路徑兼容的處理,導致代碼編譯的樣式?jīng)]有被緩存,是的每次rebuild都會去重新編譯樣式文件。

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

cra 新建的項目dev環(huán)境默認有三個文件,webpack@4.42.0


dev環(huán)境的三個js文件
html-webpack-plugin flow

由圖可以看出來,script標簽插入HTML字符串是在afterTemplateExecutionbeforeEmit之間完成的,做了代碼測試看到的結(jié)果也是如此,扒開源碼看看

文件名 作用 解釋
bundle.js webpack runtime代碼 包括webpack-dev-server
0.chunk.js 第三方庫代碼 react react-dom等
main.chunk.js 自己寫的代碼 包括自己寫的js跟css
  • webpack 怎樣把樣式代碼插入到js中的

  • webpack怎樣實現(xiàn)對樣式文件的緩存的

  • 是style-loader還是html-webpack-plugin將style標簽插入到dom中的

style-loader@1.0.0源碼各種拼接字符串還是很難讀的,找到一篇不錯的源碼解析文章webpack loader 從上手到理解系列 style-loader

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

!!從未見過,在js里是將其他類型轉(zhuǎn)為boolean,官方文檔顯示,有三種使用 loader 的方式

  • 配置(推薦):在 webpack.config.js 文件中指定 loader。
  • 內(nèi)聯(lián):在每個 import 語句中顯式指定 loader。
  • CLI:在 shell 命令中指定它們。

內(nèi)聯(lián)(inline)

可以在 import 語句或任何 等同于 "import" 的方法 中指定 loader。使用 ! 將資源中的 loader 分開。每個部分都會相對于當前目錄解析。

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

使用 ! 或者!或者-!為整個規(guī)則添加前綴,可以覆蓋webpack.config.js配置中的所有 loader 定義。具體規(guī)則如下:

  • 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的執(zhí)行是有兩個階段的。

  1. pitching 階段:loader 上的 pitch 方法,按照 后置(post)、行內(nèi)(normal)、普通(inline)、前置(pre) 的順序調(diào)用。更多詳細信息,請查看 越過 loader(pitching loader)。

  2. normal階段:loader 上的 常規(guī)方法,按照 前置(pre)、行內(nèi)(normal)、普通(inline)、后置(post) 的順序調(diào)用。模塊源碼的轉(zhuǎn)換,發(fā)生在這個階段。

官方文檔舉了兩個階段的例子

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

將會發(fā)生這些步驟:

|- 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返回的是空函數(shù)。

  • webpack怎樣將js插入到dom里的
    html-webpack-plugin的index.js -> postProcessHtml函數(shù) -> injectAssetsIntoHtml函數(shù) -> 賦值給webpack的compilation.assets
    整個流程HTML都是字符串,然后HTML字符串交給了webpack的Compiler.js處理

  • html字符串是怎樣寫入到文件里的?

content = Buffer.from(bufferOrString, "utf8")//將字符串轉(zhuǎn)為Buffer
this.outputFileSystem.writeFile(targetPath, content, err => {})//outputFileSystem為fs或者memory-fs,content為Buffer

就這兩個重要步驟

html-webpacl-plugin apply方法,官方注釋已經(jīng)寫得很明確

        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是個對象,存了文件名跟對應文件內(nèi)容的字符串。
HtmlWebpackPlugin index.js

        emitHtmlPromise.then(() => {
          callback();//出發(fā)了callAsync的回調(diào)
        });

Compiler.js emitAssets方法


image.png

這里調(diào)用了memory-fs的mkdirp創(chuàng)建目錄,然后根據(jù)文件分別新建對應的文件

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

而memory-fswriteFileSync就是將文件用二進制的形式存在了current對象里

writeFileSync

字符串轉(zhuǎn)為二進制對象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函數(shù)中給outputFileSystem賦值

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

      fileSystem = new MemoryFileSystem();

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

同樣我們?nèi)绻趙ebpack-dev-server配置writeToDisk:true,fs.js文件的toDisk函數(shù)調(diào)用fs.writeFile將文件寫入磁盤

const fs = require('fs');

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

在構(gòu)建的時候沒有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);//調(diào)用plugin的apply函數(shù)
                }
            }
        }
    } 
    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);//其實就是調(diào)用node原生的fs
        this.join = path.join.bind(path);
    }
}

module.exports = NodeOutputFileSystem;

最后在Compiler.js line 434,調(diào)用node的 fs.writeFile函數(shù)寫入文件到磁盤

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

倉庫地址 webpack-speed-measure

webpack是怎樣監(jiān)測文件的變化的?

loader 在什么時候執(zhí)行的?
loader-runner負責執(zhí)行l(wèi)oader

你真的掌握了loader么?- loader十問
從零實現(xiàn)一個 Webpack Loader

plugin跟loader是并行執(zhí)行的嗎?有什么先后順序嗎?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容