跳到主要内容

柏拉文版

2024年04月06日
柏拉文
越努力,越幸运

一、core


1.1 test/webpack/bolawen01/core/index.js

const webpack = require('./webpack.js');
const config = require('../example/webpack.config.js');

const compiler = webpack(config);

// 调用 compiler.run 启动编译
compiler.run((err,stats)=>{
if(err){
console.log(err, 'err');
}
});

1.2 test/webpack/bolawen01/core/webpack.js

const Compiler = require('./compiler.js');

function _mergeOptions(options) {
const shellOptions = process.argv.slice(2).reduce((option, argv) => {
const [key, value] = argv.split('=');
if (key && value) {
const parseKey = key.slice(2);
option[parseKey] = value;
}
return option;
}, {});
return { ...options, ...shellOptions };
}

function _loadPlugin(plugins, compiler) {
if (plugins && Array.isArray(plugins)) {
plugins.forEach(plugins => {
plugins.apply(compiler);
});
}
}

function webpack(options) {
// 合并配置, 合并命令行参数和配置文件配置 命令行参数优先级高于配置文件
const mergeOptions = _mergeOptions(options);
// 创建 Compiler 对象
const compiler = new Compiler(mergeOptions);
// 加载插件
_loadPlugin(options.plugins, compiler);
return compiler;
}

module.exports = webpack;

1.3 test/webpack/bolawen01/core/compiler.js

const Fs = require('fs');
const Path = require('path');
const { SyncHook } = require('tapable');
const { toUnixPath, tryExtensions, getSourceCode } = require('./utils.js');

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const types = require('@babel/types');

