写个 mini-webpack 实现最核心的打包功能

有了前面 babel 的基础,加上我们分析过了一些 webpack 的打包结果。接下来我们去实现webpack最核心的功能:打包功能(将入口文件及其关联的其他文件全部打包成一个文件)

核心思路

我们知道,打包其实就是将多个模块组装成一个模块

那么我们能想到最简单直接的方法就是将多个文件读出来,然后字符串拼接,最后输出到一个文件。这样做会面临几个问题:

  • 第一个问题是并不知道需要读哪些文件(依赖问题)?

简单粗暴的方式是直接将项目中的文件全部拼进去,但是有可能用到全局安装的包。所以对于要读哪些文件的这个问题的最佳解决方案还是:依赖分析,递归遍历

  • 第二个问题是变量命名冲突

因为我们之前每个模块是独立的,所以在模块之间变量命名是自由的,现在将他们组合到一起,那么这种自由就有可能导致冲突。那么解决这个问题的思路也有两个方向:1、执行环境相同的情况下,给变量将命名空间前缀;2、想办法替代或者模拟隔离环境。webpack采用的是第二种方案,也就将模块内容全部包裹到一个函数中,进而模拟模块隔离环境。

webpack打包的实现思路是:从一个入口文件开始,分析依赖,然后再深度优先的分析依赖的依赖,直到相关的依赖文件全部遍历一遍,最终建立了一个资源Map清单。最后再实现一个依赖加载函数(__webpack_require__),并且相应地修改文件中的依赖导入导出方式,让其使用我们实现的依赖加载函数去导入依赖。这样在加载依赖的时候,我们的依赖加载函数就可以从我们的资源清单中去查找并返回依赖资源,这样就实现了将多个模块组装成一个模块的目的。

递归加载依赖(Compiler)

我们先实现一个最基本的webpack结构,就是深度遍历优先的递归加载依赖。

生成资源清单

其实整体思路很简单:就是解析依赖再遍历依赖。代码如下:

js 复制代码
run() {
    const filePath = this.options.entry
    const { deps, code } = this.build(filePath)
    this.modulesCacheMap[filePath] = code

    // 使用数组结构来进行递归解析依赖
    const moduleDeps = [deps]
    while (moduleDeps.length) {
        const deps = moduleDeps.shift()
        // 深度优先遍历
        for (const path of deps) {
            if (this.modulesCacheMap[path]) {
                continue
            }
            const { deps, code } = this.build(path)
            moduleDeps.push(deps)
            this.modulesCacheMap[path] = code
        }
    }
}

我们将文件依赖分析处理的逻辑提取到 build 中去处理,代码如下:

js 复制代码
build(relativePath) {
    const { parser } = this
    // 1、加载(fs读取文件)
    parser.load(relativePath)
    // 2、解析(将文件解析成ast)
    parser.parser()
    // 3、解析并转化导入导出语句(import、require变成__webpack_require__) => deps、code
    const { deps, code } = parser.transform()

    // 加入缓存
    const module = { deps, code }
    this.modulesCacheMap[relativePath] = code

    return module
}

根据资源清单组装成打包结果

有了资源清单 map,其实接下来的转换成 code就是否容易了。对于webpack的一些方法我们这里并不打算实现,而是直接copy过来。完整代码我放在了附录的 function.js 中。

接下来再对照webpack打包结果,组装成对应的 code 结构。代码如下:

js 复制代码
generate() {
    // 使用函数进行作用域隔离(模块处理)
    const moduleCodeMap = Object.entries(this.modulesCacheMap).reduce(
        (map, [moduleId, code = '']) => {
            const fnStr = `((module, __webpack_exports__, __webpack_require__) => { eval(\`${code}\`) })`
            return { ...map, [moduleId]: fnStr }
        },
        {}
    )
    const code = `
        (() => {
            "use strict";
            var __webpack_modules__ = (${Object.entries(moduleCodeMap).reduce((str, [moduleId, fnStr]) => {
        str += '"' + moduleId + '"' + ':' + fnStr + ','
        return str
    }, '{')
        + '}'
        })
  ${Object.values(utilsFn).join(';')}
            var __webpack_exports__ = __webpack_require__('${this.options.entry}');
            })()
        `
    return code
}

