Webpack is positioned as a Bundler, and the basic solution is to package multiple JS modules into code that can be run in a browser. Next we’ll implement a simple miniWebpack known as a Bundler: an entry file that packs code into code that can be run in a browser.
Introduction of packaged projects
The directory structure for the entire demo project is shown below, where the files under SRC are the code bundler.js needs to package.
├ ─ ─ bundler. Js ├ ─ ─ package - lock. Json └ ─ ─ the SRC ├ ─ ─ index. The js ├ ─ ─ MSG. Js └ ─ ─ word. JsCopy the code
The content of each file in SRC is as follows: word.js
const word = 'miniWebpack';
export default word;
Copy the code
msg.js
import word from './word.js'
const msg = `hello ${word}`;
export default msg;
Copy the code
index.js
import msg from './msg.js'
console.log(msg)
export default index;
Copy the code
Realize bundler. Js
To implement Bundler we need to implement three parts: moduleAnalyser: module analysis. Analyze the module, get the module dependency, code and other information. MakeDependenciesGraph: Generates the dependency graph. Walk through the package project to get analysis results for all required modules. GenerateCode: Generates executable code. Provides require() functions and exports objects that generate code that can be executed in the browser.
Module analysis
Use fs module to read the contents of module; Convert file contents to abstract syntax tree AST using @babel/parser; Traverse the AST with @babel/traverse, mapping each ImportDeclaration node (the path information stored relative to the Module) to the Dependencies object. Use @babel/core in conjunction with @babel/preset-env to turn the AST into code that the browser can run.
Const moduleAnalyser = (fileName) => {// 1.fs module reads module contents from path const Content = fs.readfilesync (fileName, 'utF-8 '); Parse (content, {sourceType: 'module'}) // 3. Traverse the AST with @babel/traverse to map each ImportDeclaration node to let dependencies = {}; traverse(ast, { ImportDeclaration({ node }) { const dirName = path.dirname(fileName); const newFile = './' + path.join(dirName, node.source.value); // Key is the path relative to the current module, and value is the path relative to bundler.js. dependencies[node.source.value] = newFile; }}) // 4. Use @babel/core and @babel/preset-env, Const {code} = babel.transformFromAst(AST, null, {presets: const {code} = babel.transformFromAst(AST, null, {presets: ["@babel/preset-env"] }) return { fileName, dependencies, code } }Copy the code
Module analysis flow chart is as follows:
Calling console.log(moduleAnalyser(‘./ SRC /index.js’)) prints the following on the console:
{ fileName: './src/index.js', dependencies: { './msg.js': './src/msg.js' }, code: '"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \nexports["default"] = void 0; \n\nvar _msg = _interopRequireDefault(require("./msg.js")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_msg["default"]); \nvar _default = index; \nexports["default"] = _default; '}Copy the code
Run moduleAnalyser(module) to return fileName, Dependencies, and code information of the module. Notice in code that the import syntax has become a require function and the export syntax has become an assignment to an exports variable.
Dependency graph generation
Call moduleAnalyser(‘./ SRC /index.js’) to get the dependencies map of the entry file, then analyze the dependencies of the entry file again, and analyze the dependencies of the template again…… This is breadth-first traversal, and you can easily get the analysis results of all the modules needed for this packaging.
Const makeDependenciesGraph = (Entry) => {//entryModule: Map to entryModule = moduleAnalyser(entry); EntryModule const graphArray = [entryModule]; entryModule const graphArray = [entryModule]; for (let i = 0; i < graphArray.length; i++) { const item = graphArray[i]; //dependencies: map dependencies of the current module const {dependencies} = item; // If the current module has dependency files, If (dependencies) {for (let j in dependencies) {if (let j in dependencies) { Grapharray.push (moduleAnalyser(dependencies[j]))}}} //graph: Iterate over the graphArray to generate graphs that are more useful for packaging. Where key is fileName, value is dependencies and code const graph = {}; graphArray.forEach(item => { graph[item.fileName] = { dependencies: item.dependencies, code: item.code } }) return graph; }Copy the code
The dependency graph generation flow chart is as follows:Console. log(makeDependenciesGraph(‘./ SRC /index.js’)) prints the following on the console:
{ './src/index.js': { dependencies: { './msg.js': './src/msg.js' }, code: '"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \nexports["default"] = void 0; \n\nvar _msg = _interopRequireDefault(require("./msg.js")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_msg["default"]); \nvar _default = index; \nexports["default"] = _default; ' }, './src/msg.js': { dependencies: { './word.js': './src/word.js' }, code: '"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \nexports["default"] = void 0; \n\nvar _word = _interopRequireDefault(require("./word.js")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nvar msg = "hello ".concat(_word["default"]); \nvar _default = msg; \nexports["default"] = _default; ' }, './src/word.js': { dependencies: {}, code: '"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \nexports["default"] = void 0; \nvar word = \'miniWebpack\'; \nvar _default = word; \nexports["default"] = _default; '}}Copy the code
The generated code
We need to start generating the final runnable code. In the “module analysis” section above, we learned that in the browser executable generated with @babel/core combined with @babel/preset-env, the import syntax has become a require function, and the export syntax has become an assignment to an exports variable. So our “generate code” section needs to provide a require function and exports object.
//generateCode generates browser executable code from dependency graph const generateCode = (entry) => { Const graph = json.stringify (makeDependenciesGraph(Entry)) // Generate browser executable code from dependency graph return 'Runnable code... `}Copy the code
In order not to pollute the global scope, we wrap our code with the execute now function, passing in the dependency graph as an argument:
(function(graph){
// todo
})(${graph})
Copy the code
Find the code for the entry file in graph and run it:
return `
(function(graph){
function require(module){
eval(graph[module].code)
}
require('${entry}')
})(${graph})
Copy the code
In the code of the import file, we also need to call require to get the exports of objects that depend on the module module, so the require function must have exported objects and support internal calls to require function. But look out!! The require function is not currently declared as the require function. Define the internal use of the require function in code -> localRequire. Since we look at the compiled code, we can see that in code the require function passes the relative path to the current Module, but we need the relative path to bundler.js when we package the executable code. Again, the dependencies map we stored for each module comes in useful. LocalRequire () passes in the relative path of dependencies to module and returns the relative path of dependencies to Bundler.js, based on the graph object.
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports={};
eval(graph[module].code)
return exports;
}
require('${entry}')
})(${graph})
Copy the code
To prevent variables inside a module from polluting other modules, we immediately execute functions around eval, passing localRequire, exports, and code as arguments.
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports={};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code)
return exports;
}
require('${entry}')
})(${graph})
Copy the code
Bundler is written, and the resulting code runs directly in the browser. Calling console.log(generateCode(‘./ SRC /index.js’)) prints the following on the console:
(function(graph){ function require(module){ function localRequire(relativePath){ return require(graph[module].dependencies[relativePath]) } var exports={}; (function(require,exports,code){ eval(code) })(localRequire,exports,graph[module].code) return exports; } require('./src/index.js') })({"./src/index.js":{"dependencies":{"./msg.js":"./src/msg.js"},"code":"\"use strict\"; \n\nvar _msg = _interopRequireDefault(require(\"./msg.js\")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_msg[\"default\"]); // export default index;" },"./src/msg.js":{"dependencies":{"./word.js":"./src/word.js"},"code":"\"use strict\"; \n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n}); \nexports[\"default\"] = void 0; \n\nvar _word = _interopRequireDefault(require(\"./word.js\")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar msg = \"hello \".concat(_word[\"default\"]); \nvar _default = msg; \nexports[\"default\"] = _default;" },"./src/word.js":{"dependencies":{},"code":"\"use strict\"; \n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n}); \nexports[\"default\"] = void 0; \nvar word = 'miniWebpack'; \nvar _default = word; \nexports[\"default\"] = _default;" }})Copy the code
Assign this code to the browser console and you can see how it executes:The complete bundle.js code is as follows:
const fs = require('fs'); const path = require('path'); const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const babel = Require (' @ Babel/core ') / / moduleAnalyser: Const content = fs.readfilesync (fileName, const moduleAnalyser = (fileName) => { 'utf-8'); Parse (content, {sourceType: 'module'}) // 3. Traverse the AST with @babel/traverse, mapping each ImportDeclaration node (the path saved relative to the entry file) to the dependencies object let dependencies = {}; traverse(ast, { ImportDeclaration({ node }) { const dirName = path.dirname(fileName); //newFile is the relative path to bundler.js, which is used for packaging. const newFile = './' + path.join(dirName, node.source.value); // Key is the path relative to the current module, and value is the path relative to bundler.js. dependencies[node.source.value] = newFile; }}) // 4. Use @babel/core and @babel/preset-env, Const {code} = babel.transformFromAst(AST, null, {presets: const {code} = babel.transformFromAst(AST, null, {presets: ["@babel/preset-env"] }) return { fileName, dependencies, Const makeDependenciesGraph = (Entry) => {//entryModule: map to entryModule = const entryModule = moduleAnalyser(entry); EntryModule const graphArray = [entryModule]; entryModule const graphArray = [entryModule]; for (let i = 0; i < graphArray.length; i++) { const item = graphArray[i]; //dependencies: map dependencies of the current module const {dependencies} = item; // If the current module has dependency files, If (dependencies) {for (let j in dependencies) {if (let j in dependencies) { Grapharray.push (moduleAnalyser(dependencies[j]))}}} //graph: Iterate over the graphArray to generate graphs that are more useful for packaging. Where key is fileName, value is dependencies and code const graph = {}; graphArray.forEach(item => { graph[item.fileName] = { dependencies: item.dependencies, code: item.code } }) return graph; } //generateCode generates browser executable code from dependency graph const generateCode = (Entry) => {const graph = Json.stringify (makeDependenciesGraph(Entry)) // large closure to prevent code generated by the package from pollute the global environment // Browser executable code has require methods, has exports objects, The packaged bundler.js code needs to provide a require method and exports object. //localRequire passes in dependency paths relative to modules, according to the graph object, Return '(function(graph){function require(module){function localRequire(relativePath){return require(graph[module].dependencies[relativePath]) } var exports={}; (function(require,exports,code){ eval(code) })(localRequire,exports,graph[module].code) return exports; } require('${entry}') })(${graph}) ` } const code = generateCode('./src/index.js'); console.log(code)Copy the code
Welcome to follow the public account to read the original ~