>  기사  >  웹 프론트엔드  >  웹팩 원리에 대한 심층 소개(예제 포함)

웹팩 원리에 대한 심층 소개(예제 포함)

不言
不言앞으로
2019-01-15 10:29:133358검색
이 글은 webpack의 원리에 대한 심층적인 소개를 제공합니다(예제 포함). 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

이 글은 "간단한 언어로 된 Webpack 입문"에서 복사한 것입니다. 원리를 배우고 싶은 분들은 한 번 쳐보고, 한 번 조작하고, 다른 사람에게 설명하면 알 수 있을 거에요

읽기 전에 웹팩과 관련된 실무 경험이 있기를 바랍니다. 그렇지 않으면 읽을 수 없습니다.

이 글을 읽는 데는 몇 분이 걸리고, 이해하는 데는 오랜 시간이 걸립니다. by yourself

0 구성 파일

먼저 웹팩 구성 파일(webpack.config.js)을 간략하게 살펴보세요:

var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');

module.exports = {
  // 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。
  entry: {
    bundle: [
      'webpack/hot/dev-server',
      'webpack-dev-server/client?http://localhost:8080',
      path.resolve(__dirname, 'app/app.js')
    ]
  },
  // 文件路径指向(可加快打包过程)。
  resolve: {
    alias: {
      'react': pathToReact
    }
  },
  // 生成文件,是模块构建的终点,包括输出文件与输出路径。
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name].js'
  },
  // 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel',
        query: {
          presets: ['es2015', 'react']
        }
      }
    ],
    noParse: [pathToReact]
  },
  // webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};

1 작동 원리 개요

1.1 기본 개념

전에 webpack의 원리를 이해하려면 다음 핵심 개념을 숙달해야 합니다

  • Entry: Entry, Webpack 구성의 첫 번째 단계는 Entry부터 시작됩니다.

  • module: module, webpack에서는 하나의 모듈이 하나의 파일에 해당합니다. Webpack은 항목부터 시작하여 모든 종속 모듈을 반복적으로 찾습니다.

  • Chunk: 코드 블록, 청크는 여러 모듈로 구성되며, 코드 병합 및 분할에 사용됩니다.

  • Loader: 모듈 변환기, 사용됨 필요에 따라 새로운 콘텐츠에 모듈을 추가하세요

  • 플러그인: 확장 플러그인, 해당 이벤트는 웹팩 구성 프로세스의 특정 시간에 방송됩니다. 플러그인은 이러한 이벤트의 발생을 모니터링하고 특정 시간에 해당 작업을 수행할 수 있습니다.

1.2 프로세스 개요

webpack은 처음부터 끝까지 다음 작업을 수행합니다.

graph TD
初始化参数 --> 开始编译 
开始编译 -->确定入口 
确定入口 --> 编译模块
编译模块 --> 完成编译模块
完成编译模块 --> 输出资源
输出资源 --> 输出完成

각 단계에서 수행되는 작업은 다음과 같습니다.

  1. 초기화 매개변수: 구성 파일에서(기본 webpack.config.js) ) 및 쉘 문 최종 매개변수를 얻기 위해 매개변수를 읽고 병합합니다

  2. 컴파일 시작(컴파일): 이전 단계에서 얻은 매개변수로 Comiler 객체를 초기화하고 구성된 모든 플러그인을 로드한 후 다음을 실행하여 컴파일을 시작합니다. 객체의 run 메소드

  3. 항목 결정: 구성의 항목에 따라 모든 항목 파일 찾기

  4. 모듈 컴파일: 항목 파일에서 시작하여 구성된 모든 로더를 호출하여 모듈을 번역한 다음 찾기 모든 항목 종속 파일이 처리될 때까지 이 단계를 반복합니다.

  5. 모듈 컴파일: 네 번째 단계 이후 번역 후 각 모듈의 최종 콘텐츠와 이들 간의 종속성 을 얻습니다

  6. 출력 리소스: 모듈 간의 항목 종속성에 따라 여러 모듈을 포함하는 청크로 조립한 다음 각 청크를 별도의 파일로 변환하여 출력 목록에 추가합니다. 출력 내용

  7. 출력 완료 : 출력 내용 결정 후, 구성(webpack.config.js && shell)에 따라 출력 경로와 파일명을 결정하고, 파일 내용을 파일 시스템(fs)에 쓴다.