单文件解析处理(Parser)

单个文件解析我们主要需要注意的是加载文件和文件模块化的转换处理。

加载文件

加载文件主要需要处理的问题是添加默认文件后缀名问题,以及相对路径与绝对路径的处理。代码如下:

js 复制代码
load(filePath) {
    const { dirname } = this
    // 相对路径与绝对路径处理
    let relativePath = filePath
    let absolutePath = filePath
    if (path.isAbsolute(filePath)) {
        absolutePath = filePath
        relativePath = path.relative(dirname, filePath)
    } else {
        absolutePath = path.join(dirname, filePath)
        // 格式化一下相对路径的格式
        relativePath = path.relative(dirname, absolutePath)
    }

    // 补全文件后缀名
    const extname = this.getPathExtname(absolutePath)
    if (!absolutePath.endsWith(extname)) {
        relativePath += extname
        absolutePath += extname
    }

    // 保存路径信息
    this.absolutePath = absolutePath
    this.relativePath = relativePath

    this.file = fs.readFileSync(absolutePath, 'utf-8')
    return this.file
}

模块化规范处理

我们通过对于 webpack 的打包结果的分析可以发现:

  • 对于导入语句都会转化成 __webpack_require__
  • 对于导出语句,commonJs 会保持不变,而 esModule 会转化成 __webpack_require__.d 函数调用进行导出,如下:
js 复制代码
// 原数据
export const a = 1
export default 'Hello World'
js 复制代码
// 打包结果
__webpack_require__.r(__webpack_exports__)
__webpack_require__.d(__webpack_exports__, {
  a: () => /* binding */ a,
  default: () => __WEBPACK_DEFAULT_EXPORT__,
})
const a = 1
const __WEBPACK_DEFAULT_EXPORT__ = 'Hello World'

同时需要注意的是:对于 esModule webpack 在文件转换时会加上 __webpack_require__.r(__webpack_exports__) 进行标记,标记当前模块是 esModule 模块。

总体的转化方向是 commonJs

关于导入

commonJs

只是将 require 替换成 __webpack_require__,其余内容不变。如下:

js 复制代码
// 原数据
const { a } = require('./hello.js')
const b = require('./hello.js')
console.log(a, b)
js 复制代码
// 打包结果
const { a } = __webpack_require__('./hello.js')
const b = __webpack_require__('./hello.js')
console.log(a, b)

所以我们只需要捕获 require 定义,然后替换成 __webpack_require__ 即可。代码如下:

js 复制代码
VariableDeclaration(path) {
    const { node } = path
    // 将 require 编译成 __webpack_require__
    if (isRequire(node)) {
        const source = node.declarations[0].init.arguments[0].value
        const requireExpression = t.callExpression(
            t.identifier('__webpack_require__'),
            [t.stringLiteral(source)]
        )
        const variableDeclaration = t.variableDeclaration(node.kind, [
            t.variableDeclarator(node.declarations[0].id, requireExpression),
        ])
        path.replaceWith(variableDeclaration)
    }
}

esModule

将多种导入情况(多条)统一合并成一条导入语句,并且添加 esModule 模块类型标记(__webpack_require__.r(__webpack_exports__))。如下:

js 复制代码
// 原数据
import { a } from './hello.js'
import b from './hello.js'
import * as c from './hello.js'
console.log(a, b, c)
js 复制代码
// 打包结果
__webpack_require__.r(__webpack_exports__)
var _hello_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('./hello.js')
console.log(
  _hello_js__WEBPACK_IMPORTED_MODULE_0__.a,
  _hello_js__WEBPACK_IMPORTED_MODULE_0__['default'],
  _hello_js__WEBPACK_IMPORTED_MODULE_0__
)

从上面的打包结果来看,import 导入需要处理的情况就会比较复杂一些:

  • 首先总的来说,如果同一个路径 存在多个 import 导入,则需要将多个导入合并成一个导入
  • 其次那么就需要对导入的变量,在该模块中找到这些变量,并进行相应的使用方式的转换。

具体代码如下:

