likes
comments
collection
share

迷你版webpack实现

作者站长头像
站长
· 阅读数 9

调试webpack过程了解执行流程

 开始-合并配置------------实例化compile-------设置node文件读写能力-----通过循环挂载plugins-----处理webpack内部默认的插件(入口文件)

 开始-compiler.beforeRun-compiler.run--------compiler.beforeCompile-compiler.compile-------compile.make    

Compiler类中,构造函数内会挂载大量的钩子,这些钩子都来自tapable,挂载之后在后续的操作中,这些钩子等待被触发执行。

        this.hooks = {
            /** @type {SyncBailHook<Compilation>} */
            shouldEmit: new SyncBailHook(["compilation"]),
            /** @type {AsyncSeriesHook<Stats>} */
            done: new AsyncSeriesHook(["stats"]),
            /** @type {AsyncSeriesHook<>} */
            additionalPass: new AsyncSeriesHook([]),
            /** @type {AsyncSeriesHook<Compiler>} */
            beforeRun: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<Compiler>} */
            run: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            emit: new AsyncSeriesHook(["compilation"]),
            /** @type {AsyncSeriesHook<string, Buffer>} */
            assetEmitted: new AsyncSeriesHook(["file", "content"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            afterEmit: new AsyncSeriesHook(["compilation"]),

            /** @type {SyncHook<Compilation, CompilationParams>} */
            thisCompilation: new SyncHook(["compilation", "params"]),
            /** @type {SyncHook<Compilation, CompilationParams>} */
            compilation: new SyncHook(["compilation", "params"]),
            /** @type {SyncHook<NormalModuleFactory>} */
            normalModuleFactory: new SyncHook(["normalModuleFactory"]),
            /** @type {SyncHook<ContextModuleFactory>}  */
            contextModuleFactory: new SyncHook(["contextModulefactory"]),

            /** @type {AsyncSeriesHook<CompilationParams>} */
            beforeCompile: new AsyncSeriesHook(["params"]),
            /** @type {SyncHook<CompilationParams>} */
            compile: new SyncHook(["params"]),
            /** @type {AsyncParallelHook<Compilation>} */
            make: new AsyncParallelHook(["compilation"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            afterCompile: new AsyncSeriesHook(["compilation"]),

            /** @type {AsyncSeriesHook<Compiler>} */
            watchRun: new AsyncSeriesHook(["compiler"]),
            /** @type {SyncHook<Error>} */
            failed: new SyncHook(["error"]),
            /** @type {SyncHook<string, string>} */
            invalid: new SyncHook(["filename", "changeTime"]),
            /** @type {SyncHook} */
            watchClose: new SyncHook([]),

            /** @type {SyncBailHook<string, string, any[]>} */
            infrastructureLog: new SyncBailHook(["origin", "type", "args"]),

            // TODO the following hooks are weirdly located here
            // TODO move them for webpack 5
            /** @type {SyncHook} */
            environment: new SyncHook([]),
            /** @type {SyncHook} */
            afterEnvironment: new SyncHook([]),
            /** @type {SyncHook<Compiler>} */
            afterPlugins: new SyncHook(["compiler"]),
            /** @type {SyncHook<Compiler>} */
            afterResolvers: new SyncHook(["compiler"]),
            /** @type {SyncBailHook<string, Entry>} */
            entryOption: new SyncBailHook(["context", "entry"])
        };

实现迷你版webpack暂时不需要这么多钩子.

文件目录结构,lib文件夹下package.json中 "main": "./lib/webpack.js",迷你版webpack实现流程中一部分内容是编译前的处理,内容较多,主要是两步,一是实例化 compiler 对象( 它会贯穿整个webpack工作的过程 ),调用 compile 方法,实例化了一个compilation 对象,触发 make 监听 ,addEntry (携带context name entry 相关字段信息)方法。然后由 compiler 调用 run 方法

在compiler 实例化操作中

  1. compiler 继承 tapable,因此它具备钩子的操作能力(包括监听事件,触发事件,因为webpack是一个事件流)
  2. 在实例化了 compiler 对象之后就往它的身上挂载很多属性,其中 NodeEnvironmentPlugin 这个操作就让它具备了文件读写的能力
  3. 具备了 fs 操作能力之后又将 plugins 中的插件都挂载到了 compiler 对象身上
  4. 将内部默认的插件与 compiler 建立关系,其中 EntryOptionPlugin 处理了入口模块的 id
  5. 在实例化 compiler 的时候只是监听了 make 钩子(SingleEntryPlugin),在 SingleEntryPlugin 模块的 apply 方法中有两个钩子监听, 其中 compilation 钩子就是让 compilation 具备了利用 normalModuleFactory 工厂创建一个普通模块的能力,它就是利用一个自己创建的模块来加载需要被打包的模块。make 钩子 在 compiler.run 的时候会被触发,代码执行到这里就意味着某个模块执行打包之前的所有准备工作就完成了,由addEntry 方法调用

run方法的执行就是一堆钩子按着顺序触发(beforeRun run compile),compile 方法执行中

  1. 准备参数(其中 normalModuleFactory 是我们后续用于创建模块的)
  2. 触发beforeCompile
  3. 将第一步的参数传给一个函数,开始创建一个 compilation (newCompilation)
  4. 在调用 newCompilation 的内部,调用了 createCompilation ,触发了 this.compilation 钩子 和 compilation 钩子的监听
  5. 当创建了 compilation 对象之后就触发了 make 钩子
  6. 当触发 make 钩子监听的时候,将 compilation 对象传递了过去

template文件夹下main.ejs为模板文件Chunk.js中处理chunk数据结构基本信息

// 处理chunk结构信息
class Chunk {
    constructor(entryModule) {
        this.entryModule = entryModule
        this.name = entryModule.name
        this.files = [] // 记录当前chunk的文件信息
        this.modules = [] // 记录当前chunk包含的所有模块

    }
}

module.exports = Chunk

Compilation.js处理编译时具体要做的事情

  1. make 钩子在被触发的时候,接收到了 compilation 对象实现,它的身上挂载了很多内容
  2. 从 compilation 当中解构了三个值, entry : 当前需要被打包的模块的相对路径(./src/index.js), name: main ,context: 当前项目的根路径
  3. dep 是对当前的入口模块中的依赖关系进行处理
  4. 在 compilation实例的身上有一个 addEntry 方法,然后内部调用了 _addModuleChain 方法,去处理依赖
  5. 在 compilation 当中我们可以通过 NormalModuleFactory 工厂来创建一个普通的模块对象
  6. 在 webpack 内部默认启了一个 100 并发量的打包操作,这里用 normalModule.create()模拟实现
  7. 在 beforeResolve 里面会触发一个 factory 钩子监听,这个部分的操作其实是处理 loader
  8. 上述操作完成之后获取到了一个函数被存在 factory 里,然后对它进行了调用,在这个函数调用里又触发了一个叫 resolver 的钩子,这是在处理 loader拿到的,resolver方法就意味着所有的Loader 处理完毕
  9. ] 调用 resolver() 方法之后,就会进入到 afterResolve 这个钩子里,然后就会触发 new NormalModule
  10. 在完成上述操作之后就将module 进行了保存和一些其它属性的添加
  11. 调用 buildModule 方法开始编译、调用 build 、doBuild
const {
    Tapable,
    SyncHook
} = require('tapable')
const path = require('path')
const async = require('neo-async')
const Parser = require('./Parser')
const NormalModuleFactory = require('./NormalModuleFactory')
const Chunk = require('./Chunk')
const ejs = require('ejs')

// 实例化一个 normalModuleFactory parser 
const normalModuleFactory = new NormalModuleFactory()
const parser = new Parser()

class Compilation extends Tapable {
    constructor(compiler) {
        super()
        this.compiler = compiler
        this.context = compiler.context
        this.options = compiler.options
        // 让compilation具备文件读写能力
        this.inputFileSystem = compiler.inputFileSystem
        this.outputFileSystem = compiler.outputFileSystem

        this.entries = [] //  存放所有入口模块的数组
        this.modules = [] // 存放所有模块的数据
        this.chunks = [] // 存放当前次打包过程中产出的chunk
        this.assets = []
        this.files = []
        this.hooks = {
            succeedModule: new SyncHook(['module']),
            seal: new SyncHook(),
            beforeChunks: new SyncHook(),
            afterChunks: new SyncHook()
        }
    }

    // 完成模块编译操作 context:当前项目的根,entry:当前入口的相对路径,name:chunkName mian, callback:回调
    addEntry(context, entry, name, callback) {
        this._addModuleChain(context, entry, name, (err, module) => {
            callback(err, module)
        })
    }

    _addModuleChain(context, entry, name, callback) {
        this.createModule({
            name,
            context,
            rawRequest: entry,
            resource: path.posix.join(context, entry), // 当前操作的核心作用就是返回entry入口的绝对路径
            parser,
            moduleId: './' + path.posix.relative(context, path.posix.join(context, entry)),
        }, (entryModule) => {
            this.entries.push(entryModule)
        }, callback)

    }

    /**
     * 定义一个创建模块的方法,达到复用的目的(抽象代码来源于_addModuleChain)
     *  data 创建模块时所需要的一些属性值 
     *  doAddEntry 可选参数,在加载入口模块的时候,将入口模块的id 写入 this.entries 
     *  callback 
     */
    createModule(data, doAddEntry, callback) {
        // 创建模块工厂实例
        let module = normalModuleFactory.create(data)
        // let entryModule = normalModuleFactory.create({
        //     name,
        //     context,
        //     rawRequest: entry,
        //     resource: path.posix.join(context, entry), // 当前操作的核心作用就是返回entry入口的绝对路径
        //     parser
        // })

        const afterBuild = (err, module) => {
            // 在afterBuild当中我们需要判断一下当前次的module加载完成之后是否需要处理依赖加载
            // NormalModule里面有一个dependencies
            if (module.dependencies.length > 0) {
                // 当前逻辑标识module有需要依赖加载的模块,我们可以单独定义一个方法来实现
                this.processDependencies(module, err => {
                    callback(err, module)
                })
            } else {
                callback(err, module)
            }
        }
        this.buildModule(module, afterBuild)
        // 完成本次的build操作之后,讲module进行保存
        doAddEntry && doAddEntry(module) // 判断第二个参数是否存在
        // this.entries.push(entryModule)
        this.modules.push(module)
    }

    // 完成具体的build行为,module代表要被编译的模块,
    buildModule(module, callback) {
        module.build(this, err => {
            // 回调函数代表当前module的编译操作已经完成
            this.hooks.succeedModule.call(module)
            callback(err, module)
        })
    }

    //  当前的函数核心功能就是实现一个被依赖模块的递归加载
    processDependencies(module, callback) {
        // 加载模块的思想都是创建一个模块,然后将被加载模块的内容拿进来
        // 当前 module 依赖模块是未知数, 此时我们需要想办法让所有的被依赖的模块都加载完成之后再执行 callback?[neo-async]
        let dependencies = module.dependencies
        async.forEach(dependencies, (dependency, done) => {
            // 和NormalModule.js中定义的dependencies数组push的数据结构类似
            this.createModule({
                name: dependency.name,
                context: dependency.context,
                rawRequest: dependency.rawRequest,
                moduleId: dependency.moduleId,
                resource: dependency.resource,
                parser,
            }, null, done)
        }, callback)
    }

    // 封装
    seal(callback) {
        this.hooks.seal.call()
        this.hooks.beforeChunks.call()

        // 当前所有入口模块存放在 compilation对象的entries数组里
        // 封装chunks指的就是依据某个入口,然后找到他的所有依赖,将他们的源代码放在一起,之后再做合并
        for (const entryModule of this.entries) {
            // 分步处理chunk
            // 核心步骤:创建模块加载已有模块内容,同时记录模块信息
            const chunk = new Chunk(entryModule)

            // 保存chunk信息
            this.chunks.push(chunk)

            // 给chunk属性赋值
            chunk.modules = this.modules.filter(module => module.name === chunk.name)
        }

        // chunk流程梳理之后就进入到chunk代码处理环节,可以根据 模板文件和模块中的源代码生成chunk.js
        this.hooks.afterChunks.call(this.chunks)

        // 生成代码内容
        this.createChunkAssets()

        callback()
    }


    createChunkAssets() {
        for (let i = 0; i < this.chunks.length; i++) {
            const chunk = this.chunks[i]
            const filename = chunk.name + '.js'; // 文件名
            chunk.files.push(filename)

            // 生成具体的chunk内容

            // 获取模板文件路径
            let templatePath = path.posix.join(__dirname, 'template/main.ejs')
            // 读取模板文件中的内容
            let templateCode = this.inputFileSystem.readFileSync(templatePath, 'utf-8')
            // 获取渲染函数
            let templateRender = ejs.compile(templateCode)
            // 根据ejs语法渲染数据
            let source = templateRender({
                entryModuleId: chunk.entryModule.moduleId,
                modules: chunk.modules
            })
            console.log('filename', filename, source)
            // 输出到文件
            this.emitAssets(filename, source)
        }
    }

    emitAssets(fileName, source) {
        this.assets[fileName] = source
        this.files.push(fileName)
    }
}

module.exports = Compilation

Compiler.js文件实现compiler实例化,挂载钩子,写入文件,实现run方法。

const {
    Tapable,
    AsyncSeriesHook,
    SyncBailHook,
    SyncHook,
    AsyncParallelHook
} = require('tapable')
const Stats = require('./Stats')
const path = require('path')
const mkdirp = require('mkdirp')
const Compilation = require('./Compilation')
const NormalModuleFactory = require('./NormalModuleFactory')
const {
    emit
} = require('process')

class Compiler extends Tapable {
    constructor(context) {
        super()
        this.context = context
        // 源码中的钩子会有很多
        this.hooks = {
            done: new AsyncSeriesHook(["stats"]),
            entryOption: new SyncBailHook(["context", "entry"]),

            beforeRun: new AsyncSeriesHook(["compiler"]),
            run: new AsyncSeriesHook(["compiler"]),

            thisCompilation: new SyncHook(["compilation", "params"]),
            compilation: new SyncHook(["compilation", "params"]),

            beforeCompile: new AsyncSeriesHook(["params"]),
            compile: new SyncHook(["params"]),
            make: new AsyncParallelHook(["compilation"]),
            afterCompile: new AsyncSeriesHook(["compilation"]),

            emit: new AsyncSeriesHook(['compilation'])
        }
    }

    newCompilationParams() {
        const params = {
            normalModuleFactory: new NormalModuleFactory()
        }
        return params
    }

    createCompilation() {
        return new Compilation(this)
    }

    newCompilation(params) {
        const compilation = this.createCompilation()
        this.hooks.thisCompilation.call(compilation, params)
        this.hooks.compilation.call(compilation, params)
        return compilation
    }

    compile(callback) {
        const params = this.newCompilationParams()

        this.hooks.beforeRun.callAsync(params, err => {
            this.hooks.compile.call(params)
            const compilation = this.newCompilation(params)

            this.hooks.make.callAsync(compilation, err => {
                // 触发SingleEntryPlugin埋的钩子
                // console.log('make hook run')
                // callback(err, compilation)
                // 这里开始处理chunk
                compilation.seal(err => {
                    this.hooks.afterCompile.callAsync(compilation, err => {
                        callback(err, compilation)
                    })
                })
            })
        })

    }

    emitAssets(compilation, callback) {
        // 创建dist,在目录创建完成之后完成文件的写操作

        // 定义一个工具方法用于执行文件的生成操作
        const emitFiles = err => {
            const assests = compilation.assets // 键值对结构,键是文件名称,值是文件代码
            // 根据webpack.js中实例化时创建的结构拿到相应信息
            let outputPath = this.options.output.path
            for (let file in assests) {
                let source = assests[file]

                let targetPath = path.posix.join(outputPath, file)
                console.log(targetPath, source)
                this.outputFileSystem.writeFileSync(targetPath, source, 'utf-8')
            }
            callback(err)
        }

        // 创建目录后启动文件写入
        this.hooks.emit.callAsync(compilation, err => {
            mkdirp.sync(this.options.output.path)
            emitFiles()
        })
    }

    run(callback) {
        console.log('run function')
        const finalCallback = function (err, stats) {
            callback(err, stats)
        }
        const onCompilied = (err, compilation) => {
            
            // console.log('onCompiled function')
            // finalCallback(err, new Stats(compilation))

            // finalCallback(err, {
            //     toJson() {
            //         return {
            //             entries: [], // 当前打包入口信息
            //             chunks: [], // 当前打包代码块信息
            //             modules: [], // 模块信息
            //             assets: [], // 打包生成的资源
            //         }
            //     }
            // })

            // 这里将处理好的chunk写入到指定的dist目录
            this.emitAssets(compilation, err => {
                let stats = new Stats(compilation)
                finalCallback(err, stats)
            })
        }

        this.hooks.beforeRun.callAsync(this, (err) => {
            this.hooks.run.callAsync(this, err => {
                this.compile(onCompilied)
            })
        })

        // callback(null, {
        //     toJson() {
        //         return {
        //             entries: [], // 当前打包入口信息
        //             chunks: [], // 当前打包代码块信息
        //             modules: [], // 模块信息
        //             assets: [], // 打包生成的资源
        //         }
        //     }
        // })

    }
}

module.exports = Compiler

EntryOptionPluginSingleEntryPlugin给文件打包入口添加监听钩子

const SingleEntryPlugin = require("./SingleEntryPlugin")

const itemToPlugin = function (context, item, name) {
    return new SingleEntryPlugin(context, item, name)
}

class EntryOptionPlugin {
    apply(compiler) {
        // 添加钩子监听
        compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
            itemToPlugin(context, entry, 'main').apply(compiler)
        })
    }
}

