webpack5-源码解读系列(1)— 编译前

2,190 阅读8分钟

一,什么是webpack

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。


二,webpack 4->5

  • 持久化存储优化构建性能
  • 更好的算法与 defalut 来改善长效缓存(long-term caching)
  • 更好的 Tree Shaking 和代码生成来改善 bundle 的大小
  • 清理内部结构而不引入任何破坏性的变化;删除了polyfill脚本
  • 引入破坏性更改来为新特性做准备,以便尽可能长的使用v5版本。


三,特殊改进点

1,webpack 4 没有分析模块 export 与 import 之间的依赖关系。webpack 5 有一个新的选项 optimization.innerGraph,该选项在生产模式下默认启用,它对模块中的符号进行分析以找出从 export 到 import 的依赖关系。

import { something } from "./something";

function usingSomething() {
  return something;
}

export function test() {
   return usingSomething();
 }

内部图算法将确定仅在使用 export 的 test 时使用 something。这样可以将更多 export 标记为未使用,并从 bundle 中删除更多的代码。


2,持久缓存

image.png

webpack读取入口文件(entry),然后递归查找所依赖的模块(module),构建成一个“依赖图”,然后根据配置中的加载器(loader)和打包策略来对模块进行编译。

webpack 5利用持久缓存优化了整个流程,当检测到某个文件变化时,依照“依赖图”,只对修改过的文件进行编译,从而大幅提高了编译速度。


image.png

cache: {
    // 1. Set cache type to filesystem
  type: "filesystem",

  buildDependencies: {
    // 2. Add your config as buildDependency to get cache invalidation on config change
    config: [__filename]

    // 3. If you have other things the build depends on you can add them here
    // Note that webpack, loaders and all modules referenced from your config are automatically added
  }
}


3,删除polyfill脚本


最开始,webpack的目标是允许在浏览器中运行大多数的Node模块,但是现在模块格局已经发生了重大变化,现在有很多模块是专门为前端开发的。在v4及以前的版本中,对于大多数的Node模块将自动添加polyfill脚本(腻子脚本)。然而,这些大量繁杂的脚本都会添加到最终编译的代码中(bundle),但其实通常情况下是没有必要的。在v5版本中将尝试停止自动地添加polyfill脚本,转而专注于前端兼容模块。


4,最低NODE版本从6升级到8




目前已经可以安装 Webpack 5 了,但官网最新稳定版本还是 Webpack 4。


1
2
// 安装方式
npm install -D webpack@next webpack-cli



四,webPack4 VS webPack5

 1,目录结构

image.png

// hello.js
const something = 'something'

function usingSomething() {
  return something;
}

export function test() {
  console.log('test')
  return usingSomething();
}


//index.js
import {test} from '../src/hello'
test();



"dev": "webpack --mode development",
"build": "webpack --mode production"

2,npm run dev && build

image.pngimage.png

image.pngimage.png

image.pngimage.png

image.pngimage.png

3,export 与 import 之间的依赖关系 对比

image.pngimage.png


image.pngimage.png

4,效率提升

按演讲中的测试效果,16000 模块的单页应用使用持久化缓存,编译速度可以提高 98%。

image.png

五,全链路探寻

整体流程:

image.png

六,万物起源

const webpack = (options, callback) => {
    validateSchema(webpackOptionsSchema, options);
    compiler = createCompiler(options);
    compiler.watch(watchOptions, callback);
    return compiler;
};

七,validateSchema 模块


参数一:webpackOptionsSchema


webpckOptions.json

{
  "definitions":{...},
  "type":"object",
  "additionalProperties":false,
  "properties":{.....}
}

拿一小段代码来看:

        "loader": {
      "description": "Custom values available in the loader context.",
      "type": "object"
    },
    "mode": {
      "description": "Enable production optimizations or development hints.",
      "enum": ["development", "production", "none"]
    },
    "module": {
      "description": "Options affecting the normal modules (`NormalModuleFactory`).",
      "anyOf": [
        {
          "$ref": "#/definitions/ModuleOptions"
        }
      ]
    },

作用:针对optins的配置,进行效验,比如loader的type要求object,"$ref": "#/definitions/ModuleOptions"就是这个JSON文件下的definitions/ModuleOptions。这种写法,主要是提取公共配置,避免代码冗余。




参数二:options

module.exports = {
    context: __dirname,
    mode: 'development',
    devtool: 'source-map',
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: ['babel-loader'],
                exclude: /node_modules/,
            }
        ]
    }
}

3,validate(schema, options, {})

  • _ajv

ajv 是一个非常流行的JSON Schema验证工具,将一个JSON配置文件转换成一个对象,用于检测对象的合法性

// Node.js require:
var Ajv = require('ajv');
// or ESM/TypeScript import
import Ajv from 'ajv';
 
var ajv = new Ajv(); // options can be passed, e.g. {allErrors: true}
var validate = ajv.compile(schema);
var valid = validate(data);
if (!valid) console.log(validate.errors);
  • _ajvKeywords
