Introduction to the
Webpack is a tool to package modular JavaScript. In Webpack, all files are modules, through Loader to convert files, through Plugin to inject hooks, and finally output files composed of multiple modules. Webpack focuses on building modular projects.
A simple version of the packaging model
Let’s start with simple things. When webPack configuration has only one exit, we actually only get a bundle.js file, which contains all the JS modules we use, and can be loaded and executed directly, regardless of subcontracting. So, I can analyze its packaging ideas, there are about the following 4 steps:
- Babel is used to complete code conversion and parsing, and generate a single file dependent module Map
- Recursive analysis starts at the entry point and generates a dependency graph for the entire project
- Package each reference module into a function that executes immediately
- Write the final bundle file to bundle.js
Single file dependency module Map
We will be able to use these packages:
- @babel/ Parser: parses code into abstract syntax trees
- Babel /traverse: a tool to traverse abstract syntax trees where we can parse specific nodes and then do something like
ImportDeclaration
Get the module imported through import,FunctionDeclaration
Access to functions - Babel /core: code conversion, such as ES6 code to ES5 mode
From the function of these modules, it is already possible to deduce how to obtain the dependent modules of a single file, go Ast-> traverse Ast-> call ImportDeclaration. The code is as follows:
// exportDependencies.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const exportDependencies = (filename) = >{
const content = fs.readFileSync(filename,'utf-8')
/ / to Ast
const ast = parser.parse(content, {
sourceType : 'module' // Babel specifies that this parameter must be added, otherwise the ES Module cannot be identified
})
const dependencies = {}
// Walk through the AST abstract syntax tree
traverse(ast, {
// Call ImportDeclaration to get the module imported through import
ImportDeclaration({node}){
const dirname = path.dirname(filename)
const newFile = '/' + path.join(dirname, node.source.value)
// Save the dependent module
dependencies[node.source.value] = newFile
}
})
// The code is converted via @babel/core and @babel/preset-env
const {code} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]})return{
filename,// The file name
dependencies,// The collection of modules on which the file depends (key-value storage)
code// The converted code}}module.exports = exportDependencies
Copy the code
An example can be run:
//info.js
const a = 1
export a
// index.js
import info from './info.js'
console.log(info)
//testExport.js
const exportDependencies = require('./exportDependencies')
console.log(exportDependencies('./src/index.js'))
Copy the code
The console output is shown below:
Single file dependency module Map
With the basis of obtaining individual file dependencies, we can further derive the module dependency map of the entire project on this basis. < span style = “box-sizing: border-box; color: RGB (74, 74, 74); line-height: 22px; font-size: 14px! Important; word-break: inherit! Important;”
const exportDependencies = require('./exportDependencies')
// Entry is the path of the entry file
const exportGraph = (entry) = >{
const entryModule = exportDependencies(entry)
const graphArray = [entryModule]
for(let i = 0; i < graphArray.length; i++){
const item = graphArray[i];
// Get the set of modules that the file depends on. For dependencies, refer to exportDependencies
const { dependencies } = item;
for(let j in dependencies){
graphArray.push(
exportDependencies(dependencies[j])
)// Key code to place an entry module and all its associated modules into an array}}// Then generate the graph
const graph = {}
graphArray.forEach(item= > {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
// Graph is a file pathname: a collection of file contents
return graph
}
module.exports = exportGraph
Copy the code
I will not post the test sample chart here. If you are interested, you can run the following
The output executes the function immediately
First, when our code is loaded into the page, it needs to be executed immediately. So the bundle.js output is essentially an immediate function. We mainly pay attention to the following points:
- When we wrote modules, we used import/export. After conversion, it became require/exports
- To make require/exports work, we had to define these two things and add them to bundle.js
- In the dependency graph, the code becomes a string. To do this, use eval
So here’s what we’re going to do:
- Define a require function. The essence of the require function is to execute a module’s code and then mount the corresponding variable on the exports object
- Get the dependency graph for the entire project, starting at the entry, by calling the require method. The complete code is as follows:
const exportGraph = require('./exportGraph')
// Write a file to output.path using fs.writeFileSync
const exportBundle = require('./exportBundle')
const exportCode = (entry) = >{
// The Object must be converted to a string first, otherwise the following template string will default to the toString method of the Object, the argument becomes [Object Object].
const graph = JSON.stringify(exportGraph(entry))
exportBundle('(function(graph) {//require a module of code, Function require(module) {// Exports function require(module) {// Exports function require(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function(require, exports, code) { eval(code); })(InnerRequire, exports, graph[module].code); return exports; // Exports variables are not destroyed after function execution} require('${entry}')
})(${graph}) `)}module.exports = exportCode
Copy the code
Write the final bundle file to bundle.js
Here, you use Node’s built-in module, FS, directly to write files. According to webpack output.path, output to the corresponding directory can be. Here, for simplicity, I have fixed the output path directly as follows:
const fs = require('fs')
const path = require('path')
const exportBundle = (data) = >{
const directoryPath = path.resolve(__dirname,'dist')
if(! fs.existsSync(directoryPath)) { fs.mkdirSync(directoryPath) }const filePath = path.resolve(__dirname, 'dist/bundle.js')
fs.writeFileSync(filePath, `${data}\n`)}const access = async filePath => new Promise((resolve, reject) = > {
fs.access(filePath, (err) => {
if (err) {
if (err.code === 'EXIST') {
resolve(true)
}
resolve(false)
}
resolve(true)})})module.exports = exportBundle
Copy the code
At this point, simple packaging is complete. Let me post the results of the demo I ran. The contents of the bundle.js file are:
(function(graph) {
The essence of the require function is to execute a module's code and then mount the corresponding variable on the exports object
function require(module) {
// The essence of InnerRequire is to get the exports variable of the dependent package
function InnerRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(InnerRequire, exports, graph[module].code);
return exports;// The function returns a reference to a local variable, forming a closure. Exports variables are not destroyed after the function is executed
}
require('./src/index.js') ({})"./src/index.js": {"dependencies": {"./info.js":"./src/info.js"},"code":"\"use strict\"; \n\nvar _info = _interopRequireDefault(require(\"./info.js\")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_info[\"default\"]);"},"./src/info.js": {"dependencies": {"./name.js":"./src/name.js"},"code":"\"use strict\"; \n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n}); \nexports[\"default\"] = void 0; \n\nvar _name = require(\"./name.js\"); \n\nvar info = \"\".concat(_name.name, \" is beautiful\"); \nvar _default = info; \nexports[\"default\"] = _default;"},"./src/name.js": {"dependencies": {},"code":"\"use strict\"; \n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n}); \nexports.name = void 0; \nvar name = 'winty'; \nexports.name = name;"}})
Copy the code
At this point, the simple packaging model is complete. Need to see examples of steps to: github.com/LuckyWinty/…
Webpack packaging process overview
The running flow of WebPack is a sequential process, from start to finish:
- The initialization parameter
- Start compiling the initial Compiler object with the parameter obtained in the previous step, load all configured plug-ins, and start compiling by executing the object’s run method
- Locate all Entry files based on the Entry in the configuration
- The compiling module starts from the entry file, calls all configured Loaders to compile the module, finds out the module that the module depends on, and then recurses this step until all the entry dependent files are processed in this step
- Complete module compilation After using Loader to translate all modules in Step 4, the compiled final content of each module and their dependencies are obtained
- Output resources: Assemble chunks containing multiple modules according to the dependencies between the entries and modules, and convert each Chunk into a separate file and add it to the output list. This is the last chance to modify the output content
- Output complete: After determining the output content, determine the output path and file name based on the configuration, and write the content of the file to the file system.
In the above process, Webpack will broadcast a specific event at a specific point in time, the plug-in will execute a specific logic after listening for the event of interest, and the plug-in can call the API provided by Webpack to change the results of Webpack running. In fact, the above 7 steps can be simply summarized as initialization, compilation, output, and three processes, and this process is actually an extension of the basic model mentioned above.
Webpack packages results to simplify comparisons
Let’s take a quick look at what the webpack4 code looks like when packaged, and how it differs from the simplified model we created above (I’ve simplified it with images for better formatting) :
__webpack_require__
__webpack_exports__
__webpack_require__ implementation
Here is also put a simplified figure, because of the source code, too much! As follows:
. Note that the only optimization of webpack4 namedModules to true, the moduleId for module path, otherwise the digital id. For the convenience of developers debug and optimization in development mode. The namedModules parameter to true by default.
conclusion
It’s actually pretty easy to understand the simple model. We can more easily understand, more in-depth to understand webpack entrance packaging (should be the same mechanism can run 2 times), public pack out (because you have the cache module is loaded, only with a number of records can know how many times, this package is loaded can be pulled out for public package). Of course, there are still many details, which need to be understood patiently and carefully. Keep learning!
The resources
- Implementing a Simple Webpack
- For more information
The last
- Welcome to add my wechat (Winty230), pull you into the technology group, long-term exchange learning…
- Welcome to pay attention to “front-end Q”, seriously learn front-end, do a professional technical people…