webpack 4.0 完全讲解及源码解读
就目前前端环境而言,使用cli自动构建工具可以快速的构建项目完成项目搭建,快速完成功能,业务开发,这样水到渠成的模式深得人心,也深得科技公司信赖,因为简单,易用且方便,但是对于使用webpack构建一个0-1的项目,可能很多人只能摊手,不得不说webpack是前端工程化之中占有相当重要地位的成员,本篇文章就来说一下webpack的使用教你从0到1构建一个企业级的依赖webpack的前端工程化环境。
image.pngwebpack 是什么
如果你学习过gulp那么你应该了解gulp的本质是什么,这是一个流程优化工具,像是文件的压缩,开启一个webserver ,sass的编译等等, 这是gulp的功能,gulp是一个前端构建工具,这个工具简单易用深得人心,但是随着后前端时代的到来,越来越多的构建工具逐渐进入我们的事业,那么我们以gulp为铺垫来聊一下webpack。
webpack是什么那 ? 简单一句话,是一个前端打包工具,一言蔽之 webpack万事万物皆JS,图片是JS,css是JS,图片也是JS,一个神奇的打包工具。这个工具在技术层面和gulp是有本质区别的。如果说gulp是一个工具库,可以让我们更容易的构建我们的项目,那么webpack更像是一个压缩机,可以让我们将代码进行高性能的转义及优化。
其实官网上的一张图片非常能说明webpack的功效 :
webpack功能图解
简单总结一下webpack 的功能:
-
打包 : 可以把多个JavaScript打包成一个文件,减少服务器压力
-
转换 : 把拓展语言如ES2015,TypeScript 转换为普通的 JavaScript ,让浏览器顺利运行。
-
优化 : 前端变得越来越复杂之后,性能也会存在问题,webpack开始可以肩负起优化和性能提升的责任。
这些比较笼统概念性的描述可能不是很容易让人理解,那么在此请暂时记住或者复制粘贴这句话,以便于后面的学习及理解。
我在学习和使用webpack的时候发现其缺点只有一个,那就是难,但是我认为这并不是webpack的缺点而是开发人员本身的缺点。
ok,闲话少叙,我们开始进入正题,来正式开启我们的webpack的旅程。
先来装一个webpack:
npm install webpack webpack-cli -g|--dev
> 在这里值得注意的是webpack4开始依赖webpack-cli,必须安装webpack-cli
为了这个练习我们需要建立一个项目结构
基础项目结构.pngHome.js源码
建立一个使用了COMMONJS规范,并输出了字符串的Home.js
module.exprots = "Hello WebPack";
index.js
在index.js之中使用COMMONJS规范引入建立好的Home.js
const Home = require("./components/Home");
console.log(Home)
index.html
在src目录下的index.html之中引入index.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="./main.js"></script>
</body>
</html>
ok在此我们完成了学习的第一步,接下在我们进入到目录之中启动一个webserver,让我们的项目在浏览器之中跑起来,看一下会发生什么事情。
在调试之前我们需要使用一个webserver工具,非常好用 browsersync官网
browser-sync 全局安装
npm install -g browser-sync
进入src目录下启动browser-sync
browser-sync start --server --files "index.html"
此时会弹出一个页面在 http://localhost:3000 这个路径下使用webserver打开了你的 index.html,我们打开控制台之后就会发现一个神奇的事情,报错了。
报错.png为啥报错那? 原因非常简单,用浏览器打开,浏览器不支持require这个语法啊,你回去看一眼你的main.js 你会发现这个玩意使用了nodejs语法,浏览器不支持你能怎么办?童鞋们,回顾一下webpack的功能,回去瞅一眼,你就明白了,介玩意到了发挥它神威的时候了。
接下来让我们见证webpack的神威 。
imagewebpack --mode development --entry "./index.js" --output-path "../dist" --output-filename "main.js"
这么一长串代码不要写错了哦,这个东西非常的重要,我们逐个讲解这些语句的含义
-
**--mode development ** 这是webpack4新增的内容,表示webpack此时所处的模式,development这个模式表示开发模式。可选模式共有 "development", "production", "none"这些模式,这些东西都是啥意思我们在应用之中会给大家详细讲解。
-
--entry "./index.js" 这个表示要打包的入口文件,什么叫做入口文件那,因为webpack致力于把所有的东西都压缩到一起,所以 “入口” 这个概念实际上就是所有依赖的统一路径引入。当然在这个webpack环境之中,入口是支持 COMMONJS语法的。
-
--output-path "../dist" 和 output-filename "main.js" 分别表示了输出的路径和输出的文件名称,是单纯的命名,这个东西其实比较无所谓了。
独到这里你可能会问:这个玩意这么墨迹么? 这还只是WebPack的墨迹的冰山一角,如果想要彻底学习掌握它,请耐心读完所有文字,如果真的完全记不住,请使用 --help 大法。手动斜眼笑
--help大法示例 :
webpack --help
这时候需要将index.html 文件复制到dist文件夹 :
我们重新看一下目录结构主要看一下新创建出来的 dist 文件夹。
编译后项目结构.png 编译后的 main.js ,只是展示一下编译后端 效果,后续我会继续更新相关webpack编译原理的代码,把它理解为一坨编译后的代码就可以了,至少他是兼容的,这段代码如果阅读困难,可以直接看下一节,后面有简化后的代码。
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // 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) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // 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) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // 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 = "./index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./components/Home.js":
/*!****************************!*\
!*** ./components/Home.js ***!
\****************************/
/*! no static exports found */
/***/ (function(module, exports) {
eval("module.exports = \"hello world\";\n\n//# sourceURL=webpack:///./components/Home.js?");
/***/ }),
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval("const Home = __webpack_require__(/*! ./components/Home */ \"./components/Home.js\");\r\nconsole.log(Home);\n\n//# sourceURL=webpack:///./index.js?");
/***/ })
/******/ });
ok我理解,你现在的表情大概是这样的 :
黑人问号脸表情包为啥我么就写了一行代码就编译成了这个样子那, 说好了性能提升了? 说好了打包那 ? 别慌,我对上面的编译结果代码进行一些删减,你关注的部分应该集中在这几个部分
其实只是使用两个eval语句对代码进行了输出,代码如下:
eval("module.exports = \"hello world\";\n\n//# sourceURL=webpack:///./components/Home.js?");
eval("const Home = __webpack_require__(/*! ./components/Home */ \"./components/Home.js\");\r\nconsole.log(Home);\n\n//# sourceURL=webpack:///./index.js?");
会不会有一种我是谁我在哪我在感谢啥的赶脚出现 ? 是就对了,这样的代码阅读,对于基础薄弱的童鞋可能略微有些痛苦,但是没关系,我会用极简的代码对其工作原理进行剖析,如果感觉学习起来比较吃力,那么可以收藏本篇文章待提升后再次进行阅读。
接下来我会继续对代码进行删减,让大家能看清webpack的整体框架 :
/******/ (function(modules) { // webpackBootstrap
/******/ function __webpack_require__(moduleId) {
/******/ }
__webpack_require__(__webpack_require__.s = "./index.js");
/******/ })
/******/ ({
"./components/Home.js":(function(module, exports) {
eval("module.exports = \"hello world\";\n\n//# sourceURL=webpack:///./components/Home.js?");
}),
"./index.js":(function(module, exports, __webpack_require__) {
eval("const Home = __webpack_require__(/*! ./components/Home */ \"./components/Home.js\");\r\nconsole.log(Home);\n\n//# sourceURL=webpack:///./index.js?");
})
/******/ });
这个代码框架那,基本说明了webpack编译之后的两件事情 :
-
其实编译结果就是一个匿名函数,该匿名函数的参数是一个对象。题外话 : 这是区别于webpack3.0的因为webpack3.0是以数组的形式进行传递的。
-
webpack编译内置了一个函数 __webpack_require(){} 这个函数的核心目的用于实现浏览器的commonJS规范。
你是不是还是看不懂 , 手动斜眼笑,没关系我又帮你简化了 :
/******/ (function(modules) {
/******/ function 实现浏览器模块的功能(moduleId) {
/******/ }
实现浏览器模块的功能(__webpack_require__.s = "./index.js");
/******/ })
/******/ ({
"以路径命名的依赖":(function(module, exports) {
eval("源码的字符串");
}),
"核心文件":(function(module, exports, __webpack_require__) {
eval("源码之中的字符串");
})
/******/ });
这是中文命名的代码,可以看出来这个代码的核心,就是用对象存储编译后的代码 , 然后用一个功能进行了实现,ok我们继续阅读源码,看一下浏览器端是如何实现COMMONJS规范的。
我们来看一下 function __webpack_require__(){} 这个方法的实现 从外部先来看一下定义的方法有哪些,我删除掉了部分代码以便可以把焦点关注在正确的地方。
var installedModules = {};
function __webpack_require__(moduleId) {
}
__webpack_require__.m = modules;
__webpack_require__.c = installedModules;
__webpack_require__.d = function(exports, name, getter) {};
__webpack_require__.r = function(exports) {};
__webpack_require__.t = function(value, mode) {};
__webpack_require__.n = function(module) {};
__webpack_require__.o = function(object, property) {};
__webpack_require__.p = "";
通过这个简化之后的代码框架我们可以看出,其实这些代码都是在为核心方法 __webpack_require__提供具体的功能,期中有两个变量值得注意。
- modules 这个变量就是传入的参数我们上文提到的传入匿名函数之中以路径为key值,以包含了eval(源码之中字符串功能的方法)。
- installedModules 这个则是整个对象的缓存。
ok,各位看官请在下文的阅读中请将 modules 理解为 参数对象 ,同理 __webpack_require__.m = modules;
将installedModules 理解为 缓存 , 同理 __webpack_require__.c = installedModules;
其余功能我们逐个进行分析 :
d 和 o , 在原文中有注释,这个 d 方法用于 : 为Harmony导出定义getter函数。
很难理解对吧,没关系我们分析一下这段代码的功能。
功能代码剖析
功能函数d,o
从参数看起,共有三个参数 exports,根据COMMONJS规范我们认为这个参数是包含了功能的一个对象。所以我们将exports理解为一个存储着各种类型数据的对象。
name则表示属性名。
getter表示为某个属性设置的监听方法。
在这里如果没办法很好理解getter的话建议先去阅读一下 defineProperty 在MDN上的文档。 MDN文档链接
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
在这个功能中使用了一个功能叫做__webpack_require__.o , 这是一个非常简单的Object.hasOwnProperty反柯里化封装,至于什么是反柯里化你可以理解为 : 一个嗷嗷待哺的孩子,被后妈抢走从此没有了亲妈,以后所做的一切都是后妈指示的, 这个反柯里化是另一个课题,我们暂且只关注其实现的功能即可。
这个o功能就是实现了判定某个属性(字符串或者 Symbol) ,是否存在于对象之上返回值是布尔值。
所以我们判定这个d方法就是 , 判定 exports对象上是否存在name属性,如果不存在就给这个属性定义一个取值时的监听getter.
功能函数 r
代码注释为,给exports对象定义一个__esModule属性。
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
这个代码其实是给exports定义了一个特殊的符号,查看浏览器是否支持ES2015的Symbol,如果支持,以Symbol符号给当前对象添加上一个独一无二的标志key为 Symbol.toStringTag,值为 "Module"。
如果不支持会添加上一个 __esModule 的标志用于记录当前浏览器的支持情况,为功能做出判断。
功能函数t
代码注释为 , 创建一个独立的命名空间对象。
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
通过mode参数对value进行不同的处理 :
- mode 1 | 8 : value是model 的id值,这个id值即为路径,直接去调用主函数。
- mode 4 : value 是一个对象并且浏览器不支持 ES2015则等待ns对象准备结束之后进行返回。
- mode 2 : 会merge(合并)所有属性给ns对象。并且返回ns对象。
期间使用了 r : 去添加对象的标记,用d去给属性绑定getter,用于返回当前属性值。
功能函数n
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
根据不同的module类型进行区别,从而定义同的取值函数。
**最后的最后主函数的阅读终于来了 **
我们来看一下这个函数主函数做了些啥
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
__webpack_require__(__webpack_require__.s = "./index.js");
其实这个函数主要的作用就是自己定义了一个COMMONJS的对象,并且放入缓存;
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
这是根据COMMONJS规范定义的一个对象,没有任何特别的。
然后 modules根据传入的 moduleid进行调用 moduleid的key值就是代表的就是当前程序的入口文件路径,value值就是传入的参数函数,前文提到的带有eval()字符串的函数。
期中module中的l代表的就是当前模块是否被调用,用于判定后续模块调用的钩子函数。
源码分析到此结束。
简单总结一下:其实webpack就是利用了nodejs讲源码进行了转义之后当做字符串放到了一个eval之中,然后自己创建了一个浏览器端的对象,并对这个对象进行层层处理让这个对象符合commonjs规范,最后根据规范调用编译好的字符串。到此我们完成了webpack编译代码的阅读。
你是否有收获那? 有任何问题欢迎留言讨论。
网友评论