本篇文章主要是针对 B站Webpack从原理到实战 的知识梳理,之前写过一些 Webpack 更细节的一些知识,详情见 前端工程化(webpack),里面更详细的介绍了前端工程化、loader的使用,webpack的常用插件,Source Map等知识。

本文重点:

  1. 理解前端的模块化;
  2. 理解 Webpack 打包的核心思想;
  3. Webpack核心:loader 和 plugin,要分清楚什么情况下 loader,什么时候用 plugin,要有技术选型能力。

一、Webpack介绍

1.1 Webpack是什么?

官方定义:Webpack 是一个现代 JavaScript 应用程序的静态模块打包器

自己的解释:

  • 解释 JavaScript :Webpack 在不进行特殊配置的情况下,只认识 JavaScript 这一种语言,也只能处理 JavaScript 这一种语言。其他类型文件需要配置 loader 或者插件进行处理。
  • 解释打包器:一个项目中有很多 js 文件,如果我们保持这些 js 文件的分离,那我们就必须考虑这些文件的加载顺序,但依赖关系很复杂。这个过程也很复杂,我们很难考虑清楚;其次,性能上的开销肯定比只加载一个要多太多,要是能把 a,b,c 合成一个bundle.js 文件肯定比加载多个文件的开销小很多。

1.2 Webpack 产生的背景

1. 为什么要打包?

我们觉得原生的CSS不够好用,就提出了 Sass、Less,我们想要前端代码也拥有类型校验的能力,就有了 typescript。针对以下的问题,我们就提出了打包的方案解决:

  1. 一个项目依赖多个文件,而各个依赖文件之间的关系难以梳理,耦合程度较高,代码难以维护;
  2. 把所有依赖包都打包成为一个 js 文件(bundle.js)文件,会有效降低HTTP请求次数,进而提升性能;(一种前端性能优化策略)
  3. 逻辑多、文件多,项目复杂度提高

一种技术的出现,一定是因为过往的技术不能满足现在生产开发的需要或者说不够便捷才出现的。

2. 为什么要使用Webpack打包?

  • 针对一个项目依赖多个模块的场景,行业里提出了模块化的方案,而 Webpack 能推而广之,本质上是因为他就是一个典型的优秀的模块化方案

  • 拥有强大的打包技能;

  • 充当了“翻译官”的角色,例如:在浏览器控制台写一段 typescript 代码,浏览器直接会报错,因为浏览器看不懂 typescript 呗,而Webpack 在打包过程中,loader 就会将浏览器看不懂的代码翻译为浏览器看的懂的代码;

  • 更高级的功能有 plugin 来辅助;

  • plugin 和 loader 都是可插拔的,意思是需要他的时候就把它插进来,不需要的时候就把他删掉。Webpack强大而灵活

二、Webpack的原理与背景

2.1 理解前端模块化

1. 作用域

从作用域入手开始理解。

作用域描述的运行时代码中变量、函数、对象的可访问性,简单来说,作用域决定了代码中变量和其他资源的可见性。

(1)全局作用域

  • 在 JS 中,当我们在文件中开始写JavaScript 代码的时候,就是在全局作用域内了;
  • 在 JavaScript的执行过程中,只有一个全局作用域;
  • 全局作用域的变量和其他资源都会挂载在全局对象(浏览器中是 window,node中是 global )上。
var a = 1;
window.a; // 1

(2)局部作用域

function a(){var v = 1;
}
window.v; // undefined

2. 命名空间

先来举个命名冲突的例子:

在以前我们想引入多个 js 文件的时候,就会用多个 script 标签来引入,但这样很容易导致变量间命名冲突问题,如下:

<body><script scr="./moduleA.js"></script><script scr="./moduleB.js"></script><script scr="./moduleC.js"></script>
</body>

moduleA、moduleB、moduleC 都共用了一个全局作用域。

如果此时,在 moduleA 中声明:

var a = 1

在 moduleB 中声明:

