本篇文章主要是针对 B站Webpack从原理到实战 的知识梳理,之前写过一些 Webpack 更细节的一些知识,详情见 前端工程化(webpack),里面更详细的介绍了前端工程化、loader的使用,webpack的常用插件,Source Map等知识。
本文重点:
官方定义:Webpack 是一个现代 JavaScript 应用程序的静态模块打包器
自己的解释:
我们觉得原生的CSS不够好用,就提出了 Sass、Less,我们想要前端代码也拥有类型校验的能力,就有了 typescript。针对以下的问题,我们就提出了打包的方案解决:
一种技术的出现,一定是因为过往的技术不能满足现在生产开发的需要或者说不够便捷才出现的。
针对一个项目依赖多个模块的场景,行业里提出了模块化的方案,而 Webpack 能推而广之,本质上是因为他就是一个典型的优秀的模块化方案;
拥有强大的打包技能;
充当了“翻译官”的角色,例如:在浏览器控制台写一段 typescript 代码,浏览器直接会报错,因为浏览器看不懂 typescript 呗,而Webpack 在打包过程中,loader 就会将浏览器看不懂的代码翻译为浏览器看的懂的代码;
更高级的功能有 plugin 来辅助;
plugin 和 loader 都是可插拔的,意思是需要他的时候就把它插进来,不需要的时候就把他删掉。Webpack强大而灵活
从作用域入手开始理解。
作用域描述的运行时代码中变量、函数、对象的可访问性,简单来说,作用域决定了代码中变量和其他资源的可见性。
(1)全局作用域
var a = 1;
window.a; // 1
(2)局部作用域
function a(){
var v = 1;
}
window.v; // undefined
先来举个命名冲突的例子:
在以前我们想引入多个 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作为参数传进去
模块化类似于一个公司有多个部门构成,在软件工程中通常将具有特定功能的代码封装成一个模块,高内聚低耦合,各司其职,完成不同的功能。
模块化的优点:
模块化方案演化出:AMD、COMMONJS、ES6 MODULE
AMD:Asynchronous Module Definition
目前很少使用
// 求和模块
define("getSum", ["math"], funtion(math){
return function (a,b){
log("sum:"+ math.sum(a, b))
}
})
好处:显式的展现出模块的依赖的其他模块,且模块的定义也不再绑定在全局对象上,增强了安全性。
原本是为服务端的规范,后来 nodejs 采用 commonjs 模块化规范
要点:
好处:和AMD一样强调模块的依赖必须显式的导入,方便维护复杂模块时,各个模块引入顺序的问题
// 通过require函数来引用
const math = require("./math");
// 通过exports将其导出
exports.getSum = function(a,b){
return a + b;
}
目前使用最多,从 ES6 开始,模块化有了语法级别的原生知识。
// 通过import函数来引用
import math from "./math";
// 通过export将其导出
export function sum(a, b){
return a + b;
}
重点:
首先来分析立即执行函数的逻辑,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;
}
重点:
包管理器:让开发者便捷的获取代码和分发代码的工具
package.json 中重要字段解释:
{
"name": "demo", //包名称
"version": "1.0.0", //版本号
"description": "",
"main": "index.js", //包执行的入口
"scripts": { //自定义脚本命令
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
仓库:遵循npm特定包的站点,提供 API 让用户进行上传或下载等
依赖放在dependencies 或 devDenpendencies中,他们之间的区别:
npm install XXX -s
)npm install XXX -d
)
^version:中版本和小版本
^1.0.1 ->1.x.x
~version:小版本
~1.0.1 -> 1.0.x
version:特定版本
好处:使npm的发布者方便的把最新的小版本推送到使用者
当我们发现项目中的某个包与预想中不一致的时候,首先应该去看版本信息文件中包的来源和版本。
有 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,其作用:可以监听工程文件目录的改变,如果项目文件有更新,会自动打包并自动刷新浏览器。
其他配置项:
cacheDictionary: true/false;
resolve: extensions:['.js','.jsx','.json']
更多配置项的设置可以参考这里
重点:
除了 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'
]
}
]
}
}
与 loader 相比,plugins 机制更强调事件监听的能力,plugin 可以在 webpack 的内部监听一些事件,并且改变一些文件打包后的输出结果。它打包后的文件更小。
const path = require('path')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
...
plugins:[
new UglifyJSPlugin()
]
}
重点:
作用: 将高版本语法 ES6 转换为低版本语法
安装 babel 命令:npm install @babel/core @babel/cli
安装转换规则的命令:npm install @babel/present-env
使用方法(直接编译):babel index.js --presets=@babel/preset-env
{
"name": "demo",
"babel": {
"presets": ["@babel/presets-env"]
}
}
.babelrc
文件并在里面配置babel参数(同二)
上面的用 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插件生效
}
在配置完该有的 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"
},
}
如今单页面盛行,其有个旺盛的需求就是不刷新页面也能把变化更新出来,webpack 也集成了这个能力,运用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了')
}
})
}
重点:
可以通过 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()
]
}
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'] //根据需要写
})
]
}
Tree-Shaking 是 webpack 自身的优化特性,本质就是消除无用的代码(DCE),Tree-Shaking 就是 DCE 的一种实现。这个过程就像这个名字一样,去摇晃一棵树,树上不好的叶子和果实都会掉下来。
Tree-Shaking 究竟做了什么??
它让 webpack 自己会分析 ES6 Modules 引入的情况,去除没有使用的 import 的引入,但是是在 mode production 环境上才会消除。
希望你能有所收获!好了我去睡觉了,晚安!!