js 复制代码
const importPathMap = {} // 辅助数据结构(为了降低时间复杂度而设计的冗余数据)
const importNameInfos = []
ImportDeclaration(nodePath) {
    const { node } = nodePath
    const specifiers = node.specifiers
    const source = node.source.value

    if (!importPathMap[source]) {
        // _hello_js__WEBPACK_IMPORTED_MODULE_0__ webpack的设计是再加上一个size作为namespace,作为防止命名冲突的兜底逻辑
        const fileName = path.basename(source, path.extname(source))
        const moduleName = `_${fileName}_js__WEBPACK_IMPORTED_MODULE_${Object.keys(importPathMap).length
            }__`
        // 将 import 编译成 __webpack_require__
        const requireExpression = t.callExpression(
            t.identifier('__webpack_require__'),
            [t.stringLiteral(source)]
        )
        const variableDeclaration = t.variableDeclaration('const', [
            t.variableDeclarator(t.identifier(moduleName), requireExpression),
        ])
        nodePath.replaceWith(variableDeclaration)
        importPathMap[source] = { source, moduleName }
    } else {
        // 相同路径的import只保留一个
        nodePath.remove()
    }

    // 记录导入的原始名称(三种情况)
    const moduleName = importPathMap[source].moduleName
    const importInfoArr = specifiers.map((specifier) => {
        const name = specifier.local.name
        const type = specifier.type
        return { name, type, moduleName }
    })
    importNameInfos.push(...importInfoArr)
}

// 对应修改 import 导入变量的使用方式
const IMPORT_TYPE = {
  ImportSpecifier: 'ImportSpecifier',
  ImportDefaultSpecifier: 'ImportDefaultSpecifier',
  ImportNamespaceSpecifier: 'ImportNamespaceSpecifier',
}
Identifier(path) {
    const { node } = path
     // 辅助数据结构(为了降低时间复杂度而设计的冗余数据)
    const importNameInfosMap = importNameInfos.reduce((map, info) => {
        map[info.name] = info
        return map
    }, {})

    if (importNameInfosMap[node.name]) {
        const { name, type, moduleName } = importNameInfosMap[node.name]
        switch (type) {
            case IMPORT_TYPE.ImportSpecifier:
                node.name = `${moduleName}.${name}`
                break
            case IMPORT_TYPE.ImportDefaultSpecifier:
                node.name = `${moduleName}['default']`
                break
            case IMPORT_TYPE.ImportNamespaceSpecifier:
                node.name = moduleName
        }
    }
}

关于导出

commonJs

commonJs 导出内容不做处理,如下:

js 复制代码
// 原数据
exports.a = 1
module.exports = 2
js 复制代码
// 打包结果
exports.a = 1
module.exports = 2

这个地方webpack实际上做了更加细腻的处理,也就根据文件的模块化规范不同,生成的资源清单中往模块函数中注入的参数是不一样的。但是我们这里为了简单,统一将三参数固定成:module__webpack_exports____webpack_require__。所以我们这里对于 exports.a = 1 需要改造一下,改成 __webpack_exports__.a = 1。代码如下:

js 复制代码
if (!isEsModule) {
    // 只转换 exports
    traverse(ast, {
        AssignmentExpression(path) {
            const { node } = path;

            if (
                t.isMemberExpression(node.left) &&
                t.isIdentifier(node.left.object, { name: 'exports' }) &&
                t.isIdentifier(node.left.property)
            ) {
                const propertyName = node.left.property.name;
                const memberExpression = t.memberExpression(
                    t.identifier('__webpack_exports__'),
                    t.identifier(propertyName)
                );
                const exportDeclaration = t.assignmentExpression(
                    '=',
                    memberExpression,
                    node.right
                )
                path.replaceWith(exportDeclaration);
            }
        }
    })
    return
}

esModule

标记 esModule 模块类型,并且将需要导出的模块收集到一个 map 中使用 __webpack_require__.d 进行统一暴露。

js 复制代码
// 原数据
export const a = 1
export default 2
js 复制代码
// 打包结果
__webpack_require__.r(__webpack_exports__)
__webpack_require__.d(__webpack_exports__, {
  a: () => a,
  default: () => __WEBPACK_DEFAULT_EXPORT__,
})
const a = 1
const __WEBPACK_DEFAULT_EXPORT__ = 2

