Super Simplified WebPack with Babel Handlift
This article describes how to parse, compile and package files and their dependencies using Babel. See Babeltry
The directory structure
SRC: Packaged project for testing
Dist: Used to store the generated file, test.html used to test the generated file effect
Babeltry.config. js: configuration file
Index.js: entry for the packaging tool
Lib: The dependency method of the packaging tool
Effect of packaging
The source file
src/index.js
import { greeting } from "./greeting.js";
document.write(greeting('world'));
Copy the code
src/greeting.js
import { str } from "./hello.js";
var greeting = function(name) {
return str + ' ' + name;
}
export { greeting }
Copy the code
src/hello.js
var str = 'hello';
export { str }
Copy the code
The packaged file
dist/main.js
(function(modules){
function require(filepath){
const fn = modules[filepath];
const moudle = { exports: {}}; fn(require, moudle, moudle.exports);
return moudle.exports
}
require('E: item folder item Data Other babeltry SRC index.js') ({})'E: item folder item Data Other babeltry SRC index.js': function (require, moudle, exports) {"use strict";
var _greeting = require("./greeting.js");
document.write((0, _greeting.greeting)('world')); },'./greeting.js': function (require, moudle, exports) {"use strict";
Object.defineProperty(exports."__esModule", {
value: true
});
exports.greeting = undefined;
var _hello = require("./hello.js");
var greeting = function greeting(name) {
return _hello.str + ' ' + name;
};
exports.greeting = greeting; },'./hello.js': function (require, moudle, exports) {"use strict";
Object.defineProperty(exports."__esModule", {
value: true
});
var str = 'hello';
exports.str = str;},})
Copy the code
dist/test.html
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
<script src="./main.js"></script>
</head>
<body>
</body>
</html>
Copy the code
Start writing a packaging tool
package.json
Required dependencies, which we’ll explain in the parser section
"devDependencies": {
"babel-core": "^ 6.26.3"."babel-preset-env": "^ 1.7.0"."babel-traverse": "^ 6.26.0"."babylon": "^ 6.18.0"
}
Copy the code
babeltry.config.js
'use strict'
const path = require('path');
module.exports = {
entry: path.join(__dirname, '/src/index.js'),
output: {
path: path.join(__dirname, '/dist'),
filename: 'main.js'}};Copy the code
Set the entry of the item to be packaged and the exit location and file name of the packaged result
index.js
const Compiler = require('./lib/compiler');
const config = require('./babeltry.config');
new Compiler(config).run();
Copy the code
The contents of the index.js file are fairly simple: load the configuration and compiler, instantiate a compiler, and execute the run method
Parser. Js parser
Before we write the compiler, let’s look at how the parser is implemented
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const core = require('babel-core');
module.exports = {
getAST: (path) = > {
const file = fs.readFileSync(path, 'utf-8');
return babylon.parse(file, {
sourceType: 'module'})},getNode: (ast) = > {
const nodes = [];
traverse(ast, {
ImportDeclaration: ({ node }) = > {
nodes.push(node.source.value)
}
});
return nodes
},
transform: (ast) = > {
const { code } = core.transformFromAst(ast, null, {
presets: ["env"]});return code
}
}
Copy the code
Introducing dependencies:
Babylon: The AST abstract syntax tree is generated using the Parse method of Babylon. The abstract syntax tree is not explained here. There are many related articles available online
Babel-traverse: traverse the AST with babel-traverse. ImportDeclaration is used to fetch dependent nodes
Babel-core: generate source code through babel-core’s transformFromAst method
The parser exports three methods:
GetAST: receives a file path as an input parameter, reads the file and parses out the AST
GetNode: Takes the AST as an input parameter and returns an array that depends on the file path
Transform: Receives the AST as an input parameter and returns the source code
Compiler. Js compiler
const fs = require('fs');
const path = require('path');
const { getAST, getNode, transform } = require('./parser');
module.exports = class Compiler {
constructor(options) {
const { entry, output } = options;
this.entry = entry;
this.output = output;
this.modules = [];
}
run(){}buildMoudle(){}fillFile(){}}Copy the code
The constructor of a compiler class takes a configuration item and declares three variables, which are an entry, an exit, and an array representing a list of dependent files.
The compiler class has three methods. The run method, the compiler’s body method, does the parsing and compilation, the buildMoudle method takes the dependency path and generates an object representing the dependency file, which is each item stored in the Modules array, and the fillFile method generates the package file.
Look at the buildMoudle method first
buildMoudle(filepath, isEntry) {
let ast;
if (isEntry) {
ast = getAST(filepath);
} else {
let absolutePath = path.join(process.cwd(), '/src', filepath);
ast = getAST(absolutePath);
}
return {
filepath,
nodes: getNode(ast),
code: transform(ast)
}
}
Copy the code
BuildMoudle takes two arguments, filepath for dependent filepath and isEntry for whether it is an entry file.
It mainly does three actions, namely calls the three methods of the parser, first obtains the AST abstract syntax tree through getAST, then calls getNode and Transform as the input parameter, respectively obtains the dependent file path array and source code of the current parsed file.
Returns an object representing the current dependent file. Filepath is the dependent filepath, nodes is the array of dependent file paths for the current parsed file, and code is the source file.
run() {
const entryModule = this.buildMoudle(this.entry, true);
this.modules.push(entryModule);
// Deep traversal dependency
for(let i = 0; i < this.modules.length; i++){let _moudle = this.modules[i];
_moudle.nodes.map((node) = >{
this.modules.push(this.buildMoudle(node));
});
}
this.fillFile();
}
Copy the code
The run method starts with the entry file, walks through the modules dependency array, deeply walks through all dependency files, builds all dependency file objects and stores them in modules, and finally calls fillFile to generate the package file.
fillFile() {
let moudles = ' ';
this.modules.map((_moudle) = >{
moudles += ` '${_moudle.filepath}': function (require, moudle, exports) {${_moudle.code}}, `
})
const bundle = `
(function(modules){
function require(filepath){
const fn = modules[filepath];
const moudle = { exports: {} };
fn(require, moudle, moudle.exports);
return moudle.exports
}
require('The ${this.entry}') ({})${moudles}})
`;
const outpath = path.join(this.output.path, this.output.filename);
fs.writeFileSync(outpath, bundle, 'utf-8');
}
Copy the code
FillFile generates a package file whose body calls an anonymous method.
The anonymous method receives an object generated by our list of dependent files. Each attribute of the object is a dependent file path and the value is a method (called fn for easy memory). The FN method receives three parameters (require, moudle, exports) and the method content is the source of the dependent file.
The body of the method declares the require method and calls require with the entry file path as an input, thus entering the recursive loop.
The require method takes a path as an entry that we can pass through the closure to get the corresponding FN method in the anonymous method entry object, declare a moudle object with exports, and then call fn to return moudle.exports.
It’s a little bit confusing to say that fn’s contents are the dependency files that we compiled, like greeting.js. Whenever we get a require dependency file, we execute that dependency file and return the exported result.
The execution logic of the sample main.js should make sense.