var Ajv = require('ajv');
var ajv = new Ajv;
require('ajv-keywords')(ajv);
 
ajv.validate({ instanceof: 'RegExp' }, /.*/); // true
ajv.validate({ instanceof: 'RegExp' }, '.*'); // false

使用ajv-keywords给json schema添加自定义关键字


  • _absolutePath
 ajv.addKeyword('absolutePath', {
    errors: true,
    type: 'string',

    compile(schema, parentSchema) {}
 }



  • _ValidationError
                        case 'number':
              return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;

            case 'integer':
              return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;

            case 'string':
              return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;

            case 'boolean':
              return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;

作用:错误提示汇总,简称错误密码本



简单使用:

const Ajv = require('ajv');
 
let schema = {
  type: 'object',
  required: ['username', 'email', 'password'],
  properties: {
    username: {
      type: 'string',
      minLength: 4
    },
    email: {
      type: 'string',
      format: 'email'
    },
    password: {
      type: 'string',
      minLength: 6
    },
    age: {
      type: 'integer',
      minimum: 0
    },
    sex: {
      enum: ['boy', 'girl', 'secret'],
      default: 'secret'
    },
  }
};
 
let ajv = new Ajv();
let validate = ajv.compile(schema);
 
let valid = validate(data);
if (!valid) console.log(validate.errors);

我们声明了一个数据模式schema ,这个模式要求目标数据为一个对象,对象可以有五个字段 ,并分别定义了五个字段的类型和数据格式要求,并且其中 usernameemailpassword 必填。然后我们使用这个模式去验证用户输入的数据 data 是否满足我们的需求。



4,validateObject(schema, options)

if (Array.isArray(options)) {
    errors = Array.from(options).map(nestedOptions => validateObject(schema, nestedOptions));
 } else {
    errors = validateObject(schema, options);
 }

作用:跟进options来判断是单配置还是多配置

  const compiledSchema = ajv.compile(schema);
  const valid = compiledSchema(options);

  if (!compiledSchema.errors) {
    return [];
  }

  return valid ? [] : filterErrors(compiledSchema.errors);

作用:validate会依次从data中取出需要校验的key,按照JSON文件中的规则进行判断。

此时 拿到的compiledSchema为:

image.png


调用堆栈:

image.png


作用:如果没有问题就直接返回[],如果检测失败就交个filterErrors处理;


filterErrrors处理函数:

function filterErrors(errors) {
  let newErrors = [];
  for (const error of errors) {
    let children = [];
     newErrors = newErrors.filter(oldError => {
        children.push(oldError);
      }
    });
    newErrors.push(error);
  }
  return newErrors;
} 

八,WebpackOptionsDefaulter 模块

let compiler;   
compiler = createCompiler(options);

options = new WebpackOptionsDefaulter().process(options);

作用:传进去一个options 输出一个options

很明显是是给options添加默认配置

就是给options多挂在几个默认属性,至于怎么添加的,添加了什么, 我们来看下optionsDefaulter类


1,OptionsDefaulter 类

//工具方法
const getProperty = (obj, path) => {}

const setProperty = (obj, path, value) => {}


class OptionsDefaulter {
  constructor(){
    this.defaults = {};
    this.config = {};
  }
  //原型方法
    process(options){
  }
  set(name,config,def){
  }
}


2,getProperty 方法

const getProperty = (obj, path) => {
    let name = path.split(".");
    for (let i = 0; i < name.length - 1; i++) {
        obj = obj[name[i]];
        if (typeof obj !== "object" || !obj || Array.isArray(obj)) return;
    }
    return obj[name.pop()];
};

作用:这个函数是尝试获取对象的某个键,键的递进用点来连接,如果获取失败返回undefined。避免了多次判断 obj[key] 是否为undefined



2,webpackOptionstDefaulter类

        this.set("experiments", "call", value => ({ ...value }));
        this.set("experiments.asset", false);
        this.set("experiments.mjs", false);
        this.set("experiments.importAwait", false);
        this.set("experiments.importAsync", false);
        this.set("experiments.topLevelAwait", false);
        this.set("experiments.syncWebAssembly", false);
        this.set("experiments.asyncWebAssembly", false);
        this.set("experiments.outputModule", false);

        this.set("entry", "./src");

添加前 options

image.png


执行后options


image.png


一句话总结:WebpackOptionsDefaulter模块对options配置对象添加了大量的默认参数。


九,Compiler 模块

webpack的主要引擎,在compiler对象记录了完整的webpack环境信息,在webpack从启动到结束,compiler只会生成一次。你可以在compiler对象上读取到webpack config信息,outputPath等;


const compiler = new Compiler(options.context);


class Compiler {
  constructor(context){
    this.hooks = Object.freeze({
        shouldEmit: new SyncBailHook(["compilation"]),
        done: new AsyncSeriesHook(["stats"]),
        afterDone: new SyncHook(["stats"]),
        ....
    })
    options
    
  }
    watch(watchOptions, handler) {
    ...
  }
  run(callback){
    ....
  }
}

