希望是隐藏在群山后的星星,探索是人生道路上倔执的旅人。——布拉赫
近年来 Web 应用变得更加复杂与庞大,Web 前端技术的应用范围也更加广泛。 通过直接编写 JavaScript、CSS、HTML 开发 Web 应用的方式已经无法应对当前 Web 应用的发展。适逢其时,我们现在可以通过构建工具帮我们把复杂的工程转化为能运行在浏览器上的各种资源。
工欲善其事,必先利其器
构建其实是工程化、自动化思想在前端开发中的体现,把一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程。 历史上先后出现一系列构建工具,比如Npm Script
、Grunt
、Gulp
、Fis3
、Webpack
等,它们各有其优缺点。
由于笔者接触webpack
时间比较长,并且对其有一定的深入理解,下面我们来聊聊webpack
打包出来的代码是什么。
使用npm
初始化一个项目,然后安装webpack
、webpack-cli
依赖,在根目录下新建webpack.config.js
文件,配置如下:
// webpack.config.js
const path = require('path')
module.exports = {
mode: 'development', // 使用开发模式,打包出来的代码不会被严格压缩,方便代码分析
entry: { // 配置入口
bundle: './src/index.js'
},
output: { // 配置出口
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
接下来我们分四种情况分析webpack打包产物
- 不引入模块
- 同步加载模块
- 异步加载模块
- 使用spiltChunks时
// src/index.js
console.log('hello webpack')
我们仅在入口文件中写入console.log('hello webpack')
这行代码,然后在项目根目录下执行webpack
命令,之后在dist
目录下生成bundle.js
。现在我们打开bundle.js
探个究竟。
为了方便分析,我们把代码简化如下。
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {}
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {};
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {};
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {};
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {};
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no static exports found */
/***/ (function(module, exports) {
eval("console.log('hello webpack')\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
/******/ });
从上面打包出来的代码,我们可以知道这是一个立即执行函数,传入的参数是一个对象,即modules
,函数体定义了__webpack_require__
函数,最后返回了__webpack_require__
的执行结果。
先说modules
对象,key
表示模块的路径,value
是一个函数,函数传入了两个参数,分别是module
(存放模块信息)和exports
(暴露的对象)。
再看一下函数体的内容,首先定义了installedModules
对象和__webpack_require__
函数,然后在__webpack_require__
函数上挂载了很多静态方法和属性,这些方法和属性我们放在附录中说明。接下来我们重点来看看__webpack_require__
函数和 installedModules
对象。
// 传入参数moduleId,用于从modules对象中获取对应的函数
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // 判断模块是否已经在缓存中
// 如果在缓存中,说明模块之前已经被加载了,所以直接返回缓存中的结果
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
// 代码能执行到这里,说明模块没有在缓存中
/******/ // 创建一个模块对象,并把模块对象加入缓存
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // 从modules对象中获取模块对应函数,并执行函数
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // 把模块标记为完成加载状态
/******/ module.l = true;
/******/
/******/ // 返回模块对象的exports对象
/******/ return module.exports;
/******/ }
__webpack_require__
函数是一个核心函数,它的主要作用是定义模块,缓存模块,执行模块,并返回模块暴露的接口。
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
从上面代码我们可以看到定义的模块对象有三个属性,分别分别是i
(模块id),l
(是否完成加载),exports
(输出模块的内容)。并把定义的模块对象存入installedModules
中。
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
因为__webpack_require__
函数最后返回了module.exports
,我们重点来看一下module.exports
是怎么被赋值的,这里我们用一段代码来说明一下,如果我们在index.js
写入以下代码
// src/index.js
console.log('hello webpack')
export const a = 1
我们在打包结果可以提取到以下这段代码,这段代码说明如果我们在模块中有暴露接口(比如定义了export
或export default
)时,那么暴露的接口都会被挂载到module.exports
上。
// 在module.exports对象上定义a属性
__webpack_require__.d(__webpack_exports__, "a", function() { return a; });
当执行完模块之后,会把模块的l
属性置为true
,代表模块加载完成。
从上面的分析,我们可以知道installedModules对象主要用于缓存模块。
关于__webpack_require__
上定义的方法和属性的作用,把他们放在文章的最后面统一分析。至此,第一部分基本分析完毕。
在分析下一章之前,我们先来看一下webpack
中同步和异步加载模块的方式有那些。
模块方法 | 同步加载模块 | 异步加载模块 |
---|---|---|
ES6 | import、export | import() |
CommonJS | require() | webpack独有的require.ensure() |
AMD | define、require |
我们在index.js
同目录下新增一个log.js
文件,这个文件暴露一个logInfo
方法,并且在index.js
中引入并调用。
// src/log.js
export function logInfo () {
console.log(...arguments)
}
// src/index.js
import {logInfo} from './log'
logInfo('hello webpack')
我们把打包的代码和第一次打包的代码进行对比,发现立即执行函数函数体的定义没有变化,不同的地方在于modules
的对象。
// 简化之后的代码
// 第一次打包:
// modules['./src/index.js']:
console.log('hello webpack')
// 第二次打包:
// modules['./src/index.js']:
__webpack_require__.r(__webpack_exports__);
var _log__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./log */ "./src/log.js");
Object(_log__WEBPACK_IMPORTED_MODULE_0__["logInfo"])('hello webpack')
// modules['./src/log.js']:
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "logInfo", function() { return logInfo; });
function logInfo () {
console.log(...arguments)
}
这里我们主要分析第二次打包结果的执行流程,还记得立即执行函数最后的一行代码吗,return __webpack_require__(__webpack_require__.s = "./src/index.js")
,这行代码向__webpack_require__
函数传入入口的路径,并执行,当执行modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
就会进行以下代码,这段代码会执行__webpack_require__(/*! ./log */ "./src/log.js")
并把结果赋值给_log__WEBPACK_IMPORTED_MODULE_0__
,从第一部分的分析我们可以知道_log__WEBPACK_IMPORTED_MODULE_0__
的结果可以简化如下:
var _log__WEBPACK_IMPORTED_MODULE_0__ = {
logInfo: function () {
console.log(...arguments)
}
}
从这里我们可以知道,当我们在一个模块同步引入其他模块时,打包时会把其他模块都打包到这个模块中,在执行模块的过程中,通过路径获取模块对应的函数,并且把模块加载到缓存中。
在这之前我们已经介绍了webpack
中的模块方法,接下来我们通过import()
异步加载方式引入log.js模块
,我们把index.js
的代码修改为
// src/index.js
import('./log').then(Log => Log.logInfo('hello webpack'))
执行打包命令之后,我们看到dist目录下生成了两个chunk,分别是bundle.js
和0.js
,我们可以把bundle.js
称为主chunk,下面我们通过对两个文件的分析来了解异步加载的整体流程。
此时打包出来的代码会和之前的同步加载打包出来的代码有比较大的不同,先分析函数体代码的不同。
/******/ // install a JSONP callback for chunk loading
/******/ function webpackJsonpCallback(data) {};
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // Promise = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "main": 0
/******/ };
/******/
/******/ // script path function
/******/ function jsonpScriptSrc(chunkId) {
/******/ return __webpack_require__.p + "" + chunkId + ".bundle.js"
/******/ }
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = function requireEnsure(chunkId) {};
// 这行代码先检测window["webpackJsonp"]是否存在,如果不存在则复制一个空数组
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 缓存jsonpArray原始的push方法,作用呢?
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 将jsonpArray的push方法改为webpackJsonpCallback
/******/ jsonpArray.push = webpackJsonpCallback;
/******/ jsonpArray = jsonpArray.slice();
// 下面的这段代码可以看出,代码执行到这里的时候jsonpArray可能不是空数组,为什么? 其实这里就说明了在执行主chunk之前,jsonpArray就已经被赋值了
// 从这里也可以看出,无论是在主chunk执行之前还是在主chunk执行之后加载的其他chunk都会被webpackJsonpCallback函数执行
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
// 将oldJsonpFunction复制给parentJsonpFunction
/******/ var parentJsonpFunction = oldJsonpFunction;
我们对上面的代码做了分析, 当我们的模块中存在异步加载时,打包出来的代码会在全局对象window上创建webpackJsonp这个属性,它是一个数据,window["webpackJsonp"]有两个作用:
- 当异步模块先于主模块被加载时,异步加载的模块被存放在window["webpackJsonp"]中,等主模块完成加载,再把异步模块取出并执行
- 修改window["webpackJsonp"]数组上的push方法为webpackJsonpCallback
webpackJsonpCallback是异步加载中最重要的一个函数,为了方便理解webpackJsonpCallback
这个函数,我们需要先做一些铺垫,首先我们来看一下0.js
的代码。
从这个chunk我们可以知道,当这个chunk请求完成后,会执行以下代码,window["webpackJsonp"] push了一个数组,这个数组的第一项存放了chunk的id,用于记录chunk的加载状态,第二项是我们一开始说到的modules对象
。
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
// ...省略
}]);
看完0.js代码之后,我们接着看installedChunks对象。
记录加载的模块,key
表示模块id,value
表示模块加载状态,下面用表格表示value值所对应的状态说明。
value | 说明 |
---|---|
undefined | 模块没有被加载 |
null | 通过preloaded或prefetched预加载的模块 |
Promise | 模块正在加载中 |
0 | 模块已加载完成 |
我们知道主chunk
中installedChunks对象一开始就存在属性bundle为0,而bundle作为主chunk的id,毫无疑问,代码能执行到这里说明主chunk已经完成了加载,所以值为0。
在分析webpackJsonpCallback之前,我们还需要再分析一下_webpack_require_.e这个函数。
从代码看,函数体定义了一个数组为promises,最后返回了Promise.all(promises)。前面我们用了import('./log')
这段代码,其实这段代码最终被转化为promise,所以我们才可以在这段代码用then方法。
这个函数的主要作用是通过动态的script加载方式请求相应chunk,把chunk的的状态放入installedChunks中,并且返回一个Promise.all(),主要分析如下。
/******/ __webpack_require__.e = function requireEnsure(chunkId) {
/******/ var promises = [];
/******/
/******/ // 获取chunkId对应的状态
/******/ var installedChunkData = installedChunks[chunkId];
// 为0表示模块已经被加载,这里模块没有被加载或者处于加载中状态
/******/ if(installedChunkData !== 0) {
/******/
/******/ // 当请求的模块处于加载中的状态,并且其他地方也调用这个模块,那么会把先前模块的promises放入当前的promises中
/******/ if(installedChunkData) {
// 把原来的promise push进来
/******/ promises.push(installedChunkData[2]);
/******/ } else { // 模块未被加载
/******/ // 创建一个promise
/******/ var promise = new Promise(function(resolve, reject) {
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/ });
/******/ promises.push(installedChunkData[2] = promise);
// 从这里可知,当模块即将被加载时,installedChunks[chunkId]是一个数组,第一项为resovle,第二项为reject,第三项为promise
/******/
/******/ // 开始请求模块
/******/ var script = document.createElement('script');
/******/ var onScriptComplete;
/******/
/******/ script.charset = 'utf-8';
/******/ script.timeout = 120;
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
// jsonpScriptSrc(chunkId):拼接模块路径
/******/ script.src = jsonpScriptSrc(chunkId);
/******/
/******/ // create error before stack unwound to get useful stacktrace later
/******/ var error = new Error();
/******/ onScriptComplete = function (event) {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
// 同一个模块可能会同时发起多个请求,这里需要进行判断
/******/ var chunk = installedChunks[chunkId];
// chunk没有被成功加载
/******/ if(chunk !== 0) {
/******/ if(chunk) {
/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type);
/******/ var realSrc = event && event.target && event.target.src;
/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/ error.name = 'ChunkLoadError';
/******/ error.type = errorType;
/******/ error.request = realSrc;
/******/ chunk[1](error);
/******/ }
/******/ installedChunks[chunkId] = undefined;
/******/ }
/******/ };
/******/ var timeout = setTimeout(function(){
/******/ onScriptComplete({ type: 'timeout', target: script });
/******/ }, 120000);
// 无论加载成功还是失败都会调用onScriptComplete函数
/******/ script.onerror = script.onload = onScriptComplete;
/******/ document.head.appendChild(script);
/******/ }
/******/ }
/******/ return Promise.all(promises);
/******/ };
为了方便大家更直观的理解__webpack_require__.e
的执行,下面我们用一个流程图来表示它的执行流程。
我们知道__webpack_require__.e
中返回Promise.all(promises)
,那么promises的状态是在哪里发生改变的呢?
这里我可以告诉你的是,promises的状态发生改变的地方是在webpackJsonpCallback中,webpackJsonpCallback中执行了promises的resolve函数,使得promises的状态发生了改变。
通过对webpackJsonpCallback
函数的分析,我们可以知道这个函数的作用有:
- 改变
installedChunks[chunkId]
的状态为加载完成 - 把
其他chunk
的模块对象拷贝到主chunk
的modules对象
上 - 改变
__webpack_require__.e
中的promise状态
/******/ function webpackJsonpCallback(data) {
// 获取模块id
/******/ 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];
// 理解这个判断条件需要结合__webpack_require__.e函数
/******/ if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
// installedChunks[chunkId][0]是一个resolve函数,在__webpack_require__.e中被赋值
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
// 执行到这里,说明模块被加载完成
/******/ installedChunks[chunkId] = 0;
/******/ }
// 把加载的模块对象拷贝到modules上
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
// 为什么要这么做?
/******/ if(parentJsonpFunction) parentJsonpFunction(data);
/******/
// 执行resove函数,promise状态改变,即模块完成了加载
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ };
我们再回头来看一下主chunk
代码的执行流程:
__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./log */ "./src/log.js")).then(Log => Log.logInfo('hello webpack'))
**step1:**当执行__webpack_require__.e(/*! import() */ 0)
时,会请求id为0的chunk,并返回了一个promise
,当模块请求完成后,会执行webpackJsonpCallback
,改变了installedChunks[chunkId]
的状态,并执行了resolve
函数,即promise
状态发生改变。
step2:接着会执行__webpack_require__.bind(null, /*! ./log */ "./src/log.js")
,这个函数返回了./src/log.js
暴露的接口,即module.exports
。
step3:从module.exports
即Log
上获取logInfo
函数并执行。
至此,主chunk的代码执行完毕。
我们先来了解一下spiltChunks即代码分离是什么。
代码分离spiltChunks是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
有三种常用的代码分离方法:
- 入口起点:使用
entry
配置手动地分离代码。- 防止重复:使用
CommonsChunkPlugin
去重和分离 chunk。- 动态导入:通过模块的内联函数调用来分离代码。
下面我们采用入口起点
来对代码进行分离,继续往下看。
我们把webpack.config.js
代码修改为
// webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: {
bundle: './src/index.js',
log: './src/log.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
optimization: {
splitChunks: {
chunks: "all",
minSize: 0
}
}
}
并且把index.js
文件修改为:
// index.js
import {logInfo} from './log'
logInfo('hello webpack')
打包后,我们发现除了生成bundle.js
和log.js
,还生成了bundle~log.js
。我们在webpack.config.js
配置了两个入口,bundle和log,这两个入口都引用了相同的模块,即log.js
,所以bundle~log.js
就是被提取出来的公用模块。
下面我们把bundle.js
和之前打包出来的代码进行对比,发现有以下的不同,最重要的一点是函数体最后执行不是__webpack_require__
函数,而是checkDeferredModules
函数,下面我们会重点来分析checkDeferredModules
函数到底做了什么。
// bundle.js
/******/ (function(modules) { // webpackBootstrap
/******/ function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0];
/******/ var moreModules = data[1];
/******/ var executeModules = data[2];
/******/
/** 省略... */
/******/
/******/ // add entry modules from loaded chunk to deferred list
/******/ deferredModules.push.apply(deferredModules, executeModules || []);
/******/
/******/ // run deferred modules when all chunks ready
/******/ return checkDeferredModules();
/******/ };
/******/ function checkDeferredModules() {}
/******/ var deferredModules = [];
/******/
/** 省略... */
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./src/index.js","bundle~log"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })
/************************************************************************/
/******/ ({
/** 省略... */
/******/ });
deferredModules
是一个数组,它的子项都是数组类型,子项第一项为入口模块,子项第二项开始为依赖模块,例如deferredModules[["./src/index.js","bundle~log"]], ...)
。
deferredModules
这个数组的主要作用是子项存放入口模块id
和入口模块依赖的所有公用模块id
。它的作用在checkDeferredModules
函数中得以体现,接下来我们我看看这个重要的函数。
这个函数的作用主要是检测入口模块所依赖的其他模块是否全部完成了加载,如果是,才会通过__webpack_require__
函数执行入口模块,否则则等待所有依赖模块完成加载。
/******/ function checkDeferredModules() {
/******/ var result;
/******/ for(var i = 0; i < deferredModules.length; i++) {
/******/ var deferredModule = deferredModules[i];
/******/ var fulfilled = true;
// 注意j从1开始,因为只需要检测被依赖的模块是否完成了加载
/******/ for(var j = 1; j < deferredModule.length; j++) {
// 被依赖的模块id
/******/ var depId = deferredModule[j];
// 只要被依赖的模块有任何一个没有完成加载,那么fulfilled为false
/******/ if(installedChunks[depId] !== 0) fulfilled = false;
/******/ }
// 如果fulfilled为true,说明依赖的模块都已经完成加载
/******/ if(fulfilled) {
/******/ deferredModules.splice(i--, 1);
// 执行入口模块
/******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
/******/ }
/******/ }
/******/
/******/ return result;
/******/ }
对于一些公共模块
,在script标签
中我们先于在主chunk
之前引入,当然如果项目中有配置htmlwebpackplugin
这个插件的话,它会帮助我们做这件事。
所以,正常情况下我们会这么引入,控制台可以打印出hello webpack
。
<script src="bundle~log.js"></script>
<script src="bundle.js"></script>
而如果我们这样引入呢?这种方式同样可以打印出hello webpack
。公共模块
会在主chunk
之后加载,其实我们可以看一下webpackJsonpCallback
函数底部的一行代码return checkDeferredModules();
,即可公用代码慢于主chunk加载,但在公用代码加载进来的时候会调用webpackJsonpCallback
函数,而webpackJsonpCallback
函数又会执行checkDeferredModules
函数,这个函数如果检查到公共模块已经完成加载,就会去执行__webpack_require__
函数,从而主chunk得以执行。
<script src="bundle.js"></script>
<script src="bundle~log.js"></script>
用于暴露modules对象
用于暴露installedModules对象
判断一个对象自身是否存在某个属性
为模块的exports对象定义属性
为module对象
定义__esModule
属性,表示通过es module
方式暴露的接口。
创建一个虚拟的命名对象,具体用法在打包的代码中没有发现
兼容非es module
的模块方法
等于在webpack.config.js中output中定义的publicPath,在异步加载请求时会将这个值拼接到moduleId前面。
存放入口路径
- 为什么要学
当我们使用webpack时,刚开始是学习了解它,接着是使用它,优化它。很多时候我们只知其然,而不知其所以然,如果我们希望从第一视觉学习和使用webpack的话,那么我们就需要知道实现webpack的背后原理。我们学习一个技术的背后原理,不仅可以让我们可以更好的使用它,并且可以更好的提高我们的设计思维能力。
- 为什么要写
写完这篇文章的时候,笔者感受到了要写好一篇文章确实不容易,怎么确定好自己真的理解,怎么设计好文章结构,怎样才能让读者更容易理解,为此,笔者参阅了很多资料,分析一些好文章的写作手法写作思路,把一些复杂的逻辑用流程图表示出来,让读者可以更加清晰的看出每个环节之间的关系。写作需要有更多时间和精力的投入,当你发现写作不仅可以提高自己的软实力,更可以把知识分享出去,让更多人认识你、喜欢你的时候,你就会更多的动力继续写作。
https://webpack.js.org/concepts/
如您在阅读的过程中发现有纰漏,请及时向笔者反馈哦~
如需转载,请注明原文地址