手写webpack基本编译原理——三年前端初学webpack

目的

分析webpack打包后的文件js文件原理 ,并手写实现

前提条件
  • 了解 CommonJS
  • 了解 webpack 的作用以及基本使用方法
  • 了解 javascript 基本知识
准备工作
css 复制代码
webpack
    src
        a.js
        index.js
    package.json

创建webpack文件夹以及src / a.js / index.js

在webpack路径下执行 npm init 生成package.json文件

执行 npm i -D webpack@4 webpack-cli@3 安装webpack4版本(4版本分析打包后代码比较直观 ,看懂4版本后再安装5版本分析)

配置package.json自定义脚本

json 复制代码
"scripts": {
    "build": "webpack --mode=production",
    "build:dev": "webpack --mode=development"
 }

执行 npm run build:dev , 会生成dist文件夹 ,里面有webpack编译好的文件main.js

先不管生成的 main.js , 在dist文件夹中手动创建一个文件 my-main.js 。此时文件目录如下:

css 复制代码
webpack
    node_modules
    dist
        main.js
        my-main.js
    src
        a.js
        index.js
    package.json

a.js 文件内容

js 复制代码
console.log('This is file a');
module.exports = 'file a content'

index.js文件内容

js 复制代码
const a = require('./a')
console.log('This is file index.js')
console.log(a)

正文

思考:如果需要手动合并工程中的代码模块 ,应该怎么做 ?

第一回合

可以直接将模块中内容复制过来 ,并且将向对应的模块导入语句替换为模块导出的值 ,并且将模块导出语句删除

js 复制代码
console.log('This is file a');
// module.exports = 'file a content' // 删除

console.log('This is file index.js')
// const a = require('./a')    // 替换为 ↓
const a = "file a content"
console.log(a)
第二回合

工程复杂以后,会有很多模块 ,这样会有很多重名变量 ,并且会对全局变量造成污染

可以将每个模块的内容放在一个Object中 ,key为模块路径 ,value为function ,将对应模块内容放入function中

将这个Object作为入参 ,传入一个立即执行函数中 ,在立即执行函数中执行入口模块 ,这样就不会对全局变量造成污染了

js 复制代码
(function (modules) {
    // 执行入口模块index.js文件
})(
    {
        "./src/a.js": function () {
            console.log('This is file a');
            module.exports = 'file a content'
        },
        "./src/index.js": function () {
            console.log('This is file index.js')
            const a = require('./a')
            console.log(a)
        }
    }
)

此时可以看到 ,手动合并初具规模 ,但是在浏览器环境并不支持CommonJS社区规范 ,所以需要手动构造一个模仿CommonJS的导入导出方法提供给Object中的模块使用

js 复制代码
(function (modules) {
    // 构造CommonJS 导入API
    function require(moduleId) { // moduleId 为模块路径
        const func = modules[moduleId]; // 得到对应模块内容
        const module = { // 模拟构造CommonJS 导出API
            exports: {}
        }
        func(module, module.exports, require) // 运行对应模块
        const res = module.exports; // 此时module的值被对应模块执行后已经发生改变,得到模块导出结果
        return res
    };
    // 执行入口模块
    require('./src/index.js')  // require函数相当于是运行一个模块,得到模块的导出结果
})(
    {
        "./src/a.js": function (module, exports) {
            console.log('This is file a');
            module.exports = 'file a content'
        },
        "./src/index.js": function (module, exports, require) {
            console.log('This is file index.js')
            // const a = require('./a') // 替换为 ↓
            const a = require('./src/a.js')
            console.log(a)
        }
    }
)
第三回合

如果给立即执行函数传入的Object有很多个模块的话 ,每次调用都执行一次都会很浪费资源 ,所以要将已经获取到的模块内容缓存起来 ,用的时候如果有缓存则直接返回 ,这样就是一个基本比较完美的解决方案了