module.exports = EntryOptionPlugin

class SingleEntryPlugin {
    constructor(context, entry, name) {
        this.context = context
        this.entry = entry
        this.name = name
    }

    apply(compiler) {
        // add hook
        compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
            const {
                context,
                entry,
                name
            } = this
            console.log('make hook run')
            compilation.addEntry(context, entry, name, callback)
        })
    }
}

module.exports = SingleEntryPlugin

NodeEnvironmentPlugin.js文件主要给compiler挂载node读写文件功能

const fs = require('fs')

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

  apply(complier) {
    // 源码中还有处理日志的功能,这里暂不需要,这里只需要使compiler具备文件读写能力即可
    complier.inputFileSystem = fs
    complier.outputFileSystem = fs
  }
}

module.exports = NodeEnvironmentPlugin

NormalModule模块处理源码转换成AST语法树,替换一些关键字段,之后转换成可执行代码

const path = require('path')
const types = require('@babel/types')
const generator = require('@babel/generator').default // ast转换成代码
const traverse = require('@babel/traverse').default

class NormalModule {
    constructor(data) {
        this.context = data.context
        this.name = data.name
        this.rawRequest = data.rawRequest
        this.moduleId = data.moduleId // createMdoule参数而来
        this.parser = data.parser // 等待完成
        this.resource = data.resource
        this._source // 存放某个模块的源代码
        this._ast // 存放某个模块源代码对于AST
        this.dependencies = [] // 定义一个空数组用于保存被依赖加载的模块信息
    }

