Briefly describe the treeShaking of rollup

Briefly describe the treeShaking of rollup

1. Basic use of rollup

import {babel} from '@rollup/plugin-babel' ; import resolve from '@rollup/plugin-node-resolve' ; import commonjs from '@rollup/plugin-commonjs' ; import typescript from '@rollup/plugin-typescript ' ; Import {terser} from ' ROLLUP-plugin-terser ' ; Import postcss from ' ROLLUP-plugin-postcss' ; Import serve from 'ROLLUP-plugin-serve' ; Export default { INPUT:"src/main.js" , output : { file : "dist/rollup_bundle_1.js" , format : "es" , //Many Chinese models amd/es/iife/umd/cjs //name:'bundleName'//Need to provide when iife }, plugins : [ babel({ babelHelpers : "bundled" , exclude : "node_modules/**" , }), resolve(), commonjs(), typescript(), terser(), postcss(), serve({}) ], }; Copy code

2. Pre-knowledge

  1. magic-string (operate string to generate sourcemap)
  2. acorn (JavaScript Parser ast syntax tree transform generate)
  3. scope (scope scope chain)

3. The basic process of rollup

function rollup ( entry, filename ) { //Generate a bundle from the entrance const bundle = new Bundle({ entry }); //Build generates code and sourceMap const {code} = bundle.build(filename); //Write To the file fs.writeFileSync(filename, code); } Copy code

4. Merge module code

  1. Let's write a demo first
//index.js import {name, age} from './msg' let city = 'sz' var sex = 'boy' console .log(city, name) //msg.js export const name = 'name' export const Age = 28 duplicated code
  1. Merge code
//bundle.js Let s simply merge the code together. //Replace import {name, age} from'./msg' with const name = 'name' const age = 28 let city = 'sz' var sex = 'Boy' Console .log (City, name) copy the code
  1. Simple implementation of module merging
//rollup function rollup () { const bundle = new Bundle({ entry }); bundle.build(filename); } //bundle.js //The simplest idea is to find the import node and copy the code over //https://astexplorer.net/View the ast node class Bundle { constructor ( options ) { //Entry file data this .entryPath = path.resolve(options.entry.replace( /\.js$/ , "" ) + ".js" ); } build ( filename ) { let magicString = new MagicString.Bundle(); const code = fs.readFileSync( this .entryPath); magicString.addSource({ content : code, separator : "\n" , }); //Turn the code into the ast syntax tree and find the import statement and copy the code. //Actually each file is a module this .ast = parse(code, { ecmaVersion : 7 , sourceType : "module" , }); let sourceList = []; //Analyze the syntax tree and traverse ast to find the import statement and get the code in source this .ast.body.forEach( ( node ) => { if (node.type === "ImportDeclaration" ) { let source = node.source.value; //./msg sourceList.push(source); } }); //Copy the code in sourceList directly for ( let i = 0 ; i <sourceList.length; i++) { //Here is the relative path to become an absolute path let pathName = sourceList[i]; if (!path. isAbsolute(pathName)) { pathName = path.resolve( path.dirname( this .entryPath), pathName.replace( /\.js$/ , "" ) + ".js" ); } const code = fs.readFileSync(pathName); magicString.addSource({ content : code, separator : "\n" , }); } fs.writeFileSync(filename, magicString.toString()); } } Copy code

  1. result
//Integrate all the code through the above operations import {name, age} from "./msg" ; let city = "sz" ; var sex = "boy" ; console .log(city, name); Export const name = "name" ; Export const Age = 28 ; duplicated code
  1. rollup result
//1. Each file is a module and we need a module.js //2. Process the ast syntax tree separately //3. We need to remove the import statement export does not need //4. We will not use it Remove the tree-shaking const name = "name" ; the let City = "SZ" ; Console .log (City, name); duplicated code
  1. Optimize the code
//Basic directory structure ast analyse.js //analyze ast Scope.js // scope walk.js //traverse the ast syntax tree bundle.js //Bundle collects dependent modules, and finally All codes are packaged together and output index.js module .js//Each file is a module Rollup.js //packed inlet module utils.js //auxiliary functions duplicated code
  • bundle.js
//We add to the result in units of statements and not in units of files class Bundle { constructor ( options ) { //Entry file data this .entryPath = path.resolve(options.entry.replace( /\.js$/ , "" ) + ".js" ); } build ( filename ) { //Get the entry module let entryModule = this .fetchModule( this .entryPath); //Expand all the statements, we add statements one by one and no longer add this .statements = entryModule.expandAllStatements ( true ); const {code} = this .generate({}); fs.writeFileSync(filename, code); } //The entry module is index.js when it encounters import, there are two parameters fetchModule ( importee, importer ) { let route; if (!importer) route = importee; else { if (path.isAbsolute(importee)) route = importee; //relative path./msg else route = path.resolve( path.dirname(importer), importee.replace( /\.js$/ , "" ) + ".js" ); } if (route) { let code = fs.readFileSync(route, "utf8" ); const module = new Module({ code, path : importee, bundle : this , //instance of bundle }); return module ; } } //Generate code generate () { let magicString = new MagicString.Bundle(); //Add the traversal statement to the result this .statements.forEach( ( statement ) => { const source = statement._source.clone(); if ( /^Export/ . test(statement.type)) { if ( statement.type === "ExportNamedDeclaration" && statement.declaration.type === "VariableDeclaration" ) { //2. Kill export source.remove(statement.start, statement.declaration.start); } } magicString.addSource({ content : source, separator : "\n" , }); }); return { code : magicString.toString() }; } } Copy code
  • module.js
class Module { constructor ( {code, path, bundle} ) { this .code = new MagicString(code, { filename : path }); this .path = path; this .bundle = bundle; //get the ast syntax tree this . ast = parse(code, { ecmaVersion : 7 , sourceType : "module" , }); //Analyze the ast syntax tree //1. Add a _source attribute to execute the code of the current statement analyze( this .ast, this .code, this ); } //Expand all statements expandAllStatements () { let allStatements = []; //We now have 4 statements to traverse and expand this .ast.body.forEach( ( statement ) => { let statements = this .expandStatement(statement); allStatements.push(...statements); }); return allStatements; } expandStatement ( statement ) { //Mark as already processed statement._included = true ; let result = []; //We need to expand the statement to import we will generate a new module if (statement.type === "ImportDeclaration" ) { //Recursively create module Imported from index.js may be relative or absolute path let module = this .bundle.fetchModule(statement.source.value, this .path); const statements = module .expandAllStatements() ; result.push(...statements); } else { //1. We don t need import statements result.push(statement); } return result; } } Copy code
  • analyse
function analyse ( ast, magicString ) { ast.body.forEach( ( statement ) => { Object .defineProperties(statement, { _source : { value : magicString.snip(statement.start, statement.end) }, }); }); } Copy code
  • output
//we get the result of merging module const name = "name" ; const Age = 28 ; the let City = "SZ" ; var Sex = "Boy" ; Console .log (City, name); duplicated code

5. tree-shaking

//How do I know whether the variable is used? What are the possible variables that may be used //1. Assignment statement name = xxx name += xx AssignmentExpression //2. Function call console, etc. CallExpression //When we expand each statement To determine which variables are used in these statements, we don t want the function declaration FunctionDeclaration variable declaration VariableDeclaration //After excluding, our definition does not need to solve a few problems //1. How to know which variables are used //Analyze the statements that may use variables console.log(name, age) uses name age //Use _dependsOn to save dependent variables (this module may also be imported) //2. How to get the definition statement of the variable used//We use definitions to save the statement corresponding to all the variables //3. The variable is the original module definition or import introduced //we need to determine if there is that this module in this module otherwise by the scope of the import //we use imports to save all the variables of import copy the code
  1. Exclude what we don t need when expanding the statement
//ImportDeclaration FunctionDeclaration VariableDeclaration //expandAllStatements in module.js does not process these when expanding this .ast.body.forEach( ( statement ) => { if (statement.type === "ImportDeclaration" ) return ; if (statement .type === "VariableDeclaration" ) return ; if (statement.type === "FunctionDeclaration" ) return ; //only console.log(city, name) will be expanded and other statements are skipped let statements = this . expandStatement(statement); allStatements.push(...statements); }); //Get the result, where did our variable definition go? Copy the code
  1. Traverse the ast syntax tree
//1. Add a few properties to collect the variables that the defined variables depend on Object .defineProperties(statement, { _defines : { value : {} }, //Defined variables to distinguish imports _dependsOn : { value : {} }, //Depends The variable _included : { value : false , writable : true }, //Is it already included in the output statement _source : { value : magicString.snip(statement.start, statement.end) }, //Corresponding code }); //2. The variable _defines defined by the construction scope chain should support block-level scope scope.add(name, isBlockDeclaration) //If it is BlockStatement new Scope() //3. Find the dependent variable _dependsOn //Add if (node.type === "Identifier" ) { statement._dependsOn[node.name] = true ; } //4. Find all the variables defined in this module (this is the latitude of the module is to add all the statement statements together) //so that you can get the corresponding statement through the variable Object .keys (statement._defines) .forEach ( ( name ) => { this .definitions[name] = statement; }); Copy code
  1. Expand statement
function expandStatement ( statement ) { statement._included = true ; //The mark has been added let result = []; //1. Add the statement corresponding to the dependent variable //console.log(city, name) The dependency is [console, log, city, name ] const dependencies = Object .keys(statement._dependsOn); dependencies.forEach( ( name ) => { //Find the defined statement to add because the variable may be imported (we need to recursively create the module) imported ( let definition = this .define(name); result.push(...definition); }); //2. Add yourself to console.log(city, name) Add to enter result.push(statement); return result } //Find the statement corresponding to the variable function define ( name ) { if (hasOwnProperty( this .imports, name)) { //Indicates that it is an import let module = this .bundle.fetchModule( this .imports[name].source, this .path); // Go to the msg module to find the corresponding statement to find the statement corresponding to name return module .define(exportDeclaration.localName); } else { //own variable statement in this module = this .definitions[name]; //find the statement corresponding to city return this .expandStatement(statement); } } //so that we achieve a simple tree-shaking to get the results the let City = "sz" ; const name = "name" ; Console .log (City, name); Copy the code

6. Processing AssignmentExpression

//In addition to CallExpression, it will access the variable. //AssignmentExpression statement will also access the variable name = 123. We need to add these statements to the result. //Modify the code of msg export let name = "name" ; export const age = 28 ; name = "name-" ; + = name "AssignmentExpression" ; //first analyze the structure ast copy the code

//We need to add the modified statement to the result //1. Add a _modifies property Object when traversing the statement .defineProperties(statement, { _modifies :{ value : {} }, //modify }) //2. When collecting _dependsOn, we must also collect _modifies (read and write separately) statement._modifies[node.name] = true ; //3. Define a modification variable to save this. modifications = {}; //It may be this .modifications[name].push(statement); //4. When expanding the statement, add the modified statement to the result function expandStatement ( statement ) { //1. Process _dependsOn //2. Add yourself to the result //3. Add the modified statement to the result const defines = Object .keys(statement._defines); let statements = this .expandStatement(statement) ; result.push(...statements); } //The result contains our modified statement let city = "sz" ; city = "city" ; let name = "name" ; name = "name-" ; name += "AssignmentExpression" ; console .log(city, name); Copy code

7. Support block-level scope

//Modify the demo itself and the low version of rollup does not support it. Is this scope useless? import {name, age} from "./msg" ; if (age> 10 ) { let block = "block" ; console .log(block); } else if (age> 100 ) { console .log(name); } else { console .log( "test" ); } //got the answer { let block = "block" ; console .log(block); } //First look at the ast structure and judge which BlockStatement is generated according to the result of the test //The logic in judging the BlockStatement Copy code

//We simply deal with it and modify it directly to { let block = "block" ; let name = 'name' console .log(block); } //Expect results { let block = "block" ; console .log(block); } //The 0.3.1 version actually did not do the processing and the result was { let block = "block" ; let name = "name" ; var test = "var" ; console .log(block); } //# sourceMappingURL = bundle.js.map copy the code

//We simply add the declaration of the var variable to the parent scope //When traversing the node, we add a judgment function addToScope ( declarator, isBlockDeclaration = false ) { if (!scope.parent || !isBlockDeclaration) { //If the variable declared by var is also added to it statement._defines[name] = true ; } } //Try removing the scope of the code our code simply do not use this scope Copy the code

8. Deal with the problem of variable names

//If the variable is found, we will change its name if it is repeated //We modify the demo code //index.js was renamed when it was copied in. We need to rename import {name1} from "./name1" ; //const name ='name1' import {name2} from "./name2" ; //const name ='name2' import {name3} from "./name3" ; //const name ='name3' console .log(name1, name2, name3); //names1 const name = 'name1 ' export const name1 = name //names2 const name = 'name2' export const name2 = name //names3 constname = 'name3' export const name3 = name //get the result const name$ 2 = "name1" ; const name1 = name$ 2 ; const name$ 1 = "name2" ; const name2 = name$ 1 ; const name = "name3" ; const name3 = name; console .log(name1, name2, name3); //The principle is to rename //1. Process the variable name this .deConflict(); function deConflict after getting the statements in the build process of the bundle () { const conflicts = {}; //Command conflict conflicts[name] = true //Record conflicting variables //Record the corresponding module defines[name].push(statement._module) //Traverse conflicts and rename const replacement = name + `$ ${index + 1 } ` ; module .rename(name, replacement); } //2. Add a _module property Object during statement traversal. defineProperties(statement, { _module : { value : module }, //corresponding module }) //3. module defined method rename the this .canonicalNames = {}; //store a correspondence relationship function rename ( name, Replacement ) { the this .canonicalNames[name] = replacement; } //4. In the process of generating code, we need to modify the content of the ast node function generate () { Object .keys(statement._dependsOn) .concat( Object .keys(statement._defines)) .forEach( ( name ) => { const canonicalName = statement._module.getCanonicalName(name); if (name !== canonicalName) replacements[name] = canonicalName; }); //Replace the old name with the new name replaceIdentifiers(statement, source, replacements); } //5. Replace the content of ast function replaceIdentifiers ( statement, source, replacements ) { walk(statement, { enter ( node ) { if (node.type === "Identifier" ) { //Rename if (node.name && replacements[node.name]) { source.overwrite(node.start, node.end, replacements[node.name]); } } }, }); } //Get the result const name = "name1" ; const name1 = name; const name$ 1 = "name2" ; const name2 = name$ 1 ; const name$ 2 = "name3" ; const name3 = name$ 2 ; console . log(name1, name2, name3); Copy code

9. sourcemap file

//Let's restore the demo code first import {name, age} from "./hello" ; let msg = "123" ; console .log(age); function fn () { console .log(msg); } fn(); //The result of rollup packaging const age = "age" ; let msg = "123" ; console .log(age); function fn () { console .log(msg); } fn(); //# sourceMappingURL=rollup_bundle_map.js.map //map file { "version" : 3 , "file" : "rollup_bundle_map.js" , "sources" : [ "../src/hello.js" , "../src/map.js" ], "sourcesContent" : [ " export let name =/"name\";\nexport const age =/"age\";\nname =/"test\";\nname += 20;\n" , "import {name, age} from/" ./hello\";\nlet msg =/"123\";\nconsole.log(age);\nfunction fn() {\n console.log(msg);\n}\nfn();\n" ], "names" : [ "age" , "msg" , "console" , "log" , "fn" ], "mappings" :"AACO,MAAMA,GAAG,GAAG,KAAZ;;ACAP,IAAIC,GAAG,GAAG,KAAV;AACAC,OAAO,CAACC,GAAR,CAAYH,GAAZ;;AACA,SAASI,EAAT,GAAc;AACZF,EAAAA,OAAO,CAACC ,GAAR,CAAYF,GAAZ;AACD;;AACDG,EAAE" } //According to the result of rollup packaging, we need to do two operations. //1. Add the address of sourceMappingURL after the source code, write the file to the code and splice the string to let SOURCEMAPPING_URL = "sourceMa" ; SOURCEMAPPING_URL += "ppingURL" ; code += `\n//# ${SOURCEMAPPING_URL} = ${path.basename(filename)} .map` ; //2. Generate the sourcemap file //generate To return the content of the map const {code, map} = this .generate({}) fs.writeFileSync(filename + ".map" , map.toString()); //We directly use the generateMap method of magicString to generate the map file map: magicString.generateMap({ includeContent : true , file : options.dest, //TODO }), //There is another problem. The 0.3.1 file is inconsistent with the latest rollup generated file { "version" : 3 , "file" : "bundle_map.js" , "sources" : [ "../..//Users/xueshuai.liu/Desktop/rollup-study/rollup-0.3.1/main.js " , "../..//Users/xueshuai.liu/Desktop/rollup-study/rollup-0.3.1/hello.js" ], "sourcesContent" : [ "import {name, age} from/"./hello\";\nlet msg =/"123\";\nconsole.log(age);\nfunction fn() {\n console.log(msg);\n}\nfn();\n" , "export let name =/"name\";\nexport const age =/"age\";\nname =/"test\";\nname += 20;\n" ], "names" : [], "mappings" : "AACA;AAEA;AACA;AACA;ACJO;ADCP;AAIA" } Copy code

10. Source code debugging

//Refer to https://juejin.cn/post/6898865993289105415 //Version 0.3.1 Bundle index.js # Responsible for packaging Module index.js # Responsible for parsing modules ast Scope.js # scope chain analyse.js # Analyze ast syntax tree walk.js # Traverse the ast syntax tree finalisers # output type amd.js cjs.js es6.js index.js umd.js rollup.js # entrance utils # Tool function map-helpers.js object.js promise.js replaceIdentifiers.js Copy code

summary

Analysis of the 0.3.1 version process is mainly to analyze the process of tree-shaking 1. The code of rollup simple merge module finds the import statement by analyzing ast and then copies the code directly 2. In fact, the file is a module. By analyzing ast, you can know the variables of import and export in the module (module latitude) Dependencies and modifications of each statement in the module (statement latitude) 3. Starting from the entry file, recursively expand each statement and add it to the best output result 4. How do we perform tree-shaking? 1. We do not deal with import let function and other declaration statements. We only add them to the statement when it is used. 2. When expanding, add the statement corresponding to the dependent variable to the variable that may be imported, then new Module 3. Add yourself 4. Add the modified statement 5. There is no support for block-level scope for the time being. We can add the var variable to the parent's scope first. It seems that the scope is not very useful. 6. The sourcemap file is directly generated using the magicString method Copy code