1.搭建项目基础骨架
我们在项目中新建一个bin文件夹,在bin目录下创建start.js文件,这个文件的主要作用就是将webpack的基础配置传入编译模块(compiler),然后开启编译过程。
// bin/start.js
const path = require('path')
// 1. 读取需要打包项目的配置文件
let config = require(path.resolve('webpack.config.js'))
// 核心文件 编译器
const Compiler = require('../lib/compiler')
// 将配置传入编译器,实例化编译器
let compiler = new Compiler(config)
// 开启编译器
compiler.start()
2.编译器文件
编译器文件是核心文件,主要包含的内容为三部分
1)读取文件、转换代码
depAnalyse(filename) {
// 读取模块的内容
let content = fs.readFileSync(filename, "utf-8");
// 用于存取当前模块的所有依赖。便于后面遍历
let dependencies = {};
// 对文件内容进行解析并生成初始的抽象语法树
const ast = parser.parse(content, {
sourceType: "module", //babel官方规定必须加这个参数,不然无法识别ES Module
});
// 遍历ast 通过import找到依赖,存储依赖
traverse(ast, {
ImportDeclaration({ node }) {
// 去掉文件名 返回目录
const dirname = path.dirname(filename);
const newFile = path.join(dirname, node.source.value);
//保存所依赖的模块
dependencies[node.source.value] = newFile;
},
});
// transformFromAst 相当于是traverse和generate的结合
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
// generate 将ast转换为代码
// 把当前的依赖,和文件内容推到对象里面去
return {
filename,
dependencies, //该文件所依赖的模块集合(键值对存储)
code, //转换后的代码
};
}
上面这个函数做了几件事:
- 读取模块内容
- 用@babel/parser将ES6代码转换为ES6的抽象语法树(AST)
- 利用@babel/traverse遍历AST,找到本模块的其他依赖文件并保存
- 用babel.transformFromAst将ES6的AST转换成ES5的AST,并将AST转换成代码。
2)得到图谱
getAtlas(entry) {
const entryModule = this.depAnalyse(entry);
this.analyseObj = [entryModule];
for (let i = 0; i < this.analyseObj.length; i++) {
const item = this.analyseObj[i];
const { dependencies } = item; //拿到文件所依赖的模块集合(键值对存储)
for (let j in dependencies) {
this.analyseObj.push(this.depAnalyse(dependencies[j])); //敲黑板!关键代码,目的是将入口模块及其所有相关的模块放入数组
}
}
//接下来生成图谱
const graph = {};
this.analyseObj.forEach((item) => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code,
};
});
return graph;
}
上述函数做了几件事:
- 递归调用depAnalyse函数得到所有模块的依赖数组
- 依次对数组中的每一个元素都进行转换
- 得到图谱
最后的图谱结果为:
{
'./src/index.js': {
dependencies: { './message.js': 'src/message.js' },
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports["default"] = void 0;\n' +
'\n' +
'var _message = _interopRequireDefault(require("./message.js"));\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'var _default = _message["default"];\n' +
'exports["default"] = _default;'
},
'src/message.js': {
dependencies: { './word.js': 'src/word.js' },
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports["default"] = void 0;\n' +
'\n' +
'var _word = require("./word.js");\n' +
'\n' +
'var message = "say ".concat(_word.word);\n' +
'var _default = message;\n' +
'exports["default"] = _default;'
},
'src/word.js': {
dependencies: {},
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports.word = void 0;\n' +
"var word = 'hello';\n" +
'exports.word = word;'
}
}
3)生成webpack模版文件
上面的函数中,我们已经生成了各个文件的依赖文件及其代码,那我们要如何将这几个文件结合起来,使其输出我们想要的东西呢?
我们先来研究一下webpack打包输出的代码(精简):
(function(modules) {
function __webpack_require__(moduleId) {
var module = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
return __webpack_require__(0);
})([
(function (module, __webpack_exports__, __webpack_require__) {
// 引用 模块 1
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1);
/* harmony default export */ __webpack_exports__["default"] = ('a.js');
console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]);
}),
(function (module, __webpack_exports__, __webpack_require__) {
// 输出本模块的数据
"use strict";
/* harmony default export */ __webpack_exports__["a"] = (333);
})
]);
这个文件从整体来看,其实是一个自执行函数。
(function(modules) {
})([]);
自执行函数的入参是经过babel转换后的每一个模块的代码组成的数组,而函数的主体是webpack用来处理模块的逻辑的,作用就是执行每一个模块(module)中的代码,并将代码中导出的方法存放在module.exports中,并在模块的最后将module.exports返回,供其他模块调用。
本质上是webpack实现了自己的require函数。
按照上述webpack打包输出的代码规范,我们写了自己的代码输出模版如下:
toEmitCode(entry, graph) {
//要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object],显然不行
graph = JSON.stringify(graph);
return `
(function(graph) {
//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
function require(module) {
//localRequire的本质是拿到依赖包的exports变量
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
}
return require('${entry}')
})(${graph})`;
}
上述函数做了几件事:
- 执行每一个模块的代码,并将其导出的内容放在exports,便于其他模块通过require引用
- 将入口文件的导出exports返回作为整个函数的返回值
- 由于转换输出的代码是commonjs规范的,不能在浏览器上运行,webpack实现的自执行require函数可以帮助输出代码在浏览器上运行
3.进行打包输出
在src文件夹下新建以下3个文件:
// index.js
import message from './message.js';
export default message
// message.js
import { word } from "./word.js";
const message = `say ${word}`;
export default message;
// word.js
export const word = 'hello';
webpack.config.js配置文件如下:
// webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
}
};
在命令行执行node bin/start.js,发现在dist文件夹下生成了一个main.js文件:
(function(graph) {
//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
function require(module) {
//localRequire的本质是拿到依赖包的exports变量
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
}
return require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./message.js":"src/message.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar _default = _message[\"default\"];\nexports[\"default\"] = _default;"},"src/message.js":{"dependencies":{"./word.js":"src/word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"src/word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\nvar word = 'hello';\nexports.word = word;"}})
将以上代码在浏览器控制台执行之后,得到结果:
三.打包代码被引用
上面输出的代码当中虽然打包完成,但是这个js文件不能被其他模块通过import或者require的方式引用,因为它只是在当前作用域有效,但是如果想要它能被其他模块引用,这就要用到webpack中的output.libraryTarget。
output.libraryTarget :commonjs2;
会将打包过的代码赋值给module.exports。
可以在上述代码前面加上module.exports = 打包输出代码块
这样,就可以在别的模块利用require引用这个模块。