根据打包结果可以发现,关于 esModule 的导出首先会将要导出的全部内容(具名导出和默认导出)收集起来,存储在一个 map 中并且使用函数包裹将执行时机延后 ,再由 __webpack_require__.d 进行统一导出。

我们分成具名导出和默认导出两部分来处理。对于具名导出,我们去除前面的 export 语句,并统计导出的变量名。代码如下:

js 复制代码
const varNameSet = new Set()
traverse(ast, {
  ExportNamedDeclaration(path) {
    const { node } = path
    node.declaration.declarations.forEach((declaration) => {
      const varName = declaration.id.name
      varNameSet.add(varName)
    })
    path.replaceWith(node.declaration)
  },
})

对于默认导出其实也没有特别的,就是注意下命名的替换,代码如下:

js 复制代码
const varNameSet = new Set()
traverse(ast, {
  ExportDefaultDeclaration(path) {
    const { node } = path
    const exportDeclaration = t.variableDeclaration('const', [
      t.variableDeclarator(
        t.identifier('__WEBPACK_DEFAULT_EXPORT__'),
        node.declaration
      ),
    ])
    path.replaceWith(exportDeclaration)
    varNameSet.add('default')
  },
})

最后再根据收集到的导出变量名称集合,生成 map,并使用 __webpack_require__.d 导出。代码如下:

js 复制代码
traverse(ast, {
  Program(path) {
    const objectExpression = t.objectExpression(
      [...varNameSet].map((varName) => {
        if (varName === 'default') {
          return t.objectProperty(
            t.identifier(varName),
            t.arrowFunctionExpression(
              [],
              t.identifier('__WEBPACK_DEFAULT_EXPORT__')
            )
          )
        }
        return t.objectProperty(
          t.identifier(varName),
          t.arrowFunctionExpression([], t.identifier(varName))
        )
      })
    )

    const callExpression = t.callExpression(
      t.memberExpression(
        t.identifier('__webpack_require__'),
        t.identifier('d')
      ),
      [t.identifier('__webpack_exports__'), objectExpression]
    )
    const expressionStatement = t.expressionStatement(callExpression)
    path.node.body.splice(1, 0, expressionStatement)
  },
})

目前为止,关于导入导出的模块化处理就完成了。

总结

至此 webpack 最核心的将多个模块组装成一个模块的打包功能就实现了。当然 webpack 的打包功能远不止于此,我们实现的最简易的打包功能。其实实现打包功能的核心还是文件的导入导出转化处理。文件导入导出转换完,配合使用 __webpack_require__ 函数注入的参数变量,将模块内的信息传递出去。

最后再实现一个深度遍历优先的递归依赖解析,生成资源清单 map。有了资源清单,就可以通过一个固定的组装算法,将资源清单组装成一个模块的code,也就是打包结果。

附录

js 复制代码
// Compiler.js
const path = require('path')
const Parser = require('./Parser')
const utilsFn = require('./function')

class Compiler {
    constructor(options = {}) {
        this.options = options
        this.modulesCacheMap = {} // 用于模块缓存,将 key 统一成使用相对于 webpack.config.js 的相对路径

        // 先临时这么处理,等到后面再将 npm 包 link 到命令中
        // webpack 是以 webpack.config.js 为基准去定制相对路径
        const _path = path.resolve(
            __dirname,
            '../../webpack-test',
            this.options.entry
        )
        this.dirname = path.dirname(_path) // 打包文件的入口文件所在目录
        this.parser = new Parser(this.dirname, this.options.resolve.extensions)
    }

    // 启动webpack打包
    run() {
        // 默认 webpack.config.js entry 传入的是相对路径,且相对于 webpack.config.js 也就是项目根目录;如果不是那就是配置有问题,我们这里不处理
        const filePath = this.options.entry
        const { deps, code } = this.build(filePath)
        this.modulesCacheMap[filePath] = code

        // 使用数组结构来进行递归解析依赖
        const moduleDeps = [deps]
        while (moduleDeps.length) {
            const deps = moduleDeps.shift()
            // 深度优先遍历
            for (const path of deps) {
                if (this.modulesCacheMap[path]) {
                    continue
                }
                const { deps, code } = this.build(path)
                moduleDeps.push(deps)
                this.modulesCacheMap[path] = code
            }
        }
    }

