手写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执行

相关推荐
小明记账簿13 小时前
项目启功需要添加SKIP_PREFLIGHT_CHECK=true该怎么办?
webpack·打包
拉不动的猪1 天前
webpack编译中为什么不建议load替换ast中节点删除consolg.log
前端·javascript·webpack
PAQQ2 天前
ubuntu22.04 搭建 Opencv & C++ 环境
前端·webpack·node.js
老前端的功夫3 天前
Webpack打包机制与Babel转译原理深度解析
前端·javascript·vue.js·webpack·架构·前端框架·node.js
LYFlied4 天前
Webpack 深度解析:从原理到工程实践
前端·面试·webpack·vite·编译原理·打包·工程化
LYFlied4 天前
从循环依赖检查插件Circular Dependency Plugin源码详解Webpack生命周期以及插件开发
前端·webpack·node.js·编译原理·plugin插件开发
AI_56784 天前
前端工程化巅峰实践:Webpack5性能优化全攻略
webpack·tree shaking
一念之间lq5 天前
Elpis Webpack工程化·自我学习总结
webpack·前端工程化
LYFlied5 天前
浅谈前端构建工具核心理解&&主流工具对比
前端·webpack·软件构建·rollup·vite·开发工具·工程化
LYFlied5 天前
Webpack详细打包流程解析
前端·面试·webpack·node.js·打包·工程化