一、前言
上一章节主要介绍工程化的意义、模块、浏览器模块化局限性及它的解决方案(npm+webpack)。本章主要来进行介绍Webpack的基本概念以及快速上手(当然里面也包括一些深层次的解析)。
二、🛩️webpack核心概念
- entry:入口模版文件路径
- output:输出bundle文件路径
- module:module模块,webpack构建对象。
- bundle:输出文件,构建产物。
- chunk:中间文件,webpack构建的中间产物
- loader:文件转换器。
- plugin:插件,执行特定任务。
三、🚟快速上手
- 初始化项目
yarn init -y
- yarn安装webpack、webpack-cli
yarn add webpack webpack-cli -D
- 搭建项目结构目录
- 编写index.js文件内容
console.log("hello world");
- 编写webpack打包配置文件(webpack.config.js)
const path = require('path')
module.exports = {
mode:"development",
//入口文件
entry:'./src/index.js',
//打包后文件输出
output:{
path:path.resolve(__dirname,"./dist"),
filename:"bundle.js"
}
}
- 编写package.json中的脚本。
"scripts": {
"build":"webpack"
},
7.最后可直接通过npm run build实现打包
根据上述步骤就可以快速对代码进行打包。下面来详细分析下及打包后的结果解析。
🌍步骤分析及结果解析
1. 为什么要去安装webpack-cli这个包?
webpack下载完成后,会在node_modules中的.bin目录下生成webpack.cmd(webpack.sh)可执行文件。npm run build脚本运行时,会执行webpack命令。此命令是去执行node_modules中的.bin目录下的webpack.cmd(webpack.sh)文件。在npm run命令执行时,他会将node_modules中的.bin目录下的所有命令以软连接形式放置在环境变量中,等到命令执行结束之后就会恢复原样。
如果使用未在环境变量里申明的命令行指令则会提示无法将“....”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
在webpack.cmd中,他会去利用node执行../webpack/bin/webpack.js文件。
这个webpack.js文件一开始它就会去判断有无安装webpack-cli。
通过npm run build指令,生成bundle.js文件
2. webpack打包做了什么?
首先先看一下打包后的代码
(() => {
var __webpack_modules__ = ({
"./src/index.js":(() => {
eval("console.log(\"hello world\");\n\n//# sourceURL=webpack://quickly-start/./src/index.js?");
})
});
var __webpack_exports__ = {};
__webpack_modules__["./src/index.js"]();
})();
从打包后的程序来看,打包后的文件其实就是一个闭包,它是以入口文件开始,依次去遍历他的依赖模块,将模块放置在__webpack_modules__对象中。该对象以一个模块为键值对,键为导入模块或者入口文件配置指定的路径,值为以eval函数为函数体的一个箭头函数,eval函数主要执行的是文件内具体的内容以及sourceMap的内容。关于sourceMap他其实是提供了打包后内容与我们编写的源码进行映射,方便我们通过控制台去调试。感兴趣可以自行搜查哦。最后则是通过__webpack_modules__["./src/index.js"]();入口文件模块调用开始运行代码。
✏️Loader
在实际开发中,webpack只可打包以.js为后缀的文件,如果遇到非.js为后缀的文件则webpack会进行报错,因为它对其他后缀是是不进行处理的。这个时候就需要我们的loader了。
先上例子:
<!-- index.html -->
<script src="../dist/bundle.js"></script>
<body>
<div class="box"></div>
</body>
/*index.css*/
.box{
width: 100px;
height: 100px;
background-color: red;
}
我们通过import在index.js中引入index.css。
/*index.js*/
import './index.css'
console.log("hello world");
这个时候去进行npm run build会进行报错。
报错信息提示需要我们去进行安装相对应文件类型解析的loader来解决不识别的问题.我们可安装配置css-loader来进行css文件解析。
yarn add css-loader -D
//webpack.config.js
const path = require('path')
module.exports = {
mode:"development",
entry:'./src/index.js',
output:{
path:path.resolve(__dirname,"./dist"),
filename:"bundle.js"
},
module:{
rules:[
{
//test根据正则匹配文件
test:/\.css$/,
//指定loader来对匹配文件进行解析
use:['css-loader']
}
]
}
}
当通过webpack进行打包之后,运行结果如下。
可见,后缀为.css文件能够正常打包,打包后只生成了一个bundle.js文件.这也就意味着loader是只负责将指定后缀文件打包成js文件(CSS in Js)。我们将打包后的bundle.js引用至html中并且浏览器打开。发现样式并未生效。
css为什么不会生效?
打包后的代码太长,我将代码分割成部分进行一一分析。
__webpack_modules___:如上述第一个打包文件分析,它是根据入口文件出发,遍历依赖模块,将其全部放置在__webpack_modules___中。
var __webpack_modules__ = ({
"./src/index.css":((module, __webpack_exports__, __webpack_require__) =>{....}),
"./node_modules/css-loader/dist/runtime/api.js":((module) => {...}),
"./node_modules/css-loader/dist/runtime/noSourceMaps.js":((module) => {....}),
"./src/index.js":((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {...})
__webpack_module_cache__:见名知意,它是已加载模块的缓存对象。__webpack_require__:模块导入函数
function __webpack_require__(moduleId) {
//缓存模块中是否有当前模块,如有直接将缓存模块的exports导出
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
//创建模块并且将模块放入缓存中
var module = __webpack_module_cache__[moduleId] = {
id: moduleId,
exports: {}
};
//传入当前模块ID、模块导出对象、__webpack_require__方法
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
在__webpack_require__上又挂载了四个方法:
__webpack_require__.o:__webpack_require__.o被赋值为一个箭头函数,该函数接受两个参数:obj和prop。这个函数使用Object.prototype.hasOwnProperty.call(obj, prop)来检查obj对象是否直接拥有prop这个属性(而不是从原型链上继承的)。
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
- __webpack_require__.r:这个函数做的事情就是去做一个模块约定,方便规范之间的相互转换(实际就是在exports上挂载__esModule属性以及根据条件挂载Symbol.toStringTag属性)
__webpack_require__.r = (exports) => {
//当`Symbol` 类型存在且`Symbol.toStringTag` 存在,则exports上添加一个名为 `Symbol.toStringTag` 的属性,其值为字符串 `'Module'`
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
//使用 `Object.defineProperty` 方法为 `exports` 对象添加一个名为 `__esModule` 的属性,其值为 `true`
Object.defineProperty(exports, '__esModule', { value: true });
};
__webpack_require__.d:将definition对象中的key与值全部挂载至传入的exports中。
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
// 挂载到exports上,对象访问属性可枚举为true
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
__webpack_require__.n:处理模块的导出,确保无论是ES模块还是CommonJS模块,都能以统一的方式获取其导出内容。
__webpack_require__.n = (module) => {
// 定义一个getter函数,根据module是否是ES模块(即是否有__esModule属性)来决定返回什么
var getter = module && module.__esModule ?
// 如果是ES模块,getter函数返回模块的默认导出(module['default'])
() => (module['default']) :
// 如果不是ES模块(可能是CommonJS模块),getter函数返回整个module对象
() => (module);
// 使用__webpack_require__.d来定义getter函数的属性描述符
__webpack_require__.d(getter, { a: getter });
// 返回getter函数
return getter;
};
默认导出与整个模块的区别:在ES模块中,你可以使用export default来导出一个默认值,也可以使用多个export语句来导出多个值。而CommonJS模块(如Node.js中的模块)通常导出整个模块对象。
好了铺垫了这么多,下面开始进入正题:
- 闭包函数从入口文件
./src/index.js开始执行.
var __webpack_exports__ = __webpack_require__("./src/index.js");
- 由上方可知,
__webpack_require__实际就是开始执行./src/index.js属性对应的箭头函数。箭头函数即执行下列的eval函数。
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _index_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index.css */ \"./src/index.css\");\n\r\nconsole.log(\"hello world\");\n\n//# sourceURL=webpack://quickly-start/./src/index.js?");
看起来其实比较复杂,但是实际分析就是做了三个事情:
-
__webpack_require__.r(__webpack_exports__);在exports中利用__webpack_require__.r函数加了ESModule属性标识. -
__webpack_require__("./src/index.css");去加载./src/index.css模块。并将导出内容赋值_index_css__WEBPACK_IMPORTED_MODULE_0__属性。关于这里加载
index.css根据代码而言主要看他module.exports导出的是什么?-
__webpack_require__.d(__webpack_exports__, {"default": () => (__WEBPACK_DEFAULT_EXPORT__)});这里在一开始就利用__webpack_require__.d将后者挂载到前者上。 -
在文件执行的最后将CSS代码保存到___CSS_LOADER_EXPORT___变量中
___CSS_LOADER_EXPORT___.push([module.id, `.box{\r\n width: 100px;\r\n height: 100px;\r\n background-color: red;\r\n}`, \"\"]) -
将
___CSS_LOADER_EXPORT___赋值给__WEBPACK_DEFAULT_EXPORT__。通过第一点合并exports导出const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
-
-
console.log("hello world");执行模块内其他的内容。
由上面我们可以看出,css-loader就是将我们在index.css写的样式内容导出,并没有做其他事情,所以样式无法显示在页面上。 至此我们引出另一个loader来进行解决这个问题(style-loader)。
- 添加style-loader模块
yarn add style-loader -D
- 将style-loader配置在
webpack.config.js中(配置在use数组中即可),但是必须放置在css-loader前面,因为loader是从右往左来进行的
{
test:/\.css$/,
use:['style-loader','css-loader']
}
在执行npm run build重新打包
我们就可看到正常运行的css代码啦!!
那么,我们通过检查元素可以看,他是直接把css代码通过<style>标签将css加到了head上。
我们也可以通过编译后的代码来进行验证。
- 他先是以
css-loader去加载index.css文件,并且按照上面一样将值保存至_node_modules_css_loader_dist_cjs_js_index_css__WEBPACK_IMPORTED_MODULE_6__变量中.
var _node_modules_css_loader_dist_cjs_js_index_css__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/index.css\");
style-loader加载了多个模块到__webpack_modules__模块中,它们其实大概流程就是创建style标签,并且根据条件添加一些属性,最后就将css加入到这个标签里面去。我们可以根据以下部分代码来看。
//1、在insertStyleElement模块中主要创建style标签及添加他的属性。
function insertStyleElement(options) {
var style = document.createElement("style");
options.setAttributes(style, options.attributes);
options.insert(style);
return style;
}
//在styleTagTransform模块中,将css加载进入style标签内。
function styleTagTransform(css, style) {
........
style.appendChild(document.createTextNode(css));
}
当然中间还做了很多事情,这里就不细讲了。
通过以上例子,我们可以知道,其实loader就是去做一个其他后缀文件到js文件的一个转换(转换器)。
😿Plugin
我们知道loader实际上是一个转换器,对于loader而言,他只是负责将其他文件转换成js文件。但是针对于实际开发来说,不可能将代码全部打包到一个文件中,这样子会造成文件过大从而加载性能问题😂。所以这个时候就引出了我们的Plugin。
webpack就是一条生产线,经过一系列处理流程将源文件转换成输出结果。webpack 通过 Tapable(类似于事件的注册监听一个库) 来组织这条复杂的生产线。webpack在运行过程中,流程节点会进行广播事件从而进行相对应时间的触发。这其实就是消息的订阅与发布。对于我们开发者而言,根据自身需求,找到对应的钩子函数,往上面挂载自己的任务。在webpack构建的时候,可进行触发执行。
🌄钩子函数是什么?
钩子函数从本质上而言,它就是事件。在webpack中,它将各个流程节点进行相对应事件触发,进而方便我们可直接介入编译过程。而钩子函数就是webpack构建时触发对应事件之后执行的函数
🌍webpack构建流程
-
初始化流程:Webpack首先会从配置文件(如webpack.config.js)和Shell语句中读取并合并参数,初始化所需的插件和配置插件等执行环境所需要的参数。此阶段,Webpack会根据提供的参数初始化compiler对象,并注册所有配置的插件。这些插件会监听Webpack构建生命周期的事件节点,并做出相应的反应。
-
编译构建流程:
- 确定入口:Webpack从配置文件中指定的entry入口开始,解析文件并构建AST(抽象语法树),找出依赖,递归地进行下去。
- 编译模块:在递归过程中,Webpack会根据文件类型和loader配置,调用所有配置的loader对文件进行转换。然后,找出该模块依赖的其他模块,再递归进行本步骤,直到所有入口依赖的文件都经过了处理。
-
输出流程:
- 完成模块编译并输出:递归完成后,Webpack会得到每个文件的结果,包含每个模块以及他们之间的依赖关系。根据entry配置,Webpack会生成代码块(chunk)。
- 输出完成:最后,Webpack会将所有的chunk转换成文件并输出到文件系统。
有两个概念需要注意:Compiler、Compilation。 Compiler:compiler 在 webpack 构建之初就已经创建,并且贯穿webpack整个生命周期,只要是做webpack编译,就会去创建一个compiler。它是全局有且仅有一个。 Compilation:Compilation 在 webpack 准备编译模块时创建,它代表的是当前此次编译对象。为什么需要Compilation?我们在使用watch模式的时候,当源代码发生改变时,需要重新编译模块,但是compiler可以继续使用。如果使用compiler初始化则需要注册所有的plugin。这显然是一种浪费。
在plugin而言,webpack在Compiler和Compilation提供了许多钩子,方便我们注入我们的自定义的钩子函数进而可以介入编译过程。关于钩子,点击此处查看。
✨自定义plugin
我们在以打包后文件bundle.js中,添加一个页脚作为练习。
- 创建plugin文件夹及
FooterPlugin.js文件 - 下载webpack-sources包
npm install webpack-sources -D
FooterPlugin.js编写
//顾名思义:拼接功能
const {ConcatSource} = require('webpack-sources')
class FooterPlugin{
constructor(options){
//外部传入options保存
console.log('FooterPlugin',options);
this.options = options;
}
apply(compiler){
compiler.hooks.compilation.tap('FooterPlugin',compilation=>{
//processAssets钩子时机:生成资源文件后对其进行处理
compilation.hooks.processAssets.tap('FooterPlugin',()=>{
//获取此次编译全部的chunks
const chunks = compilation.chunks;
for(const chunk of chunks){
for(const file of chunk.files){
//将页脚写入文件内
const comment = `/*${this.options.banner}*/`;
compilation.updateAsset(file,old=>new ConcatSource(old,'\n',comment))
}
}
})
})
}
}
module.exports = FooterPlugin;
- webpack配置文件进行注册。
const FooterPlugin = require('./plugin/FooterPlugin')
module.exports = {
...webpack其他配置
plugins:[
new FooterPlugin({
banner:"fightingBoy 出品"
})
]
}
最后就可以执行npm run build来进行构建啦。
如结果所示,成功显示!!!
本章主要是针对于基本概念、loader、plugin的讲解,如果对你有帮助,给我点个赞吧🤩!!!!