{"id":20916190,"url":"https://github.com/emosheeep/bundler","last_synced_at":"2026-04-30T20:32:02.776Z","repository":{"id":42913699,"uuid":"249419721","full_name":"emosheeep/bundler","owner":"emosheeep","description":"对比Webpack，使用Babel+Node实现一个100行的小型打包工具。学习demo，用来理解webpack的原理，附详细的README和文章","archived":false,"fork":false,"pushed_at":"2023-01-05T16:51:52.000Z","size":680,"stargazers_count":1,"open_issues_count":14,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-01T03:04:29.396Z","etag":null,"topics":["babel","nodejs","webpack"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/emosheeep.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-03-23T12:04:44.000Z","updated_at":"2020-03-30T13:13:47.000Z","dependencies_parsed_at":"2023-02-04T07:46:13.408Z","dependency_job_id":null,"html_url":"https://github.com/emosheeep/bundler","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/emosheeep/bundler","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emosheeep%2Fbundler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emosheeep%2Fbundler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emosheeep%2Fbundler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emosheeep%2Fbundler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/emosheeep","download_url":"https://codeload.github.com/emosheeep/bundler/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emosheeep%2Fbundler/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32476682,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-30T13:12:12.517Z","status":"ssl_error","status_checked_at":"2026-04-30T13:12:06.837Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["babel","nodejs","webpack"],"created_at":"2024-11-18T16:20:23.628Z","updated_at":"2026-04-30T20:32:02.760Z","avatar_url":"https://github.com/emosheeep.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 前言\nWebpack很强大，作为前端开发人员我们必须熟练掌握。但它的原理其实并不难理解，甚至很简单。毕竟所有复杂的事物都是由简单的事物组合形成的。不光是Webpack，像Vue，React这样成熟的前端框架亦是如此。\n\n读完本文，你会认识到：\n1. Webpack打包本质还是使用fs模块读写文件，加以组合。\n2. Babel真的很强大，方便我们分析源代码，提取有用的信息。\n3. 如果你了解过loader，你就会知道读取源代码之后可以如何操作，而不是仅仅进行简单的字符串匹配。\n\n另外，**希望你能跟着自己实现一遍，代码量真的不大**。\n\n[【掘金】对比Webpack，使用Babel+Node实现一个100行的小型打包工具](https://juejin.im/post/5e7882f1e51d4526e808172e)\n\n# 预备知识\n先看一个例子，也许你还不知道，node其实还有这样一个彩蛋：\n\n新建test.js输入一行代码：\n```\n/* test.js */\nconsole.log(arguments.callee.toString())\n```\n在命令行中输入`node test.js`运行结果如下：\n```\nfunction (exports, require, module, __filename, __dirname) {\n    console.log(arguments.callee.toString())\n}\n```\n**注意这是控制台输出的代码，也就是console.log()的输出结果**。\n\n由于`arguments.callee`这个属性指向函数的调用者，我们使用toString()转化后发现这居然是一个函数，由此说明，node的代码其实是运行在一个函数中的。我们写的代码最终会被这样一个函数包裹，常用的`require，module，exports，__dirname, __filename`都是这个函数的参数，所以我们才随处可用。\n\n# 进入正题\n## 代码结构\n\n![](https://user-gold-cdn.xitu.io/2020/3/23/17106d3b4200bcb7?w=272\u0026h=194\u0026f=png\u0026s=9039)\n\n- message.js：定义了两个变量，并导出\n```\nexport const message = 'qin'\nexport const weather = 'sunny day'\n```\n\n- say.js: 定义一个函数并导出\n```\nexport default function (name) {\n    console.log(`hello ${name}`)\n}\n```\n- main.css: 样式文件\n```\n#app {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    animation: breath 2s ease infinite;\n}\n@keyframes breath {\n    from, to {\n        width: 100px;\n        height: 100px;\n        background-color: black;\n    }\n    50% {\n        width: 200px;\n        height: 200px;\n        background-color: red;\n    }\n}\n```\n- main.js：入口文件\n```\nimport hello from './js/say.js'\nimport { message, weather } from './js/message.js'\nimport './css/main.css'\n\nhello(message)\n\nhello(`今天的天气是：${weather}`)\n```\n## 打包思路\n1. 首先我们要从入口文件`main.js`开始，递归解析依并读取文件内容。可以使用`@babel/parser`来实现。\n2. 获取文件内容之后做相应的处理，例如css我们需要使用一点js代码构建style节点，并插入页面中，也就是常说的`CSS in JS`这个概念。\n3. 将所有资源合并成一个文件，实现打包。打包后的代码要运行在浏览器环境中，所以为了避免产生全局污染，我们需要将打包后的代码放进闭包中运行，并为其传递所运行需要的参数，所以，打包后的代码整体结构如下：\n```\n(function (参数) {\n    /* 函数体 */\n})(传参)\n```\n### 面临的问题\n1. 浏览器不认import语法，我们需要使用babel转换为ES5\n2. 我们的打包工具运行在node环境中，打包过程中势必使用CommonJs的模块规范，即使用require和module.exports来组织模块之间的引用关系。但问题是浏览器中没有require，没有module，没有exports。\n\n聪明的你应该想到了，**开篇提到的例子**就是为了解决这个问题。\n\n### 借鉴webpack\n配置webpack进行打包，具体配置非常简单这里就不贴代码了。如果你还不会配置的话，或许需要先学习webpack的基础知识。\n\n我删剪了部分代码，那不属于我们讨论的范畴，最后生成的bundle.js内容如下：\n```\n(function(modules) {\n\t// webpack中用来模拟node环境下的require函数\n \tfunction __webpack_require__(path) {\n        // 构造一个模块\n        var module = { exports: {} };\n\t\t// 执行模块对应的函数\n        modules[path].call(module.exports, module, module.exports,__webpack_require__);\n        \n\t\t// 返回模块加载的的结果\n        return module.exports;\n \t}\n\n \t__webpack_require__(\"./src/main.js\");\n }) ({\n    \"./src/css/main.css\": (function(module, exports, __webpack_require__) {\n        eval(\"var api = __webpack_require__(/*! ../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js */ \\\"./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\\\");\\n            var content = __webpack_require__(/*! !../../node_modules/css-loader/dist/cjs.js!./main.css */ \\\"./node_modules/css-loader/dist/cjs.js!./src/css/main.css\\\");\\n\\n            content = content.__esModule ? content.default : content;\\n\\n            if (typeof content === 'string') {\\n              content = [[module.i, content, '']];\\n            }\\n\\nvar options = {};\\n\\noptions.insert = \\\"head\\\";\\noptions.singleton = false;\\n\\nvar update = api(content, options);\\n\\nvar exported = content.locals ? content.locals : {};\\n\\n\\n\\nmodule.exports = exported;\\n\\n//# sourceURL=webpack:///./src/css/main.css?\");\n    }),\n\n    \"./src/js/message.js\": (function(module, __webpack_exports__, __webpack_require__) {\n        \"use strict\";\n        eval(\"__webpack_require__.r(__webpack_exports__);\\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \\\"message\\\", function() { return message; });\\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \\\"weather\\\", function() { return weather; });\\nconst message = 'qin';\\nconst weather = 'sunny day';\\n\\n//# sourceURL=webpack:///./src/js/message.js?\");\n    }),\n\n    \"./src/js/say.js\": (function(module, __webpack_exports__, __webpack_require__) {\n        \"use strict\";\n        eval(\"__webpack_require__.r(__webpack_exports__);\\n/* harmony default export */ __webpack_exports__[\\\"default\\\"] = (function (name) {\\n  console.log(`hello ${name}`);\\n});\\n\\n//# sourceURL=webpack:///./src/js/say.js?\");\n    }),\n\n    \"./src/main.js\": (function(module, __webpack_exports__, __webpack_require__) {\n        \"use strict\";\n        eval(\"__webpack_require__.r(__webpack_exports__);\\n/* harmony import */ var _js_say_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./js/say.js */ \\\"./src/js/say.js\\\");\\n/* harmony import */ var _js_message_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./js/message.js */ \\\"./src/js/message.js\\\");\\n/* harmony import */ var _css_main_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./css/main.css */ \\\"./src/css/main.css\\\");\\n/* harmony import */ var _css_main_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_css_main_css__WEBPACK_IMPORTED_MODULE_2__);\\n\\n\\n\\nObject(_js_say_js__WEBPACK_IMPORTED_MODULE_0__[\\\"default\\\"])(_js_message_js__WEBPACK_IMPORTED_MODULE_1__[\\\"message\\\"]);\\nObject(_js_say_js__WEBPACK_IMPORTED_MODULE_0__[\\\"default\\\"])(`今天的天气是：${_js_message_js__WEBPACK_IMPORTED_MODULE_1__[\\\"weather\\\"]}`);\\n\\n//# sourceURL=webpack:///./src/main.js?\");\n    })\n\n});\n```\n可以看到，整体是一个闭包函数，传递的参数为已加载的所有的模块组成的对象。这里可以看到，**模块就是一个个的函数，即开篇提到的例子**。闭包函数的主体是，通过模拟的require函数找到对应模块并调用。至于eavl，不用多说了吧？传入代码内容字符串就会执行了。\n\n# 开始实现\n需要用到的工具如下\n```\nconst fs = require('fs')\nconst path = require('path')\nconst parser = require('@babel/parser') // 生成抽象语法树\nconst { transformFromAst } = require('@babel/core') // 转换es6语法\nconst { default: traverse } = require('@babel/traverse') // 抽象语法树分析\n\n```\n注意，traverse模块是ES6 Module，所以使用CommonJs引入时需要加上default\n\n快速安装：\n```\nnpm install @babel/parser @babel/core @babel/traverse @babel/parser -D\n```\n\n## 解析文件内容\n好的，现在在根目录下新建bundler.js用来打包，我们的打包流程将写在这里。首先实现analyze函数：\n```\n/**\n * 通过路径读取文件并解析\n * @param {String} filePath \n * @return {Object} 解析结果\n */\nconst analyze = function (filePath) {\n    const content = fs.readFileSync(filePath, 'utf-8')\n    const ast = parser.parse(content, { sourceType: 'module' })\n    const dependencies = [] \n    // 转换es6语法，并得到转换后的源代码\n    const { code } = transformFromAst(ast, null, {\n        presets: ['@babel/env']\n    })\n    // 分析依赖\n    traverse(ast, {\n        // 分析依赖的钩子\n        ImportDeclaration ({ node }) {\n            dependencies.push(node.source.value) // 获得所有依赖\n        }\n    })\n    return {\n        filePath,\n        dependencies,\n        code\n    }\n}\n```\n这里解释下traverse函数的作用。我们使用`@babel/parser`生成抽象语法树AST，就是一个描述代码结构的JSON对象，这个对象中包含了语法信息。我们可以打印下\n```\nconsole.log(ast.program.body)\n```\n结果是一个数组，我截取了数组中的一个元素，如下：\n```\nNode {\n    type: 'ImportDeclaration',\n    start: 0,\n    end: 31,\n    loc: SourceLocation { start: [Position], end: [Position] },\n    specifiers: [ [Node] ],\n    source: Node {\n      type: 'StringLiteral',\n      start: 18,\n      end: 31,\n      loc: [SourceLocation],\n      extra: [Object],\n      value: './js/say.js'\n    }\n}\n```\n可以看到`type: 'ImportDeclaration'`说明这是一个import引入语法，如此一来，我们就可以轻松的拿到对应的依赖，如上例是`./js/say.js`\n\ntraverse中的ImportDeclaration钩子，参数中包含node属性，这就是我们需要找的依赖文件，我们将它保存起来用于下面的分析。\n\n## 递归解析依赖\n通过对代码的依赖分析，获取所有资源，用于最后的打包\n```\n/**\n * 通过入口文件递归解析依赖，并返回所有的依赖\n * @param {String} entryFile 入口文件\n * @return 依赖的所有代码\n */\nconst getAssets = function (entryFile) {\n    const entry = analyze(entryFile)\n    const dependencies = [entry] // 起初依赖只包含入口，随着遍历不断加入\n    for (const asset of dependencies) {\n        // 获取目录名\n        const dirname = path.dirname(asset.filePath)\n        asset.dependencies.forEach(relPath =\u003e {\n            // 将相对路径转换为绝对路径，相对路径是基于dirname的\n            const absolutePath = path.join(dirname, relPath)\n            // 处理css文件\n            if (/\\.css$/.test(absolutePath)) {\n                const content = fs.readFileSync(absolutePath, 'utf-8')\n                // 使用js插入style节点\n                const cssInsertCode = `\n                    const stylesheet = document.createElement('style');\n                    stylesheet.innerText = ${JSON.stringify(content)};\n                    document.head.appendChild(stylesheet);\n                `\n                dependencies.push({\n                    filePath: absolutePath,\n                    relPath, // 记得保存相对路径，因为require的时候需要用到\n                    dependencies: [],\n                    code: cssInsertCode\n                })\n            } else {\n                const child = analyze(absolutePath)\n                child.relPath = relPath // 同上\n                dependencies.push(child) // 递归解析\n            }            \n        })\n    }\n    return dependencies\n}\n```\n\n## 开始打包\n打包的目的是将文件合并，由于浏览器环境限制，我们需要构造闭包，还要模拟node的环境变量。\n```\n/**\n * 打包流程主函数\n * @param {String} entry 入口文件\n * @return void\n */\nconst bundle = function (entry) {\n    const dependencies = getAssets(entry)\n    // 将依赖构建成对象\n    const deps = dependencies.map(dep =\u003e {\n        const filePath = dep.relPath || entry\n        // 路径和模块形成映射\n        return `'${filePath}':function (exports, require, module) { ${dep.code} }`\n    })\n\n    // 构造require函数，babel解析后的代码是node环境下的，我们需要构造相应的函数\n    // 来模拟原生require，从我们构建的deps对象中获取相应模块函数\n    const result = `(function(deps){\n        function require(path){\n            // 构造一个模块，表示当前模块\n            const module = { exports: {} }\n            // 执行对应的模块，并传入参数\n            deps[path](module.exports, require, module)\n            // 返回模块导出的内容，也就是require函数获取到的内容\n            return module.exports\n        }\n        require('${entry}') // 从入口文件开始执行\n    })({${deps.join(',')}})`\n\n    // 如果你想压缩成一行可以加上这个，但是相应的要安装babel-preset-minify\n    // const ast = parser.parse(result, { sourceType: 'script' })\n    // const { code } = transformFromAst(ast, null, {\n    //     presets: ['minify']\n    // })\n    \n    // 写入文件\n    fs.writeFileSync('./public/vendors.js', result) // 如果你压缩了，这里填code\n}\n\n// 运行打包\nbundle('./src/main.js')\n```\n\n需要注意的是，**我们要将代码以为本的形式拼接在一起**，否则代码将会直接运行生成结果，这不是我们想要的。牢记，我们是在拼接代码。\n\n`${deps.join(',')}`得到的内容是一个字符串，我们用一个大括号括起来，在运行时就相当于是一个对象了，即`{${deps.join(',')}}`。\n\n也许你会想直接构造一个对象然后使用JSON.stringify不就好了吗。实际上不行，因为我们的这个对象的键值对中，key可以是字符串，但是value不行，value是我们模拟的一个node模块，是一个函数，JSON.stringify会导致我们最终获取到的是函数的字符串，而不是函数。\n\n# 验收成果\n打包后的vendors.js内容如下：\n```\n(function (deps) {\n  function require(path) {\n    const module = {\n      exports: {}\n    }\n    deps[path](module.exports, require, module)\n    return module.exports\n  }\n  require('./src/main.js')\n})({\n  './src/main.js': function (exports, require, module) {\n    \"use strict\";\n\n    var _say = _interopRequireDefault(require(\"./js/say.js\"));\n\n    var _message = require(\"./js/message.js\");\n\n    require(\"./css/main.css\");\n\n    function _interopRequireDefault(obj) {\n      return obj \u0026\u0026 obj.__esModule ? obj : {\n        \"default\": obj\n      };\n    }\n\n    (0, _say[\"default\"])(_message.message);\n    (0, _say[\"default\"])(\"\\u4ECA\\u5929\\u7684\\u5929\\u6C14\\u662F\\uFF1A\".concat(_message.weather));\n  },\n  './js/say.js': function (exports, require, module) {\n    \"use strict\";\n\n    Object.defineProperty(exports, \"__esModule\", {\n      value: true\n    });\n    exports[\"default\"] = _default;\n\n    function _default(name) {\n      console.log(\"hello \".concat(name));\n    }\n  },\n  './js/message.js': function (exports, require, module) {\n    \"use strict\";\n\n    Object.defineProperty(exports, \"__esModule\", {\n      value: true\n    });\n    exports.weather = exports.message = void 0;\n    var message = 'qin';\n    exports.message = message;\n    var weather = 'sunny day';\n    exports.weather = weather;\n  },\n  './css/main.css': function (exports, require, module) {\n    const stylesheet = document.createElement('style');\n    stylesheet.innerText = \"#app {\\r\\n    position: absolute;\\r\\n    top: 50%;\\r\\n    left: 50%;\\r\\n    transform: translate(-50%, -50%);\\r\\n    animation: breath 2s ease infinite;\\r\\n}\\r\\n@keyframes breath {\\r\\n    from, to {\\r\\n        width: 100px;\\r\\n        height: 100px;\\r\\n        background-color: black;\\r\\n    }\\r\\n    50% {\\r\\n        width: 200px;\\r\\n        height: 200px;\\r\\n        background-color: red;\\r\\n    }\\r\\n}\";\n    document.head.appendChild(stylesheet);\n  }\n})\n```\n对比webpack的结果，是不是很相似？只不过我们没有使用eval函数，而是将代码直接写在函数体中。\n\n新建html文件并引入vendors.js\n```\n\u003cdiv id=\"app\"\u003e\u003c/div\u003e\n\u003cscript src=\"./vendors.js\"\u003e\u003c/script\u003e\n```\n\n结果如下：\n\n![动画效果](https://user-gold-cdn.xitu.io/2020/3/23/171072cc87bb2530?w=405\u0026h=328\u0026f=gif\u0026s=282117)\n\n![控制台](https://user-gold-cdn.xitu.io/2020/3/23/171072d508f511bc?w=368\u0026h=147\u0026f=png\u0026s=6294)\n\n生效了，没问题。\n\n这就是我们自制的一个小型打包工具啦~喜欢点个赞哈😊\n# 补充\n在webpack打包代码结果展示那里，我删除的代码是关于webpack的一些更高级的功能的。例如webpack内置了缓存机制，一个模块加载过后就会缓存起来，并赋予id值，然后标记为已加载。以后再加载这个模块的时候通过标记判断，加载过的话就直接读缓存。\n\n我们构建的module是这样的：\n```\nconst module = { exports: {} }\n```\n而`__webpack_require__`中构建的module是这样的：\n```\n// installedModules就是缓存\nvar module = installedModules[moduleId] = {\n    i: moduleId, // 通过id来获取\n    l: false, // loaded：标识是否加载过\n    exports: {}\n};\n```\n\n# 参考\n\u003e [【掘金】实现小型打包工具](https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5c10c75af265da6135726f6c)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femosheeep%2Fbundler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Femosheeep%2Fbundler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femosheeep%2Fbundler/lists"}