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/…