    // build 接收的路径都是相对路径
    build(relativePath) {
        const { parser } = this
        // 1、加载(fs读取文件)
        parser.load(relativePath)
        // 2、解析(将文件解析成ast)
        parser.parser()
        // 3、解析并转化导入导出语句(import、require变成__webpack_require__) => deps、code
        const { deps, code } = parser.transform()

        // 加入缓存
        const module = { deps, code }
        this.modulesCacheMap[relativePath] = code

        return module
    }

    // 生成输出资源
    generate() {
        // 使用函数进行作用域隔离(模块处理)
        const moduleCodeMap = Object.entries(this.modulesCacheMap).reduce(
            (map, [moduleId, code = '']) => {
                const fnStr = `((module, __webpack_exports__, __webpack_require__) => { eval(\`${code}\`) })`
                return { ...map, [moduleId]: fnStr }
            },
            {}
        )
        const code = `
            (() => {
                "use strict";
                var __webpack_modules__ = (${Object.entries(moduleCodeMap).reduce((str, [moduleId, fnStr]) => {
            str += '"' + moduleId + '"' + ':' + fnStr + ','
            return str
        }, '{')
            + '}'
            })
      ${Object.values(utilsFn).join(';')}
                var __webpack_exports__ = __webpack_require__('${this.options.entry}');
                })()
            `
        return code
    }
}

module.exports = Compiler
js 复制代码
// Parser.js
const fs = require('fs')
const path = require('path')

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const t = require('@babel/types')
const { isRequire, IMPORT_TYPE, escapeQuotes } = require('./utils')

class Parser {
    constructor(dirname, extensions = []) {
        this.dirname = dirname
        this.extensions = extensions
        this.file = null
        this.ast = null
        this.code = ''
        this.relativePath = ''
        this.absolutePath = ''
        this.isEsModule = false
    }

    // 根据路径加载文件
    load(filePath) {
        const { dirname } = this
        // 相对路径与绝对路径处理
        let relativePath = filePath
        let absolutePath = filePath
        if (path.isAbsolute(filePath)) {
            absolutePath = filePath
            relativePath = path.relative(dirname, filePath)
        } else {
            absolutePath = path.join(dirname, filePath)
            // 格式化一下相对路径的格式
            relativePath = path.relative(dirname, absolutePath)
        }

        // 补全文件后缀名
        const extname = this.getPathExtname(absolutePath)
        if (!absolutePath.endsWith(extname)) {
            relativePath += extname
            absolutePath += extname
        }

        // 保存路径信息
        this.absolutePath = absolutePath
        this.relativePath = relativePath

        this.file = fs.readFileSync(absolutePath, 'utf-8')
        return this.file
    }

    parser() {
        this.ast = parser.parse(this.file, {
            sourceType: 'module',
        })
        return this.ast
    }

    // 转换AST
    transform() {
        // 模块规范标记(如果是esModule使用__webpack_require__.r进行标记)
        this.isEsModule = this.markModuleType()

        // 处理导入(转换导入语句并收集deps)
        this.handleImport()
        const deps = this.getDeps()

        // 处理导出
        this.handleExport()
        const code = this.getCode()
        return { deps, code }
    }