class Compiler {
constructor(options) {
this.options = options;

this.hooks = {
// 开始编译 Hook
run: new SyncHook(),
// 写入文件之前 Hook
emit: new SyncHook(),
// 完成编译 Hook
done: new SyncHook()
};

// 保存所有入口模块对象
this.entries = new Set();
// 保存所有依赖模块对象
this.modules = new Set();
// 保存所有代码块 (chunk) 对象
this.chunks = new Set();
// 保存本次产出的文件对象
this.assets = new Set();
// 保存本次编译所有产出的文件名
this.files = new Set();
// 相对路径根路径
this.rootPath = this.options.context || toUnixPath(process.cwd());
}

/**
* @description: compiler.run() 启动编译
* @param {*} callback
*/
run(callback) {
// 触发 run Hook
this.hooks.run.call();
// 获取入口配置对象
const entry = this.getEntry();
// 编译入口文件
this.buildEntryModule(entry);
// 导出列表之后将每个 chunk 转化称为单独的文件加入到输出列表 assets 中
this.exportFile(callback);
}

/**
* @description: 获取入口文件路径
*/
getEntry() {
let entry = Object.create(null);
const { entry: optionsEntry } = this.options;
if (typeof optionsEntry === 'string') {
entry['main'] = optionsEntry;
} else {
entry = optionsEntry;
}
// 将 entry 的路径转换为绝对路径
Object.keys(entry).forEach(key => {
const value = entry[key];
if (!Path.isAbsolute(value)) {
// 转化为绝对路径的同时, 将路径中的 \ 转换为 /
entry[key] = toUnixPath(Path.join(this.rootPath, value));
}
});

return entry;
}

/**
* @description: buildEntryModule 编译入口文件
* @param {*} entry
*/
buildEntryModule(entry) {
Object.keys(entry).forEach(entryName => {
const entryPath = entry[entryName];
// 调用 buildModule 实现真正的模块编译逻辑
const entryObj = this.buildModule(entryName, entryPath);
this.entries.add(entryObj);
// 根据当前入口文件和模块的相互依赖关系,组装成为一个个包含当前入口所有依赖模块的 chunk
this.buildUpChunk(entryName, entryObj);
});
}

/**
* @description: buildModule 编译模块
* 1. **通过`fs`模块根据入口文件路径读取文件源代码**
2. **读取文件内容之后,调用所有匹配的`loader`对模块进行处理得到返回后的结果**
3. **得到 `loader` 处理后的结果后,通过 `babel` 分析 `loader` 处理后的代码,进行代码编译。(这一步编译主要是针对 `require` 语句,修改源代码中 `require` 语句的路径)**
4. **如果该入口文件没有依赖与任何模块(`require`语句),那么返回编译后的模块对象**
5. **如果该入口文件存在依赖的模块,递归 `buildModule` 方法进行模块编译**
*/
buildModule(moduleName, modulePath) {
// 1. 读取文件内容
const originSourceCode = (this.originSourceCode = Fs.readFileSync(
modulePath,
'utf-8'
));
this.moduleCode = originSourceCode;
// 2. 调用 loader 对文件内容进行编译
this.handleLoader(modulePath);
// 3. 进行模块编译, 获得最终 module 对象
const module = this.handleWebpackCompiler(moduleName, modulePath);
return module;
}

/**
* @description: handleLoader 匹配 Loader 对文件进行编译处理
* @param {*} modulePath
*/
handleLoader(modulePath) {
const matchLoaders = [];
// 1. 获取所有传入的 loader 规则
const rules = this.options.module.rules;
rules.forEach(loader => {
const testRule = loader.test;
if (testRule.test(modulePath)) {
if (loader.loader) {
// 对应 { test: /\.js$/g, loader: 'xx-loader' }
matchLoaders.push(loader.loader);
} else {
// 对应 { test: /\.js$/g, use: ['xx-loader'] }
matchLoaders.push(...loader.use);
}
}

// 2. 从后往前执行 Loader, 传入源代码
for (let i = matchLoaders.length - 1; i >= 0; i--) {
// 对应 matchLoaders = ['绝对路径/xx-loader', '绝对路径/xx-loader']
// require 引入对应 loader
const loaderFn = require(matchLoaders[i]);
// 调用 loader 对源代码进行编译
this.moduleCode = loaderFn(this.moduleCode);
}
});
}

/**
* @description: handleWebpackCompiler 进行模块编译, 获得最终 module 对象
* @param {*} moduleName
* @param {*} modulePath
*/
handleWebpackCompiler(moduleName, modulePath) {
// 将当前模块相对于项目启动根目录计算出相对路径 作为模块ID
const moduleId = './' + Path.posix.relative(this.rootPath, modulePath);
// 创建模块对象
const module = {
id: moduleId,
name: [moduleName], // 该模块所属的入口文件
dependencies: new Set() // 该模块所依赖模块绝对路径地址
};
// 将源代码转换为 AST
const ast = parser.parse(this.moduleCode, {
sourceType: 'module'
});
// 遍历 AST, 分析依赖模块
traverse(ast, {
CallExpression: nodePath => {
const node = nodePath.node;
if (node.callee.name === 'require') {
const requirePath = node.arguments[0].value;
// 寻找模块绝对路径 当前模块路径+require()对应相对路径
const moduleDirName = Path.posix.dirname(modulePath);
const absolutePath = tryExtensions(
Path.posix.join(moduleDirName, requirePath),
this.options.resolve.extensions,
requirePath,
moduleDirName
);
// 生成moduleId - 针对于跟路径的模块ID 添加进入新的依赖模块路径
const moduleId =
'./' + Path.posix.relative(this.rootPath, absolutePath);
// 通过babel修改源代码中的require变成__webpack_require__语句
node.callee = types.identifier('__webpack_require__');
// 修改源代码中require语句引入的模块 全部修改变为相对于跟路径来处理
node.arguments = [types.stringLiteral(moduleId)];
// 转化为ids的数组 好处理

const alreadyModules = Array.from(this.modules).map(i => i.id);
if (!alreadyModules.includes(moduleId)) {
// 为当前模块添加require语句造成的依赖(内容为相对于根路径的模块ID)
module.dependencies.add(moduleId);
} else {
// 已经存在的话 虽然不进行添加进入模块编译 但是仍要更新这个模块依赖的入口
this.modules.forEach(value => {
if (value.id === moduleId) {
value.name.push(moduleName);
}
});
}
}
}
});
// 遍历结束根据AST生成新的代码
const { code } = generator(ast);
// 为当前模块挂载新的生成的代码
module._source = code;
// 递归依赖深度遍历 存在依赖模块则加入到 this.modules 中
module.dependencies.forEach(dependency => {
const depModule = this.buildModule(moduleName, dependency);
this.modules.add(depModule);
});
return module;
}

/**
* @description: buildUpChunk 根据入口文件和依赖模块组装 chunks
* @param {*} entryName
* @param {*} entryObj
*/
buildUpChunk(entryName, entryObj) {
const chunk = {
name: entryName, // 每一个入口文件作为一个 chunk
entryModule: entryObj, // entry 编译后的对象
modules: Array.from(this.modules).filter(i => {
return i.name.includes(entryName);
}) // // 寻找与当前 entry 有关的所有 module
};
// 将chunk添加到this.chunks中去
this.chunks.add(chunk);
}

/**
* @description: 将chunk加入输出列表中去
* @param {*} callback
*/
exportFile(callback) {
const output = this.options.output;
// 根据 chunks 生成 assets 内容
this.chunks.forEach(chunk => {
const parseFileName = output.filename.replace('[name]', chunk.name);
this.assets[parseFileName] = getSourceCode(chunk);
});
// 触发 emit Hook
this.hooks.emit.call();
// 先判断目录是否存在 存在直接fs.write 不存在则首先创建
if (!Fs.existsSync(output.path)) {
Fs.mkdirSync(output.path);
}
// files中保存所有的生成文件名
this.files = Object.keys(this.assets);
// 将 assets 中的内容生成打包文件 写入文件系统中
Object.keys(this.assets).forEach(fileName => {
const filePath = Path.join(output.path, fileName);
Fs.writeFileSync(filePath, this.assets[fileName]);
});
// 触发 emit Hook
this.hooks.done.call();
callback(null, {
toJson: () => {
return {
entries: this.entries,
modules: this.modules,
chunks: this.chunks,
assets: this.assets,
files: this.files
};
}
});
}
}

