本篇文章主要是针对 B站Webpack从原理到实战 的知识梳理,之前写过一些 Webpack 更细节的一些知识,详情见 前端工程化(webpack),里面更详细的介绍了前端工程化、loader的使用,webpack的常用插件,Source Map等知识。
本文重点:
- 理解前端的模块化;
- 理解 Webpack 打包的核心思想;
- 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。针对以下的问题,我们就提出了打包的方案解决:
- 一个项目依赖多个文件,而各个依赖文件之间的关系难以梳理,耦合程度较高,代码难以维护;
- 把所有依赖包都打包成为一个 js 文件(bundle.js)文件,会有效降低HTTP请求次数,进而提升性能;(一种前端性能优化策略)
- 逻辑多、文件多,项目复杂度提高
一种技术的出现,一定是因为过往的技术不能满足现在生产开发的需要或者说不够便捷才出现的。
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 loaded
module.l = true;
// return the exxports of the module
return module.exports;
}
1. webpack打包过程/核心思路
- 从入口文件开始,分析整个应用的依赖树,即依赖了哪些模块;
- 将每个依赖模块包装起来,放到一个数组中等待调用;
- 实现模块加载的方法,并把它放到模块执行的环境中,确保模块间可以互相调用;
- 把执行入口文件的逻辑放在一个函数表达式中,并立即执行这个函数。
三、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, //服务端口,默认是8080
publicPath: '/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作用:
-
把指定的页面复制一份放到根目录里面去,复制的页面放到内存里,源文件是在磁盘中;
-
为 html 文件中引入的外部资源如 script、link 动态添加每次 compile 后的 hash,防止引用缓存的外部文件问题;
-
可以生成创建 html 入口文件,比如单页面可以生成一个html 文件入口,配置N个html-webpack-plugin可以生成N个页面入口;
-
在复制出来的内存中的页面中自动注入内存里打包了的脚本。
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').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
7.2 构建过程优化
- 减少查找:exclude、include
- 减少解析:noParse
- 增加干活的“员工”:多线程 parrlel,happyPack,thread-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 环境上才会消除。
希望你能有所收获!好了我去睡觉了,晚安!!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/150412.html