위 프로세스에서 webpack은 특정 시점에 특정 이벤트를 브로드캐스팅하고, 플러그인은 이벤트를 수신하고 해당 로직을 실행하며, 플러그인은 webpack에서 제공하는 API를 호출하여 실행 결과를 변경할 수 있습니다. of webpack

1.3 프로세스 세부정보

웹팩 구축 프로세스는 다음 세 단계로 나눌 수 있습니다.

  1. 초기화: 빌드 시작, 구성 매개변수 읽기 및 병합, 플러그인 로드, 컴파일러 인스턴스화

  2. 컴파일: Entry에서 시작하여 각 모듈에 대해 해당 로더를 순차적으로 호출하여 파일의 내용을 번역한 다음 찾기 모듈이 의존하고 있는 모듈을 재귀적으로 컴파일한다. 출력: 컴파일된 모듈을 덩어리로 결합하고, 덩어리를 파일로 변환하여 파일 시스템에 출력한다. 한 번만 실행하면 위와 같지만, 리스닝 모드를 켰을 때 과정은 아래와 같습니다

    graph TD
    
      初始化-->编译;
      编译-->输出;
      输出-->文本发生变化
      文本发生变化-->编译
  3. 1.3.1 초기화 단계
  4. 초기화 단계에서 발생하는 이벤트는 다음과 같습니다

Event

Description

인스턴스화 컴파일러에서 플러그인 인스턴스화 문 new Plugin()을 실행합니다. 플러그인 로드environmentEntry-optionAfter-pluginsAfter-resolvers

#### 1.3.2 컴파일 단계(이벤트 이름은 모두 소문자)

초기화 매개변수 구성 파일에서 쉘 문의 매개변수를 읽고 병합하여 최종 매개변수를 얻습니다. 이 프로세스는 또한 구성 파일
컴파일러를 인스턴스화하고 이전 단계에서 전달 컴파일러는 획득한 매개변수를 사용하여 파일 모니터링 및 컴파일 시작을 담당합니다. 컴파일러 인스턴스에는 완전한 웹팩 구성이 포함되어 있으며 전역적으로 컴파일러 인스턴스는 하나만 있습니다.
플러그인의 적용 메소드를 순서대로 호출하면 플러그인이 모든 후속 이벤트 노드를 모니터링할 수 있습니다. 동시에 컴파일러 인스턴스에 대한 참조를 플러그인에 전달하여 플러그인이 컴파일러를 통해 웹팩 API를 호출할 수 있도록 합니다
컴파일러에 Node.js 스타일 파일 시스템 적용을 시작하세요. 후속 파일 찾기 및 읽기를 용이하게 하는 객체
구성된 항목을 읽고, 각 항목에 대해 해당 EntryPlugin을 인스턴스화하고, 항목의 후속 재귀 구문 분석을 준비합니다.
호출 모두 내장되어 있고 구성된 플러그인
의 적용 메소드는 구성에 따라 리졸버를 초기화하며, 리졸버는 파일 시스템에서 지정된 경로의 파일을 찾는 역할을 담당합니다
runWatch-runcompilecompilationmakeafter-compileinvalid컴파일 단계에서 로더가 호출되기 때문에 컴파일 단계에서 가장 중요한 이벤트는 컴파일입니다. 그리고 각 모듈의 == 변환 == 작업이 완료됩니다. 다음 표와 같이 컴파일 단계에서 많은 작은 이벤트가 발생합니다.
이벤트 설명
컴파일 시작
은 듣기 모드입니다. 컴파일, 파일이 변경되면 다시 컴파일됩니다
은 플러그인에 새 컴파일이 곧 시작될 것임을 알리고 컴파일러 개체를 플러그인
으로 가져옵니다. webpack이 개발 모드에서 실행 중일 때 파일 변경이 감지될 때마다 새로운 컴파일이 생성됩니다. Compilation 객체에는 현재 모듈 리소스, 컴파일된 리소스, 변경된 파일 등이 포함됩니다. 컴파일 개체는 플러그인 확장을 위한 많은 이벤트 콜백도 제공합니다
새 컴파일 개체가 생성되고 해당 항목에서 파일을 읽으며 파일은 == 다음에 따라 컴파일됩니다. 파일 형식과 컴파일된 로더를 확인합니다. 컴파일 후 해당 파일이 의존하는 파일을 찾아서 재귀적으로 컴파일하고 구문 분석합니다
한 번의 컴파일 실행이 완료됩니다
오류가 발생하면 , 변경 이벤트가 트리거되지 않습니다. 이 이벤트는 webpack을 종료하게 합니다.