    markModuleType() {
        let isEsModule = false
        traverse(this.ast, {
            Program(path) {
                path.traverse({
                    // import
                    ImportDeclaration() {
                        isEsModule = true
                        path.stop() // 停止遍历
                    },
                    // export、export default
                    ExportDeclaration() {
                        isEsModule = true
                        path.stop()
                    },
                    // require
                    CallExpression(path) {
                        if (t.isIdentifier(path.node.callee, { name: 'require' })) {
                            isEsModule = isEsModule || false
                            path.stop()
                        }
                    },
                    // module.exports、exports(当esModule和commonJs混用时,优先esModule)
                    AssignmentExpression(path) {
                        if (
                            t.isMemberExpression(path.node.left) &&
                            t.isIdentifier(path.node.left.object, { name: 'module' }) &&
                            t.isIdentifier(path.node.left.property, { name: 'exports' })
                        ) {
                            isEsModule = isEsModule || false
                            path.stop()
                        } else if (
                            t.isIdentifier(path.node.left) &&
                            t.isIdentifier(path.node.left, { name: 'exports' })
                        ) {
                            isEsModule = isEsModule || false
                            path.stop()
                        }
                    },
                })
                // 添加 esModule 标记
                if (isEsModule) {
                    path.unshiftContainer(
                        'body',
                        t.expressionStatement(
                            t.callExpression(
                                t.memberExpression(
                                    t.identifier('__webpack_require__'),
                                    t.identifier('r')
                                ),
                                [t.identifier('__webpack_exports__')]
                            )
                        )
                    )
                }
            },
        })
        return isEsModule
    }

    // 导入统一转换成 __webpack_require__
    handleImport() {
        const importPathMap = {}
        const importNameInfos = []
        traverse(this.ast, {
            // import 1. 具名导入;2. 默认导入;3. 全部导入
            ImportDeclaration(nodePath) {
                const { node } = nodePath
                const specifiers = node.specifiers
                const source = node.source.value

                if (!importPathMap[source]) {
                    // _hello_js__WEBPACK_IMPORTED_MODULE_0__ webpack的设计是再加上一个size作为namespace,作为防止命名冲突的兜底逻辑
                    const fileName = path.basename(source, path.extname(source))
                    const moduleName = `_${fileName}_js__WEBPACK_IMPORTED_MODULE_${Object.keys(importPathMap).length
                        }__`
                    // 将 import 编译成 __webpack_require__
                    const requireExpression = t.callExpression(
                        t.identifier('__webpack_require__'),
                        [t.stringLiteral(source)]
                    )
                    const variableDeclaration = t.variableDeclaration('const', [
                        t.variableDeclarator(t.identifier(moduleName), requireExpression),
                    ])
                    nodePath.replaceWith(variableDeclaration)
                    importPathMap[source] = { source, moduleName }
                } else {
                    nodePath.remove()
                }

                // 记录导入的原始名称(三种情况)
                const moduleName = importPathMap[source].moduleName
                const importInfoArr = specifiers.map((specifier) => {
                    const name = specifier.local.name
                    const type = specifier.type
                    return { name, type, moduleName }
                })
                importNameInfos.push(...importInfoArr)
            },
            // require
            VariableDeclaration(path) {
                const { node } = path
                // 将 require 编译成 __webpack_require__
                if (isRequire(node)) {
                    const source = node.declarations[0].init.arguments[0].value
                    const requireExpression = t.callExpression(
                        t.identifier('__webpack_require__'),
                        [t.stringLiteral(source)]
                    )
                    const variableDeclaration = t.variableDeclaration(node.kind, [
                        t.variableDeclarator(node.declarations[0].id, requireExpression),
                    ])
                    path.replaceWith(variableDeclaration)
                }
            },
        })

        // 使用到 import 导入到变量需要进行替换
        traverse(this.ast, {
            Identifier(path) {
                const { node } = path
                const importNameInfosMap = importNameInfos.reduce((map, info) => {
                    map[info.name] = info
                    return map
                }, {})

                if (importNameInfosMap[node.name]) {
                    const { name, type, moduleName } = importNameInfosMap[node.name]
                    switch (type) {
                        case IMPORT_TYPE.ImportSpecifier:
                            node.name = `${moduleName}.${name}`
                            break
                        case IMPORT_TYPE.ImportDefaultSpecifier:
                            node.name = `${moduleName}['default']`
                            break
                        case IMPORT_TYPE.ImportNamespaceSpecifier:
                            node.name = moduleName
                    }
                }
            },
        })
    }