1, tapable 模块(超级管家

image.png


Tapable的核心功能就是依据不同的钩子将注册的事件在被触发时按序执行。它是典型的”发布订阅模式“,webpack的灵活配置得益于 Tapable 提供强大的钩子体系,让编译的每个过程都可以“钩入”,在webpack中订阅的事件最终都会放入taps栈中,订阅分为三种类型async, sync, promise,调用时会通过_createCall方法调用HookCodeFactory的create方法创建委托函数


compiler-hooks钩子:webpack.js.org/api/compile…



2,NodeEnvironmentPlugin

    new NodeEnvironmentPlugin({
        infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);

作用:该插件的主要作用是给complier初始化输入输出文件系统和监视文件系统。

class NodeEnvironmentPlugin {
    constructor(options) {
        this.options = options || {};
    }
  apply(compiler){
    compiler.inputFileSystem = new CachedInputFileSystem(fs, 60000);
        const inputFileSystem = compiler.inputFileSystem;
        compiler.outputFileSystem = fs;
        compiler.intermediateFileSystem = fs;
        compiler.watchFileSystem = new NodeWatchFileSystem(
            compiler.inputFileSystem
        );
    //第一个钩子
    compiler.hooks.beforeRun.tap()
  }
}
  • 输出文件系统:非常简单,就是包装了一层node原生的fsapi。
  • 输入文件系统:复杂,目前还没有看见使用的地方。
  • 监视文件系统:复杂,目前还没有看见使用的地方,但是主要的作用就是监视文件改动及热更新等。


    if (Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }

作用:webpack.config.js中的plugins挂载:



紧接着 注册两个钩子

    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();

image.png

3,WebpackOptionsApply模块

    compiler.options = new WebpackOptionsApply().process(options, compiler);
  • optionsApply
class OptionsApply {
    process(options, compiler) {}
}
  • WebpackOptionsApply
class WebpackOptionsApply extends OptionsApply {
  constructor() {
        super();
    }
  process(options, compiler) {
    compiler.outputPath = options.output.path;
        compiler.recordsInputPath = options.recordsInputPath || options.recordsPath;
        compiler.recordsOutputPath =
        options.recordsOutputPath || options.recordsPath;
    ...
    
    case "web": {
                    const JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin");
                    const FetchCompileWasmPlugin = require("./web/FetchCompileWasmPlugin");
                    const FetchCompileAsyncWasmPlugin = require("./web/FetchCompileAsyncWasmPlugin");
                    const NodeSourcePlugin = require("./node/NodeSourcePlugin");
                    const ChunkPrefetchPreloadPlugin = require("./prefetch/ChunkPrefetchPreloadPlugin");
                    new JsonpTemplatePlugin().apply(compiler);
                    new FetchCompileWasmPlugin({
                        mangleImports: options.optimization.mangleWasmImports
                    }).apply(compiler);
                    new FetchCompileAsyncWasmPlugin().apply(compiler);
                    new NodeSourcePlugin(options.node).apply(compiler);
                    new LoaderTargetPlugin(options.target).apply(compiler);
                    new ChunkPrefetchPreloadPlugin().apply(compiler);
                    break;
                }
    ....
    
        new JavascriptModulesPlugin().apply(compiler);
          new JsonModulesPlugin().apply(compiler);
    
    ....
    
    compiler.hooks.afterPlugins.call(compiler);
    ...
  }
}

作用:

  • 1.处理options.target参数
  • 2.处理options.output,options.externals,options.devtool参数
  • 3.对于引用了巨量的模块把把this指向compiler对象
  • 4.处理options.optimization 的moduleIds和chunkIds属性
  • 5,处理各种插件
  • 6,hooks事件流


这个模块主要是根据options选项的配置,设置compile的相应的插件,属性,里面写了大量的 apply(compiler); 使得模块的this指向compiler


new Compiler时:

image.png


执行完毕时:

image.png

执行完毕后的hooks:

image.png


戏说下:


楚汉争霸,楚国大军进攻,探子回报,一支穿云箭射出,options


汉高祖刘邦收到后,着令密探司(validateSchema)查验情报真伪,密探司对照密码本(validate)翻译无误,确认楚军,人数 10W,步兵,骑兵齐全,约定乌江大战,


刘邦命张良WebpackOptionsDefaulter ),根据国内情况,做好站前动员,针对密报,输出详细的章程。(new options),


再命 韩信为大将军(Compiler),萧何为后勤主管(tapable),制定作战计划(HOOKS),并派出侦查营(NodeEnvironmentPlugin),随时通报敌军情况。


韩信受命后,先根据密报中敌军队伍配置(plugins),按照相克原则排兵布阵,并对整个做作战计划进行了详细的补充,比如战争前,战争中,战争后等各个阶段做好方案和准备。


方案完成后,上报刘邦,通过,大军开拔,,,,,


万事具备,只欠东风:

compiler.run()


十,未完待续。。。。。