eventprogramseal출력해야 할 파일이 모두 생성되었습니다. 출력해야 할 파일과 출력할 필요가 없는 파일을 플러그인에 물어보세요출력할 파일을 결정한 후 파일 출력을 실행합니다. ==여기에서 출력 내용을 얻고 수정할 수 있습니다= =after-mit
module-loader 로더를 사용하여 모듈을 변환한 후 acorn을 사용하여 변환된 내용을 구문 분석하고 해당 추상 구문 트리(AST)를 출력하여 웹팩의 코드 분석을 용이하게 합니다
구성된 입력 모듈에서 시작 AST에서 다른 모듈을 가져오는 require 문을 만나면 종속 모듈 목록에 추가됩니다. 동시에 새로 발견된 모듈을 재귀적으로 분석하고 최종적으로 모든 모듈의 종속성을 파악합니다.
모든 모듈과 종속 모듈은 Loader를 통해 변환되며, 종속성에 따라 청크가 생성됩니다. Explanation
should-emit
emit


파일 출력 완료
done

전체 컴파일 및 출력 과정을 성공적으로 완료

실패 에서 특정 오류 원인을 얻을 수 있습니다.

在输出阶段已经得到了各个模块经过转化后的结果和其依赖关系,并且将相应的模块组合在一起形成一个个chunk.在输出阶段根据chunk的类型,使用对应的模板生成最终要输出的文件内容. |

//以下代码用来包含webpack运行过程中的每个阶段
//file:webpack.config.js

const path = require('path');
//插件监听事件并执行相应的逻辑
class TestPlugin {
  constructor() {
    console.log('@plugin constructor');
  }

  apply(compiler) {
    console.log('@plugin apply');

    compiler.plugin('environment', (options) => {
      console.log('@environment');
    });

    compiler.plugin('after-environment', (options) => {
      console.log('@after-environment');
    });

    compiler.plugin('entry-option', (options) => {
      console.log('@entry-option');
    });

    compiler.plugin('after-plugins', (options) => {
      console.log('@after-plugins');
    });

    compiler.plugin('after-resolvers', (options) => {
      console.log('@after-resolvers');
    });

    compiler.plugin('before-run', (options, callback) => {
      console.log('@before-run');
      callback();
    });

    compiler.plugin('run', (options, callback) => {
      console.log('@run');
      callback();
    });

    compiler.plugin('watch-run', (options, callback) => {
      console.log('@watch-run');
      callback();
    });

    compiler.plugin('normal-module-factory', (options) => {
      console.log('@normal-module-factory');
    });

    compiler.plugin('context-module-factory', (options) => {
      console.log('@context-module-factory');
    });

    compiler.plugin('before-compile', (options, callback) => {
      console.log('@before-compile');
      callback();
    });

    compiler.plugin('compile', (options) => {
      console.log('@compile');
    });

    compiler.plugin('this-compilation', (options) => {
      console.log('@this-compilation');
    });

    compiler.plugin('compilation', (options) => {
      console.log('@compilation');
    });

    compiler.plugin('make', (options, callback) => {
      console.log('@make');
      callback();
    });

    compiler.plugin('compilation', (compilation) => {

      compilation.plugin('build-module', (options) => {
        console.log('@build-module');
      });

      compilation.plugin('normal-module-loader', (options) => {
        console.log('@normal-module-loader');
      });

      compilation.plugin('program', (options, callback) => {
        console.log('@program');
        callback();
      });

      compilation.plugin('seal', (options) => {
        console.log('@seal');
      });
    });

    compiler.plugin('after-compile', (options, callback) => {
      console.log('@after-compile');
      callback();
    });

    compiler.plugin('should-emit', (options) => {
      console.log('@should-emit');
    });

    compiler.plugin('emit', (options, callback) => {
      console.log('@emit');
      callback();
    });

    compiler.plugin('after-emit', (options, callback) => {
      console.log('@after-emit');
      callback();
    });

    compiler.plugin('done', (options) => {
      console.log('@done');
    });

    compiler.plugin('failed', (options, callback) => {
      console.log('@failed');
      callback();
    });

    compiler.plugin('invalid', (options) => {
      console.log('@invalid');
    });

  }
}
#在目录下执行
webpack
#输出以下内容
@plugin constructor
@plugin apply
@environment
@after-environment
@entry-option
@after-plugins
@after-resolvers
@before-run
@run
@normal-module-factory
@context-module-factory
@before-compile
@compile
@this-compilation
@compilation
@make
@build-module
@normal-module-loader
@build-module
@normal-module-loader
@seal
@after-compile
@should-emit
@emit
@after-emit
@done
Hash: 19ef3b418517e78b5286
Version: webpack 3.11.0
Time: 95ms
    Asset     Size  Chunks             Chunk Names
