1. Build the basic framework of the project
We create a new bin folder in the project and create a start.js file in the bin directory. The main function of this file is to pass the basic configuration of WebPack into the compiler module and start the compilation process.
// bin/start.js
const path = require('path')
// 1. Read the configuration file of the project to be packaged
let config = require(path.resolve('webpack.config.js'))
// Core file compiler
const Compiler = require('.. /lib/compiler')
// Pass the configuration to the compiler to instantiate the compiler
let compiler = new Compiler(config)
// Start the compiler
compiler.start()
Copy the code
2. Compiler files
The compiler file is the core file, which mainly contains three parts: 1) read the file and convert the code
depAnalyse(filename) {
// Read the contents of the module
let content = fs.readFileSync(filename, "utf-8");
// To access all dependencies of the current module. Easy to traverse later
let dependencies = {};
// Parse the contents of the file and generate the initial abstract syntax tree
const ast = parser.parse(content, {
sourceType: "module".// Babel specifies that this parameter must be added, otherwise the ES Module cannot be identified
});
// Walk through the AST to find dependencies and store dependencies
traverse(ast, {
ImportDeclaration({ node }) {
// Remove the file name to return to the directory
const dirname = path.dirname(filename);
const newFile = path.join(dirname, node.source.value);
// Save the dependent moduledependencies[node.source.value] = newFile; }});// transformFromAst is equivalent to traverse and generate
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]});// generate converts the AST to code
// Push the current dependencies and file contents into the object
return {
filename,
dependencies, // The collection of modules on which the file depends (key-value storage)
code, // The converted code
};
}
Copy the code
The function above does several things:
- Reading module contents
- Convert ES6 code to ES6 Abstract Syntax tree (AST) with @babel/ Parser
- Traverse the AST with @babel/traverse to find and save other dependency files for this module
- Use babel.transformFromAst to convert the AST of ES6 to the AST of ES5, and convert the AST to code.
2) Get the atlas
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; // Get the set of modules on which the file depends (key-value pair storage)
for (let j in dependencies) {
this.analyseObj.push(this.depAnalyse(dependencies[j])); // Knock on the blackboard! Key code to place an entry module and all its associated modules into an array}}// Then generate the graph
const graph = {};
this.analyseObj.forEach((item) = > {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code,
};
});
return graph;
}
Copy the code
The above functions do several things:
- Recursively call the depAnalyse function to get an array of dependencies for all modules
- Each element in the array is converted in turn
- Get the map
The final atlas results are as follows:
{
'./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; '}}Copy the code
3) Generate the webpack template file above the function, we have generated the dependencies of each file and its code, so how do we combine these files, make it output what we want? Let’s take a look at the webPack output (lite) :
(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__) {
// Reference module 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__) {
// Output the data of this module
"use strict";
/* harmony default export */ __webpack_exports__["a"] = (333); })]);Copy the code
The file as a whole is a self-executing function.
(function(modules) {
})([]);
Copy the code
The input parameter of the self-executing function is an array of the code of each module after the Babel conversion, while the body of the function is used by Webpack to handle the module logic, which is to execute the code of each module and store the exported methods of the code in module.exports. Module.exports is returned at the end of the module for other modules to call. Essentially, WebPack implements its own require function.
According to the code specification of webPack output above, we wrote our own code output template as follows:
toEmitCode(entry, graph) {
// The Object must be converted to a string first, otherwise the template string below will default to the toString method of the Object, the argument becomes Object Object
graph = JSON.stringify(graph);
return '(function(graph) {//require a module of code, Function require(module) {// Exports function require(module) {// Exports function require(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function(require, exports, code) { eval(code); })(localRequire, exports, graph[module].code); return exports; // Exports variables are not destroyed after function execution} return require('${entry}')
})(${graph}) `;
}
Copy the code
The above functions do several things:
- Executes the code for each module and places its exported content in exports for other modules to reference through require
- Returns the export of the entry file as the return value of the entire function
- Since the output code of the transformation is commonJS compliant and cannot be run in a browser, the self-executing require function implemented by WebPack can help the output code run in a browser
3. Create the following three files in the SRC folder:
// index.js
import message from './message.js';
export default message
Copy the code
// message.js
import { word } from "./word.js";
const message = `say ${word}`;
export default message;
Copy the code
// word.js
export const word = 'hello';
Copy the code
The webpack.config.js configuration file is as follows:
// webpack.config.js
const path = require("path");
module.exports = {
mode: "development".entry: "./src/index.js".output: {
filename: "main.js".path: path.resolve(__dirname, "dist"),}};Copy the code
Nodebin /start.js file (dist) :
(function(graph) {
The essence of the require function is to execute a module's code and then mount the corresponding variable on the exports object
function require(module) {
// The essence of localRequire is to get the exports variable of the dependent package
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require.exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;// The function returns a reference to a local variable, forming a closure. Exports variables are not destroyed after the function is executed
}
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;"}})
Copy the code
After executing the above code on the browser console, we get the result:
Package code is referenced
This js file cannot be referenced by other modules through import or require because it is only valid in the current scope, but if you want it to be referenced by other modules, you need output.libraryTarget in webpack. The output. LibraryTarget: commonjs2; The packaged code is assigned to module.exports.
Module. exports = package output blocks in front of the above code so that other modules can reference this module using require.
An article about Webpack modularity can be found here
Source address point here