When using projects in webpack, we can use esModule, commonJS, and import(moduleName) for lazy module loading. How does webpack do all this?
1. Preparation:
1.1. Use webapck@4 webpack- cli@3
"html-webpack-plugin": "4", "webpack": "4", "webpack-cli": "3"
1.2 document structure
1.3, webpack.config.js
const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { devtool: false, mode: 'development', entry: './src/index.js', output: { filename: 'js/[name].js', path: path.resolve(__dirname, 'dist') }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ template: './src/index.html', }) ], optimization: { chunkIds: 'named', splitChunks: { cacheGroups: { commons: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', }, }, } } }
1.4 packing command
npx webpack or in package Add in JSON
"scripts": { "build": "webpack" }
Then execute npm run build
2. CommonJS module packaging
2.1. Use commonJS export module and commonJS import module
// index.js const title = require('./login') console.log('commonjs File packaging analysis') // login.js // commonJS export module.exports = "It's very cold today!"
After the preparation work is completed, execute the packaging command to delete useless comments, collapse the relevant code and run debugging.
As you can see, the packaged file is IIFE The incoming object is a collection of objects whose previous two file paths are used as key names and each function is used as an object (in fact, it is our dependent collection. The key name can be configured through webpack.config.js. By default, it is named by the current path plus file name).
When you enter the self executing function, you can see that__ webpack_require__ There are many methods and properties attached to this
- __ webpack_require__.m exported modules object
- __ webpack_require__.c exported module cache
- __ webpack_require__.d define getter method for exports
- __ webpack_require__.r is defined for the exports to be exported__ Esmodule and symbol toStringTag(esModule)
- __webpack_require__.t
- __ webpack_require__.n n method defines the getter method to obtain the module value for module, and non esmodule returns the current module
- __ webpack_ require__. The O method determines whether the object has a certain attribute, and the tool function
- __ webpack_ require__. p public_ It is configured in the path configuration file. The default is an empty string, which will be used when using jsonp to dynamically import
We continue to run. When the code reaches the final return, it will call__ webpack_require__ (this method is the core method. In fact, it is very simple.), At this time, the program is loading the entry file (this is index.js)
function __webpack_require__(moduleId) { // Check for cache if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a module object and save it in the cache for next use var module = (installedModules[moduleId] = { i: moduleId, // Module ID l: false, // Has it been loaded exports: {}, // The exports object returned externally and the things exported by the module are mounted on this object }); // Call the call method to execute the current module modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ); // Tag module loaded module.l = true; // Returns the exports object of the module return module.exports; }
In__ webpack_require__ Internally, the code in the current module will be executed. When executing for the first time, we see index JS has a pair of login JS, which is wrapped by webpack__ webpack_require__ This method, whether required or import syntax, will execute this method.
// dits/main.js { "./src/index.js": function (module, exports, __webpack_require__) { const title = __webpack_require__(/*! ./login */ "./src/login.js"); console.log("commonjs File packaging analysis"); console.log(title, "title"); }, "./src/login.js": function (module, exports) { module.exports = "It's very cold today!"; console.log("login.js Yes"); }, }
Run to__ webpack_require__ (/ *!. / login * / ". / SRC / login.js") here, login will be loaded JS. login.js has little content, just module Exports = 'it's very cold today', which can be seen from the above analysis__ webpack_require__ The loaded code will return its exports So login.com is loaded JS, the value of title should be our module Exports the value exported.
The import and export analysis of commonJS specification is completed.
Conclusion: when using commonJS to import and export modules, webpack will use its own__ webpack_require__ Method to load the module.
2.2. Import the module using commonJS specification, and package the esModule export module
Change index JS and login.js, execute packaging
// index.js const object = require('./login') console.log('commonjs File packaging analysis') console.log(object.default, 'default') console.log(object.user, 'user') // login.js export const user = { name: 'Outlaw maniac-Zhang San', age: 33 } export default 'It's very cold today!' console.log('login.js Yes')
Export results. Here we only look at the modules in IIFE, because the above contents are the same.
{ "./src/index.js": /*! no static exports found */ function (module, exports, __webpack_require__) { const object = __webpack_require__(/*! ./login */ "./src/login.js"); console.log("commonjs File packaging analysis"); console.log(object.default, "default"); console.log(object.user, "user"); }, "./src/login.js": /*! exports provided: user, default */ function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d( __webpack_exports__, "user", function () { return user; } ); const user = { name: "Outlaw maniac-Zhang San", age: 33, }; /* harmony default export */ __webpack_exports__["default"] = "It's very cold today!"; console.log("login.js Yes"); }, }
You can see that these contents are added when exporting using esModule.
__ webpack_require__.r defines the esModule ID for the exports object
__webpack_require__.r = function (exports) { // Judge whether the current environment is es6. If so, set a 'Module' attribute for exports if (typeof Symbol !== "undefined" && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); } // Set a default for exports`__ esModule ` value, used to identify esModule Object.defineProperty(exports, "__esModule", { value: true }); };
__ webpack_require__.d is the attribute on the exports object and defines getter s
__webpack_require__.d = function (exports, name, getter) { // Judge whether the current exports object has a property value. If not, redefine the property and set the getter method if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter }); } }; // Tool function to judge whether the object has a certain attribute value __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
As you can see, in login JS, a getter method is set for the user object, and the return is defined in login The user object in the JS code block, which is the user object we defined in the file. At the same time, the data returned by export defaults is also passed__ webpack_exports__["default"] is assigned, so index JS is executing const object =__ webpack_ require__ (/*! ./login */ "./src/login.js"); In this code, the value in the object is the result we need
3. esModule import module packaging
3.1. Use esmodule to import modules and export them in esmodule mode
Change index JS and login JS, and then perform the packaging operation
// login.js export const user = { name: 'Outlaw maniac-Zhang San', age: 33 } export default 'It's very cold today!' console.log('login.js Yes') // index.js import title, { user } from './login' console.log('commonjs File packaging analysis') console.log(title, 'default') console.log(user, 'user')
Packaging results
{ "./src/index.js": /*! no exports provided */ function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); var _login__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/login.js"); console.log("commonjs File packaging analysis"); console.log(_login__WEBPACK_IMPORTED_MODULE_0__["default"], "default"); console.log(_login__WEBPACK_IMPORTED_MODULE_0__["user"], "user"); }, "./src/login.js": /*! exports provided: user, default */ function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, "user", function () { return user; }); const user = { name: "Outlaw maniac-Zhang San", age: 33, }; __webpack_exports__["default"] = "It's very cold today!"; console.log("login.js Yes"); }, }
As above, this file is IIFE, but the content has changed. Compared with 4.2, the only difference is the way to obtain the value.
3.2. Import and export commonJS using esModule
Change the file and then perform packaging
// login.js module.exports = "It's very cold today!" // index.js import title from './login.js' console.log('esmodule File packaging analysis')
Packaging results:
{ "./src/index.js": /*! no exports provided */ function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); var _login__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/login.js"); var _login__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_login__WEBPACK_IMPORTED_MODULE_0__); console.log(_login__WEBPACK_IMPORTED_MODULE_0___default.a, "default"); }, "./src/login.js": /*! no static exports found */ function (module, exports) { module.exports = "It's very cold today!"; console.log("login.js Yes"); }, }
As you can see, when using this method to export, there is an n method call at the place where the file is imported
, let's see what this n method does
__webpack_require__.n = function (module) { var getter = module && module.__esModule ? // Is the previous module the current tag__ esModule function getDefault() {return module['default'];} : // If it is esmodule, the default value will be returned function getModuleExports() {return module;}; // Otherwise, return to modify module __webpack_require__.d(getter, 'a', getter); // Call the d method for the module, redefine the getter, redefine the attribute a, and use it to get the value later return getter; };
Continue to look down. When you execute the code to get the title, go back and call the getter method defined by the d method defined before to get the default exported value. In this way, when you export using commonjs and import using esModule, you can get the value correctly. Otherwise, normally, an error will be reported when esModule imports commonjs
esModule import commonjs module
With the blessing of webpack, we can mix the code of commonjs esModule specification in the project to introduce npm package.
4. Module lazy loading
In webpack, we are allowed to use import('module ') to load modules lazily.
4.1 preparations
Still use the above configuration, just change the content of our packaged file
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Test package file analysis</title> </head> <body> <button id="btn">load</button> </body> </html>
In index JS file, we create a button to trigger the loading of login JS content, and then after the import module succeeds, create a new button again. This button is used for a dynamic import.
// index.js entry file, which completes the asynchronous loading of login by registering a click event JS content const btn = document.getElementById('btn') btn.addEventListener('click', () => { import(/*webpackChunkName: "login"*/'./login.js').then(login => { console.log(login, 'login -------->') // Create a new button and load the login module again const button = document.createElement('button') button.innerHTML = 'Reload Login modular' button.onclick = () => { import(/*webpackChunkName: "login"*/'./login.js').then(module => { console.log('<<<<<<loaded login again ------') console.log(module, 'module') console.log('loaded login again ------->>>>>>') }) } document.body.appendChild(button) }) }) console.log('index js Document execution')
// login.js module.exports = 'login.js value' console.log('loginjs Yes')
Click the load button, and then click the load Login module again button to get the following results
As you can see,
- The import method returns a promise. It returns an object processed by webpack, which is the above object__ webpack_require__ Processed and returned exports object
- There is login in the network JS network request, a script file of script is added in the head tag
Look at the results of webpack packaging
You can see that when lazy loading is used, two new method calls will appear
- __ webpack_require.e method of loading chunk
__ webpack_require.t create a make namespace for the current module
At the same time, there are some other codes in the function body of IIFE
The main function here is to directly call the webpackJsonpCallback method when calling the push method from window['webpackJsonp '],
Declare that a jsonpArray shares an array space with window['webpackJsonp ']
Then assign the webpackJsonpCallback method to jsonparray Push, which establishes a link between window['webpackJsonp '] and webpackJsonpCallback, that is, call window['webpackJsonp'] The push method executes the webpackJsonpCallback method
Because it is the first time to load and the jsonpArray array is an empty array, the following for loop will not be executed
Then let's see what the modules that need to be loaded asynchronously are packaged into by webpack(window['webpackJsonp'] = window['webpackJsonp'] || []).push([ ['login'], { './src/login.js': function (module, exports) { module.exports = 'login.js value'; console.log('loginjs Yes'); }, }, ]);
In this file, when executing login JS module, the push method will be called, in fact, the webpackJsonpCallback method will be called
function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; // add "moreModules" to the modules object, // then flag all "chunkIds" as loaded and fire callback var moduleId, chunkId, i = 0, resolves = []; for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } for (moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if (parentJsonpFunction) parentJsonpFunction(data); while (resolves.length) { resolves.shift()(); } }
Load login for the second time js
After triggering the Login module to be loaded again
Dynamically load login JS, execute__ wepack_require__ Method, the previous cache will be found without initiating a resource request again
Summary / extension:
- Webpack will use its own one for the compilation of js files__ webpack_require method, and call different methods according to different ways of referencing modules. The ultimate goal is to export the exported module content through the exports object, so that we can obtain the correct value
- In fact, the lazy loading module dynamically creates a script tag. Through the promise packaging, we can gracefully obtain the module after successful loading
- How does webpack know the rules of our import module and export module? adopt ast
- What's the difference between rollup and rollup? Rollup only packages esmodule, while webpack takes the old and the new
Welcome to leave a message for discussion!