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

相关推荐
liangshanbo12151 天前
面试题:Webpack 的 publicPath 有什么作用?
前端·webpack·node.js
liangshanbo12153 天前
Webpack vs Vite 底层原理高频追问的问题
前端·webpack·node.js
liangshanbo12153 天前
面试题:AI 对话上下文超限怎么办?
面试·webpack·devops
liangshanbo12154 天前
Webpack 文件指纹面试题整理
前端·webpack·node.js
liangshanbo12154 天前
Webpack 常见 Plugin 面试题
前端·webpack·node.js
liangshanbo12154 天前
Webpack Tree Shaking 面试题整理
前端·webpack·node.js
陪我一起学编程4 天前
vue3笔记
前端·webpack·vue3·vite·脚手架·vue3笔记·组件式
liangshanbo12154 天前
Webpack的分包策略面试题
前端·webpack·node.js
liangshanbo12156 天前
面试题:如何优化 Webpack 的打包速度?
前端·webpack·node.js
晚风醉蝶7 天前
webpack 模块提取
前端·webpack·node.js·ast·逆向分析