module.exports = Compiler;

1.4 test/webpack/bolawen01/core/utils.js

const Fs = require('fs');

/**
* @description: toUnixPath 统一路径分隔符为 /
* @param {*} path
* 作用: 因为不同操作系统下,文件分隔路径是不同的。这里我们统一使用 / 来替换路径中的 \\。后续我们会使用模块相对于rootPath的路径作为每一个文件的唯一ID,所以这里统一处理下路径分隔符。
*/
function toUnixPath(path) {
return path.replace(/\\/g, '/');
}

/**
* @description: tryExtensions 按照传入的规则为文件添加后缀
* @param {*} modulePath
* @param {*} extensions
* @param {*} originModulePath
* @param {*} moduleContext
*/
function tryExtensions(
modulePath,
extensions,
originModulePath,
moduleContext
) {
// 优先尝试不需要扩展名选项: 防止用户如果已经传入了后缀时,我们优先尝试直接寻找,如果可以找到文件那么就直接返回。找不到的情况下才会依次尝试。
extensions.unshift('');
for (let extension of extensions) {
if (Fs.existsSync(modulePath + extension)) {
return modulePath + extension;
}
}
throw new Error(
`No module, Error: Can't resolve ${originModulePath} in ${moduleContext}`
);
}

/**
* @description: getSourceCode 接收传入的 chunk 对象,从而返回该 chunk 的源代码。
* @param {*} chunk
*/
function getSourceCode(chunk) {
const { name, entryModule, modules } = chunk;
return `
(()=>{
var __webpack_modules__ = {
${modules
.map(module => {
return `'${module.id}': (module)=> {
${module._source}
}`;
})
.join(',')}
};

// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId){
var cachedModule = __webpack_module_cache__[moduleId];
if(cachedModule !== undefined){
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {}
})
__webpack_modules__[moduleId](module,module.exports, __webpack_require__);
return module.exports;
}

var __webpack_exports__ = {};

(()=>{
${entryModule._source}
})()
})();
`;
}

module.exports = {
toUnixPath,
tryExtensions,
getSourceCode
};

二、example


2.1 test/webpack/bolawen01/example/src/entry1.js

const { a, b } = require('./a.js');

console.log('a', a);
console.log('b', b);

2.2 test/webpack/bolawen01/example/src/entry2.js

const { a, b } = require('./a.js');

console.log('a', a);
console.log('b', b);

2.3 test/webpack/bolawen01/example/webpack.config.js

const Path = require('path');
const Plugin1 = require('../plugins/plugin1.js');
const Plugin2 = require('../plugins/plugin2.js');

function resolve(...path) {
return Path.resolve(__dirname, ...path);
}

module.exports = {
devtool: false,
mode: 'development',
context: process.cwd(),
entry: {
entry1: resolve('./src/entry1.js'),
entry2: resolve('./src/entry2.js')
},
output: {
path: resolve('./build'),
filename: '[name].js'
},
plugins: [new Plugin1(), new Plugin2()],
resolve: {
extensions: ['.js', '.ts']
},
module: {
rules: [
{
test: /\.js/,
use: [resolve('../loaders/loader1.js'), resolve('../loaders/loader1.js')]
}
]
}
};