bundle.js  3.03 kB       0  [emitted]  main
   [0] ./main.js 44 bytes {0} [built]
   [1] ./show.js 114 bytes {0} [built]

2 输出文件分析

2.1 举个栗子

下面通过 Webpack 构建一个采用 CommonJS 模块化编写的项目,该项目有个网页会通过 JavaScript 在网页中显示 Hello,Webpack

运行构建前,先把要完成该功能的最基础的 JavaScript 文件和 HTML 建立好,需要如下文件:

页面入口文件 index.html


  <meta>


<p></p>
<!--导入 Webpack 输出的 JavaScript 文件-->
<script></script>

JS 工具函数文件 show.js

// 操作 DOM 元素,把 content 显示到网页上
function show(content) {
  window.document.getElementById('app').innerText = 'Hello,' + content;
}

// 通过 CommonJS 规范导出 show 函数
module.exports = show;

JS 执行入口文件 main.js

// 通过 CommonJS 规范导入 show 函数
const show = require('./show.js');
// 执行 show 函数
show('Webpack');

Webpack 在执行构建时默认会从项目根目录下的 webpack.config.js 文件读取配置,所以你还需要新建它,其内容如下:

const path = require('path');

module.exports = {
  // JavaScript 执行入口文件
  entry: './main.js',
  output: {
    // 把所有依赖的模块合并输出到一个 bundle.js 文件
    filename: 'bundle.js',
    // 输出文件都放到 dist 目录下
    path: path.resolve(__dirname, './dist'),
  }
};

由于 Webpack 构建运行在 Node.js 环境下,所以该文件最后需要通过 CommonJS 规范导出一个描述如何构建的 Object 对象。

|-- index.html
|-- main.js
|-- show.js
|-- webpack.config.js

一切文件就绪,在项目根目录下执行 webpack 命令运行 Webpack 构建,你会发现目录下多出一个 dist目录,里面有个 bundle.js 文件, bundle.js 文件是一个可执行的 JavaScript 文件,它包含页面所依赖的两个模块 main.jsshow.js 及内置的 webpackBootstrap 启动函数。 这时你用浏览器打开 index.html 网页将会看到 Hello,Webpack

2.2 bundle.js文件做了什么

看之前记住:一个模块就是一个文件,

首先看下bundle.js长什么样子:

웹팩 원리에 대한 심층 소개(예제 포함)

注意:序号1处是个自执行函数,序号2作为自执行函数的参数传入

具体代码如下:(建议把以下代码放入编辑器中查看,最好让index.html执行下,弄清楚执行的顺序)