js 复制代码
(function (modules) {
    // 缓存模块导入结果
    const moduleExportsCache = {}
    // 构造CommonJS 导入API
    function require(moduleId) { // moduleId 为模块路径
        if(moduleExportsCache[moduleId]) {
            // 如果有缓存,直接返回缓存结果
            return moduleExportsCache[moduleId]
        }
        const func = modules[moduleId]; // 得到对应模块内容
        const module = { // 模拟构造CommonJS 导出API
            exports: {}
        }
        func(module, module.exports, require) // 运行对应模块
        const res = module.exports; // 得到模块导出结果
        moduleExportsCache[moduleId] = res // 缓存导出模块
        return res
    };
    // 执行入口模块
    require('./src/index.js')  // require函数相当于是运行一个模块,得到模块的导出结果
})(
    {
        "./src/a.js": function (module, exports) {
            console.log('This is file a');
            module.exports = 'file a content'
        },
        "./src/index.js": function (module, exports, require) {
            console.log('This is file index.js')
            // const a = require('./a') // 替换为 ↓
            const a = require('./src/a.js')
            console.log(a)
        }
    }
)

webpack打包后的文件分析

打开之前编译好的main.js文件 , 删掉注释以及一些兼容性配置以后 ,剩余的代码就是下面这样

可以看到和上面写的代码非常相似 ,不过webpack为了不和CommonJS规范冲突 ,将变量名改为了__webpack_require_ ,还有就是入参中的模块内容使用eval()执行 , 将每个eval里面的内容拆出来以后 ,得到以下代码

js 复制代码
(function (modules) {
})
({
    "./src/a.js":
      (function (module, exports) {
        console.log('This is file a');
        module.exports = 'file a content';
        // eval("console.log('This is file a');\r\nmodule.exports = 'file a content'\n\n//# sourceURL=webpack:///./src/a.js?");
      }),

    "./src/index.js":
      (function (module, exports, __webpack_require__) {
        const a = __webpack_require__("./src/a.js");
        console.log('This is file index.js');
        nconsole.log(a);
        // eval("const a = __webpack_require__(/*! ./a */ \"./src/a.js\")\r\nconsole.log('This is file index.js')\r\nconsole.log(a)\r\n\n\n//# sourceURL=webpack:///./src/index.js?");
      })
  });

思考:为什么使用eval()执行模块中的内容 ?

  • 了解eval()原理
  • 将下面两段代码在浏览器环境中运行并根据报错点击到对应源码位置查看区别
js 复制代码
eval("var b = null;\nb.fun()")

eval("var b = null;\nb.fun();\n//#  sourceURL=./src/eval.js")

结尾

执行 npm run build ,生成main.js文件后 ,查看生产环境编译后的代码 ,可以看到webpack将变量名字变为简写 ,并且将代码压缩为一行且没有任何注释

尝试安装其他第三方库,例如:jquery , 在入口文件或其他模块导入,执行打包命令后观察编译后代码 ,同样webpack将jquery模块的内容放在立即执行函数的入参中 ,并使用eval执行

相关推荐
一大树18 小时前
Webpack 配置与优化全攻略:从基础到进阶实战
webpack
布兰妮甜2 天前
Vite 为什么比 Webpack 快?原理深度分析
前端·webpack·node.js·vite
一枚前端小能手4 天前
🚀 Webpack构建等到怀疑人生?试试这几个优化
前端·webpack
拾光拾趣录5 天前
模块联邦(Module Federation)微前端方案
前端·webpack
萌萌哒草头将军5 天前
🚀🚀🚀 Webpack 项目也可以引入大模型问答了!感谢 Rsdoctor 1.2 !
前端·javascript·webpack
最爱吃南瓜7 天前
JS逆向实战案例之----【通姆】252个webpack模块自吐
开发语言·javascript·爬虫·webpack·js逆向·算法模拟
爱敲代码的小旗7 天前
Webpack 5 高性能配置方案
前端·webpack·node.js
桃桃乌龙_95277 天前
受不了了,webpack3.x升级到webpack4.x
前端·webpack
前端缘梦7 天前
深入理解Webpack配置:入口与出口的细节解析
前端·webpack·前端工程化
yuanmenglxb20049 天前
解锁webpack核心技能(二):配置文件和devtool配置指南
前端·webpack·前端工程化