This is the 14th day of my participation in the August More Text Challenge. For details, see:August is more challenging
Webpack’s relationship to Babel
Babel is a JavaScript compiler. (Compile the syntax that the browser does not know into the syntax that the browser does know.) Webpack is a static module wrapper for modern JavaScript applications. (Project package)
Babel is a toolchain for converting code written in ECMAScript 2015+ syntax to backward-compatible JavaScript syntax so that it can run in current and older browsers or other environments. The Babel toolchain is made up of a number of tools that simplify the use of Babel whether you are an “end user” or are integrating Babel.
What Babel can do for you:
- The syntax conversion
- Adding missing features to the target environment via Polyfill (through third-party Polyfill modules such as Core-JS)
- Source code conversion (Codemods)
Babel compilation process
Babel generates the configuration
Package. json Project configuration file
"devDependencies": {
"@babel/cli": "7.10.5"."@babel/core": "7.11.1"."@babel/plugin-proposal-class-properties": "7.10.4"."@babel/plugin-proposal-decorators": "7.10.5"."@babel/plugin-proposal-do-expressions": "7.10.4"."@babel/plugin-proposal-object-rest-spread": "7.11.0"."@babel/plugin-syntax-dynamic-import": "7.8.3"."@babel/plugin-transform-react-jsx": "7.12.17"."@babel/plugin-transform-runtime": "7.11.0"."@babel/preset-env": "7.11.0"."@babel/preset-react": "7.12.13"."@babel/preset-typescript": "7.12.17". }Copy the code
Babel, babel-Loader, @babel/core, @babel/preset-env, @babel/polyfill, and **@babel/plugin-transform-runtime**, What does all this do?
@babel/cli
@babel/ CLI is a tool that can be used at the terminal (command line).
npm install --save-dev @babel/core @babel/cli ./node_modules/.bin/babel src --out-dir lib
Copy the code
@ Babel/core:
@babel/core is the core of Babel. It is responsible for scheduling the various components of Babel to compile the code. It is the organizer and scheduler of the entire behavior.
The transform method calls transformFileRunner to compile the file, starting with the loadConfig method to generate the complete configuration. The code in the file is then read and compiled against this configuration.
const transformFileRunner = gensync<[string, ?InputOptions], FileResult | null> (function* (filename, opts) {
constoptions = { ... opts, filename };const config: ResolvedConfig | null = yield* loadConfig(options);
if (config === null) return null;
const code = yield* fs.readFile(filename, "utf8");
return yield* run(config, code); });Copy the code
@ Babel/preset – env:
This is a set of preset plug-ins that contain a set of related plug-ins, which are used in the Bable to guide how to transcode. This plug-in contains all translation rules for es6 to ES5
The Babel website explains this as follows:
Transformations come in the form of plugins, which are small JavaScript programs that instruct Babel on how to carry out transformations to the code. You can even write your own plugins to apply any transformations you want to your code. To transform ES2015+ syntax into ES5 we can rely on official plugins like
@babel/plugin-transform-arrow-functions
The syntax transformation from ES6 to ES5 is implemented in the form of a plug-in, either your own plug-in or an official plug-in such as @babel/plugin-transform-arrow-functions.
We can see from this, we need to transform what new syntax, can the list of related plug-ins, but it is very complicated, because we often need to according to the different versions of compatible browser to determine the need to introduce what plug-ins, in order to solve this problem, the Babel provides us with a default plug-in group, The @babel/preset-env allows flexibility in deciding which plug-ins to provide based on the option parameters
{
"presets": ["es2015"."react"."stage-1"]."plugins": [["transform-runtime"], ["import", {
"libraryName": "cheui-react"."libraryDirectory": "lib/components"."camel2DashComponentName": true // default: true}}]]Copy the code
The @babel/preset-env is a smart preset that allows you to use the latest JavaScript without micromanaging what syntax transformations (and optional browser multiple padding) are required for the target environment.
@babel/preset-env takes any target environments you specify and checks them against their mappings to compile a list of plug-ins and pass them to Babel.
@ Babel/polyfill:
@babel/preset-env just provides the rules for syntax conversions, but it doesn’t make up for some new features that the browser is missing, such as built-in methods and objects like Promise, array. from, etc. Polyfill is needed for js’s padding. Make up for these new features missing from earlier versions of the browser.
It’s important to note that the polyfill is quite large and, if not specified, will shimmer all the new ES6 features that are missing from your target browser. However, the part of the conversion that we don’t use is meaningless and causes the volume of the package to increase unnecessarily, so we usually configure ** “useBuiltIns” in the presets option: “Usage”, so that on the one hand we only use the spacer for the new function, on the other hand, we don’t need to introduce import ‘@babel/polyfill ‘separately, it will be automatically injected where it is used.
We use @babel/ CLI to run Babel from the terminal, using @babel/polyfill to simulate all the new JavaScript features, Env preset only codes conversion and loads polyfills for features that we use and are missing in the target browser.
@babel/polyfill
The @babel/ Polyfill module contains core-JS and a custom ReGenerator Runtime to simulate the full ES2015+ environment.
npm install --save @babel/polyfill
Copy the code
Note that the –save parameter is used instead of –save-dev, because this is a polyfill that needs to be run before your source code.
The env PRESET we used provides a “useBuiltIns” parameter, when this parameter is set to “Usage”, the last tuning measure mentioned above is loaded, which only includes the polyfill you need. With this new parameter, Babel will examine all of your code for missing functionality in the target environment, and then include only the required polyfills.
babel-loader:
@babel/core, @babel/preset-env, and @babel/polyfill are all used to transform es6 syntax and make up for missing functionality, but when we use WebPack to package JS, WebPack doesn’t know how to call these rules to compile JS. This is where babel-Loader is needed, which acts as an intermediate bridge to tell WebPack what to do with JS by calling the APIS in Babel/Core.
@ Babel/plugin – transform – runtime:
The shim for a Polyfill is to mount functionality missing from the target browser on a global variable, so when developing a class library, third-party module, or component library, you can no longer use Babel-Polyfill, which might cause global contamination. Instead, use transform-Runtime. Transform-runtime is non-invasive, meaning it doesn’t contaminate your existing methods. It gives a different name to the method that needs to be transformed, otherwise it will directly affect the business code that uses the library,
Babelrc If we do not configure anything, the package file will not change. You need to configure Babel in the babelrc file as follows. Then pack. We’ll look at how this configuration works later.
{
"presets": ["@babel/preset-env"]}Copy the code
@babel/cli parses the command line, but Babel does not compile with only the arguments on the command line. There are some key parameters missing, the plug-in information configured in the.babelrc file.
@babel/core the first step before performing the transformFile operation is to read the configuration in the.babelrc file.
Babel first determines if there is a specified configuration file (-config-file) in the command line. If not, Babel looks for the default configuration file in the current root directory. The default file name is defined as follows. The priority is from top to bottom.
// babel-main\packages\babel-core\src\config\files\configuration.js
const RELATIVE_CONFIG_FILENAMES = [
".babelrc".".babelrc.js".".babelrc.cjs".".babelrc.mjs".".babelrc.json",];Copy the code
Babelrc files are often configured with plugins and presets. Plugins are what do the real work in Babel, and the code is transformed by them. But as plugins grow, managing them is a challenge. So Babel puts a bunch of plugins together, called preset.
For plugins and presets in Babelrc, Babel converts each item into a ConfigItem. Presets are an array of ConfigItems, and so are plugins.
Assuming the following.babelrc file, this json configuration will be generated.
{
"presets": ["@babel/preset-env"]."plugins": ["@babel/plugin-proposal-class-properties"]}plugins: [
ConfigItem {
value: [Function].options: undefined.dirname: 'babel\\babel-demo'.name: undefined.file: {
request: '@babel/plugin-proposal-class-properties'.resolved: 'babel\\babel-demo\\node_modules\\@babel\\plugin-proposal-class-properties\\lib\\index.js'}}].presets: [
ConfigItem {
value: [Function].options: undefined.dirname: 'babel\\babel-demo'.name: undefined.file: {
request: '@babel/preset-env'.resolved: 'babel\\babel-demo\\node_modules\\@babel\\preset-env\\lib\\index.js'}}]Copy the code
For plugins, Babel loads the contents in order, parsing out pre, visitor, and other objects defined in the plug-in. Since presets contain several preset plugins and even new presets, Babel needs to parse the contents of preset and parse out the plugins contained within them. In the case of @babel/preset-env, Babel will resolve 40 of the plugins, and then resolve the plugins in the presets again.
An interesting point here is that the parsed list of plug-ins is handled by unshift inserted into the head of a list.
if (plugins.length > 0) { pass.unshift(... plugins); }Copy the code
For example, presets are written as [” ES2015 “, “stage-0”]. Since stage-x is a proposal for Javascript syntax, this part may depend on ES6 syntax. When parsing, you need to first parse the new syntax into ES6, and then parse ES6 into ES5. That’s where unshift comes in. The plug-in in the new PRESET is preferred.
Of course, regardless of the order of presets, the plugins in our defined plugins always have the highest priority. The reason for this is that the plugins in the plugins insert the header of the pair column using unshift after the presets are processed.
The resulting configuration consists of options and passes. In most cases, the presets in Options are an empty array, and the plugins store the collection of plugins. The content in passes is the same as that in options.plugins.
{
options: {
babelrc: false.caller: {name: "@babel/cli"},
cloneInputAst: true.configFile: false.envName: "development".filename: "babel-demo\src\index.js".plugins: Array(41),
presets: []}passes: [Array(41)]}Copy the code
Babel performs the compilation
process
Let’s take a look at the main code for Run
export function* run(config: ResolvedConfig, code: string, ast: ? (BabelNodeFile | BabelNodeProgram),) :Handler<FileResult> {
const file = yield* normalizeFile(
config.passes,
normalizeOptions(config),
code,
ast,
);
const opts = file.opts;
try {
yield* transformFile(file, config.passes);
} catch (e) {
...
}
let outputCode, outputMap;
try {
if(opts.code ! = =false) { ({ outputCode, outputMap } = generateCode(config.passes, file)); }}catch (e) {
...
}
return {
metadata: file.metadata,
options: opts,
ast: opts.ast === true ? file.ast : null.code: outputCode === undefined ? null : outputCode,
map: outputMap === undefined ? null : outputMap,
sourceType: file.ast.program.sourceType,
};
}
Copy the code
The first is to execute the normalizeFile method, which turns code into an abstract syntax tree (AST). We then execute the transformFile method, which takes our list of plug-ins. What we do is modify the AST contents according to our plug-in. Finally, the generateCode method is executed to convert the modified AST into code. Parse, transform, and generate. Let’s look at each process in detail.
Parsing (Prase)
To understand the parsing process, it’s important to understand the abstract syntax tree (AST), which represents the syntax structure of a programming language as a tree, with each node in the tree representing a structure in the source code. Different languages have different rules for generating an AST. In JS, an AST is just a JSON string that describes the code.
const a = 1
{
"type": "Program"."start": 0."end": 11."body": [{"type": "VariableDeclaration"."start": 0."end": 11."declarations": [{"type": "VariableDeclarator"."start": 6."end": 11."id": {
"type": "Identifier"."start": 6."end": 7."name": "a"
},
"init": {
"type": "Literal"."start": 10."end": 11."value": 1."raw": "1"}}]."kind": "const"}]."sourceType": "module"
}
Copy the code
Back to the normalizeFile method, which calls the Parser method.
export default function* normalizeFile(
pluginPasses: PluginPasses,
options: Object, code: string, ast: ? (BabelNodeFile | BabelNodeProgram),) :Handler<File> {... ast =yield* parser(pluginPasses, options, code); . }Copy the code
Parser iterates through all the plug-ins to see which one defines the parserOverride method. For the sake of understanding, let’s skip this part and look at the parse method, which is a method provided by @babel/ Parser that installs JS code as an AST.
Under normal circumstances, @babel/ Parser’s rules are fine for AST conversion, but if we need to customize the syntax, or modify/extend the rules, @babel/ Parser won’t be enough. Babel’s idea is that you can write your own parser and then use the plugin to specify that parser as the compiler for Babel.
import { parse } from "@babel/parser";
export default function* parser(
pluginPasses: PluginPasses,
{ parserOpts, highlightCode = true, filename = "unknown"} :Object,
code: string,
) :Handler<ParseResult> {
try {
const results = [];
for (const plugins of pluginPasses) {
for (const plugin of plugins) {
const { parserOverride } = plugin;
if (parserOverride) {
const ast = parserOverride(code, parserOpts, parse);
if(ast ! = =undefined) results.push(ast); }}}if (results.length === 0) {
return parse(code, parserOpts);
} else if (results.length === 1) {
yield* []; // If we want to allow async parsers.return results[0];
}
throw new Error("More than one plugin attempted to override parsing.");
} catch(err) { ... }}Copy the code
Now it makes sense to look back at the previous loop, which iterates through a plug-in where the parserOverride method is defined and the user is assumed to have specified a custom compiler. We know from the code that the plug-in defines no more than one compiler, otherwise Babel will not know which compiler to execute.
Here is an example of a custom compiler plug-in.
const parse = require("custom-fork-of-babel-parser-on-npm-here");
module.exports = {
plugins: [{
parserOverride(code, opts) {
returnparse(code, opts); }}]}Copy the code
The javascript to AST process relies on @babel/ Parser, and users can override the default by writing their own parser plugin. The @babel/ Parser process is quite complicated, so we’ll look at it separately, as long as it turns JS code into an AST.
Transform
The AST needs to be changed depending on the plug-in content, so let’s take a look at what the next plug-in will look like. As shown below, the Babel plug-in returns a function, takes a Babel Object, and returns Object. Pre and POST are triggered when entering and leaving the AST respectively, so they are used to initialize and delete objects respectively. A visitor defines a method to retrieve a specific node in a tree.
module.exports = (babel) = > {
return {
pre(path) {
this.runtimeData = {}
},
visitor: {},
post(path) {
delete this.runtimeData
}
}
}
Copy the code
Once you understand the structure of the plug-in, it’s easy to look at the transformFile method. First Babel adds a plug-in loadBlockHoistPlugin to the collection of plugins for sorting, no need to go into detail. Then execute the pre method of the plug-in, wait until all the pre methods of the plug-in have been executed, and then execute the method in the visitor (not simply execute the method, but execute it when the corresponding node or property is encountered according to the visitor pattern, see the Babel plug-in manual for details). To optimize, Babel merges multiple visitors into one, traverses the AST node using traverses, and executes the plug-in during the traverse. Finally, the post method of the plug-in is executed.
import traverse from "@babel/traverse";
function* transformFile(file: File, pluginPasses: PluginPasses) :Handler<void> {
for (const pluginPairs of pluginPasses) {
const passPairs = [];
const passes = [];
const visitors = [];
for (const plugin of pluginPairs.concat([loadBlockHoistPlugin()])) {
const pass = new PluginPass(file, plugin.key, plugin.options);
passPairs.push([plugin, pass]);
passes.push(pass);
visitors.push(plugin.visitor);
}
for (const [plugin, pass] of passPairs) {
const fn = plugin.pre;
if (fn) {
const result = fn.call(pass, file);
yield* []; . }}// merge all plugin visitors into a single visitor
const visitor = traverse.visitors.merge(
visitors,
passes,
file.opts.wrapPluginVisitorMethod,
);
traverse(file.ast, visitor, file.scope);
for (const [plugin, pass] of passPairs) {
const fn = plugin.post;
if (fn) {
const result = fn.call(pass, file);
yield* []; . }}}}Copy the code
At the heart of this phase is the plug-in, which uses the Visitor visitor pattern to define what to do when a particular node is encountered. Babel puts traversals of AST trees and nodes on @babel/traverse packs.
Generate
After the AST conversion is complete, you need to regenerate the AST code.
@babel/ Generator provides the default generate method, which can be customized through the plug-in’s generatorOverride method if needed. This method corresponds to the parserOverride in the first phase. After the object code is generated, sourceMap related code is also generated.
import generate from "@babel/generator";
export default function generateCode(pluginPasses: PluginPasses, file: File,) :{
outputCode: string,
outputMap: SourceMap | null,} {const { opts, ast, code, inputMap } = file;
const results = [];
for (const plugins of pluginPasses) {
for (const plugin of plugins) {
const { generatorOverride } = plugin;
if (generatorOverride) {
const result = generatorOverride(
ast,
opts.generatorOpts,
code,
generate,
);
if(result ! = =undefined) results.push(result); }}}let result;
if (results.length === 0) {
result = generate(ast, opts.generatorOpts, code);
} else if (results.length === 1) {
result = results[0]; . }else {
throw new Error("More than one plugin attempted to override codegen.");
}
let { code: outputCode, map: outputMap } = result;
if (outputMap && inputMap) {
outputMap = mergeSourceMap(inputMap.toObject(), outputMap);
}
if (opts.sourceMaps === "inline" || opts.sourceMaps === "both") {
outputCode += "\n" + convertSourceMap.fromObject(outputMap).toComment();
}
if (opts.sourceMaps === "inline") {
outputMap = null;
}
return { outputCode, outputMap };
}
Copy the code
If this article helped you, remember to like 👍 collection and follow oh 😊, hope to like a lot more…
If there are any mistakes in this post, feel free to correct them in the comments section
The articles
- ☞ Front end interview: HTTP and web topics
- In 2021, the front end of the interview knowledge essential factory
- July front – end high-frequency interview questions
- How browsers work
- Analyze the differences between TCP and UDP
- Thorough understanding of browser caching mechanisms
- How does JavaScript affect DOM tree building
- JavaScript event Model
- Learn more about modern Web browsers
- Deploy the Nextjs project on a Linux Ali Cloud server
- Snowpack – faster front-end build tool
- Learn more about JavaScript memory leaks
- Describe the hash mode and history mode of the front-end route
- Use of BFC and IFC CSS styles
- CSS Performance Optimization
- Quickly write a prototype chain that satisfies you and your interviewer
- CommonJS, AMD, CMD, ES6 Module
- How WebPack works and the difference between Loader and Plugin
- Interpret HTTP1 / HTTP2 / HTTP3