(function(modules) { // webpackBootstrap
  // 1. 缓存模块
  var installedModules = {};
  // 2. 定义可以在浏览器使用的require函数
  function __webpack_require__(moduleId) {

    // 2.1检查模块是否在缓存里,在的话直接返回
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 2.2 模块不在缓存里,新建一个对象module=installModules[moduleId] {i:moduleId,l:模块是否加载,exports:模块返回值}
    var module = installedModules[moduleId] = {
      i: moduleId,//第一次执行为0
      l: false,
      exports: {}
    };//第一次执行module:{i:0,l:false,exports:{}}
    // 2.3 执行传入的参数中对应id的模块 第一次执行数组中传入的第一个参数
          //modules[0].call({},{i:0,l:false,exports:{}},{},__webpack_require__函数)
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 2.4 将这个模块标记为已加载
    module.l = true;
    // 2.5 返回这个模块的导出值
    return module.exports;
  }
  // 3. webpack暴露属性 m c d n o p
  __webpack_require__.m = modules;
  __webpack_require__.c = installedModules;
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        configurable: false,
        enumerable: true,
        get: getter
      });
    }
  };
  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };
  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
  __webpack_require__.p = "";
  // 4. 执行reruire函数引入第一个模块(main.js对应的模块)
  return __webpack_require__(__webpack_require__.s = 0);
})
([ // 0. 传入参数,参数是个数组

  /* 第0个参数 main.js对应的文件*/
  (function(module, exports, __webpack_require__) {

    // 通过 CommonJS 规范导入 show 函数
    const show = __webpack_require__(1);//__webpack_require__(1)返回show
    // 执行 show 函数
    show('Webpack');

  }),
  /* 第1个参数 show.js对应的文件 */
  (function(module, exports) {

    // 操作 DOM 元素,把 content 显示到网页上
    function show(content) {
      window.document.getElementById('app').innerText = 'Hello,' + content;
    }
    // 通过 CommonJS 规范导出 show 函数
    module.exports = show;

  })
]);

以上看上去复杂的代码其实是一个自执行函数(文件作为自执行函数的参数),可以简写如下:

(function(modules){
    //模拟require语句
    function __webpack_require__(){}
    //执行存放所有模块数组中的第0个模块(main.js)
    __webpack_require_[0]
})([/*存放所有模块的数组*/])

bundles.js能直接在浏览器中运行的原因是,在输出的文件中通过__webpack_require__函数,定义了一个可以在浏览器中执行的加载函数(加载文件使用ajax实现),来模拟Node.js中的require语句。

原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。

修改main.js,改成import引入模块

import show from './show';
show('Webpack');

在目录下执行webpack,会发现:

  1. 生成的代码会有所不同,但是主要的区别是自执行函数的参数不同,也就是2.2代码的第二部分不同

([//自执行函数和上面相同,参数不同
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__show__ = __webpack_require__(1);

Object(__WEBPACK_IMPORTED_MODULE_0__show__["a" /* default */])('Webpack');


}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = show;
function show(content) {
  window.document.getElementById('app').innerText = 'Hello,' + content;
}


})
]);

参数不同的原因是es6的import和export模块被webpack编译处理过了,其实作用是一样的,接下来看一下在main.js中异步加载模块时,bundle.js是怎样的

2.3异步加载时,bundle.js代码分析

main.js修改如下

import('./show').then(show=>{
    show('Webpack')
})

构建成功后会生成两个文件

  1. bundle.js  执行入口文件

  2. 0.bundle.js 异步加载文件

其中0.bundle.js文件的内容如下:

webpackJsonp(/*在其他文件中存放的模块的ID*/[0],[//本文件所包含的模块
/* 0 */,
/* 1 show.js对应的模块 */
(function(module, __webpack_exports__, __webpack_require__) {

  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
  /* harmony export (immutable) */ 
  __webpack_exports__["default"] = show;

  function show(content) {
    window.document.getElementById('app').innerText = 'Hello,' + content;
  }

})
]);

bundle.js文件的内容如下:

注意:bundle.js比上面的bundle.js的区别在于:

  1. 多了一个__webpack_require__.e,用于加载被分割出去的需要异步加载的chunk对应的文件

  2. 多了一个webpackJsonp函数,用于从异步加载的文件中安装模块

(function(modules) { // webpackBootstrap
    // install a JSONP callback for chunk loading
  var parentJsonpFunction = window["webpackJsonp"];
  // webpackJsonp用于从异步加载的文件中安装模块
  // 将webpackJsonp挂载到全局是为了方便在其他文件中调用
  /**
   * @param chunkIds 异步加载的模块中需要安装的模块对应的id
   * @param moreModules 异步加载的模块中需要安装模块列表
   * @param executeModules 异步加载的模块安装成功后需要执行的模块对应的index
   */
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
        // add "moreModules" to the modules object,
        // then flag all "chunkIds" as loaded and fire callback
        var moduleId, chunkId, i = 0, resolves = [], result;
        for(;i {
    show('Webpack')
})


/***/ })
]);

컴파일 및 출력 중 오류가 발생하여 webpack이 종료되면, 플러그인은 이 이벤트

위 내용은 웹팩 원리에 대한 심층 소개(예제 포함)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제