var a = 2

在 moduleC 中:

var b = a + 1
console.log(b) //3,不是2

moduleB 中 a 的声明就会覆盖掉 moduleA 中 a 的声明

重要!!!在任何一个 JavaScript文件中,进行顶层作用域的变量或函数声明,都会暴露在全局中,使得其他脚本也能获取到其他脚本中的变量,就很容易导致命名冲突。简单来说就是,js中代码执行的顶层作用域就是全局作用域,变量和函数定义很容易冲突。

解决方案一:给变量加上命名空间

// moduleA.js
var Susan = {name: "susan",sex: "female",tell: function(){console.log("我是:",this.name)}
}

给变量加上命名空间可以解决命名冲突的问题,但又会带来一个安全问题,我们只希望暴露一个方法显示个人信息就行,而不希望这些个人信息能够随意被更改,但方案一中,moduleC 直接用 Susan.name = 'Jack' 就可以直接修改 Susan 的信息,所以方案一无法保证模块属性内部安全性。

单纯的添加命名空间只能起到解决命名冲突,避免变量被覆盖的作用。

解决方案二:命名空间 + 闭包(用函数作用域保护变量)

写法一:

// 定义模块内的闭包作用域(模块作用域),以moduleA为例
// 立即执行函数
var SusanModule = (function(){var Susan = {// 自由变量name: "susan",// 自由变量sex: "female",// 只允许访问tell方法,不能访问和修改其他属性return {tell: function(){console.log("我是:",this.name)console.log("我的性别是:",this.sex)}}}
})()

什么是自由变量?简单来说是跨作用域的变量,可以点击这里进行参考。(里面有一个句很好的知识点:创建这个函数的时候,这个函数的作用域就已经决定了,而是不是在调用的时候

这个时候,用 SusanModule.name 会返回 undefined,访问不到内部属性了,函数作用域有个独立于全局作用域的作用空间,外部访问不到,从而用闭包很好的保护了变量。

写法二:把挂载的过程放在立即执行函数的内部(推荐)

这也是早期模块实现的方法

(function(window){var name = "susan"var sex = "female"functioon tell(){console.log("我是:",this.name)}window.susanModule = {tell}
})(window)// window作为参数传进去

3. 模块化

模块化类似于一个公司有多个部门构成,在软件工程中通常将具有特定功能的代码封装成一个模块,高内聚低耦合,各司其职,完成不同的功能。

模块化的优点:

  • 作用域封装:通过接口的方式暴露方法,但保护了模块内部的安全,也避免了污染全局命名空间的问题。
  • 可重用性
  • 解除耦合,方便维护

2.2 模块化方案进化史

模块化方案演化出:AMD、COMMONJS、ES6 MODULE

1. AMD(异步模块定义)

AMD:Asynchronous Module Definition

目前很少使用

// 求和模块
define("getSum", ["math"], funtion(math){return function (a,b){log("sum:"+ math.sum(a, b))
}
})
  • 第一个参数:模块的名称
  • 第二个参数:模块的依赖
  • 第三个参数:函数或对象

好处:显式的展现出模块的依赖的其他模块,且模块的定义也不再绑定在全局对象上,增强了安全性。

2. COMMONJS

原本是为服务端的规范,后来 nodejs 采用 commonjs 模块化规范

要点:

  • commonjs 中每个文件就是一个模块,并且拥有属于他的作用域和上下文;
  • 模块的依赖通过require函数引入;
  • 如果想要把模块的接口暴露给外部,那就需要通过exports将其导出。

好处:和AMD一样强调模块的依赖必须显式的导入,方便维护复杂模块时,各个模块引入顺序的问题

// 通过require函数来引用
const math = require("./math");// 通过exports将其导出
exports.getSum = function(a,b){return a + b;
}

3. ES6 MODULE

目前使用最多,从 ES6 开始,模块化有了语法级别的原生知识。

// 通过import函数来引用
import math from "./math";// 通过export将其导出
export function sum(a, b){return a + b;
}

2.3 Webpack打包原理

重点:

  • Webpack与立即执行函数的关系
  • Webpack 打包的核心逻辑

首先来分析立即执行函数的逻辑,Webpack中也是使用立即执行函数的思想:

抽象出来的大体结构:

(function(module) {var installedModules = {}; // 放置已经被加载过的模块//webpack加载模块的核心function __webpack_require__(moduleId){// SOME CODE}//最后加载工程的入口模块return __webpack_require__(0); // entry file
})([ /* modules array */])

核心方法的实现:

function __webpack_require__(moduleId){// check if module is in cache 检查需要加载的这个模块是否已经加载过if(installedModules[moduleId]){return installedModules[moduleId].exports;}// create a new module (and put into cache)var module = installedModules[moduleId] = {i: moduleId,l: false,exports: {}};// exe the module func 模块逻辑执行modules[moduleId].call{module.exports,module,module.exports,__webpack_require__};// flag the module as loadedmodule.l = true;// return the exxports of the modulereturn module.exports;
}

1. webpack打包过程/核心思路

  1. 从入口文件开始,分析整个应用的依赖树,即依赖了哪些模块;
  2. 将每个依赖模块包装起来,放到一个数组中等待调用;
  3. 实现模块加载的方法,并把它放到模块执行的环境中,确保模块间可以互相调用;
  4. 把执行入口文件的逻辑放在一个函数表达式中,并立即执行这个函数。

三、npm的相关知识

重点:

  • 理解包管理器
  • 熟悉npm核心特性
  • 理解npm仓库和依赖的概念
  • 理解npm语义化版本
  • 掌握使用npm自定义工程脚本的方法

3.1 配置开发环境——npm与包管理器

包管理器:让开发者便捷的获取代码和分发代码的工具

package.json 中重要字段解释:

{"name": "demo", //包名称"version": "1.0.0",  //版本号"description": "","main": "index.js", //包执行的入口"scripts": {  //自定义脚本命令"test": "echo \"Error: no test specified\" && exit 1"},"author": "","license": "ISC"
}

3.2 理解npm仓库和依赖的概念

仓库:遵循npm特定包的站点,提供 API 让用户进行上传或下载等

依赖放在dependencies 或 devDenpendencies中,他们之间的区别:

  • dependencies:是生产环境依赖,常放置于功能相关的依赖,安装包时的命令(npm install XXX -s
  • devDenpendencies:是开发环境的依赖,常放置只有开发时才用到的包,如:ESLint,安装包时的命令(npm install XXX -d

3.3 npm语义化版本

  • ^version:中版本和小版本

    ^1.0.1 ->1.x.x

  • ~version:小版本

    ~1.0.1 -> 1.0.x

  • version:特定版本

好处:使npm的发布者方便的把最新的小版本推送到使用者

3.4 npm install过程

  • 寻找包版本信息文件(package.json),依照它来进行安装
  • 查找package.json中的依赖,并检查项目中其他的版本信息文件
  • 如果发现了新包,就更新版本信息文件

当我们发现项目中的某个包与预想中不一致的时候,首先应该去看版本信息文件中包的来源和版本。

四、Webpack.config.js

有 webpack.config.js 配置文件的话,webpack就会按照webpack.config.js里面的配置进行打包

假设现在项目结构如下:

webpack.config.js 设置如下:

// webpack.config.js
const path = require('path')module.exports = {entry: './app.js', //入口文件路径output: {//__dirname 表示当前目录下的名字path: path.join(__dirname, 'dist'),//必须是绝对路径filename: 'bundle.js' //打包输出的文件名},devServer: {port: 3000,  //服务端口,默认是8080publicPath: '/dist' //打包后文件所在文件夹}
}

在工程中可以建立 webpack-dev-server,其作用:可以监听工程文件目录的改变,如果项目文件有更新,会自动打包并自动刷新浏览器

其他配置项:

  • 是否缓存,可以提升webpack打包执行的速度,配置如下:
cacheDictionary: true/false;
  • .js .jsx .json文件引用时候,不需要加入后缀,只需要文件名即可,但是重名的还是需要全名,配置如下:
resolve: extensions:['.js','.jsx','.json']

更多配置项的设置可以参考这里

五、Webpack 核心特性

重点:

  • 掌握”一切皆模块和loader“的思想
  • 理解 Webpack 中的”关键人物“

5.1 loader

除了 JavaScript,如果需要用 webpack 打包其他类型的文件,都需要配置响应的 loader,所以 loader 是增强扩宽了webpack 的功能。如要打包 css 文件,就需要安装 css-loader。

需要注意的是

  • css-loader 只是解决了css语法解析的问题,只用css-loader是不能将样式加载到页面上的,还需要 style-loader

  • loader的配置顺序和他的加载顺序是相反的,所以 style-loader 必须放在 css-loader 之前!!!

const path = require('path')module.exports = {...module: {rules: [{test: /\.css$/, //是需要匹配文件的正则表达式use: ['style-loader','css-loader']}]}
}

5.2 plugins

与 loader 相比,plugins 机制更强调事件监听的能力,plugin 可以在 webpack 的内部监听一些事件,并且改变一些文件打包后的输出结果。它打包后的文件更小

const path = require('path')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')module.exports = {...plugins:[new UglifyJSPlugin()]
}

六、Webpack 构建工程

重点:

  • 掌握 babel 的用法及原理
  • 掌握高频 loader 和 plugins 的用法
  • 掌握生产级别的 webpack 配置方法

6.1 babel

作用: 将高版本语法 ES6 转换为低版本语法

安装 babel 命令:npm install @babel/core @babel/cli

babel 的配置方法一:在命令行中直接配置转换规则

安装转换规则的命令:npm install @babel/present-env

使用方法(直接编译):babel index.js --presets=@babel/preset-env

babel 的配置方法二:在 package.json中,加入babel配置参数

{"name": "demo", "babel": {"presets": ["@babel/presets-env"]}
}

babel 的配置方法三:在 package.json 文件同目录下,创建.babelrc文件并在里面配置babel参数(同二)

6.2 html-webpack-plugin

上面的用 babel-loader 处理 js 是文件级别的。而 index.html 是作为一个入口被处理,所以 index.html 的处理是一个节点维度的,而这种节点维度的处理往往使用 plugin,这里处理 html 的 plugin 是 html-webpack-plugin。

html-webpack-plugin作用:

  1. 把指定的页面复制一份放到根目录里面去,复制的页面放到内存里,源文件是在磁盘中;

  2. 为 html 文件中引入的外部资源如 script、link 动态添加每次 compile 后的 hash,防止引用缓存的外部文件问题;

  3. 可以生成创建 html 入口文件,比如单页面可以生成一个html 文件入口,配置N个html-webpack-plugin可以生成N个页面入口;

  4. 在复制出来的内存中的页面中自动注入内存里打包了的脚本

plugin 往往以构造函数的形式存在,要使用首先就得把他引进来。

// 1.导入 HTML 插件,获得一个构造函数
const HtmlPlugin = require('html-webpack-plugin');// 2.创建HTML插件实例对象
const htmlPlugin = new HtmlPlugin({template: './src/index.html', // 指定原文件的存放路径,想复制的文件filename: './index.html'  // 指定生成的文件的存放路径和文件名
});module.exports = {plugins: [htmlPlugin], //3.通过 plugins 节点,是htmlPlugin插件生效
}

6.3 简化命令行中命令的运行

在配置完该有的 loader、plugin 还有参数之后,我们开始打包运行,在命令行输入 webpack-dev-server --open --config --open 和 --config 都是给 webpack-dev-server 配置的参数,这里只有两个配置,如果有更多的配置,直接在命令行中书写又复杂又容易写错,为了简化命令行的书写,这是我们会在 package.json 中的 scripts 通过自定义命令行去自定义 build 命令和 start / dev 命令,如下:

{"scripts": {  "test": "echo \"Error: no test specified\" && exit 1","build": "webpack --mode production","start": "webpack-dev-server --mode development -open"},
}

6.4 HMR(热更新/热替换)

如今单页面盛行,其有个旺盛的需求就是不刷新页面也能把变化更新出来,webpack 也集成了这个能力,运用HMR来实现。

1. HMR 的实现

在 plugin 中加入:webpack.HotModuleReplacementPlugin()

//webpack.config.js
cosnt webpack = require('webpack')
module.exports = {...plugins:[new UglifyJSPlugin(),new webpack.HotModuleReplacementPlugin()],derServer: {hot: true}
}

还需在入口文件进行配置:

//入口文件
if(module.hot) {module.hot.accept(error => {if(error) {console.log('热替换出现bug了')}})
}

七、Webpack与前端性能

重点:

  • 打包结果优化:空间维度,希望打包出来的文件体积尽可能小,在传输过程中就快,用户体验度就好;
  • 构建过程优化:时间维度的优化,关心的是开发过程中构建文件的事件尽可能短;
  • Tree-Shaking

7.1 打包结果优化

可以通过 TerserPlugin 来进行插件的定制,webpack 已经预置,所以不需要安装。在 optimization 中进行具体的配置

const webpack = require('webpack')module.exports = {optimization: {minimizer: [new TerserPlugin({// 加快构建速度cache:true,parrlel: true,// 多线程处理,因为压缩比较耗时间terserOptions : {compress: {//删除掉一些没有用的代码unused: true, drop_debugger: true,drop_console: true,dead_code:true}}})]}
}

打包结果可视化

如何评价打包结果的好坏:可以使用 webpack 分析器webpack-bundle-analyzer 可视化打包结果的成分,他是个plugin。

安装:npm install webpack-bundle-analyzer

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPluginmodule.exports = {plugins: [new BundleAnalyzerPlugin()]
}

7.2 构建过程优化

  • 减少查找:exclude、include
  • 减少解析:noParse
  • 增加干活的“员工”:多线程 parrlel,happyPackthread-loader
  • 预编译
  • 缓存:cache
  • 优化 Source Map

thread-loader:针对 loader 进行优化,把 loader 放在线程池 worker 里,达到多线程构建的目的,使用时必须放在所有的 loader 配置项最前面。loader形式存在。

happyPack多进程模型,加速代码的构建。根据CPU的数量创建线程池。plugin形式存在。

const HappyPack = require('happypack')
const happyThreadPool = HappyPack.ThreadPool({size:OscillatorNode.cpus().length})module.exports = {plugins: [new HappyPack({id:'jsx',threads: happyThreadPool,loaders:['babel-loader'] //根据需要写})]
}

7.3 Tree-Shaking

Tree-Shaking 是 webpack 自身的优化特性,本质就是消除无用的代码(DCE),Tree-Shaking 就是 DCE 的一种实现。这个过程就像这个名字一样,去摇晃一棵树,树上不好的叶子和果实都会掉下来。

Tree-Shaking 究竟做了什么??

它让 webpack 自己会分析 ES6 Modules 引入的情况,去除没有使用的 import 的引入,但是是在 mode production 环境上才会消除

希望你能有所收获!好了我去睡觉了,晚安!!

Webpack 究竟是什么?如何理解Webpack相关推荐

  1. 【Webpack】1362- 通过插图来理解webpack

    image.png 相信每个前端开发者都听说过webpack.作为前端开发最重要的构建工具,它极大地提高了我们的开发效率. 虽然网上有很多关于Webpack的教程,但由于Webpack本身的复杂性,很 ...

  2. 深入理解Webpack核心模块Tapable钩子[异步版]

    接上一篇文章 深入理解Webpack核心模块WTApable钩子(同步版) tapable中三个注册方法 1 tap(同步) 2 tapAsync(cb) 3 tapPromise(注册的是Promi ...

  3. 深入理解Webpack核心模块Tapable钩子[同步版]

    记录下自己在前端路上爬坑的经历 加深印象,正文开始- tapable是webpack的核心依赖库 想要读懂webpack源码 就必须首先熟悉tapable ok.下面是webapck中引入的tapab ...

  4. webpack 究竟是什么,初学者晕头转向

    第 9 阶段:搞懂.搞透前端构建第 1 天 对于前端新手来说,不能理解为啥前端要使用 webpack 进行打包.曾经,刚入门前端的时候我也有这个疑问.今天我来帮大家重新认识一下 webpack. 如果 ...

  5. 【Webpack】1047- 轻松理解webpack热更新原理

    一.前言 - webpack热更新 Hot Module Replacement,简称HMR,无需完全刷新整个页面的同时,更新模块.HMR的好处,在日常开发工作中体会颇深:节省宝贵的开发时间.提升开发 ...

  6. 认识webpack、理解webpack与grunt、glup的核心区别01

    认识webpack 什么是webpack 官方解释 At its core,webpack is a static module bundler for modern JavaScript appli ...

  7. 玩转webpack(一)下篇:webpack的基本架构和构建流程

    欢迎大家前往腾讯云社区,获取更多腾讯海量技术实践干货哦~ 作者:QQ会员技术团队 接玩转webpack(一)上篇:webpack的基本架构和构建流程 文件生成阶段 这个阶段的主要内容,是根据 chun ...

  8. (23/24) webpack实战技巧:如何在webpack环境中使用Json

    (23/24) webpack实战技巧:如何在webpack环境中使用Json 在webpack1或者webpack2版本中,若想在webpack环境中加载Json文件,则需要加载一个json-loa ...

  9. vue webpack 自动打开页面_vue中webpack技术详解

    1.为什么要使用webpack: 因为我们想把资源整合.如在项目index.html文件中为了请求变得更少我们可以新建一个叫main.js的项目入口文件(里面有引用其它的各种资源,而index.htm ...

最新文章

  1. Kotlin的解析(下)
  2. EChart 标题 title 样式,x轴、y轴坐标显示,调整图表位置等
  3. linux重启后root密码错误,Linux技巧| 解决Debian Root密码忘记的问题
  4. react native 0.56.0
  5. 软件设计是怎样炼成的(6)——打造系统的底蕴(数据库设计)(下篇)
  6. 22.c语言各种输入输出与错误处理
  7. Atitit.实现反向代理(1)----url rewrite 配置and内容改写 and -绝对路径链接改写 java php
  8. nginx源码分析之线程池
  9. 应急响应病毒分析查杀集合
  10. iOS开发通过微信学习hijack(一)函数劫持
  11. 全球十大管理咨询公司
  12. Burp Spider 使用指南
  13. Solidity入门-开发众筹智能合约
  14. win10 labelme 使用记录
  15. 2021/8/12 网络机顶盒
  16. 针对谷氨酰胺运输体的小分子抑制剂
  17. python读excel并写入_Python读取Excel文件并写入数据库
  18. Python小游戏贪吃蛇
  19. steemIT深度研究总结
  20. Mars3d的html 模板中使用element-ui 组件参考

热门文章

  1. 【php + MySQL + Android】本地实验环境搭建
  2. UML设计java程序_利用UML序列图设计Java应用程序详解
  3. Linux Rootkit躲避内核检测
  4. java实现简单扫码登录功能(模仿微信网页版扫码)
  5. Tobii pro lab学习笔记1
  6. 安全配置管理 (SCM):建立安全的基础
  7. BDB (Berkeley DB)数据库简介(转载)
  8. css实现动态渐变闪烁功能
  9. 【Python】批量移动同类型文件到其他文件夹的办公技巧
  10. 机器视觉中的像素、分辨率、灰度值等概念