    build(compilation, callback) {
        // 从文件中读取到将来需要被加载的module内容
        // 如果当前不是js模块则需要Loader进行处理,最终返回js模块
        // 上述的操作完成之后就可以将js代码转换为AST语法树
        // 当前js模块内部可能又引用了很多其他的模块,因此我们需要递归完成,前面的完成之后,重复执行即可

        this.doBuild(compilation, err => {
            this._ast = this.parser.parse(this._source)

            // 这里的_ast就是当前的module的语法树,我们可以对它进行修改,最后再将ast转换为code代码
            traverse(this._ast, {
                CallExpression: (nodepath) => {
                    let node = nodepath.node

                    // 定位require所在的节点
                    if (node.callee.name === 'require') {
                        // 获取原始请求路径
                        let modulePath = node.arguments[0].value // './extra
                        // 获取当前被加载的模块名称
                        let moduleName = modulePath.split(path.posix.sep).pop() // extra
                        // 当前打包器只处理js
                        let extName = moduleName.indexOf('.') === -1 ? '.js' : ''
                        moduleName += extName // extra.js
                        // 最终我们想要读取当前js里面的内容,我们需要一个绝对路径
                        let depResource = path.posix.join(path.posix.dirname(this.resource), moduleName)
                        // 将当前的模块id定义
                        let depmoduleID = './' + path.posix.relative(this.context, depResource) // 得到了'./src/extra.js'
                        console.log(depmoduleID) // ./src/extra

                        // 记录当前被依赖的模块信息,方便后面递归加载
                        this.dependencies.push({
                            name: this.name, // 将来需要动态修改
                            context: this.context,
                            rawRequest: moduleName,
                            moduleId: depmoduleID,
                            resource: depResource
                        })

                        // 替换内容,1,require-> __webpack_require__
                        node.callee.name = "__webpack_require__"
                        node.arguments = [types.stringLiteral(depmoduleID)]
                    }
                }
            })

            // 将修改后的ast转换为可执行code
            let {
                code
            } = generator(this._ast)
            this._source = code
            callback(err)
        })
    }

