# 前言
我们在研究一些源码时,并不是为了能够写出一个一模一样的源码。比如你研究Vue
源码并不是为了你能借此有所启发写出另一个前端框架(当然这也不排除有这种可能性),而是为了能更好的理解源码,理解我们所用的语言或框架的思想和逻辑,以便在日后的开发过程中更好的应用。
通过研究源码我们学习到了某种思想,某种处理逻辑,这样长此以往不断运用到自己的项目中,你才能更好的成长,最终向大佬更进一步。
今天我们一起来探究一下webpack
源码,看是否对你有帮助~~
# 准备工作
创建 package.json 文件,内容如下
{
"name": "source-code",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"bin": {
"mypack": "./bin/mypack.js"
}
}
注 解:
bin
:指在node
环境下配置的命令行
"mypack": "./bin/mypack.js"
:指运行mypack
命令时运行./bin/mypack.js
文件
在 bin 文件夹下创建 mypack.js 文件,内容如下
#! /usr/bin/env node
console.log('start')
注 解:
#! /usr/bin/env node
:指运行的环境是node
当前目录结构为:
这时需要将我们的这个源码包映射到全局上,可以让其他的项目可以使用mypack
命令,即运行npm link
命令进行映射。
映射完成后,我们需要将源码包的那个全局命令映射到开发项目里,这样在项目里就可以使用npx mypack
命令了,即运行npm link mypack
即可,映射成功后,结果如下:
这时运行npx mypack
命令会输出start
即将我们的源码包
和开发项目
映射成功,nice ~~
# 打包逻辑分析
在我们运行npx mypck
打包命令后,大概是做这么几件事:
- 找到当前运行路径下的配置文件
webpack.config.js
- 通过
Compiler
核心类根据拿到的配置文件处理打包逻辑
mypack.js文件
#! /usr/bin/env node
const { resolve } = require('path');
const Compiler = require('../lib/Compiler');
// 找到当前路径下的 webpack.config.js 文件
const config = require(resolve('webpack.config.js'));
// 通过配置文件处理打包逻辑
const compiler = new Compiler(config);
// 通过 run 方法启动打包
compiler.run();
在bin
文件夹同级创建lib
文件夹来存放我们核心的处理逻辑类文件
Compiler.js 文件
const { resolve } = require('path');
class Compiler {
constructor(config) {
// 配置文件
this.config = config;
// 入口文件路径
this.extryId;
// 所有模块依赖
this.modules = {};
// 入口路径
this.entry = config.entry;
// 当前工作路径
this.root = process.cwd();
};
/**
* 创建模块的依赖关系
* @param {*} modulePath:入口的绝对路径
* @param {*} isEntry:是否是入口文件
* @memberof Compiler
*/
buildModule(modulePath, isEntry) {
};
// 发射一个打包后的文件
emitFile() {
};
// 执行打包
run() {
// 1. 创建模块的依赖关系
this.buildModule(resolve(this.root, this.entry), true);
// 2. 发射一个打包后的文件
this.emitFile();
}
}
module.exports = Compiler;
# 完善 Compoiler 中的 buildModule 方法
Compiler.js 文件
const { resolve, relative, dirname } = require('path');
const fs = require('fs');
class Compiler {
constructor(config) {
// 配置文件
this.config = config;
// 入口文件路径
this.extryId;
// 所有模块依赖
this.modules = {};
// 入口路径
this.entry = config.entry;
// 当前工作路径
this.root = process.cwd();
};
// 读取路径下的模块内容
getSoure(modulePath) {
return fs.readFileSync(modulePath, 'utf8');
};
/**
* 源码解析
* @param {*} source:源码
* @param {*} moduleName:路径
*/
parseCode(source, moduleName) {
console.log('开始解析源码了');
return {
sourceCode: '源码',
dependencies: '依赖'
}
};
/**
* 创建模块的依赖关系
* @param {*} modulePath:入口的绝对路径
* @param {*} isEntry:是否是入口文件
* @memberof Compiler
*/
buildModule(modulePath, isEntry) {
// 拿到模块内容
const source = this.getSoure(modulePath);
// 拿到模块的相对路径 = modulePath - this.root
// relative(this.root, modulePath):获得相对路径
const moduleName = './' + relative(this.root, modulePath);
// 保存入口的名字
if (isEntry) {
this.entryId = moduleName;
}
// 把 source 源码进行改造,并返回一个依赖列表
// dirname(moduleName):获得 moduleName 的父路径
const { sourceCode, dependencies } = this.parseCode(source, dirname(moduleName));
console.log(sourceCode, dependencies);
};
// 发射一个打包后的文件
emitFile() {
};
// 执行打包
run() {
// 1. 创建模块的依赖关系
this.buildModule(resolve(this.root, this.entry), true);
// 2. 发射一个打包后的文件
this.emitFile();
}
}
module.exports = Compiler;
写到这里,运行命令npx mypack
会输出如下内容:
其实到现在为止就做了两件事:
- 第一:拿到配置文件
webpack.config.js
,并且通过读取内容获得entry
入口路径 - 第二:通过
root
工作路径等一些操作,拿到入口文件的相对路径src/index.js
再拼上./
得到终的./src/index.js
,以及入口文件的父路径src
。
具体parseCode
方法是怎么解析源码的,我们接着往下看 ~~
# 完善 Compoiler 中的 parseCode 方法
这里要实现parseCode方法实现AST语法书需要借助单个模块,如下:
npm install babylon // 把源码转换成 ast
npm install @babel/traverse // 遍历节点
npm install @bebel/types // 把节点替换掉
npm install @babel/generator
安装完成后,完善parseCode
和buildModule
两个方法,代码如下:
const { resolve, relative, dirname, extname, join } = require('path');
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generator = require('@babel/generator').default;
class Compiler {
constructor(config) {
// 配置文件
this.config = config;
// 入口文件路径
this.extryId;
// 所有模块依赖
this.modules = {};
// 入口路径
this.entry = config.entry;
// 当前工作路径
this.root = process.cwd();
};
// 读取路径下的模块内容
getSoure(modulePath) {
return fs.readFileSync(modulePath, 'utf8');
};
/**
* 源码解析
* @param {*} source:源码
* @param {*} parentPath:路径
*/
parseCode(source, parentPath) {
// 把源码解析成ast
let ast = babylon.parse(source);
let dependencies = []; // 依赖数组
// 遍历ast
traverse(ast, {
CallExpression(p) {
let node = p.node;
if (node.callee.name == 'require') {
// 重命名
node.callee.name = '__webpack_require__';
// 拿到模块的引用名字
let moduleName = node.arguments[0].value;
// 拿到模块的依赖
moduleName = moduleName + (extname(moduleName) ? '' : '.js');
moduleName = './' + join(parentPath, moduleName);
// 把所有依赖放在数组里
dependencies.push(moduleName);
// 替换依赖内容
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
// 把转换后的生成源码
let sourceCode = generator(ast).code;
return { sourceCode, dependencies }
};
/**
* 创建模块的依赖关系
* @param {*} modulePath:入口的绝对路径
* @param {*} isEntry:是否是入口文件
* @memberof Compiler
*/
buildModule(modulePath, isEntry) {
// 拿到模块内容
const source = this.getSoure(modulePath);
// 拿到模块的相对路径 = modulePath - this.root
// relative(this.root, modulePath):获得相对路径
const moduleName = './' + relative(this.root, modulePath);
// 保存入口的名字
if (isEntry) {
this.entryId = moduleName;
}
// 把 source 源码进行改造,并返回一个依赖列表
// dirname(moduleName):获得 moduleName 的父路径
const { sourceCode, dependencies } = this.parseCode(source, dirname(moduleName));
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode;
// 遍历所有依赖项
dependencies.forEach(dep => {
// 递归加载附属模块
this.buildModule(join(this.root, dep), false);
})
};
// 发射一个打包后的文件
emitFile() {
};
// 执行打包
run() {
// 1. 创建模块的依赖关系
this.buildModule(resolve(this.root, this.entry), true);
// modules是依赖对象,this.entryId 是入口文件
console.log(this.modules, this.entryId);
// 2. 发射一个打包后的文件
this.emitFile();
}
}
module.exports = Compiler;
最后能在run
方法里输出console.log(this.modules, this.entryId)
,nice ~~
# 完善 emitFile 方法
这个方法主要是拿到输出的目录,并把文件输出,为了省事不拼接字符串,我们利用ejs
模块
npm install ejs
在lib
文件夹下创建模板文件main.ejs
,内容如下:
(function(modules) { // webpackBootstrap
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
__webpack_require__.m = modules;
__webpack_require__.c = installedModules;
__webpack_require__.d = function(exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
__webpack_require__.r = function(exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
__webpack_require__.t = function(value, mode) {
if (mode & 1) value = __webpack_require__(value);
if (mode & 8) return value;
if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if (mode & 2 && typeof value != 'string')
for (var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
__webpack_require__.p = "";
return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})
({
<%for(let key in modules){%>
"<%-key%>":
(function(module, exports,__webpack_require__) {
eval(`<%-modules[key]%>`);
}),
<%}%>
});
完整的Compiler.js
如下:
const { resolve, relative, dirname, extname, join } = require('path');
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generator = require('@babel/generator').default;
const ejs = require('ejs');
class Compiler {
constructor(config) {
// 配置文件
this.config = config;
// 入口文件路径
this.extryId;
// 所有模块依赖
this.modules = {};
// 入口路径
this.entry = config.entry;
// 当前工作路径
this.root = process.cwd();
};
// 读取路径下的模块内容
getSoure(modulePath) {
return fs.readFileSync(modulePath, 'utf8');
};
/**
* 源码解析
* @param {*} source:源码
* @param {*} parentPath:路径
*/
parseCode(source, parentPath) {
// 把源码解析成ast
let ast = babylon.parse(source);
let dependencies = []; // 依赖数组
// 遍历ast
traverse(ast, {
CallExpression(p) {
let node = p.node;
if (node.callee.name == 'require') {
// 重命名
node.callee.name = '__webpack_require__';
// 拿到模块的引用名字
let moduleName = node.arguments[0].value;
// 拿到模块的依赖
moduleName = moduleName + (extname(moduleName) ? '' : '.js');
moduleName = './' + join(parentPath, moduleName);
// 把所有依赖放在数组里
dependencies.push(moduleName);
// 替换依赖内容
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
// 把转换后的生成源码
let sourceCode = generator(ast).code;
return { sourceCode, dependencies }
};
/**
* 创建模块的依赖关系
* @param {*} modulePath:入口的绝对路径
* @param {*} isEntry:是否是入口文件
* @memberof Compiler
*/
buildModule(modulePath, isEntry) {
// 拿到模块内容
const source = this.getSoure(modulePath);
// 拿到模块的相对路径 = modulePath - this.root
// relative(this.root, modulePath):获得相对路径
const moduleName = './' + relative(this.root, modulePath);
// 保存入口的名字
if (isEntry) {
this.entryId = moduleName;
}
// 把 source 源码进行改造,并返回一个依赖列表
// dirname(moduleName):获得 moduleName 的父路径
const { sourceCode, dependencies } = this.parseCode(source, dirname(moduleName));
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode;
// 遍历所有依赖项
dependencies.forEach(dep => {
// 递归加载附属模块
this.buildModule(join(this.root, dep), false);
})
};
// 发射一个打包后的文件
emitFile() {
// 输出路径
let main = join(this.config.output.path, this.config.output.filename);
// 模板路径
let templateStr = this.getSoure(join(__dirname, 'main.ejs'));
console.log(templateStr)
// 根据模板和代码输出编译后的代码块
let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules });
this.assets = {};
// 资源中路径对应的代码
this.assets[main] = code;
// 文件写入到哪
fs.writeFileSync(main, this.assets[main]);
};
// 执行打包
run() {
// 1. 创建模块的依赖关系
this.buildModule(resolve(this.root, this.entry), true);
// 2. 发射一个打包后的文件
this.emitFile();
}
}
module.exports = Compiler;
至此,我们写的包可以处理简单的js
模块打包了,但很弱,还需要加上loader
与plugins
等处理,我们下篇继续,nice ~~