Build the column series catalog entry
Dandan Huang, Medical support group, Wedoctor Front End Technology Department. A senior cat slave, love static also love to move without a mark to switch the fine division program.
Overview of Rollup
Website address: rollupjs.org/guide/en/
What is a Rollup
What does Rich Harris, author of Rollup, have to say? Rollup is a modular packaging tool. Essentially, it merges JavaScript files. And you don’t need to manually specify their order, or worry about variable name conflicts between files. Its internal implementation is a little more complicated than that, but that’s what it does — merge.
Contrast Webpack
Webpack is a familiar tool for the front end, it provides powerful capabilities to build front-end resources, including HTML/JS/TS/CSS /less/ SCSS… Fonts, including images/fonts… Binary file. Because WebPack is so powerful, webPack produces a lot of redundant code when it comes to resource packaging (as you can see if you’ve checked the Webpack bundle files).
For some projects (especially libraries) that only have JS files and no other static resource files, using Webpack is a bit too big to use, because webpack bundles are slightly larger, slower, and less readable. At this point, you can select Rollup
Rollup is a module wrapper with SUPPORT for ES6 modules, for Tree shaking, but not for WebPack code-splitting, module hot updates, etc., which means it is better used as a wrapper for library projects than for application projects.
Simple summary
For webPack applications and Rollup libraries, react/ Vue /anngular are using Rollup as a packaging tool
Rollup the knowledge
Before explaining how Rollup is built, there are a few precursors
magic-string
Magic-string is a string manipulation library written by Rollup. This library encapsulates some common methods of string manipulation
var MagicString = require('magic-string') var magicString = new MagicString('export var name = "zhangsan"') // All of the following operations are based on a native string // similar to intercepting the string console.log(magicstring.snip (0, Log (magicstring.remove (0, 7).toString()) // var name = "zhangsan" // multiple modules, Let bundleString = new magicString.bundle (); bundleString.addSource({ content: 'console.log(hello)', separator: '\n' }) bundleString.addSource({ content: 'console. The log (world)', the separator: '\ n'})/principle similar to / / / / / let the STR = '/ / STR + =' console. The log (hello); \n' // str += 'console.log(world); \n' console.log(bundleString.toString()) // hello // worldCopy the code
AST
Javascript Parse converts code to an abstract syntax tree AST that defines the structure of the code. By manipulating the tree, we can accurately locate declaration statements, assignment statements, operator statements, and so on, and analyze, optimize, and change the source code: Main.js
// main.js
import { a } from './a'
console.log(a)
Copy the code
This is what an AST looks like:
"Start ": 0, "end": 40, "body": 0, "end": 40, "body": [// body is an array, each statement is a statement under body {"type": "ImportDeclaration", // ImportDeclaration type" start": 0, "end": 23, "specifiers": [ { "type": "ImportSpecifier", "start": 9, "end": 10, "imported": { "type": "Identifier", "start": 9, "end": 10, "name" : "a" / / import module named after the name of 'a'}, "local" : {" type ":" Identifier ", "start" : 9, "end" : 10, "name" : Imported. Name}}], "source": {"type": "Literal", "start": 18, "end": 23, "value": ". / a ", / / import path '/' a "raw" : "'/a '"}}, {" type" : "ExpressionStatement", "start" / / expression type: 24, "end" : 38, "expression": {"type": "CallExpression", // CallExpression type" start": 24, "end": 38, "callee": {"type": "MemberExpression", "start": 24, "end": 35, "object": { "type": "Identifier", "start": 24, "end": 31, "name": "console" }, "property": { "type": "Identifier", "start": 32, "end": 35, "name": "log" }, "computed": false, "optional": false }, "arguments": [ { "type": "Identifier", "start": 36, "end": 37, "name": "a" } ], "optional": false } } ], "sourceType": "module" }Copy the code
AST workflow
Parse converts the code into an abstract syntax tree with many EstREE nodes. Transform converts the abstract syntax tree. Generate generates new code from the transformed abstract syntax tree in the previous step
acorn
Acorn is a JavaScript syntax parser that parses JavaScript strings into syntax abstract trees. If you want to learn about an AST syntax tree, go to astexplorer.net/
Scope/scope chain
In JS, scope is used to specify the rule of variable access scope, scope chain is composed of a series of variable objects of the current execution environment and the upper execution environment, it guarantees the current execution environment to meet the access permission of variables and functions of the ordered access
Third, a Rollup
How does Rollup work?
You give it an entry file — usually index.js. Rollup will use Acorn to read the parsed file — which will return us something called an Abstract Syntax tree (AST). Once you have an AST, you can find out a lot about your code, such as what import declarations it contains.
Suppose the index.js file header has a line like this:
import foo from './foo.js';
Copy the code
This means that Rollup needs to load, parse, and analyze the./foo.js introduced in index.js. Repeat parsing until no more modules are loaded. More importantly, all of these operations are pluggable, so you can import from node_modules or compile ES2015 into ES5 code using SourcemAP-aware.
In Rollup, a file is a module, and each module generates an AST abstract syntax tree based on the file’s code.
Analyze the AST node to see if it calls a function method or reads a variable. If so, check if it is in the current scope. If not, look up until you find the module’s top scope. If this module is not found, it means that the function or method depends on another module and needs to be imported from another module. If we find a method in another module that depends on another module, we will recursively read the other module, and we will loop until we have no dependent module. We will find out where the variable or method was defined, and we will include the definition statement to avoid any other irrelevant code, okay
Look at the following code, let’s do it first:
// index.js
import { foo } from "./foo";
foo()
var city = 'hangzhou'
function test() {
console.log('test')
}
console.log(test())
Copy the code
// foo.js
import { bar } from "./bar";
export function foo() {
console.log('foo')
}
Copy the code
// bar.js
export function bar() {
console.log('bar')
}
Copy the code
// rollup.config.js export default {input: './ SRC /index.js', output: {file: './dist/bundle.js', // format: 'CJS ', // output format AMD ES6 life umd CJS name: 'bundleName', // If output format life, umd needs to specify a global variable}};Copy the code
Run the NPM run build command and you will get the following results:
'use strict';
function foo() {
console.log('foo');
}
foo();
function test() {
console.log('test');
}
console.log(test());
Copy the code
Above, we can see that Rollup just merges your code — there’s no waste. The resulting package can also be better shrunk. Some call them “scope handlers.” Second, it removes unused code from the modules you import. This is called “treeshaking.” Rollup is a modular packaging tool.
Next, let’s go into the source code and analyze the Rollup build process in detail
Rollup Builds process analysis
Rollup source structure
│ bundle.js (0 bundles, 1 bundles, 2 bundles, 3 bundles, 3 bundles, 4 bundles, 4 bundles). │ ExternalModule (ExternalModule, ExternalModule, ExternalModule, ExternalModule, ExternalModule) │ module.js // Module module, module instance. │ rollup.js // the rollup function, the start of everything, calls it for packaging. │ ├─ OBJECT // Object // Object // Object // Object // Object // Object // Object. │ scope.js // Generate Scope instances for each AST node when analyzing the node. │ walk.js // Walk is a recursive call to the AST node to analyze. │ ├─ Finalisers │ cjs.js │ index.js │ ├─ utils │ ├─ helpers //Copy the code
Rollup Build process
We enter the file with index.js, which depends on foo.js, and foo depends on bar.js
// index.js
import { foo } from "./foo";
foo()
var city = 'hangzhou'
function test() {
console.log('test')
}
console.log(test())
Copy the code
// foo.js
import { bar } from "./bar";
export function foo() {
console.log('foo')
}
Copy the code
// bar.js
export function bar() {
console.log('bar')
}
Copy the code
The debug!!!!!!
// debug.js const path = require('path') const rollup = require('./lib/rollup' Path.resolve (__dirname, 'SRC /main.js') rollup(entry, 'bundle.js')Copy the code
1.new Bundle(), build()
The first step is to generate a Bundle instance, the wrapper. The build package is then executed
// rollup.js let Bundle = require('./ Bundle ') function rollup(entry, outputFileName) { Const bundle = new bundle ({entry}) // Call the build method to compile bundle.build(outputFileName)} module.exports = rollupCopy the code
lib/bundle.jsGo from the entry path (in the bundle, we will first process the suffix of the entry file) and find its module definition. In fetchModule, an instance of the Module is generatedIf we look at the code in the red box, we see that a Module is returned
2.new Module()
Each file is a Module, and each Module has a Module instance. In the Module instance, the Acorn library’s parse() method is called to parse the code into an AST.Analyze the generated ASTanalyseLet’s first look at the AST generated by the entry file index.jsAs you can see, ast.body is an array, and the five statements corresponding to index.js expand the ast tree as follows:
{" type ":" the Program ", "start" : 0, "end" : 128, "the body" : [{" type ":" ImportDeclaration ", "start" / / import statement: 0, "end" : 31, "specifiers": [ { "type": "ImportSpecifier", "start": 9, "end": 12, "imported": { "type": "Identifier", "start": 9, "end": 12, "name": "foo" }, "local": { "type": "Identifier", "start": 9, "end": 12, "name": "foo" } } ], "source": { "type": "Literal", "start": 20, "end": 30, "value": "./foo.js", "raw": "\"./foo.js\"" } }, { "type": "ExpressionStatement", "start": 32, "end": 37, "expression": { "type": "CallExpression", "start": 32, "end": 37, "callee": { "type": "Identifier", "start": 32, "end": 35, "name": "foo" }, "arguments": [], "optional": false } }, { "type": "VariableDeclaration", "start": 38, "end": 59, "declarations": [ { "type": "VariableDeclarator", "start": 42, "end": 59, "id": { "type": "Identifier", "start": 42, "end": 46, "name": "city" }, "init": { "type": "Literal", "start": 49, "end": 59, "value": "hangzhou", "raw": "'hangzhou'" } } ], "kind": "var" }, { "type": "FunctionDeclaration", "start": 61, "end": 104, "id": { "type": "Identifier", "start": 70, "end": 74, "name": "test" }, "expression": false, "generator": false, "async": false, "params": [], "body": { "type": "BlockStatement", "start": 77, "end": 104, "body": [ { "type": "ExpressionStatement", "start": 83, "end": 102, "expression": { "type": "CallExpression", "start": 83, "end": 102, "callee": { "type": "MemberExpression", "start": 83, "end": 94, "object": { "type": "Identifier", "start": 83, "end": 90, "name": "console" }, "property": { "type": "Identifier", "start": 91, "end": 94, "name": "log" }, "computed": false, "optional": false }, "arguments": [ { "type": "Literal", "start": 95, "end": 101, "value": "test", "raw": "'test'" } ], "optional": false } } ] } }, { "type": "ExpressionStatement", "start": 106, "end": 125, "expression": { "type": "CallExpression", "start": 106, "end": 125, "callee": { "type": "MemberExpression", "start": 106, "end": 117, "object": { "type": "Identifier", "start": 106, "end": 113, "name": "console" }, "property": { "type": "Identifier", "start": 114, "end": 117, "name": "log" }, "computed": false, "optional": false }, "arguments": [ { "type": "CallExpression", "start": 118, "end": 124, "callee": { "type": "Identifier", "start": 118, "end": 122, "name": "test" }, "arguments": [], "optional": false } ], "optional": false } } ], "sourceType": "module" }Copy the code
We use the AST tree to analyse what we are doing in our analyse.
Imports = {}; imports = {}; imports = {}; imports = {}; This.exports = {}; this.exports = {}; // Stores all exports of the current module
this.imports = {}; This.exports = {}; this.exports = {}; This.ast.body.foreach (node => {if (node.type === = 'ImportDeclaration') {// This is an import statement let source = node.source.value; // Which module imported let Specifiers = Node.specifiers; ForEach (specifier => {const name = specifier.imported.name; //name const localName = specifier.local.name; This. Imports [localName] = {name, localName, source}}); //}else if(/^Export/.test(node.type)){else if(node.type === 'ExportNamedDeclaration') {// This is an Export Let declaration = node.declaration; //VariableDeclaration if (declaration.type === 'VariableDeclaration') { let name = declaration.declarations[0].id.name; this.exports[name] = { node, localName: name, expression: declaration } } } }); analyse(this.ast, this.code, this); // Found _gust and _dependsOnCopy the code
Imports = “** import {foo} from “./foo”; ** exports:{} indicates no export statement
Second step: analyse(this.ast, this.code, this); // Find _gust and _dependsOn
We define the following fields: _defines: {value: {}},// Specifies all global variables defined by the current module _dependsOn: _value: {value: false, writable: included {value: false, writable: included {value: false, writable: included} True},// whether this statement has been included in the packaging result to prevent repeated packaging _source: {value: Magicstring.snip (statement.start, statement.end)} // MagicString. snip returns the magicString instance Clone
Analyze the scope between each AST node, build a Scope tree,
function analyse(ast, magicString, module) { let scope = new Scope(); Statement.body. ForEach (statement.body. ForEach => {var function const let = function; // Create a global scope within a module; // Iterate over all the top-level nodes in the current syntax tree addToScope(declaration) { var name = declaration.id.name; // Get the declared variable scope.add(name); if (! Parent.parent) {statement._defines the space between the site and the site; }} Object. DefineProperties (statement, {_defines: {value: {}},// deposit all global variables defined by the current module _dependsOn: {value: _included: {value: false, writable: {value: false, writable: {value: false, writable: {value: false, writable: // MagicStry.snip returns an instance of magicString (clone _source: magicString). { value: magicString.snip(statement.start, statement.end) } }); // This step builds our scope chain walk(statement, {enter(node) {let newScope; if (! node) return switch (node.type) { case 'FunctionDeclaration': const params = node.params.map(x => x.name); if (node.type === 'FunctionDeclaration') { addToScope(node); } // If I iterate over a function declaration, I create a newScope object newScope = newScope ({parent: Scope,// parent is the current Scope params}); break; Case 'VariableDeclaration: / / will not generate a new scope node. The declarations. The forEach (addToScope); break; } if (newScope) {// if the current node declared a newScope // if the node generated a newScope, then a _scope will be placed on the node, Object.defineproperty (node, '_scope', {value: newScope}); scope = newScope; }}, leave(node) {if (node._scope) {if (node._scope) {if (node._scope); }}}); }); ast._scope = scope; _dependsOn ast.body. ForEach (statement => {walk(statement, { enter(node) { if (node._scope) { scope = node._scope; If (node.type === 'Identifier') {// recurse up from the current scope, if (node.type === 'Identifier'); Const definingScope = scope.findDefiningScope(node.name); const definingScope = scope.findDefiningScope(node.name); if (! definingScope) { statement._dependsOn[node.name] = true; }}}, leave(node) {if (node._scope) {scope = scope. Parent; }}}); }); }Copy the code
The breakpoint shows that ** _DEFINES and _dependsOn ** has deposited the current variable and the imported variable, respectivelyThis. definitions = {}; Store the definitions for global variables in definitions
// module.js this.definitions = {}; This.ast.body.foreach (statement => {object.keys (statement._defines). ForEach (name => {key is the global variable name, This. Definitions [name] = statement; definitions[name] = statement; }); });Copy the code
Step 4: Expand statements, expand all statements of the current module, and put statements of variables defined in these statements into the result
If (statement.type === ‘ImportDeclaration’) {return} Import {foo} from “./foo”; We don’t need this statement, return off
ExpandAllStatements () {let allStatements = []; // Expand the statements in this module and put the variable statements in the result. this.ast.body.forEach(statement => { if (statement.type === 'ImportDeclaration') {return} let statements = this.expandStatement(statement); allStatements.push(... statements); }); return allStatements; }Copy the code
**expandStatement: ** Find the variables that the current node depends on and find the declaration statements for those variables. These statements may be declared in the current module, or they may be declared in the imported module and put in the result
expandStatement(statement) { let result = []; const dependencies = Object.keys(statement._dependsOn); [name] dependencies. ForEach (name => {name => {name}) Let definition = this.define(name); result.push(... definition); }); if (! statement._included) { statement._included = true; Result.push (statement); // Add result.push(statement); } return result; }Copy the code
Const Module = this.bundle.fetchModule(importdata.source, this.path); Gets the module that imported the variable
Const importData = this.imports[name]; import (name) {// importData = this.imports[name]; // Const Module = this.bundle.fetchModule(importdata.source, this.path); // This module also has const exportData = module.exports[importdata.name]; // Return the declaration of the imported module variable return module.define(exportData.localname); } else {//definitions [name]; let statement = this.definitions[name]; if (statement && ! statement._included) { return this.expandStatement(statement); } else { return []; }}}Copy the code
This. Statements is the array of all the tokens we returned after analyzing them
This is a lot of analysis, but it boils down to the following:
- Collect import and export variables
- Establish mapping relationships for future use
- Collect variables defined by all statements
- Establish the corresponding relationship between variables and declaration statements to facilitate subsequent use
- Filter import statements
- Delete keywords
- When the statement is output, determine whether the variable is import
- If so, you need to recursively collect variables that depend on the file again
- Otherwise direct output
- Build dependencies, create a scope chain, and leave it to the./ SRC /ast/analyse.js file
- Attach _source, _DEFINES (variable defined by the current module), _dependsOn(externally dependent variable), _included(whether it has been included in the output statement) to each statement in the abstract syntax tree
- Collect the variables defined on each statement and create a chain of scopes
3. The generate ()
For example, the code for the function foo() introduced from foo.js looks like this: export function foo() {}. Rollup removes export and becomes function foo() {}. Because they’re going to be packaged together, we don’t need to export.
Step 2: Add the source code of the AST node to magicString. This operation is essentially equivalent to spelling strings.** Return magicString.toString() **. Returns the merged source code
generate() {
let magicString = new MagicString.Bundle();
this.statements.forEach(statement => {
const source = statement._source;
if (statement.type === 'ExportNamedDeclaration') {
source.remove(statement.start, statement.declaration.start);
}
magicString.addSource({
content: source,
separator: '\n'
});
});
return { code: magicString.toString() };
}
Copy the code
Finally, output to ‘dist/bundle.js’
summary
In a nutshell, a Rollup build does the following:
- Get the contents of the entry file, wrap it as a Module, and generate an abstract syntax tree
- Perform dependency parsing on the entry file abstract syntax tree
- Generate the final code
- Write to object file
Four,
The above code implementation process can help you simple implementation of a rollup packaging process, but also is only the rollup source code for the abstract, convenient for everyone to understand the principle of rollup packaging, many of the details are not written out, if interested, you can read the source code.
5. Reference materials
zhuanlan.zhihu.com/p/372808332
Github.com/xitu/gold-m…
Blog.csdn.net/q411020382/…