    handleExport() {
        const { isEsModule, ast } = this
        if (!isEsModule) {
            // 只转换 exports
            traverse(ast, {
                AssignmentExpression(path) {
                    const { node } = path;

                    if (
                        t.isMemberExpression(node.left) &&
                        t.isIdentifier(node.left.object, { name: 'exports' }) &&
                        t.isIdentifier(node.left.property)
                    ) {
                        const propertyName = node.left.property.name;
                        const memberExpression = t.memberExpression(
                            t.identifier('__webpack_exports__'),
                            t.identifier(propertyName)
                        );
                        const exportDeclaration = t.assignmentExpression(
                            '=',
                            memberExpression,
                            node.right
                        )
                        path.replaceWith(exportDeclaration);
                    }
                }
            })
            return
        }
        // 具名导出:去除 export
        // 默认导出:编译成 const __WEBPACK_DEFAULT_EXPORT__ = xxx
        // 导出内容记录在 map 中
        // 根据 map 生成 __webpack_require__.d(__webpack_exports__, map)
        const varNameSet = new Set()
        traverse(ast, {
            ExportNamedDeclaration(path) {
                const { node } = path
                node.declaration.declarations.forEach((declaration) => {
                    const varName = declaration.id.name
                    varNameSet.add(varName)
                })
                path.replaceWith(node.declaration)
            },
            ExportDefaultDeclaration(path) {
                const { node } = path
                const exportDeclaration = t.variableDeclaration('const', [
                    t.variableDeclarator(
                        t.identifier('__WEBPACK_DEFAULT_EXPORT__'),
                        node.declaration
                    ),
                ])
                path.replaceWith(exportDeclaration)
                varNameSet.add('default')
            },
        })
        // 生成 __webpack_require__.d
        traverse(ast, {
            Program(path) {
                const objectExpression = t.objectExpression(
                    [...varNameSet].map((varName) => {
                        if (varName === 'default') {
                            return t.objectProperty(
                                t.identifier(varName),
                                t.arrowFunctionExpression(
                                    [],
                                    t.identifier('__WEBPACK_DEFAULT_EXPORT__')
                                )
                            )
                        }
                        return t.objectProperty(
                            t.identifier(varName),
                            t.arrowFunctionExpression([], t.identifier(varName))
                        )
                    })
                )

                const callExpression = t.callExpression(
                    t.memberExpression(
                        t.identifier('__webpack_require__'),
                        t.identifier('d')
                    ),
                    [t.identifier('__webpack_exports__'), objectExpression]
                )
                const expressionStatement = t.expressionStatement(callExpression)
                path.node.body.splice(1, 0, expressionStatement)
            },
        })
    }

    // 获取文件依赖
    getDeps() {
        const _this = this
        const deps = new Set()

        traverse(this.ast, {
            VariableDeclaration(path) {
                const { node } = path
                if (
                    node.declarations.length === 1 &&
                    t.isCallExpression(node.declarations[0].init) &&
                    t.isIdentifier(node.declarations[0].init.callee, {
                        name: '__webpack_require__',
                    })
                ) {
                    const source = node.declarations[0].init.arguments[0].value
                    // 文件中写的可能是相对路径,也可能是绝对路径
                    const relativePath = _this.getRelativePath(source)
                    deps.add(relativePath)

                    // 顺便转换一下导入路径
                    node.declarations[0].init.arguments[0].value = relativePath
                }
            },
        })

        return deps
    }

    // 转换 AST 为 CODE
    getCode() {
        const { code } = generate(this.ast, { concise: true })
        this.code = escapeQuotes(code)
        return this.code
    }

    getPathExtname(absolutePath) {
        const { extensions } = this
        const filePaths = [
            absolutePath,
            ...extensions.map((ext) => absolutePath + ext),
        ]
        for (let _path of filePaths) {
            if (!fs.existsSync(_path)) {
                continue
            }
            return path.extname(_path)
        }
    }

    // 这个相对路径是模块相对于入口文件的位置
    // webpack将这个相对路径作为 moduleId
    getRelativePath(sourcePath) {
        const { dirname, extensions, absolutePath } = this
        const currentFileDirname = path.dirname(absolutePath)

        if (!path.isAbsolute(sourcePath)) {
            // 如果是相对路径,那就是相对于当前文件所在位置的相对路径
            sourcePath = path.resolve(currentFileDirname, sourcePath)
        }

        // 补充后缀名
        const extname = this.getPathExtname(sourcePath, extensions)
        if (extname && !sourcePath.endsWith(extname)) {
            sourcePath += extname
        }

        return path.relative(dirname, sourcePath)
    }
}

