本文中,“webpackSimple”指项目,而“localWebpack”或者“demo-start'”均指手写的webpack工具
上节,我们再本地开发了一个极简npm包,这节我们顺道来完善它
先新建一个webpack项目,并打包
image.png目录结构很简单,文件详情如下:
//index.js
let str = require("./a.js");
console.log(str);
//a.js
let b = require("./base/b.js");
module.exports = "a" + b;
//b.js
module.exports = "b";
//webpack.config.js
let path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
}
};
//打包后的bundle.js(我删掉了一些多余的部分,为了更清晰的看出webpack具体做了什么)
(function(modules) {
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;
}
return __webpack_require__((__webpack_require__.s = "./src/index.js"));
})({ // 将引用的文件以键值对的形式引入,创建依赖关系
"./src/a.js": function(module, exports, __webpack_require__) {
eval(
'let b = __webpack_require__(/*! ./base/b.js */ "./src/base/b.js");\r\nmodule.exports = "a" + b;\r\n\n\n//# sourceURL=webpack:///./src/a.js?'
);
},
"./src/base/b.js": function(module, exports) {
eval(
'module.exports = "b";\r\n\n\n//# sourceURL=webpack:///./src/base/b.js?'
);
},
"./src/index.js": function(module, exports, __webpack_require__) {
eval(
'let str = __webpack_require__(/*! ./a.js */ "./src/a.js");\r\n\r\nconsole.log(str);\r\n\n\n//# sourceURL=webpack:///./src/index.js?'
);
}
});
以上,我们可以看出,webpack内部将引用的文件以键值对的形式创建了依赖关系。并且在它内部实现了一个require方法“webpack_require”来达到文件的引用,使用“eval”来运行文件内容
下边,我们开始自己写,修改上节的极简npm包,目录结构如下:
#!/usr/bin/env node
// 1) 需要找到当前执行名的路径,拿到webpack.config.js
let path = require("path");
// config配置文件
let config = require(path.resolve("webpack.config.js")); // 首先拿到用户的webpack配置
let Compiler = require("../lib/Compiler.js");
let compiler = new Compiler(config); // 编译配置
// 标识运行编译
compiler.run();
//Compiler.js
let path = require("path");
let fs = require("fs");
class Compiler {
constructor(config) {
//entry,output。。。也就是webpack.config.js
this.config = config;
// 需要保存入口文件的路径
this.entryId; // './src/index.js',这个留到后边补充
// 需要保存所有的模块依赖
this.modules = {};
this.entry = config.entry; // 入口路径
// 工作路径(运行命令的路径)
this.root = process.cwd();
}
getSource(modulePath) {
let content = fs.readFileSync(modulePath, "utf8");
return content;
}
// 构建模块
buildModules(modulePath, isEntry) {
let source = this.getSource(modulePath); //“绝对路径”, 编码utf-8
// 模块ID(查看webpack打包后的文件,文件key值为相对路径,而我们此处拿到的是绝对路径,因此需要减去这些前置路径)
// modulePath = modulePath - this.root
let moduleName = "./" + path.relative(this.root, modulePath); //这样,就解析出了一个相对路径
console.log(source, moduleName); // 这里,我们要打印出文件的内容和路径
}
emitFile() {
// 发射文件
}
run() {
// 执行
// 创建模块的依赖关系
this.buildModules(path.resolve(this.root, this.entry), true); // 入口路径, 是否主模块
// 发射一个文件 -> 打包后的文件
this.emitFile();
}
}
module.exports = Compiler;
在webpackSimple项目下运行‘npx demo-start’
然后修改buildModules函数
// 构建模块
buildModules(modulePath, isEntry) {
let source = this.getSource(modulePath); //“绝对路径”, 编码utf-8, 这里,拿到主入口的内容
// 模块ID(查看webpack打包后的文件,文件key值为相对路径,而我们此处拿到的是绝对路径,因此需要减去这些前置路径)
// modulePath = modulePath - this.root
let moduleName = "./" + path.relative(this.root, modulePath); //这样,就解析出了一个相对路径
if (isEntry) {
this.entryId = moduleName; // 保存入口的名字
}
// 解析,需要把source源码进行改造,返回一个依赖列表
// 还进行一些别的工作,比如将"./a.js"解析为"./src/a.js"
// 这些工作都由parse方法来完成,此处的parse函数接下来详细解释
let { sourceCode, dependencies } = this.parse(
source,
path.dirname(moduleName)
);
// 把相对路径和模块中的内容对应起来({文件名:解析后的文件内容}
this.modules[moduleName] = sourceCode;
}
接着我们要来完善parse函数,这里就要引入一个新的概念了 ---- AST语法树
首先需要引入三个包
//babylon 主要是把源码转化为AST
//@babel/traverse (遍历到对应节点)通过AST生成一个便于操作、转换的path对象,供我们的babel插件处理
//@babel/types 替换节点
//@babel/generator 读取AST并将其转换为代码和源码映射。
// 解析源码
parse(source, parentPath) {
// AST解析语法树
let ast = babylon.parse(source); //转换成ast
let dependencies = []; //依赖的数组
traverse(ast, {
// 遍历ast
CallExpression(p) {
// 调用表达式, 即functionName()这种形式的
// 以下内容,详见 https://astexplorer.net/
let node = p.node; //对应的节点
if (node.callee.name === "require") {
node.callee.name = "__webpack_require__";
let moduleName = node.arguments[0].value; // 这里就取到了模块引用的名字
moduleName = moduleName + (path.extname(moduleName) ? "" : ".js"); // 加文件后缀 -> ./a.js
moduleName = "./" + path.join(parentPath, moduleName); //改造路径 -> './src/a.js'
dependencies.push(moduleName);
node.arguments = [t.stringLiteral(moduleName)]; //重写节点的arguments -> 改写源码
}
}
});
let sourceCode = generator(ast).code;
return { // 解析后的源码和依赖return出去
sourceCode,
dependencies
};
}
// 构建模块
buildModules(modulePath, isEntry) {
let source = this.getSource(modulePath); //“绝对路径”, 编码utf-8, 这里,拿到主入口的内容
// 模块ID(查看webpack打包后的文件,文件key值为相对路径,而我们此处拿到的是绝对路径,因此需要减去这些前置路径)
// modulePath = modulePath - this.root
let moduleName = "./" + path.relative(this.root, modulePath); //这样,就解析出了一个相对路径
if (isEntry) {
this.entryId = moduleName; // 保存入口的名字
}
// 解析,需要把source源码进行改造,返回一个依赖列表
// 还进行一些别的工作,比如将"./a.js"解析为"./src/a.js", 解析依赖关系
let { sourceCode, dependencies } = this.parse(
source,
path.dirname(moduleName)
);
// 把相对路径和模块中的内容对应起来({文件名:解析后的文件内容})
this.modules[moduleName] = sourceCode;
dependencies.forEach(dep => { //循环各个依赖
// 副模块的加载
this.buildModules(path.join(this.root, dep), false);
});
}
*在run方法中加一行 *
run() {
// 执行
// 创建模块的依赖关系
this.buildModules(path.resolve(this.root, this.entry), true); // 入口路径, 是否主模块
console.log(this.modules, this.entryId);
// 发射一个文件 -> 打包后的文件
this.emitFile();
}
运行后:
可以看到,将modules整理为 key: value的形式,并且也将entryId赋为index.js的路径+文件名。
接下来,就用这个对象渲染输出了。
上边我们把webpack生成的bundle.js简化了,为了减少工作量,我们将它复制出来,并且新建一个模板文件main.ejs(你需要先安装ejs并且引入)
//main.ejs
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
orts: {}
});
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
return __webpack_require__((__webpack_require__.s = "<%-entryId%>"));
})({
<%for(let key in modules){%> // 这里遍历modules,生成key: value形式的文件映射
"<%-key%>":
(function(module, exports, __webpack_require__) {
eval(`<%-modules[key]%>`);
}),
<%}%>
});
最后一步,在Compiler.js中渲染
emitFile() {
// 发射文件(输出)
//用数据渲染ejs
//拿到配置的出口
let main = path.join(this.config.output.path, this.config.output.filename);
let templateStr = this.getSource(path.join(__dirname, "main.ejs"));
let code = ejs.render(templateStr, { // 用entryId和modules渲染ejs模板
entryId: this.entryId,
modules: this.modules
});
this.assets = {}; // 因为输出的文件可能不止一个js
// 资源中路径对应的代码
this.assets[main] = code;
fs.writeFileSync(main, this.assets[main]);
}
大功告成,现在,这个‘demo-start’就有了简单的webpack的功能。我们可以在webpackSimple项目下试用:
demo-start.gif
正确输出了“ab”
网友评论