    doBuild(compilation, callback) {
        this.getSource(compilation, (err, source) => { // 获取源码 
            // 处理读到的文件内容
            this._source = source
            callback()
        })
    }

    getSource(compilation, callback) {
        compilation.inputFileSystem.readFile(this.resource, 'utf-8', callback)
    }
}

module.exports = NormalModule

NormalModuleFactory工厂类,创建NormalModule实例

const NormalModule = require('./NormalModule')

class NormalModuleFactory {
    create(data) {
        return new NormalModule(data)
    }
}

module.exports = NormalModuleFactory

Parser编译工具类

const {
    Tapable
} = require('tapable')
const babylon = require('babylon')

class Parser extends Tapable {
    parse(source) {
        return babylon.parse(source, {
            sourceType: 'module', // 代表是一个模块
            plugins: ['dynamicImport'], // 当前插件可以支持import()动态导入的语法
        })
    }
}

module.exports = Parser

SingleEntryPlugin单文件打包入口,埋入钩子

class SingleEntryPlugin {
    constructor(context, entry, name) {
        this.context = context
        this.entry = entry
        this.name = name
    }

    apply(compiler) {
        // add hook
        compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
            const {
                context,
                entry,
                name
            } = this
            console.log('make hook run')
            compilation.addEntry(context, entry, name, callback)
        })
    }
}