// 类的使用场景:
// 首先一个功能需求需要化分成多个步骤去完成(每一个步骤可以抽象成一个方法)
// 其实,方法之间存在先后顺序。但是有一些基础数据在各个方法进行计算时都需要,这个时候,可以将方法设计成柯里化,将这些公共数据固定下来;
// 另外一种就是设计成类,将这些数据保存到this上,这样所有的方法就可以直接使用了。

// 如果方法之间不存在过多的公共数据,那么其实可以不用设计成类,直接使用函数就可以了。
module.exports = Parser
js 复制代码
// utils.js
const t = require('@babel/types')

const isRequire = (node) => {
  return (
    node.declarations.length === 1 &&
    t.isCallExpression(node.declarations[0].init) &&
    t.isIdentifier(node.declarations[0].init.callee, { name: 'require' })
  )
}

const IMPORT_TYPE = {
  ImportSpecifier: 'ImportSpecifier',
  ImportDefaultSpecifier: 'ImportDefaultSpecifier',
  ImportNamespaceSpecifier: 'ImportNamespaceSpecifier',
}

const uniq = (arr = [], key = '') => {
  const keys = new Set()
  const uniqueArray = []
  for (const obj of arr) {
    if (!keys.has(obj[key])) {
      keys.add(obj[key])
      uniqueArray.push(obj)
    }
  }
  return uniqueArray
}

function escapeQuotes(str) {
  // 使用正则表达式替换双引号和单引号
  return str.replace(/(["'])/g, '\\$1')
}

module.exports = {
  isRequire,
  IMPORT_TYPE,
  uniq,
  escapeQuotes,
}
js 复制代码
// function.js
exports['__webpack_require__'] = `
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
  // Check if module is in cache
  var cachedModule = __webpack_module_cache__[moduleId]
  if (cachedModule !== undefined) {
    return cachedModule.exports
  }
  // Create a new module (and put it into the cache)
  var module = (__webpack_module_cache__[moduleId] = {
    // no module.id needed
    // no module.loaded needed
    exports: {},
  })

  // Execute the module function
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__)

  // Return the exports of the module
  return module.exports
}
`

exports['__webpack_require__.d'] = `
(() => {
  // define getter functions for harmony exports
  __webpack_require__.d = (exports, definition) => {
    for (var key in definition) {
      if (
        __webpack_require__.o(definition, key) &&
        !__webpack_require__.o(exports, key)
      ) {
        Object.defineProperty(exports, key, {
          enumerable: true,
          get: definition[key],
        })
      }
    }
  }
})();
`

exports['__webpack_require__.o'] = `
(() => {
  __webpack_require__.o = (obj, prop) =>
    Object.prototype.hasOwnProperty.call(obj, prop)
})();
`

exports['__webpack_require__.r'] = `
(() => {
  // define __esModule on exports
  __webpack_require__.r = (exports) => {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, {
        value: 'Module',
      })
    }
    Object.defineProperty(exports, '__esModule', { value: true })
  }
})()
`
相关推荐
浮华似水3 分钟前
Javascirpt时区——脱坑指南
前端
王二端茶倒水5 分钟前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
_oP_i10 分钟前
Web 与 Unity 之间的交互
前端·unity·交互
钢铁小狗侠12 分钟前
前端(1)——快速入门HTML
前端·html
凹凸曼打不赢小怪兽38 分钟前
react 受控组件和非受控组件
前端·javascript·react.js
狂奔solar1 小时前
分享个好玩的,在k8s上部署web版macos
前端·macos·kubernetes
qiyi.sky1 小时前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
清云随笔1 小时前
axios 实现 无感刷新方案
前端
鑫宝Code1 小时前
【React】状态管理之Redux
前端·react.js·前端框架
忠实米线1 小时前
使用pdf-lib.js实现pdf添加自定义水印功能
前端·javascript·pdf