精华内容
下载资源
问答
  • webpack打包流程

    2021-04-26 18:29:17
    在前端工程化中,大家用得比较多的就是webpack了,今天就总结一下webpack打包流程。 首先给一个目录结构 src add.js:求和 minus.js:求差 index.js:入口文件 bundle.js:打包文件的js代码 //有人知道那种文件...

    在前端工程化中,大家用得比较多的就是webpack了,今天就总结一下webpack打包流程。

    首先给一个目录结构

    src
    	add.js:求和
    	minus.js:求差
    	index.js:入口文件
    bundle.js:打包文件的js代码
    //有人知道那种文件目录是如何弄的么?知道的,请给我留言,非常感谢!
    

    bundle.js代码:

    // 文件读取
    const fs = require('fs')
    // 文件解析成AST
    const parser = require('@babel/parser')
    // AST中收集依赖
    const traverse = require('@babel/traverse').default
    // 路径拼接
    const path = require('path')
    // ES6=>ES5
    const babel = require('@babel/core')
    
    
    const getModuleInfo = (file) => {
    	// file:入口文件
    	// 获取文件的目录
        const abPath = path.dirname(file)
        console.log('获取文件的目录:', abPath);
        
        // readFileSync:可以不用回调函数,如果用readFile需要定义回调函数。
        // 拿到文件内容
        const body = fs.readFileSync(file, 'utf8')
        console.log('文件内容', body);
        // 解析文件内容构建成AST语法树,主要是得到文件的路径依赖关系
        const AST = parser.parse(body, {
            sourceType: 'module'
            // 表示我们解析的是ES6模块
        })
        // 节点数组,在value中存放着路径信息
        console.log(AST.program.body);
    
        // 收集当前文件所依赖的文件地址
        const deps = {}
        traverse(AST, {
            ImportDeclaration({ node }) {
            	// 将当前节点的路径依赖存进deps中
            	// mac电脑:const p1 = './' + path.join(abPath, node.source.value)
                const p1 = '.\\' + path.join(abPath, node.source.value)
                deps[node.source.value] = p1
            }
        })
        console.log('文件依赖:', deps);
        
        // babel转化
        const { code } = babel.transformFromAst(AST, null, {
            presets: ['@babel/preset-env']
        })
        console.log(code);
    }
    getModuleInfo('./src/index.js')
    

    结果如图
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    显然,我们可以得到文件的依赖信息,并做了代码转化,所以我们后面要做的就是对我们所导入的文件进行递归解析,这里就不赘述了,原理上是差不多的。

    总结: webpack打包流程可以简单概括为

    1、读取入口文件;
    2、分析入口文件,得到其所依赖的文件信息;
    3、递归读取每个文件的依赖信息,生成AST树,得到关系依赖图;
    4、代码转化,生成ES5代码
    
    展开全文
  • 创建一个webpack打包流程 首先安装webpack插件 mkdir webpack-demo && cd webpack-demo npm init -y npm install --save-dev webpack npm install --save lodash 创建index.html文件 <html> <...

    创建一个webpack打包流程

    首先安装webpack插件

    mkdir webpack-demo && cd webpack-demo
    npm init -y 
    npm install --save-dev webpack
    npm install --save lodash

    创建index.html文件

    <html>
      <head>
        <title>webpack 2 demo</title>
      </head>
      <body>
        <script src="./dist/bundle.js"></script>
      </body>
    </html>

    创建app/index.js文件

    'use strict';
    import _ from 'lodash';//加载
    
    function component () {
      var element = document.createElement('div');
    
      /* lodash is required for the next line to work */
      element.innerHTML = _.join(['Hello','webpack'], ' ');
    
      return element;
    }
    
    document.body.appendChild(component());

    建立一个webpack.config.js文件

    var path = require('path');//调用path模块
    module.exports = {
      entry: './app/index.js',//入口
      output: {
        filename: './bundle.js',//输出文件
        path: path.resolve(__dirname, 'dist')//输出目录
      }
    };

    使用npm命令行执行

    {
      ...
      "scripts": {
        "build": "webpack -p"
      },
      ...
    }
    
    npm run build

    package.json

    {
      "name": "create",
      "version": "1.0.0",
      "description": "mkdir webpack-demo && cd webpack-demo\r npm init -y\r npm install --save-dev webpack",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build" : "webpack -p"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "webpack": "^2.2.1"
      },
      "dependencies": {
        "lodash": "^4.17.4"
      }
    }
    

    官网传送门

    转载于:https://www.cnblogs.com/caijw/p/6394563.html

    展开全文
  • webpack打包流程实际上是根据文件间的依赖关系对其进行静态分析,然后将这些模块按指定规则生成静态资源,webpack会将这些关系整理成一个依赖关系图(dependency graph),整理完成后再根据整个依赖关系图来生成文件...

    webpack打包的流程实际上是根据文件间的依赖关系对其进行静态分析,然后将这些模块按指定规则生成静态资源,webpack会将这些关系整理成一个依赖关系图(dependency graph),整理完成后再根据整个依赖关系图来生成文件,所以我们可以大致的将整个打包流程分为4步

    1. 根据入口文件,分析依赖
    2. 逐级分析,确定用到的所有模块
    3. 将这些模块以及他们之间的关系生成一个依赖关系图
    4. 依据这个依赖关系图生成文件

    为了完成这几个步骤,我们需要

    1. 一个帮助我们分析单个模块的module analyzer的函数
    2. 一个帮助我们生成dependency graph的函数
    3. 一个帮助我们根据dependency graph生成代码的函数

    确定了需求,我们就可以着手来实现了
    首先新建一个项目
    我的项目路径如下
    在这里插入图片描述

    // index.js
    import sayHi from './sayHi.js'
    sayHi()
    
    // sayHi.js
    import {name} from './sub/name.js'
    import say from './sub/say.js'
    export default function(){
        say(`hi ${name}`)
    }
    
    // name.js
    export const name = 'xz'
    
    // say.js
    export default function(message){
        console.log(message)
    }
    

    module analyzer
    module analyzer负责帮我们分析模块依赖并将该模块的代码转换成浏览器可执行的代码,我们需要用到babel来帮助我们进行分析

    const path = require("path");
    const fs = require("fs");
    // babel parser 用于生成抽象语法树
    const parser = require("@babel/parser");
    // babel core 用于将代码转换为浏览器可以运行的格式
    const core = require("@babel/core");
    
    // 分析模块依赖并将该模块的代码转换成浏览器可执行的代码
    const analyzeModule = (filepath) => {
      // 读取代码内容
      const content = fs.readFileSync(filepath, "utf-8");
      // 根据代码内容生成抽象语法树
      const ast = parser.parse(content, {
      	// es module引入需要加上sourceType: "module"
        sourceType: "module",
      });
      // 遍历代码部分,找到所有引入语句
      const dependencies = {};
      ast.program.body.forEach((item) => {
        // 将所有引用语句整理
        if (item.type == "ImportDeclaration") {
          const dirname = path.dirname(filepath);
          // 将路径转换为相对于文件根目录的相对路径
          const newFilePath = "./" + path.join(dirname, item.source.value);
          // dependencies以键值对的格式同时记录了相对于引用文件的路径以及相对于文件根目录的相对路径,后面需要用到
          dependencies[item.source.value] = newFilePath;
          // dependencies: { './sayHi.js': './src/sayHi.js' }
        }
      });
      // 将代码转换成浏览器可以运行的代码
      const { code } = core.transformFromAst(ast, null, {
        // 需要使用babel/preset-env
        presets: ["@babel/preset-env"],
      });
      // 返回关于这个文件的依赖以及转换后的代码
      return {
        file: filepath,
        dependencies,
        code,
      };
    };
    

    dependency graph
    完成了模块分析部分的代码,现在就需要来生成整个依赖图谱了,要根据入口文件递归找到所有用到的模块

    // 根据入口文件生成依赖图谱
    const createDependenciesGraph = (entry) => {
      // 将入口文件生成的模块分析结果放到graph中,遍历graph
      const graph = [analyzeModule(entry)];
      for (let i = 0; i < graph.length; i++) {
        // 如果有dependencies,遍历dependencies
        if (graph[i].dependencies) {
          for (let key in graph[i].dependencies) {
            // 通过analyzeModule分析每个dependencies,然后将分析结果添加至graph中
            graph.push(analyzeModule(graph[i].dependencies[key]));
          }
        }
      }
      // 此时的graph是数组格式的依赖图谱,为了方便使用将其转换为对象形式
      const dependenciesGraph = {};
      graph.forEach((item) => {
        dependenciesGraph[item.file] = {
          dependencies: item.dependencies,
          code: item.code,
        };
      });
      /*
      数据格式如下:
    {
      './src/index.js': {
        dependencies: { './sayHi.js': './src\\sayHi.js' },
        code: 代码
      },
      './src/sayHi.js':{
        dependencies: { 
     		'./sub/name.js': './src/sub/name.js',
          	'./sub/say.js': './src/sub/say.js'
    	},
        code: 代码
      },
      ....
    }
      */
      return dependenciesGraph;
    };
    

    generate code
    完成了依赖图谱,这时候就需要来将依赖图谱整理为代码了
    要让代码运行起来,首先要看看生成的代码是什么样子的
    在这里插入图片描述
    这是sayHi.js生成之后的文件
    他用到了一个exports对象以及一个require方法
    exports用于将自身的内容导出,require用于引入其他的依赖文件
    而生成的代码本身并没有提供这2个东西,所以我们需要自己完善
    内部的逻辑比较复杂,我们一步一步来

    首先,我们先根据入口生成依赖图谱,然后使用一个闭包的环境来执行代码,将这个依赖图谱传入,要注意因为我们是用字符串来拼接一段代码,所以传的时候要做一下转换

    // 根据依赖图谱生成代码
    const generateCode = (entry) => {
      const dependenciesGraph = createDependenciesGraph(entry);
      return `
        (function(dependenciesGraph){
    
        }(${JSON.stringify(dependenciesGraph)}))
      `;
    };
    

    然后我们来补充require方法
    require方法根据对应的模块,来用eval执行对应的代码,要注意执行代码时也要放在一个闭包环境中

    // 根据依赖图谱生成代码
    const generateCode = (entry) => {
      const dependenciesGraph = createDependenciesGraph(entry);
      return `
        (function(dependenciesGraph){
    		function require(module){
    		  (function(code){
                eval(code);
              }(dependenciesGraph[module].code))
    		}
            require('${entry}')
        }(${JSON.stringify(dependenciesGraph)}))
      `;
    };
    

    这时候由于生成的代码内部使用的路径并不是相对于根目录的路径,而我们的依赖图谱中的每项的key是相对于根目录的路径,所以会导致无法找到对应的代码
    在这里插入图片描述
    对此我们需要用一个新的require方法,这个require方法实际就是通过这个相对路径,找到相对于根目录的路径,然后执行require方法,将这个方法作为实参传入,函数内部通过形参require接受,这样内部调用require,实际上就是调用的这个新的require方法

    // 根据依赖图谱生成代码
    const generateCode = (entry) => {
      const dependenciesGraph = createDependenciesGraph(entry);
      return `
        (function(dependenciesGraph){
            function require (module){
              // 新的require方法,用于路径转换
              function localRequire(relativePath){
                return require(dependenciesGraph[module].dependencies[relativePath])
              };
              (function(require,code){
                eval(code);
              }(localRequire,dependenciesGraph[module].code))
              // 将方法传入,替换函数内部的require
            }
            require('${entry}')
        }(${JSON.stringify(dependenciesGraph)}))
      `;
    };
    

    至此require部分我们已经完成,还需要补充一个exports对象,这个exports对象直接是空对象即可,生成的代码会自动向里面添加一些属性,然后return这个exports对象即可

    const generateCode = (entry) => {
      const dependenciesGraph = createDependenciesGraph(entry);
      return `
        (function(dependenciesGraph){
            function require (module){
              function localRequire(relativePath){
                return require(dependenciesGraph[module].dependencies[relativePath])
              };
              var exports = {};
              (function(require,exports,code){
                eval(code);
              }(localRequire,exports,dependenciesGraph[module].code))
              return exports
            }
            require('${entry}')
        }(${JSON.stringify(dependenciesGraph)}))
      `;
    };
    

    至此生成代码部分的逻辑完成,我们可以将它生成一个js文件

    fs.mkdirSync(path.join(__dirname, "dist"));
    const code = generateCode("./src/index.js");
    const filePath = path.join(__dirname, "dist", "index.js");
    fs.writeFileSync(filePath, code, "utf-8");
    

    通过node执行该文件,得到生成后的代码如下
    在这里插入图片描述
    将代码复制到console直接运行,可以看到代码正确执行
    在这里插入图片描述
    至此,我们完成了一个十分简易的bundler,和webpack的差距还是非常大的,但我们可以通过这个简单的bundler,了解webpack运行的一些原理
    不过如果想要更深入的了解webpack或者编写loader和plugins的话可能就需要阅读webpack的源码,对内在的原理进行更深层次的了解
    不过对于使用者来说,webpack是一个强大的工具,我们可以通过他的配置,快速的达成我们想要的一些目的。并且它架构设计合理,扩展性高,通过社区弥补了大量功能,所以使它成为目前前端工程化的最佳选择,能适用于各种开发场景,是一个非常值得我们学习的前端工具

    展开全文
  • webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:首先会从配置文件和Shell语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数;初始化完成后会调用Compiler的run...

    webpack是当前最为流行,最为重要的打包构建工具,也是体现程序员水平的标准之一。那么webpack的打包流程到底是什么呢?话不多说,直接开干:

    webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:首先会从配置文件和 Shell 语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数;初始化完成后会调用Compilerrun来真正启动webpack编译构建过程,webpack的构建流程包括compilemakebuildsealemit阶段,执行完这些阶段就完成了构建过程。

    webpack的事件流机制 

    Webpack是基于事件流的插件集合,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,Tapable是一个类似Node.js的EventEmitter的库,主要是控制钩子函数的发布与订阅,控制着webpack的插件系统。Webpack中最核心的负责编译的Compiler和负责创建的捆绑包的Compilation都是Tapable实例。

    Tapable库暴露很多Hook类,为插件提供挂载的钩子。

    const {
    	SyncHook,  // 同步钩子
    	SyncBailHook, // 同步熔断钩子
    	SyncWaterfallHook, // 同步流水钩子
    	SyncLoopHook, // 同步循环钩子
    	AsyncParallelHook, // 异步并发钩子
    	AsyncParallelBailHook, // 异步并发熔断钩子
    	AsyncSeriesHook, // 异步串行钩子
    	AsyncSeriesBailHook, // 异步串行熔断钩子
    	AsyncSeriesWaterfallHook // 异步串行流水钩子
     } = require("tapable");
    const hook = new SyncHook(["arg1", "arg2", "arg3"]);

     Tapable提供了同步和异步绑定钩子的方法,并且他们都有绑定事件以及执行事件对应的方法。

    Async*
    绑定: tapAsync/tapPromise/tap   执行: callAsync/promise
    Sync*
    绑定: tap                       执行: call

    Tapable实例:

    const { SyncHook } = require("tapable");
    let queue = new SyncHook(['name']); //所有的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。
    
    // 订阅
    queue.tap('1', function (name, name2) {// tap 的第一个参数是用来标识订阅的函数的
        console.log(name, name2, 1);
        return'1'
    });
    queue.tap('2', function (name) {
        console.log(name, 2);
    });
    queue.tap('3', function (name) {
        console.log(name, 3);
    });
    
    // 发布
    queue.call('webpack', 'webpack-cli');// 发布的时候触发订阅的函数 同时传入参数
    
    // 执行结果:
    /*
    webpack undefined 1 // 传入的参数需要和new实例的时候保持一致,否则获取不到多传的参数
    webpack 2
    webpack 3
    */

    Webpack中Tapable的应用:

    if (Array.isArray(options)) {
        compiler = new MultiCompiler(
    	    Array.from(options).map(options => webpack(options))
    	);
    } else if (typeof options === "object") {
    	// 1 做初始的操作
    	options = new WebpackOptionsDefaulter().process(options);
    	compiler = new Compiler(options.context);
    	compiler.options = options;
    	// 2  必须插件有apply接受compiler 参数
    	new NodeEnvironmentPlugin({
    		infrastructureLogging: options.infrastructureLogging
    	}).apply(compiler);
    	// 插件接收compiler对象上的hooks,议案事件触发,插件也会执行操作
    	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);
    			}
    		}
    	}
    	compiler.hooks.environment.call();
    	compiler.hooks.afterEnvironment.call();
    	compiler.options = new WebpackOptionsApply().process(options, compiler);
    }

    webpack的总体打包流程

    Webpack首先会把配置参数和命令行的参数及默认参数合并,并初始化需要使用的插件和配置插件等执行环境所需要的参数;初始化完成后会调用Compiler的run来真正启动webpack编译构建过程,webpack的构建流程包括compile、make、build、seal、emit阶段,执行完这些阶段就完成了构建过程。这其实就是我们上面所讲到的。

    • 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。

    • 开始编译: 根据我们的webpack配置注册好对应的插件调用 compile.run 进入编译阶段,在编译的第一阶段是 compilation,他会注册好不同类型的module对应的 factory,不然后面碰到了就不知道如何处理了。

    • 编译模块: 进入 make 阶段,会从 entry 开始进行两步操作:第一步是调用 loaders 对模块的原始代码进行编译,转换成标准的JS代码, 第二步是调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系。每个模块都会记录自己的依赖关系,从而形成一颗关系树。

    • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。

    • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

    webpack的流程图

    webpack打包的三个阶段 

    webpack的打包总体来说其实是分为三个阶段:初始化阶段、编译阶段、输出文件阶段

    初始化阶段:

    • 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。这个过程中还会执行配置文件中的插件实例化语句 new Plugin()。

    • 初始化默认参数配置: new WebpackOptionsDefaulter().process(options)

    • 实例化Compiler对象:用上一步得到的参数初始化Compiler实例,Compiler负责文件监听和启动编译。Compiler实例中包含了完整的Webpack配置,全局只有一个Compiler实例。

    • 加载插件: 依次调用插件的apply方法,让插件可以监听后续的所有事件节点。同时给插件传入compiler实例的引用,以方便插件通过compiler调用Webpack提供的API。

    • 处理入口: 读取配置的Entrys,为每个Entry实例化一个对应的EntryPlugin,为后面该Entry的递归解析工作做准备。

    编译阶段:

    • run阶段:启动一次新的编译。this.hooks.run.callAsync。

    • compile: 该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上compiler对象。

    • compilation: 当Webpack以开发模式运行时,每当检测到文件变化,一次新的Compilation将被创建。一个Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation对象也提供了很多事件回调供插件做扩展。

    • make:一个新的 Compilation 创建完毕主开始编译  完毕主开始编译this.hooks.make.callAsync。

    • addEntry: 即将从 Entry 开始读取文件。

    • _addModuleChain: 根据依赖查找对应的工厂函数,并调用工厂函数的create来生成一个空的MultModule对象,并且把MultModule对象存入compilation的modules中后执行MultModule.build。

    • buildModules: 使用对应的Loader去转换一个模块。开始编译模块,this.buildModule(module)  buildModule(module, optional, origin,dependencies, thisCallback)。

    • build: 开始真正编译模块。

    • doBuild: 开始真正编译入口模块。

    • normal-module-loader: 在用Loader对一个模块转换完后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),以方便Webpack后面对代码的分析。

    • program: 从配置的入口模块开始,分析其AST,当遇到require等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。

    文件输出阶段:

    • seal: 封装 compilation.seal seal(callback)。

    • addChunk: 生成资源 addChunk(name)。

    • createChunkAssets: 创建资源 this.createChunkAssets()。

    • getRenderManifest: 获得要渲染的描述文件 getRenderManifest(options)。

    • render: 渲染源码 source = fileManifest.render()。

    • afterCompile: 编译结束   this.hooks.afterCompile。

    • shouldEmit: 所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。this.hooks.shouldEmit。

    • emit: 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。

    • done: 全部完成     this.hooks.done.callAsync。

    Webpack执行流程源码分析

    下面我们从源码去分析一下webpack的执行流程:

    1、初始化入口分析 entery-option

    webpack内部会有一个默认的配置,在 webpack.js 入口处理函数中,初始化了所有的默认配置, 在WebpackOptionsDefaulter()中,配置了很多关于 resolve 和 resolveLoader 的配置。主要有三种 对模块的默认配置,输出output,解析optimization,加载resolve模块以及resolveLoader。我们首先看一下webpack.js 入口:

    // ~/webpack/lib/webpack.js
           if (Array.isArray(options)) {
    	       compiler = new MultiCompiler(
    			Array.from(options).map(options => webpack(options))
    		);
    	} else if (typeof options === "object") {
    		// 做初始的操作
    		options = new WebpackOptionsDefaulter().process(options);
    
    		compiler = new Compiler(options.context);
    		compiler.options = options;
    		// 可以看出插件必须有apply以及接受compiler 参数
    		new NodeEnvironmentPlugin({
    			infrastructureLogging: options.infrastructureLogging
    		}).apply(compiler);
    		// 插件接收compiler对象上的hooks,议案事件触发,插件也会执行操作
    		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);
    				}
    			}
    		}
    		compiler.hooks.environment.call();
    		compiler.hooks.afterEnvironment.call();
    		compiler.options = new WebpackOptionsApply().process(options, compiler);
    	}

    process方法将我们写的 webpack 的配置 和默认的配置合并。并且 process 过程里会注入关于 normal/context/loader 的默认配置的获取函数。

    // ~/webpack/lib/WebpackOptionsApply.js
    new EntryOptionPlugin().apply(compiler);
    compiler.hooks.entryOption.call(options.context, options.entry);

    EntryOptionPlugin的实现:

    // ~/webpack/lib/EntryOptionPlugin.js
    const SingleEntryPlugin = require("./SingleEntryPlugin");
    const MultiEntryPlugin = require("./MultiEntryPlugin");
    const DynamicEntryPlugin = require("./DynamicEntryPlugin");
    
    const itemToPlugin = (context, item, name) => {
    	if (Array.isArray(item)) {
    		returnnew MultiEntryPlugin(context, item, name);
    	}
    	returnnew SingleEntryPlugin(context, item, name);
    };
    
    module.exports = class EntryOptionPlugin {
    	apply(compiler) {
    		compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
    		   // string 类型则为 new SingleEntryPlugin
    		   // array 类型则为 new MultiEntryPlugin
    			if (typeof entry === "string" || Array.isArray(entry)) {
    				itemToPlugin(context, entry, "main").apply(compiler);
    			} elseif (typeof entry === "object") {
    			    // 对于 object 类型,遍历其中每一项
    				for (const name ofObject.keys(entry)) {
    					itemToPlugin(context, entry[name], name).apply(compiler);
    				}
    			} elseif (typeof entry === "function") {
    			    // function 类型则为 DynamicEntryPlugin
    				new DynamicEntryPlugin(context, entry).apply(compiler);
    			}
    			returntrue;
    		});
    	}
    };

    在 EntryOptionsPlugin中注册了 entryOption 的事件处理函数,根据 entry 值的不同类型(string/array/object中每一项/functioin)实例化和执行不同的 EntryPlugin:string 对应 SingleEntryPlugin;array 对应 MultiEntryPlugin;function 对应 DynamicEntryPlugin。而对于 object 类型来说遍历其中的每一个 key,将每一个 key 当做一个入口,并根据类型 string/array 的不同选择 SingleEntryPlugin 或 MultiEntryPlugin。在最后执行 compiler.run(callback)。

    2、run方法

    // ~/webpack/lib/Compiler.js
            run(callback) {
    		if (this.running) return callback(new ConcurrentCompilationError());
    		const finalCallback = (err, stats) => {
    			this.running = false;
    			if (err) {
    				this.hooks.failed.call(err);
    			}
    
    			if (callback !== undefined) return callback(err, stats);
    		};
                  // 计算编译进行时间的初始时间点
    		const startTime = Date.now();
    		this.running = true;
    		// 当编译完成后需要执行的函数,处理编译完成后的事情
    		const onCompiled = (err, compilation) => {
    			if (err) return finalCallback(err);
    
    			if (this.hooks.shouldEmit.call(compilation) === false) {
    				const stats = new Stats(compilation);
    				stats.startTime = startTime;
    				stats.endTime = Date.now();
    				this.hooks.done.callAsync(stats, err => {
    					if (err) return finalCallback(err);
    					return finalCallback(null, stats);
    				});
    				return;
    			}
    			// 调用emitAsset方法,emitAsset主要负责写入文件输出文件
    			this.emitAssets(compilation, err => {
    				if (err) return finalCallback(err);
    
    				if (compilation.hooks.needAdditionalPass.call()) {
    					compilation.needAdditionalPass = true;
    
    					const stats = new Stats(compilation);
    					stats.startTime = startTime;
    					stats.endTime = Date.now();
    					//继续异步调用时间流
    					this.hooks.done.callAsync(stats, err => {
    						if (err) return finalCallback(err);
    						//  这次多了一个时间流,调用额外编译,告知编译终于编完了
    						this.hooks.additionalPass.callAsync(err => {
    							if (err) return finalCallback(err);
    							this.compile(onCompiled);
    						});
    					});
    					return;
    				}
    
    				this.emitRecords(err => {
    					if (err) return finalCallback(err);
    
    					const stats = new Stats(compilation);
    					stats.startTime = startTime;
    					stats.endTime = Date.now();
    					this.hooks.done.callAsync(stats, err => {
    						if (err) return finalCallback(err);
    						return finalCallback(null, stats);
    					});
    				});
    //最终总结,无论走那个分支都是 new Stats(compilation) 返回stats的回调函数,
    //按照目前的流程走的是最后一个分支,调用 this.emitRecords
    			});
    		};
    		// this.hooks.beforeRun this.hooks.run 主要是构建一个架子,面向切面编程
    		this.hooks.beforeRun.callAsync(this, err => {
    			if (err) return finalCallback(err);
    
    			this.hooks.run.callAsync(this, err => {
    				if (err) return finalCallback(err);
    
    				this.readRecords(err => {
    					if (err) return finalCallback(err);
    					// 执行compile 方法 /把onCompiled函数传入,调用compile
    					this.compile(onCompiled);
    				});
    			});
    		});
    	}

    3、compile方法

    // ~/webpack/lib/Compiler.js
             compile(callback) {
    		const params = this.newCompilationParams();
    		this.hooks.beforeCompile.callAsync(params, err => {
    			if (err) return callback(err);
    
    			this.hooks.compile.call(params);
    			// 新建一个 compilation
    			// compilation 里面也定义了 this.hooks , 原理和 Compiler 一样
    			const compilation = this.newCompilation(params);
    // 执行make函数, 从 entry开始递归分析依赖,对每个依赖模块进行build,
    //对complie.hooks.make进行执行
    			this.hooks.make.callAsync(compilation, err => {
    				if (err) return callback(err);
    
    				compilation.finish(err => {
    					if (err) return callback(err);
    
    					compilation.seal(err => {
    						if (err) return callback(err);
    
    						this.hooks.afterCompile.callAsync(compilation, err => {
    							if (err) return callback(err);
    
    							return callback(null, compilation);
    						});
    					});
    				});
    			});
    		});
    	}

    4、make的实现

    在complie中进行 this.hooks.make.callAsync,在compiler.hooks.make.tapAsync进行监听,执行监听 监听SingleEntryPlugin中的插件,webpack入口条目,参数是单入口字符串,单入口数组,多入口对象还是动态函数,无论是什么都会调用compilation.addEntry方法,这个方法会执行_addModuleChain,将入口文件加入需要编译的体积中。然后少量中的文件被一个一个处理,文件中的import约会了其他的文件又会通过addModuleDependencies加入到编译层次中。最终当这个编译堆栈中的内容完成被处理完时,就完成了文件到模块的转化。

    // ~/webpack/lib/SingleEntryPlugin.js
    apply(compiler) {
    	compiler.hooks.compilation.tap(
    		"SingleEntryPlugin",
    		(compilation, { normalModuleFactory }) => {
    			compilation.dependencyFactories.set(
    				SingleEntryDependency,
    				normalModuleFactory
    			);
    		}
    	);
    	// 对compiler.hooks进行tapAsync 进行异步绑定,在compiler进行监听
    	compiler.hooks.make.tapAsync(
    		"SingleEntryPlugin",
    		(compilation, callback) => {
    			const { entry, name, context } = this;
    			const dep = SingleEntryPlugin.createDependency(entry, name);
    			compilation.addEntry(context, dep, name, callback);
    		}
    	);

    5、addEntry

    // ~/webpack/lib/Compilation.js
    addEntry(context, entry, name, callback) {
    	// context => 默认为process.cwd()
        // entry => dep => SingleEntryDependency
        // name => 单入口默认为main
        // callback => 后面的流程
            const slot = {
                name: name,
                module: null
            };
            // 初始为[]
            this.preparedChunks.push(slot);
            this._addModuleChain(context, entry, (module) => { /**/ }, (err, module) => { /**/ });
        }

    6、_addModuleChain

    _addModuleChain主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。通过 ModuleFactory.create *ModuleFactory.create方法创建模块,(有NormalModule , MultiModule , ContextModule , DelegatedModule 等)

    // ~/webpack/lib/Compilation.js
    class Compilation extends Tapable {
        // ...
        _addModuleChain(context, dependency, onModule, callback) {
            // profile => options.profile
            // 不传则start为undefined
            const start = this.profile && Date.now();
            // bail => options.bail
            const errorAndCallback = this.bail ? (err) => {
                callback(err);
            } : (err) => {
                err.dependencies = [dependency];
                this.errors.push(err);
                callback();
            };
    
            if (typeof dependency !== "object" || dependency === null || !dependency.constructor) {
                thrownewError("Parameter 'dependency' must be a Dependency");
            }
            // dependencyFactories包含了所有的依赖集合
            const moduleFactory = this.dependencyFactories.get(dependency.constructor);
            if (!moduleFactory) {
                thrownewError(`No dependency factory available for this dependency type: ${dependency.constructor.name}`);
            }
    
    this.semaphore.acquire(() => {
    	moduleFactory.create(
    		{
    			contextInfo: {
    				issuer: "",
    				compiler: this.compiler.name
    			},
    			context: context,
    			dependencies: [dependency]
    		},
    		(err, module) => {
    			if (err) {
    				this.semaphore.release();
    				return errorAndCallback(new EntryModuleNotFoundError(err));
    			}
    			let afterFactory;
    			if (currentProfile) {
    				afterFactory = Date.now();
    				currentProfile.factory = afterFactory - start;
    			}
    			// 将所有的依赖放入modules
    			const addModuleResult = this.addModule(module);
    			module = addModuleResult.module;
    				onModule(module);
    				dependency.module = module;
    			module.addReason(null, dependency);
    				const afterBuild = () => {
    					if (addModuleResult.dependencies) {
    					this.processModuleDependencies(module, err => {
    						if (err) return callback(err);
    						callback(null, module);
    					});
    				} else {
    					return callback(null, module);
    					}
    			};
    			if (addModuleResult.issuer) {
    				if (currentProfile) {
    					module.profile = currentProfile;
    				}
    			}
    			if (addModuleResult.build) {
    			// 执行buildModule
    			this.buildModule(module, false, null, null, err => {
    			if (err) {
    				this.semaphore.release();
    					return errorAndCallback(err);
    				}
    			if (currentProfile) {
    			    const afterBuilding = Date.now();
    		    	currentProfile.building = afterBuilding - afterFactory;
    			}
    			this.semaphore.release();
    				afterBuild();
    			});
    		} else {
    			this.semaphore.release();
    			this.waitForBuildingFinished(module, afterBuild);
    			}
    		}
    			);
    		});
        }
    }
    _addModuleChain 和 addModuleDependencies 函数中都会调用 this.semaphore.acquire 这个函数的具体实现在 lib/util/Semaphore.js 文件中。
    
    // lib/util/Semaphore.js
    class Semaphore {
       constructor(available) {
          // available 为最大的并发数量
       	this.available = available;
       	this.waiters = [];
       	this._continue = this._continue.bind(this);
       }
    
       acquire(callback) {
       	if (this.available > 0) {
       		this.available--;
       		callback();
       	} else {
       		this.waiters.push(callback);
       	}
       }
    
       release() {
       	this.available++;
       	if (this.waiters.length > 0) {
       		process.nextTick(this._continue);
       	}
       }
    
       _continue() {
       	if (this.available > 0) {
       		if (this.waiters.length > 0) {
       			this.available--;
       			const callback = this.waiters.pop();
       			callback();
       		}
       	}
       }
    }

    对外暴露的只有两个个方法:

    • acquire: 申请处理资源,如果有闲置资源(即并发数量)则立即执行处理,并且闲置的资源减1;否则存入等待队列中。

    • release: 释放资源。在 acquire 中会调用 callback 方法,在这里需要使用 release 释放资源,将闲置资源加1。同时会检查是否还有待处理内容,如果有则继续处理

    这个 Semaphore 类借鉴了在多线程环境中,对使用资源进行控制的 Semaphore(信号量)的概念。其中并发个数通过 available 来定义,那么默认值是多少呢?在 Compilation.js 中可以找到 this.semaphore = new Semaphore(options.parallelism || 100); 复制代码 默认的并发数是 100,注意这里说的并发只是代码设计中的并发,不要和js的单线程特性搞混了。

    7、buildModule

    对modules进行build包括 调用loader处理源文件,使用acorn生成AST并且遍历AST,遇到require创建依赖depende,并且加入数组.

    // ~/webpack/lib/Compilation.js
    buildModule(module, optional, origin, dependencies, thisCallback) {
    	let callbackList = this._buildingModules.get(module);
    	if (callbackList) {
    		callbackList.push(thisCallback);
    		return;
    	}
    	this._buildingModules.set(module, (callbackList = [thisCallback]));
    	const callback = err => {
    		this._buildingModules.delete(module);
    		for (const cb of callbackList) {
    			cb(err);
    		}
    	};
    	this.hooks.buildModule.call(module);
    	// 开始build,主要执行的是NormalModuleFactory生成的NormalModule中的build方法,中的build方法,打开NormalModule
    	module.build(
    		this.options,
    		this,
    		this.resolverFactory.get("normal", module.resolveOptions),
    		this.inputFileSystem,
    		error => {
    			const errors = module.errors;
    			for (let indexError = 0; indexError < errors.length; indexError++) {
    				const err = errors[indexError];
    				err.origin = origin;
    				err.dependencies = dependencies;
    				if (optional) {
    					this.warnings.push(err);
    				} else {
    					this.errors.push(err);
    				}
    			}
    			const warnings = module.warnings;
    			for (
    				let indexWarning = 0;
    				indexWarning < warnings.length;
    				indexWarning++
    			) {
    				const war = warnings[indexWarning];
    				war.origin = origin;
    				war.dependencies = dependencies;
    				this.warnings.push(war);
    			}
    			const originalMap = module.dependencies.reduce((map, v, i) => {
    				map.set(v, i);
    				return map;
    			}, newMap());
    			module.dependencies.sort((a, b) => {
    				const cmp = compareLocations(a.loc, b.loc);
    				if (cmp) return cmp;
    				return originalMap.get(a) - originalMap.get(b);
    			});
    			if (error) {
    				this.hooks.failedModule.call(module, error);
    				return callback(error);
    			}
    			this.hooks.succeedModule.call(module);
    			return callback();
    		}
    	);
    }

    8、build

    //~/webpack/lib/NormalModule.js
    try {
    // 调用parse方法,创建依赖Dependency并放入依赖数组,这里会将 source 转为 AST,分析出所有的依赖著作权归原作者所有。
      const result = this.parser.parse(
        this._ast || this._source.source(), {
           current: this,
           module: this,
           compilation: compilation,
           options: options
        },
        (err, result) => {
           if (err) {
              handleParseError(err);
           } else {
              handleParseResult(result);
           }
        });
        if (result !== undefined) {
        // parse is sync
          handleParseResult(result);
         }
     }
    
    build(options, compilation, resolver, fs, callback) {
       this.buildTimestamp = Date.now();
       this.built = true;
       this._source = null;
       this._sourceSize = null;
       this._ast = null;
       this._buildHash = "";
       this.error = null;
       this.errors.length = 0;
       this.warnings.length = 0;
       this.buildMeta = {};
       this.buildInfo = {
         cacheable: false,
         fileDependencies: newSet(),
         contextDependencies: newSet(),
         assets: undefined,
         assetsInfo: undefined
       };
       returnthis.doBuild(options, compilation, resolver, fs, err => {
         this._cachedSources.clear();
       // if we have an error mark module as failed and exit
         if (err) {
             this.markModuleAsErrored(err);
             this._initBuildHash(compilation);
                 return callback();
         }
         // check if this module should !not! be parsed.
        // if so, exit here;
         const noParseRule = options.module && options.module.noParse;
            if (this.shouldPreventParsing(noParseRule, this.request)) {
               this._initBuildHash(compilation);
               return callback();
         }
       const handleParseError = e => {
       const source = this._source.source();
       const loaders = this.loaders.map(item =>
          contextify(options.context, item.loader)
       );
       const error = new ModuleParseError(this, source, e, loaders);
           this.markModuleAsErrored(error);
           this._initBuildHash(compilation);
             return callback();
       };
      const handleParseResult = result => {
         this._lastSuccessfulBuildMeta = this.buildMeta;
         this._initBuildHash(compilation);
           return callback();
       };
       try {
       // / 调用parse方法,创建依赖Dependency并放入依赖数组,这时传入 parse 方法中的就是 loader 处理之后,
       // 返回的 extraInfo.webpackAST,类型是 AST 对象。这么做的好处是什么呢?如果 loader 处理过程中已经执行过将文件转化为 AST 了,
        // 那么这个 AST 对象保存到 extraInfo.webpackAST 中,在这一步就可以直接复用,以避免重复生成 AST,提升性能。
    
        const result = this.parser.parse(
           this._ast || this._source.source(), {
               current: this,
               module: this,
               compilation: compilation,
               options: options
           },
           (err, result) => {
              if (err) {
                 handleParseError(err);
            } else {
                 handleParseResult(result);
            }
         }
         );
         if (result !== undefined) {
           // parse is sync
         handleParseResult(result);
         }
       } catch (e) {
         handleParseError(e);
       }
       });
    }

    9、doBuild

    //~/webpack/lib/NormalModule.js     
    doBuild(options, compilation, resolver, fs, callback) {
            const loaderContext = this.createLoaderContext(
                resolver,
                options,
                compilation,
                fs
            );
            // 获取loader相关的信息并转换成webpack需要的js文件
            runLoaders({
                    resource: this.resource,
                    loaders: this.loaders,
                    context: loaderContext,
                    readResource: fs.readFile.bind(fs)
                },
                (err, result) => {
                    if (result) {
                        this.buildInfo.cacheable = result.cacheable;
                        this.buildInfo.fileDependencies = newSet(result.fileDependencies);
                        this.buildInfo.contextDependencies = newSet(
                            result.contextDependencies
                        );
                    }
    
                    if (err) {
                        if (!(err instanceofError)) {
                            err = new NonErrorEmittedError(err);
                        }
                        const currentLoader = this.getCurrentLoader(loaderContext);
                        const error = new ModuleBuildError(this, err, {
                            from: currentLoader &&
                                compilation.runtimeTemplate.requestShortener.shorten(
                                    currentLoader.loader
                                )
                        });
                        return callback(error);
                    }
    
                    const resourceBuffer = result.resourceBuffer;
                    const source = result.result[0];
                    const sourceMap = result.result.length >= 1 ? result.result[1] : null;
                    const extraInfo = result.result.length >= 2 ? result.result[2] : null;
                    // runLoader 结果是一个数组:[source, sourceMap, extraInfo], extraInfo.webpackAST 如果存在,则会被保存到 module._ast 中。
                    // 也就是说,loader 除了返回处理完了 source 之后,还可以返回一个 AST 对象。在 doBuild 的回调中会优先使用 module._ast
                    
                    if (!Buffer.isBuffer(source) && typeof source !== "string") {
                        const currentLoader = this.getCurrentLoader(loaderContext, 0);
                        const err = newError(
                            `Final loader (${
    							currentLoader
    								? compilation.runtimeTemplate.requestShortener.shorten(
    										currentLoader.loader
    								  )
    								: "unknown"
    						}) didn't return a Buffer or String`
                        );
                        const error = new ModuleBuildError(this, err);
                        return callback(error);
                    }
    
                    this._source = this.createSource(
                        this.binary ? asBuffer(source) : asString(source),
                        resourceBuffer,
                        sourceMap
                    );
                    // ExtraInfo.webpackAST 如果存在,则会被保存到 module._ast 中。
                    this._sourceSize = null;
                    this._ast =
                        typeof extraInfo === "object" &&
                        extraInfo !== null &&
                        extraInfo.webpackAST !== undefined ?
                        extraInfo.webpackAST :
                        null;
                    // 回build中执行ast(抽象语法树,编译过程中常见结构,vue、babel原理都有)
                    return callback();
                }
            );
        }

    整个 parse 的过程关于依赖的部分,我们总结一下:

    • 将 source 转为 AST(如果 source 是字符串类型)

    • 遍历 AST,遇到 import 语句就增加相关依赖,代码中出现 A(import 导入的变量) 的地方也增加相关的依赖。

    所有的依赖都被保存在 module.dependencies 中,一共有下面4个

    HarmonyCompatibilityDependency
    HarmonyInitDependency
    ConstDependency
    HarmonyImportSideEffectDependency
    HarmonyImportSpecifierDependency

    到此 build 阶段就结束了,回到 module.build 的回调函数。对于所有的依赖再次经过 create->build->add->processDep。如此递归下去,最终我们所有的文件就都转化为了 module,并且会得到一个 module 和 dependencies 的关系结构,这个结构会交给后续的 chunck 和 生成打包文件代码使用。module 生成的过程结束之后,最终会回到 Compiler.js 中的 compile 方法的 make 事件回调中,module 生成的过程就结束了。

    10、Seal函数

    seal函数之后的流程,就主要和chunk函数生成有关了。回调的 seal 方法中,将运用这些 module 以及 module 的 dependencies 信息整合出最终的 chunck。

    • module,就是不同的资源文件,包含了你的代码中提供的例如:js/css/图片 等文件,在编译环节,webpack 会根据不同 module 之间的依赖关系去组合生成 chunk。webpack 打包构建时会根据你的具体业务代码和 webpack 相关配置来决定输出的最终文件,具体的文件的名和文件数量也与此相关。而这些文件就被称为 chunk。例如在你的业务当中使用了异步分包的 API。

    chunk由 module 组成,一个 chunk 可以包含多个 module,它是 webpack 编译打包后输出的最终文件。例如:

    import('./foo.js').then(bar => bar())

    在最终输出的文件当中,foo.js会被单独输出一个 chunk 文件。这些生成的 chunk 文件当中即是由相关的 module 模块所构成的。下面是seal函数的入口:

    ~/webpack/lib/Compiler.js
            compile(callback) {
    		const params = this.newCompilationParams();
    		this.hooks.beforeCompile.callAsync(params, err => {
    			if (err) return callback(err);
    
    			this.hooks.compile.call(params);
    			// 新建一个 compilation
    			// compilation 里面也定义了 this.hooks , 原理和 Compiler 一样
    			const compilation = this.newCompilation(params);
    			// 执行make函数,四:从 entry开始递归分析依赖,对每个依赖模块进行build,对complie.hooks.make进行执行
    			this.hooks.make.callAsync(compilation, err => {
    				if (err) return callback(err);
    
    				compilation.finish(err => {
    					if (err) return callback(err);
    
    					compilation.seal(err => {
    						if (err) return callback(err);
    
    						this.hooks.afterCompile.callAsync(compilation, err => {
    							if (err) return callback(err);
    
    							return callback(null, compilation);
    						});
    					});
    				});
    			});
    		});
    	}

    下面我们看下seal函数具体实现:

    // ~/lib/Compilation.js
       seal(callback) {
      	this.hooks.seal.call();
    
      	while (
      		this.hooks.optimizeDependenciesBasic.call(this.modules) ||
      		this.hooks.optimizeDependencies.call(this.modules) ||
      		this.hooks.optimizeDependenciesAdvanced.call(this.modules)
      	) {
      		/* empty */
      	}
      	this.hooks.afterOptimizeDependencies.call(this.modules);
    
      	this.hooks.beforeChunks.call();
      	// 根据 addEntry 方法中收集到入口文件组成的 _preparedEntrypoints 数组
      	for (const preparedEntrypoint ofthis._preparedEntrypoints) {
      		constmodule = preparedEntrypoint.module;
      		const name = preparedEntrypoint.name;
      		const chunk = this.addChunk(name); // 入口 chunk 且为 runtimeChunk
      		const entrypoint = new Entrypoint(name); // 每一个 entryPoint 就是一个 chunkGroup
      		entrypoint.setRuntimeChunk(chunk); // 设置 runtime chunk
      		entrypoint.addOrigin(null, name, preparedEntrypoint.request);
      		this.namedChunkGroups.set(name, entrypoint); // 设置 chunkGroups 的内容
      		this.entrypoints.set(name, entrypoint);
      		this.chunkGroups.push(entrypoint);
      		// 建立起 chunkGroup 和 chunk 之间的关系
      		GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
      		// 建立起 chunk 和 module 之间的关系
      		GraphHelpers.connectChunkAndModule(chunk, module);
    
      		chunk.entryModule = module;
      		chunk.name = name;
    
      		this.assignDepth(module);
      	}
      	buildChunkGraph(
      		this,
      		/** @type {Entrypoint[]} */ (this.chunkGroups.slice())
      	);
      	// 对 module 进行排序
      	this.sortModules(this.modules);
      	// 创建完 chunk 之后的 hook
      	this.hooks.afterChunks.call(this.chunks);
    
      	this.hooks.optimize.call();
    
      	while (
      		this.hooks.optimizeModulesBasic.call(this.modules) ||
      		this.hooks.optimizeModules.call(this.modules) ||
      		this.hooks.optimizeModulesAdvanced.call(this.modules)
      	) {
      		/* empty */
      	}
      	// 优化 module 之后的 hook
      	this.hooks.afterOptimizeModules.call(this.modules);
    
      	while (
      		this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups) ||
      		this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups) ||
      		// 主要涉及到 webpack config 当中的有关 optimization 配置的相关内容
      		this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
      	) {
      		/* empty */
      	}
      			// 优化 chunk 之后的 hook
      	this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);
    .....
    
      }

    在这个过程当中首先遍历 webpack config 当中配置的入口 module,每个入口 module 都会通过addChunk方法去创建一个 chunk,而这个新建的 chunk 为一个空的 chunk,即不包含任何与之相关联的 module。之后实例化一个 entryPoint,而这个 entryPoint 为一个 chunkGroup,每个 chunkGroup 可以包含多的 chunk,同时内部会有个比较特殊的 runtimeChunk(当 webpack 最终编译完成后包含的 webpack runtime 代码最终会注入到 runtimeChunk 当中)。到此仅仅是分别创建了 chunk 以及 chunkGroup,接下来便调用GraphHelpers模块提供的connectChunkGroupAndChunk及connectChunkAndModule方法来建立起 chunkGroup 和 chunk 之间的联系,以及 chunk 和 入口 module 之间(这里还未涉及到依赖 module)的联系。

    11、createChunkAssets

    // ~/lib/Compilation.js
       createChunkAssets() {
      	const outputOptions = this.outputOptions;
      	const cachedSourceMap = newMap();
      	/** @type {Map<string, {hash: string, source: Source, chunk: Chunk}>} */
      	const alreadyWrittenFiles = newMap();
      	for (let i = 0; i < this.chunks.length; i++) {
      		const chunk = this.chunks[i];
      		chunk.files = [];
      		let source;
      		let file;
      		let filenameTemplate;
      		try {
      			// 1 chunk.entry判断是mainTemplate(入口文件打包)还是chunkTemplate(异步加载js打包模板)
      		    const template = chunk.hasRuntime()
      			? this.mainTemplate
      			: this.chunkTemplate;
      				// 2 生成manifest 对象
      		    const manifest = template.getRenderManifest({
      				chunk,
      				hash: this.hash,
      				fullHash: this.fullHash,
      				outputOptions,
      				moduleTemplates: this.moduleTemplates,
      				dependencyTemplates: this.dependencyTemplates
      			}); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
      			for (const fileManifest of manifest) {
      				const cacheName = fileManifest.identifier;
      				const usedHash = fileManifest.hash;
      				filenameTemplate = fileManifest.filenameTemplate;
      				const pathAndInfo = this.getPathWithInfo(
      					filenameTemplate,
      					fileManifest.pathOptions
      				);
      				file = pathAndInfo.path;
      				const assetInfo = pathAndInfo.info;
    
      				// check if the same filename was already written by another chunk
      				const alreadyWritten = alreadyWrittenFiles.get(file);
      				if (alreadyWritten !== undefined) {
      					if (alreadyWritten.hash === usedHash) {
      						if (this.cache) {
      							this.cache[cacheName] = {
      								hash: usedHash,
      								source: alreadyWritten.source
      							};
      						}
      						chunk.files.push(file);
      						this.hooks.chunkAsset.call(chunk, file);
      						continue;
      					} else {
      						thrownewError(
      							`Conflict: Multiple chunks emit assets to the same filename ${file}` +
      								` (chunks ${alreadyWritten.chunk.id} and ${chunk.id})`
      						);
      					}
      				}
      				if (
      					this.cache &&
      					this.cache[cacheName] &&
      					this.cache[cacheName].hash === usedHash
      				) {
      					// 3 chunk 打包封装的入口
      					source = this.cache[cacheName].source;
      				} else {
      					source = fileManifest.render();
      					// Ensure that source is a cached source to avoid additional cost because of repeated access
      					if (!(source instanceof CachedSource)) {
      						// 4 存放cacheEntry源码
      						const cacheEntry = cachedSourceMap.get(source);
      						if (cacheEntry) {
      							source = cacheEntry;
      						} else {
      							const cachedSource = new CachedSource(source);
      							cachedSourceMap.set(source, cachedSource);
      							source = cachedSource;
      						}
      					}
      					if (this.cache) {
      						this.cache[cacheName] = {
      							hash: usedHash,
      							source
      						};
      					}
      				}
      				this.emitAsset(file, source, assetInfo);
      				chunk.files.push(file);
      				this.hooks.chunkAsset.call(chunk, file);
      				alreadyWrittenFiles.set(file, {
      					hash: usedHash,
      					source,
      					chunk
      				});
      			}
      		} catch (err) {
      			this.errors.push(
      				new ChunkRenderError(chunk, file || filenameTemplate, err)
      			);
      		}
      	}
      }

    流程应该是这样的:

     从上图可以看出不同的chunk处理模版不一样,根据chunk的entry判断是选择mainTemplate(入口文件打包模版)还是chunkTemplate(异步加载js打包模版);选择模版后根据模版的template.getRenderManifest生成manifest对象,该对象中的render方法就是chunk打包封装的入口;mainTemplate和chunkTemplate的唯一区别就是mainTemplate多了wepback执行的bootsrap代码。当调用render时会调用template.renderChunkModules方法,该方法会创建一个ConcatSource容器用来存放chunk的源码,该方法接下来会对当前chunk的module遍历并执行moduleTemplate.render获得每一个module的源码;在moduleTemplate.render中获取源码后会触发插件去封装成wepack需要的代码格式;当所有的module都生成完后放入ConcatSource中返回;并以该chunk的输出文件名称为key存放在Compilation的assets中。

    12、this.emitAssets 

    webpack 调用 Compiler 中的 emitAssets() ,按照 output 中的配置项异步将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。要注意的是,若想对结果进行处理,则需要在 emit 触发后对自定义插件进行扩展。

    // ~/webpack/lib/Compiler.js
                this.emitAssets(compilation, err => {
    		if (err) return finalCallback(err);
    
    		if (compilation.hooks.needAdditionalPass.call()) {
    		    compilation.needAdditionalPass = true;
    
    		    const stats = new Stats(compilation);
    		    stats.startTime = startTime;
    		    stats.endTime = Date.now();
    		    //继续异步调用时间流
    		    this.hooks.done.callAsync(stats, err => {
    			if (err) return finalCallback(err);
    			//  这次多了一个时间流,调用额外编译,告知编译终于编完了
    			this.hooks.additionalPass.callAsync(err => {
    			    if (err) return finalCallback(err);
    			    this.compile(onCompiled);
    			});
    		    });
    		    return;
    	      }
            

    13、compiler.hooks.emit.callAsync()

    最后我们到达了compiler.emitAssets方法体中。在compiler.emitAssets中会先调用this.hooks.emit生命周期,之后根据webpack config文件的output配置的path属性,将文件输出到指定的文件夹。至此,你就可以在./debug/dist中查看到调试代码打包后的文件了。

    // ~/webpack/lib/Compiler.js
    this.hooks.emit.callAsync(compilation, () => {
        outputPath = compilation.getPath(this.outputPath, {})
        mkdirp(this.outputFileSystem, outputPath, emitFiles)
     })

    这就是webpack的构建流程。。。。太难了。。。。。

    展开全文
  • webpack打包流程进阶

    2019-10-03 02:02:29
    1.生成Source Maps(使调试更容易) source-map ...在一个单独的文件中生成一个不带列映射的map,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号)...
  • 1)安装webpack-cli npm install webpack-cli -g webpack -v 安装成功,查看版本号 2)新建webpack.config.js文件 module.exports = { entry: "./runoob1.js", //入口文件 output: { ...3)打包 webpack
  • 我们一般用vue-cli脚手架搭建的项目,会用webpack打包,因为vue-cli是基于webpack的. 为什么打包? 就是将所有的脚本文件js或css文件等都合并到一个文件 步骤: npm run build 会多一个dist文件 将dist文件里的代码上传...
  • 注意并不是所有的所有的问题的解决办法都对于你适应,因为每个人...1.利用webpack打包时,用这个命令:webpack .\src\main.js .\dist\bundle.js 发现由于版本号太高,导致打包错。 换成:webpack .\src\main.js -o...
  • 浅谈WebPack打包流程,原理

    千次阅读 2019-03-24 14:15:55
    每次在命令行输入 webpack 后,操作系统都会去调用./node_modules/.bin/webpack这个 shell 脚本。这个脚本会去调用./node_modules/webpack/bin/webpack.js并追加输入的参数,如 -p , -w 。(图中 webpack.js 是 ...
  • 一、初始化项目 输入:cnpm init 一路按enter键即可 二、安装webpack (1) 全局安装webpack ,我们... (2) 通常我们会将webpack安装到项目依赖,这样就可以使用本地版本的webpack,我们在命令行输入: ...
  • 1、 webpack.config.js(多入口多出口) module.exports = { entry:{ app:'./index.js', vendors:'./index1.js' }, //多入口文件 output: { filename: '[name].js', path: __dirname + '/dist' }, module: ...
  • webpack初体验2.1 安装webpack2.2 创建项目目录结构2.3 npm初始化2.4 代码编写2.4.1 math.js(模块代码)2.4.2 main.js(入口文件)2.4.3 index.html(浏览器展示文件)2.5 webpack打包2.6 修改引入文件为打包输出...
  • 先写一下我的例子: const path = require('path');...const cleanwpPlug = require('clean-webpack-plugin'); //这里是引用那个清除的插件,注意该插件只能清除一个js module.exports = { entry: {...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 808
精华内容 323
关键字:

webpack打包流程