module.exports = SingleEntryPlugin

Stats提取compilation主要字段给写入文件使用

class Stats {
    constructor(compilation) {
        this.entries = compilation.entries
        this.modules = compilation.modules
        this.chunks = compilation.chunks
        this.files = compilation.files
    }

    toJson() {
        return this
    }
}

module.exports = Stats

WebpackOptionsApply文件打包执行

const EntryOptionPlugin = require("./EntryOptionPlugin")

class WebpackOptionsApply {
    process(options, compiler) {
        new EntryOptionPlugin().apply(compiler)
        
        compiler.hooks.entryOption.call(options.context, options.entry)
    }

}

module.exports = WebpackOptionsApply

webpack.js实现webpack流程主要步骤

const Compiler = require("./Compiler")
const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin')


const webpack = function (options) {
    // 实例化 compiler 对象
    let compiler = new Compiler(options.context)
    compiler.options = options
    // 初始化 NodeEnvironmentPlugin(让compiler具体文件读写能力)
    new NodeEnvironmentPlugin().apply(compiler)
    // 挂载所有 plugins 插件至 compiler 对象身上
    if (options.plugins && Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            plugin.apply(compiler)
        }
    }
    // 挂载所有 webpack 内置的插件(入口)
    compiler.options = new WebpackOptionsApply().process(options, compiler)

    // 最后返回
    return compiler

}

module.exports = webpack

webpack.js处理打包主流程

const Compiler = require("./node/Compiler")
const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin')
const WebpackOptionsApply = require("./node/WebpackOptionsApply")


const webpack = function (options) {
    // 实例化 compiler 对象
    let compiler = new Compiler(options.context)
    compiler.options = options
    // 初始化 NodeEnvironmentPlugin(让compiler具体文件读写能力)
    new NodeEnvironmentPlugin().apply(compiler)
    // 挂载所有 plugins 插件至 compiler 对象身上
    if (options.plugins && Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            plugin.apply(compiler)
        }
    }
    // 挂载所有 webpack 内置的插件(入口)
    new WebpackOptionsApply().process(options, compiler)

    // 最后返回
    return compiler

}

module.exports = webpack

测试代码

let webpack = require('./myPack')
let options = require('./webpack.config.js')

let complier = webpack(options)

complier.run((err, stats) => {
  console.log(err)
  console.log(stats)
})

运行测试代码,执行通过,符合预期