目录
- 1、模块化
- 1. CommonJS/AMD/CMD
- 1.1 背景
- 1.2 CommonJS规范的核心变量
- 1.3 exports(module.exports)和require本质
- 1.4 exports和module.exports的关系/区别
- 1.5 实际开发用:module.exports = {}
- 1.6 require(X)的查找规则
- (1)X是一个Node核心模块,例如path等
- (2)X是以 ./ 或 ../ 或 /(根目录)开头的
- (3)直接是一个X(没有路径),并且X不是一个核心模块
- 1.7 CommonJS的缺点
- 2. ESModule
- 2.1 背景
- 2.2 ESModule的核心变量
- 2.3 ESModule的导入/导出方式
- 2种导出方式
- 3种导入方式
- 2.4 export和import结合使用
- 2.4 default默认导出
- 默认导出方式
- 如何导入
- 2.5 ES Module的解析流程
- 构建阶段
- 实例化阶段 – 求值阶段
- 2、包管理
- 1. NPM
- 1.1 常见的配置文件
- 1.2 常见的属性
- (1)基本属性:
- (2)private属性
- (3)main属性
- (4)scripts属性
- (5)dependencies属性
- (6)devDependencies属性
- (7)peerDependencies属性
- 1.3 依赖的版本管理
- (1)npm的包通常需要遵从semver版本规范,
- (2)我们这里解释一下 `^` 和 `~` 的区别**:
- 1.4 npm install 原理
- 1.5 npm发布自己的包
- (1)注册npm账号
- (2)创建一个待发布的包(项目)
- (3)在命令行登录:npm login
- (4)发布到npm registry上:npm publish
- (5)更新仓库
- (6)删除发布的包:npm unpublish
- (7)删除发布的包:npm deprecate
- 3、打包工具
- 3. Webpack基本使用
- 3.1 核心概念
- 3.2 quick start
- (1)关于package.json文件中`"build": "webpack"`执行原理
- (2)关于vue中`"serve": "vue-cli-service serve",`执行的理解
- 3.3 基本版`bundle.js`源码结构解读
- 3.4 添加映射后的源码结构解读
- (1)添加`devtool:'source-map',`
- (2)生成的`bundle.js`文件
- (3)生成的`bundle.js.map`文件
- 3.5 添加css后的源码结构解读
- (1)添加`module:{}',`
- (2)生成index.css,并在index.js中引入
- (3)生成的`bundle.js`文件
- (4)问题:当前样式是没有插入dom中的
- 3.6 创建自定义文件类型:自定义loader开发
- (1)创建自定义文件`test.imooc`,并在index.js中引入
- (2)创建自定义loader
- (3)`webpack.config.js`添加自定义loader
- 3.7 创建自定义plugin
- (1)创建自定义插件`FooterPlugin.js`
- (2)`webpack.config.js`添加自定义Plugin
- (3)打包结果
- 3.8 webpack自带插件/特性
- (1)ProvidePlugin
- (2)Tree Shaking
- 3.9 webpack社区插件
- (1)HtmlWebpackPlugin
- (2)CopyWebpackPlugin
- (3)mini-css-extract-plugin
- (4)uglifyjs-webpack-plugin
- (5)CssMinimizerWebpackPlugin
- 4. webpack优化
- 4.1 加快打包速度
- (1)output属性的hash值用法
- 4.2 图片优化(资源模块webpack5才有的)
- (1)module/rules下关于图片的优化
- 4.3 代码压缩和分割
- (1)js代码压缩
- (2)css代码压缩
- (3)代码分割
- 4.4 tree shaking
- (1)对单独文件进行tree shaking
- (2)对某个文件里的代码进行tree shaking
- 4.5 构建性能优化
- (1)构建速度分析speed-measure-webpack-plugin
- (2)构建体积分析webpack-bundle-analyzer
- (3)辅助工具:cross-env动态修改环境变量
- (4)thread-laoder多进程
- (5)分包:DllPlugin & DllReferencePlugin
- (6)利用缓存提升二次构建速度
- (7)图片压缩 image-webpack-loader
- (8)删除无用的css样式purgecss-webpack-plugin
- 5、前端性能监控
- 5.1 window.Performance
- 5.2 performanceObserve
- 5.2.1 前端DOM加载性能:
- 5.2.2 前端HTML模板和资源的加载性能:
- 5.2.3 前端性能度量mark和measure:
- 5.2.4 前端渲染性能:
1、模块化
1. CommonJS/AMD/CMD
1.1 背景
在ES6(2015)之前
,为了让JavaScript支持模块化,涌现出了很多不同的模块化规范:AMD
、CMD
、CommonJS
等
注意:
AMD
、CMD
已经不用了CommonJS
在nodejs/webpack
中依然流行。
1.2 CommonJS规范的核心变量
核心变量包括:exports
、module.exports
、require
;
exports
和module.exports
可以负责对模块中的内容进行导出;require
函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
demo案例
// 导出模块
// test.js
const bar = 'zs'
function foo(){ return 'test' }
exports.bar = bar
exports.foo = foo
// 导入模块
const {bar, foo} = require('./test.js')
console.log(bar) // 'zs'
console.log(foo()) // 'test'
1.3 exports(module.exports)和require本质
导出
exports.bar = bar
exports
其实就是一个空对象,所谓导出,其实就是往空对象
里塞属性和方法。
导入
// 等同于 const test = require('./test.js')
// test 就是一个对象
const {bar, foo} = require('./test.js')
require
的本质,其实是将路径文件中导出的对象,进行引用赋值给变量
。
因此,test变量就是exports导出的对象
1.4 exports和module.exports的关系/区别
小结:
exports
和module.exports
其实是一个东西,都指向同一内存地址exports(0x001)
,因此对于require
没啥区别- 但是,如果写成
module.exports = {}
,require
导入对象就跟exports(0x001)
没有关系了,只有新对象(0x002)
有关系。- 所以,实际开发中,还是会写成
module.exports = {}
格式
1.5 实际开发用:module.exports = {}
1.6 require(X)的查找规则
(1)X是一个Node核心模块,例如path等
直接返回核心模块,并且停止查找
(2)X是以 ./ 或 …/ 或 /(根目录)开头的
-
第一步:将X当做一个文件在对应的目录下查找
- 如果有后缀名,按照后缀名的格式查找对应的文件
- 如果没有后缀名,会按照如下顺序:
- 直接查找文件X
- 查找X.js文件
- 查找X.json文件
- 查找X.node文件
-
第二步:没有找到对应的文件,将X作为一个目录
- 查找目录下面的index文件
- 查找X/index.js文件
- 查找X/index.json文件
- 查找X/index.node文件
- 查找目录下面的index文件
-
如果没有找到,那么报错:not found
(3)直接是一个X(没有路径),并且X不是一个核心模块
- 先从当前路径下的node_modules文件夹中查找,
- 如果有,参考上面第(2)个情况处理
- 如果没有,往上一级目录下的node_modules文件夹中查找,
- 一直找到根目录,如果都没有就会报错。
小结:
这个其实就是真实项目中,通过require引用npm下载包的原因。
1.7 CommonJS的缺点
- CommonJS加载模块是同步的:
- 同步的意味着
只有等到对应的模块加载完毕,当前模块中的内容才能被运行
; - 这个
在服务器不会有什么问题
,因为服务器加载的js文件都是本地文件
,加载速度非常快;
- 同步的意味着
- 如果将它应用于浏览器呢?
- 浏览器加载js文件需要
先从服务器将文件下载下来
,之后再加载运行
; - 那么采用
同步
的就意味着后续的js代码都无法正常运行
,即使是一些简单的DOM操作
;
- 浏览器加载js文件需要
- 所以在浏览器中,我们通常不使用CommonJS规范:
- 当然在webpack中使用CommonJS是另外一回事;
- 因为它会将我们的代码转成浏览器可以直接执行的代码;
- 在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD
- 但是目前一方面现代的
浏览器已经支持ES Modules
,另一方面借助于webpack等工具可以实现对CommonJS或者ES
Module代码的转换; AMD和CMD已经使用非常少了
;
- 但是目前一方面现代的
2. ESModule
2.1 背景
从ES6(2015)开始
,JavaScript推出了自己的模块化方案,即ESModule
ES Module
和CommonJS
的模块化有一些不同之处:
一方面它使用了import
和export
关键字;
另一方面它采用编译期的静态分析
,并且也加入了动态引用
的方式;
2.2 ESModule的核心变量
核心变量包括:export
、import
;
export
负责将模块内的内容导出;import
负责从其他模块导入内容;
注意:
export{}
中的{}
并不是一个对象,而是属于ES Module的语法结构一部分。
所以跟CommonJS
的module.exports
的对象不是一个东西。
2.3 ESModule的导入/导出方式
2种导出方式
// 1.导出方式一:
export const bar = "zs"
export function foo() {return 'zs'}
// 2.导出方式二:
// export {
// bar,
// foo
// }
3种导入方式
// 1.导入方式一:
// import { name, age, sayHello } from "./foo.js"
// 2.导入方式二: 导入时给标识符起别名
// import { name as fname, age, sayHello } from "./foo.js"
// 3.导入时可以给整个模块起别名
import * as foo from "./foo.js"
2.4 export和import结合使用
// index.js
// 原来的方式
import { formatCount, formatDate } from './format.js'
import { parseLyric } from './parse.js'
export {
formatCount,
formatDate,
parseLyric
}
// 优化一:
// export { formatCount, formatDate } from './format.js'
// export { parseLyric } from './parse.js'
// 优化二:
// export * from './format.js'
// export * from './parse.js'
小结:(多了几个语法糖而已)
export { bar } from './format.js'
export * from './format.js'
2.4 default默认导出
默认导出方式
// 第一种
// 定义函数
function parseLyric() {
return ["歌词"]
}
// 默认导出
export default parseLyric
// 第二种
// 定义标识符直接作为默认导出
export default function() {
return ["新歌词"]
}
// 注意事项: 一个模块只能有一个默认导出
如何导入
// 重新命名即可,aaa 是重新命名的。
import aaa from "./parse_lyric.js"
2.5 ES Module的解析流程
ES Module的解析过程可以划分为三个阶段:
- 阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record);
- 阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向
对应的内存地址。 - 阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;
构建阶段
实例化阶段 – 求值阶段
2、包管理
1. NPM
1.1 常见的配置文件
1.2 常见的属性
(1)基本属性:
name
是项目的名称;(必填)
version
是当前项目的版本号;(必填)
description
是描述信息,很多时候是作为项目的基本描述;
author
是作者相关信息(发布时用到);
license
是开源协议(发布时用到);
(2)private属性
private
属性记录当前的项目是否是私有的;
当值为true时,npm是不能发布它的
,这是防止私有项目或模块发布出去的方式;
(3)main属性
设置程序的入口
- 比如我们使用axios模块 const axios = require(‘axios’);
- 如果有main属性,实际上是找到对应的main属性查找文件的;
(4)scripts属性
scripts属性用于配置一些脚本命令
,以键值对
的形式存在;
配置后我们可以通过npm run
命令的key来执行这个命令;
npm start和npm run start的区别是什么?
- 它们是等价的;
- 对于常用的 start、 test、stop、restart可以省略掉run直接通过 npm start等方式运行;
(5)dependencies属性
dependencies
属性是指定无论开发环境还是生成环境都需要依赖的包
;
通常是我们项目实际开发用到的一些库模块vue、vuex、vue-router、react、react-dom、axios等等;
与之对应的是devDependencies;
(6)devDependencies属性
一些包只在开发环境需要,而生成环境是不需要的
,比如webpack、babel等;
这个时候我们会通过 npm install webpack --save-dev
,将它安装到devDependencies属性中;
(7)peerDependencies属性
还有一种项目依赖关系是对等依赖
,也就是你依赖的一个包,它必须是以另外一个宿主包为前提的
;
比如element-plus是依赖于vue3的,ant design是依赖于react、react-dom;
1.3 依赖的版本管理
我们会发现安装的依赖版本出现:^2.0.3或~2.0.3,这是什么意思呢?
(1)npm的包通常需要遵从semver版本规范,
- semver版本规范是X.Y.Z:
X主版本号(major)
:当你做了不兼容的 API 修改(可能不兼容之前的版本);Y次版本号(minor)
:当你做了向下兼容的功能性新增(新功能增加,但是兼容之前的版本);Z修订号(patch)
:当你做了向下兼容的问题修正(没有新功能,修复了之前版本的bug);
(2)我们这里解释一下 ^
和 ~
的区别**:
x.y.z
:表示一个明确的版本号
;^x.y.z
:表示x是保持不变的,y和z永远安装最新的版本
;~x.y.z
:表示x和y保持不变的,z永远安装最新的版本
;
1.4 npm install 原理
1.5 npm发布自己的包
(1)注册npm账号
通过官网注册即可
(2)创建一个待发布的包(项目)
(3)在命令行登录:npm login
(4)发布到npm registry上:npm publish
(5)更新仓库
- 修改完只有,重新npm publish即可
- 修改版本号(最好符合semver规范)
- 重新发布
(6)删除发布的包:npm unpublish
(7)删除发布的包:npm deprecate
3、打包工具
3. Webpack基本使用
3.1 核心概念
entry
:入口模块文件路径,根据入口文件找引用。(比如vue的:main.js
)output
:输出bundle.js文件路径**(最终打包的输出物:bundle.js
)**module
:模块,webpack构建对象,包含rules
,rules里有loader
bundle
:输出文件,webpack构建产物chunk
:中间文件,webpack构建的中间产物loader
:文件转换器plugin
:插件,执行特定任务
注意:
css-loader中有个postcss-loader
工具,很多与css相关的处理,可以通过这个工具里的插件进行处理。
postcss-preset-env
插件,将一些现代的CSS特性,转成大多数浏览器认识的CSS。包括自动补齐CSS3前缀。所以实际开发不用autoprefixer。postcss-plugins-px2rem
插件,移动端可用 px => rem转换(flexible.js中75px:1rem)
3.2 quick start
- 创建一个
test-webpack
文件夹 npm init -y
初始化项目- 创建
src/index.js
,写入测试代码
console.log('hello webpack!');
- 创建
public/index.html
,引入打包后的脚本
<script src="../dist/bundle.js"></script>
- 创建
webpack.config.js
,并填写配置
const path = require('path')
module.exports = {
mode:'development',
entry:{
bundle:'./src/index.js', // 可以自定义入口文件的key值,比如自定义bundle,默认是main
},
output: {
// filename:'bundle.js', // 固定写死的名称
filename:'[name].js', // 这里的name是获取entry的key值
filename:'[hash].js', // 也可以改成hash值,只有文件变化时,hash打包的hash值才会变化
path:path.resolve(__dirname, './dist'),
}
}
- 执行
npm i -D webpack webpack-cli
安装包 - 配置
build
命令为webpack
- 执行
npm run build
完成打包构建
(1)关于package.json文件中"build": "webpack"
执行原理
① package.json下有脚本script的执行命令。当执行npm run build时
。
"scripts": {
"build": "webpack"
},
② 执行的是node_module/webpack/package.json的bin命令
。(跟require查找规则有关系)(相当于执行的是bin/webpack.js
)
"bin": {
"webpack": "bin/webpack.js"
},
注
bin项用来指定各个内部命令对应的可执行文件的位置。
③ bin/webpack.js 文件中,最终执行的是runCli(cli)
,它引用了webpack-cli/package.json的bin命令
。
const runCli = cli => {
const path = require("path");
const pkgPath = require.resolve(`${cli.package}/package.json`);
// eslint-disable-next-line node/no-missing-require
const pkg = require(pkgPath); // 这里是引用webpack-cli的package.json文件
// eslint-disable-next-line node/no-missing-require
require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
// path.dirname()方法用于获取给定路径的目录名称
};
const cli = {
name: "webpack-cli",
package: "webpack-cli",
binName: "webpack-cli",
installed: isInstalled("webpack-cli"),
url: "https://github.com/webpack/webpack-cli"
};
if (!cli.installed) {
// ...省略代码
} else {
runCli(cli);
}
④ webpack-cli/package.json的bin命令
"bin": {
"webpack-cli": "./bin/cli.js"
},
⑤ 执行./bin/cli.js
const runCLI = require("../lib/bootstrap");
// ...省略代码
runCLI(process.argv);
⑥ 执行../lib/bootstrap
const WebpackCLI = require("./webpack-cli");
const runCLI = async (args) => {
// Create a new instance of the CLI object
const cli = new WebpackCLI();
try {
await cli.run(args);
}
catch (error) {
cli.logger.error(error);
process.exit(2);
}
};
module.exports = runCLI;
⑦ 最后:生成WebpackCLI,并运行命令。
(2)关于vue中"serve": "vue-cli-service serve",
执行的理解
vue-cli-service
相当于一个webpack-cli
,或者是node ./src/index.js
中的node
,反正是可以看做为一个执行某个文件的工具。- 而
后面的serve
会当做参数执行进去。 - 可以参考的解释:https://www.cnblogs.com/goloving/p/16306638.html
所以当我们运行
npm run serve
时,
其实是运行的vue-cli-service serve
这条命令,
就相当于运行node_modules/.bin/vue-cli-service.cmd serve
。
cmd这个脚本
会使用node
去运行vue-cli-service.js
这个 js 文件
vue-cli-service.cmd的源代码
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@vue\cli-service\bin\vue-cli-service.js" %*
3.3 基本版bundle.js
源码结构解读
(() => {
var __webpack_modules__ = ({
"./src/index.js": //module key (key值)
(() => {
eval("console.log('hello webpack!');\n\n//# sourceURL=webpack://test-webpack/./src/index.js?");
}) // module source code (value值)
});
var __webpack_exports__ = {};
__webpack_modules__["./src/index.js"]();// 执行
})();
3.4 添加映射后的源码结构解读
(1)添加devtool:'source-map',
const path = require('path')
module.exports = {
mode:'development',
devtool:'source-map',
entry:'./src/index.js',
output:{
path:path.resolve(__dirname, './dist'),
filename:'bundle.js'
}
}
(2)生成的bundle.js
文件
// bundle.js
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
console.log('hello webpack!');
/******/ })()
;
//# sourceMappingURL=bundle.js.map // 这句话就是指定映射文件
(3)生成的bundle.js.map
文件
// bundle.js.map
{
"version": 3,
"file": "bundle.js", // 映射的文件
"mappings": ";;;;;AAAA,8B", // 具体哪行代码进行映射,一个【;】表示省略一行,A表示开始映射的行数,具体映射的代码看【sourcesContent】
"sources": [ //浏览器【Sources】中生成的source文件
"webpack://test-webpack/./src/index.js"
],
"sourcesContent": [ // 具体映射的代码内容,也会显示在浏览器【Sources】映射文件中
"console.log('hello webpack!');"
],
"names": [],
"sourceRoot": ""
}
3.5 添加css后的源码结构解读
(1)添加module:{}',
const path = require('path')
module.exports = {
mode:'development',
devtool:'source-map',
entry:'./src/index.js',
output:{
path:path.resolve(__dirname, './dist'),
filename:'bundle.js'
},
module:{
rules:[
{
test: /\.css$/,
use: ['css-loader']
}
]
}
}
(2)生成index.css,并在index.js中引入
/* index.css */
.test {
width: 100px;
height: 100px;
background-color: red;
}
// index.js
import './index.css'
console.log('hello webpack!');
<body>
<div class="test"></div>
</body>
(3)生成的bundle.js
文件
// bundle.js
(() => { // webpackBootstrap
"use strict";
var __webpack_modules__ = ({
"./src/index.css":
// 【2.1】
((module, __webpack_exports__, __webpack_require__) => {
// 这一部分是处理生成map映射的逻辑----start
... // 省略部分代码
// 这一部分是处理生成map映射的逻辑----end
var ___CSS_LOADER_EXPORT___ = _node_modules_store_css_loader_6_7_3_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_store_css_loader_6_7_3_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));
// Module
___CSS_LOADER_EXPORT___.push([module.id, ".test {\r\n width: 100px;\r\n height: 100px;\r\n background-color: red;\r\n}\r\n", "",{"version":3,"sources":["webpack://./src/index.css"],"names":[],"mappings":"AAAA;EACE,YAAY;EACZ,aAAa;EACb,qBAAqB;AACvB","sourcesContent":[".test {\r\n width: 100px;\r\n height: 100px;\r\n background-color: red;\r\n}\r\n"],"sourceRoot":""}]);
// Exports
// ----这才是css-loader输出的内容
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
}),
... // 省略部分代码
});
// The module cache --------缓存
var __webpack_module_cache__ = {};
// The require function ------webpack引入函数
// 【2】
function __webpack_require__(moduleId) {
// Check if module is in cache ----先看看有没有缓存数据
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache) ---添加到缓存中
var module = __webpack_module_cache__[moduleId] = {
id: moduleId,
// no module.loaded needed
exports: {}
};
// Execute the module function ---- 执行__webpack_modules__方法
// 【2.1】
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
... // 省略部分代码
(() => {
// define __esModule on exports
// 【1】
// 就是在下面的var __webpack_exports__ = {};添加了一个__esModule属性
__webpack_require__.r = (exports) => {
//设置了两个属性
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
var __webpack_exports__ = {};
debugger
(() => {
// 这一块是css编译的代码
__webpack_require__.r(__webpack_exports__);// 【1】
/* harmony import */ var _index_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index.css */ "./src/index.css");// 【2】
// 这里是js编译的代码
console.log('hello webpack!');
})();
})()
;
//# sourceMappingURL=bundle.js.map
(4)问题:当前样式是没有插入dom中的
原因:
/* harmony import */ var _index_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index.css */ "./src/index.css");// 【2】
这行代码的最终结果,只是生成了样式内容,但后续是没有对样式的处理。
// Module
___CSS_LOADER_EXPORT___.push([module.id, ".test {\r\n width: 100px;\r\n height: 100px;\r\n background-color: red;\r\n}\r\n", "",{"version":3,"sources":["webpack://./src/index.css"],"names":[],"mappings":"AAAA;EACE,YAAY;EACZ,aAAa;EACb,qBAAqB;AACvB","sourcesContent":[".test {\r\n width: 100px;\r\n height: 100px;\r\n background-color: red;\r\n}\r\n"],"sourceRoot":""}]);
此时,就需要style-loader
将生成的样式内容,添加到DOM中。
3.6 创建自定义文件类型:自定义loader开发
(1)创建自定义文件test.imooc
,并在index.js中引入
// test.imooc
<script>
export default {
a: 1,
b: 2
}
</script>
// index.js
import './index.css'
import value from './test.imooc'
console.log('value:', value)
console.log('hello webpack!');
(2)创建自定义loader
const REG = /^<script>([\s\S]+?)<\/script>$/;
module.exports = function (source) {
console.log('source:', source)
const __source = source.match(REG)
console.log('__source:', __source)
console.log('__source[1]:', __source[1])
console.log('__source[0]:', __source[0])
return __source && __source[1] ? __source[1]: source
}
// 测试:用来对loader进行单独测试,排查当前文件bug 的
// 判断当前模块是否为主模块(直接运行当前js文件,而不是在其他地方引用,运行其他文件),如果是主模块,require.main = module
if (require.main === module) {
const source = `<script>
export default {
a: 1,
b: 2
}
</script>`
const match = source.match(REG)
console.log('match:', match)
}
注意:
- loader的定义,还是比较简单的,就是上面的格式
- 难的是关于模块信息的提取,涉及到一些算法。
(3)webpack.config.js
添加自定义loader
const path = require('path')
module.exports = {
mode:'development',
devtool:'source-map',
entry:'./src/index.js',
output:{
path:path.resolve(__dirname, './dist'),
filename:'bundle.js'
},
module:{
rules:[
{
test: /\.css$/,
use: ['style-loader','css-loader']
},
{
test: /\.imooc$/,
use: [path.resolve(__dirname,'./loader/imooc-loader.js')]
}
]
}
}
3.7 创建自定义plugin
(1)创建自定义插件FooterPlugin.js
const {ConcatSource} = require('webpack-sources') // cnpm i -D webpack-sources
class FooterPlugin {
constructor(options) {
console.log('options:', options)
this.options = options
}
apply(compiler) {
compiler.hooks.compilation.tap('FooterPlugin',compilation =>{
compilation.hooks.processAssets.tap('FooterPlugin', ()=>{
// 获取chunks
const chunks = compilation.chunks
console.log('chunks:', chunks)
// 获取chunk
for (const chunk of chunks) {
// 获取chunk里的file
for (const file of chunk.files) {
console.log('file:', file)
// 对内容进行处理
const comment = `/*${this.options.banner}*/`
// 更新chunk 的 file文件
compilation.updateAsset(file, old=>{
return new ConcatSource(old,'\n',comment)
})
}
}
})
})
}
}
module.exports = FooterPlugin;
(2)webpack.config.js
添加自定义Plugin
const path = require('path')
const webpack = require('webpack')
const FooterPlugin = require('./plugin/FooterPlugin.js')
module.exports = {
mode:'development',
devtool:'source-map',
entry:'./src/index.js',
output:{
path:path.resolve(__dirname, './dist'),
filename:'bundle.js'
},
module:{
rules:[
{
test: /\.css$/,
use: ['style-loader','css-loader']
},
{
test: /\.imooc$/,
use: [path.resolve(__dirname,'./loader/imooc-loader.js')]
}
]
},
plugins:[
// webpack自带的Plugin
new webpack.BannerPlugin({
banner:'----开始前端工程化学习'
}),
// 自定义Plugin
new FooterPlugin({
banner:'自定义的webpack插件'
})
]
}
(3)打包结果
// bundle.js
/*! ----开始前端工程化学习 */
/******/ (() => { // webpackBootstrap...
/******/ })()
;
/*自定义的webpack插件*/
//# sourceMappingURL=bundle.js.map
3.8 webpack自带插件/特性
只需要引用webpack包
即可,使用时通过new webpack.插件()
生成实例对象。
(1)ProvidePlugin
作用
每当在模块中遇到作为自由变量的标识符时,就会自动加载模块,并使用加载的模块的导出(或支持命名导出的属性)填充标识符
通俗理解:
- 老系统,比如用jQuery的,通过 $ 调用jquery方法,此时就需要配置一下$对应的jQuery库。
引用和配置
const webpack = require('webpack');
module.exports = {
// ...
plugins: [
new webpack.ProvidePlugin({
$:'jquery',
jQuery:'jquery'
})
],
}
Demo使用:此时$就会生效
// 此时的$就会生效
import 'flexslider'
$(function(){
$(window).scroll(function(){
var ws=$(window).scrollTop();
if(ws>60){
$(".head").addClass("ding").css({"background":"rgba(255,255,255,"+ws/300+")"});
}else{
$(".head").removeClass("ding").css({"background":"#fff"});
}
});
})
(2)Tree Shaking
作用
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)
通俗理解:
- 删除无用代码。
Tree Shaking触发条件
- 通过
解构
的方式获取方法,可以触发treeshaking - 调用的 npm包 必须使用
ESModule规范
。- 即可以是:
export function a() {}
, - 但不是能是:
export default {a,b}
,也就是default对象里面不能有多个。
- 即可以是:
// import {get} from 'lodash' // 这个就无法触发treeshaking,因为他是用commonJS规范
import {get} from 'lodash-es' // 可以用它
console.log(get({a:1},'a'));
3.9 webpack社区插件
一般是需要通过npm单独下载后进行独立引用。
(1)HtmlWebpackPlugin
作用
该插件将为你生成一个 HTML5 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。
通俗理解:
- 把一个html给打包到public文件中,并自动引入生成bundle.js文件
安装
cnpm install --save-dev html-webpack-plugin
引用
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
filename:'index.html', // 模板名称,待追加bundle.js的html
template:'./src/index.html', // 模板地址
chunks:['index'] // 多个入口文件时使用,index是entry的key值,代表将生成bundle.js添加到对应的文件中
})
],
}
(2)CopyWebpackPlugin
作用
将某些路径下的文件,拷贝到其他的路径中。一般是拷贝静态资源,比如图片等。
安装
cnpm install copy-webpack-plugin --save-dev
引用
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
// ...
plugins: [
new CopyPlugin({
patterns: [ // 用来匹配路径,可以匹配多个,加多个对象就行
{ // 将from下的文件,拷贝到to中。(此处是将src的文件拷贝到dist中)
from: path.resolve(__dirname,'./src/img'),
to: path.resolve(__dirname,'./dist/img')
},
],
}),
],
}
(3)mini-css-extract-plugin
作用
将打包后的bundle.js文件中的css代码抽离出来,形成单独的css文件
安装
cnpm install mini-css-extract-plugin --save-dev
引用
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
// ...
module:{
rules:[
{
test:/\.css$/,
use:[MiniCssExtractPlugin.loader,'css-loader'], // 剥离js中的css代码,替代原来的style-loader
}
]
},
plugins: [
new MiniCssExtractPlugin(
{ // 生成css文件
filename:'css/[name].css', // 生成的css文件路径。[name]也是entry的key
chunkFilename:'css/[name].chunk.css' // chunk文件的路径
}
),
],
}
(4)uglifyjs-webpack-plugin
作用
js代码的压缩。
安装
cnpm i -D uglifyjs-webpack-plugin
引用
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
module:{},
optimization:{ // 对上面的module对象的结果进行优化,比如代码压缩等
minimize: true, // 开始压缩
minimizer:[ // 配置压缩插件
new UglifyJsPlugin({ // js代码压缩
sourceMap:true // 让UglifyJsPlugin里也可以使用sourceMap功能
}),
]
},
}
(5)CssMinimizerWebpackPlugin
作用
对css代码进行压缩
安装
cnpm install css-minimizer-webpack-plugin --save-dev
引用
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
module:{},
optimization:{ // 对上面的module对象的结果进行优化,比如代码压缩等
minimize: true, // 开始压缩
minimizer:[ // 配置压缩插件
new CssMinimizerPlugin(), // css代码压缩
]
},
}
4. webpack优化
4.1 加快打包速度
(1)output属性的hash值用法
作用
只有文件变化时,hash打包的hash值才会变化。加快打包速度。
const path = require('path')
module.exports = {
mode:'development',
entry:{
bundle:'./src/index.js', // 可以自定义入口文件的key值,比如自定义bundle,默认是main
},
output: {
// filename:'bundle.js', // 固定写死的名称
filename:'[name].js', // 这里的name是获取entry的key值
filename:'[hash].js', // 也可以改成hash值,只有文件变化时,hash打包的hash值才会变化
path:path.resolve(__dirname, './dist'),
}
}
4.2 图片优化(资源模块webpack5才有的)
(1)module/rules下关于图片的优化
module.exports = {
// ...
module:{
rules:[
{
test:/\.(png|svg|jpg|jpeg|gif)$/i,
type:'asset',
parser: { // 对图片的处理
dataUrlCondition: { // 如果一个模块源码(图片)大小小于 maxSize,那么模块会被作为一个 Base64 编码的字符串注入到包中, 否则模块文件会被生成到输出的目标目录中。
maxSize: 8 * 1024, // 8kb
},
},
generator: { //图片输出路径
filename: 'images/[name].[hash][ext]', // [name]表示图片名称,[hash]是哈希值,[ext]文件格式
},
}
]
}
}
4.3 代码压缩和分割
(1)js代码压缩
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
module:{},
optimization:{ // 对上面的module对象的结果进行优化,比如代码压缩等
minimize: true, // 开始压缩
minimizer:[ // 配置压缩插件
new UglifyJsPlugin({ // js代码压缩
sourceMap:true // 让UglifyJsPlugin里也可以使用sourceMap功能
}),
]
},
}
(2)css代码压缩
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
module:{},
optimization:{ // 对上面的module对象的结果进行优化,比如代码压缩等
minimize: true, // 开始压缩
minimizer:[ // 配置压缩插件
new CssMinimizerPlugin(), // css代码压缩
]
},
}
(3)代码分割
module.exports = {
optimization:{ // 对module结果进行优化
minimize: true, // 开始压缩
minimizer:[ // 配置压缩插件
new UglifyJsPlugin({
sourceMap:true // 让UglifyJsPlugin里也可以使用sourceMap功能
}),
new CssMinimizerPlugin(),
],
splitChunks:{
chunks: 'all', // 分割的类型,all指所有
minSize: 100 * 1024, // 最小的分割单位,对下面的cacheGroups也是生效的
name:'common', // 分割后js文件的名称,也可以使用默认的
cacheGroups: { // 单独对某个npm包打包,
jquery: { // 对npm的jquery进行单独打包
name:'jquery', // 打包后的名称
test: /jquery/, // 匹配文件的正则
chunks: 'all',
}
}
}
},
}
4.4 tree shaking
作用
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)
通俗理解:
- 删除无用代码。
(1)对单独文件进行tree shaking
触发条件:
- 通过
解构
的方式获取方法,可以触发treeshaking- 调用的 npm包 必须使用
ESModule规范
。
- 即可以是:
export function a() {}
,- 但不是能是:
export default {a,b}
,也就是default对象里面不能有多个。
// import {get} from 'lodash' // 这个就无法触发treeshaking,因为他是用commonJS规范
import {get} from 'lodash-es' // 可以用它
console.log(get({a:1},'a'));
(2)对某个文件里的代码进行tree shaking
触发条件:
解构
+ 使用ESModule规范
mode必须等于production
的时候才会触发。
// webpack.config.js
module.exports = {
mode:'production'
}
// 入口文件:index.js
// test.js导出2个方法,test1和test2,但只引用test1
import {test1} from 'test.js'
test1()
4.5 构建性能优化
参考链接:https://juejin.cn/post/7078491632605069348
- 查找并诊断性能瓶颈:
- 构建速度分析:影响构建性能和开发效率
- 构建体积分析:影响页面访问性能
- 构建性能优化常用方法:
- 通过多进程加快构建速度
- 通过分包减小构建目标容量
- 减少构建目标加快构建速度
(1)构建速度分析speed-measure-webpack-plugin
作用
这个插件测量你的webpack构建速度。
安装
cnpm install --save-dev speed-measure-webpack-plugin
引用及使用
// 引入
const path = require('path');
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
// 创建实例
const smp = new SpeedMeasurePlugin({
disable: !(process.env.MEASURE ==='true'), // 是否禁用,process.env.MEASURE是根据script里设置的环境变量取值
outputFormat:'human' // 信息输出格式,一般就用默认值就行
});
module.exports = {
// 使用:包裹webpack即可
configureWebpack:smp.wrap({
resolve:{
alias:{
'src':path.resolve(__dirname,'./src'),
}
}
})
}
(2)构建体积分析webpack-bundle-analyzer
作用
使用交互式可缩放树图可视化webpack输出文件的大小
安装
cnpm install --save-dev webpack-bundle-analyzer
引用及使用
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
configureWebpack:{
resolve:{
alias:{
'src':path.resolve(__dirname,'./src'),
}
},
plugins:[
new BundleAnalyzerPlugin() // 默认配置就可以,也可以修改参数
]
}
}
(3)辅助工具:cross-env动态修改环境变量
作用
在script中,动态设置环境变量。
安装
cnpm install --save-dev cross-env
使用
// 动态添加一个环境变量:NODE_ENV=production
{
"scripts": {
"serve": "cross-env NODE_ENV=production vue-cli-service serve"
}
}
(4)thread-laoder多进程
作用
开启多线程,加快构建速度。
注意:在项目简单时,使用thread-laoder多进程,有可能耗时会更长。
安装
cnpm install --save-dev thread-loader
使用:Vue-Cli中已经有集成,不需要单独配置了
vue-cli的参考:https://cli.vuejs.org/zh/config/#parallel
module.exports = {
// 包裹webpack即可
configureWebpack:smp.wrap({
resolve:{
alias:{
'src':path.resolve(__dirname,'./src'),
}
},
module:{
rules:[
{
test:/\.js$/,
exclude:/node_module/,
use: [ // 开启thread-laoder多进程
{
loader:'thread-loader',
options: {
worker:2,
}
}
]
}
]
},
plugins:[
new BundleAnalyzerPlugin()
]
})
}
(5)分包:DllPlugin & DllReferencePlugin
步骤
- 分包:定义webpack.dll.config.js,使用DllPlugin配置分包,定义scripts命令,执行命令,完成分包
- 排除分包:在vue.config.js中,使用DllReferencePlugin引用manifest文件排除分包
- 拷贝dll:将dll拷贝至项目目录下,用CopyWebpackPlugin(暂时不用这个,第四步add-asset-html-webpack-plugin会一块执行)
- 引用dll:使用add-asset-html-webpack-plugin引用分包文件
1. 分包配置文件
// 在根目录下新建 webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');
const dllPath = './dll';
module.exports = {
mode:'production',
entry:{ // 哪些需要分包,可以设置多个
vue: ['vue', 'vue-router', 'vuex'], // dependencies中所有的包都可以分包
// scroll: ['better-scroll']
},
output:{
path: path.join(__dirname, dllPath), // 输出路径
filename:'[name].dll.js', // 输出文件名,[name]是entry的key值
library: '[name]_[hash]',
},
plugins: [
new webpack.DllPlugin({
path:path.join(__dirname,dllPath,'[name]-manifest.json'), // 生成的manifest文件,用于vue.config.js打包时排除当前的分包
name:'[name]_[hash]', // 必须和library保持一致
context:process.cwd(),
})
]
}
1. 设置脚本
"scripts": {
"dll": "webpack --config webpack.dll.config.js"
},
1. 执行脚本
npm run dll
2. 排除分包文件
// vue.config.js
const webpack = require('webpack');
module.exports = {
// 包裹webpack即可
configureWebpack:smp.wrap({
plugins:[
new BundleAnalyzerPlugin(),
//告诉webpack哪些库不需要打包
new webpack.DllReferencePlugin({
context: __dirname,
manifext:path.resolve(__dirname, './dll/vue-manifest.json') // 打包要排除的文件(之前分包的文件)
})
]
})
}
3. 引用dll
安装cnpm i -D add-asset-html-webpack-plugin
插件
// vue.config.js
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
// 包裹webpack即可
configureWebpack:smp.wrap({
plugins:[
new BundleAnalyzerPlugin(),
//告诉webpack哪些库不需要打包
new webpack.DllReferencePlugin({
context: __dirname,
manifext:path.resolve(__dirname, './dll/vue-manifest.json') // 打包要排除的文件(之前分包的文件)
}),
// 拷贝分包文件到打包后的文件中,并在打包后的html中进行引用
new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname, './dll/vue.dll.js'),
})
]
})
}
(6)利用缓存提升二次构建速度
module.exports = {
// 包裹webpack即可
configureWebpack:smp.wrap({
cache: {
type:'filesystem', // 类型
cacheDirectory: path.resolve(__dirname, './node_modules/.cache_temp'), // 缓存目录放在哪里
}
})
}
(7)图片压缩 image-webpack-loader
安装cnpm i -D image-webpack-loader
插件
module.exports = {
// 包裹webpack即可
configureWebpack:smp.wrap({
module:{
rules:[
{
test: /\.(gif|png|jpe?g|svg|webp)$/i,
use:[{
loader:'image-webpack-loader',
options: { // 具体配置可以参考npmjs中的示例
mozjpeg: {
progressive: true,
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.90], //图片质量的修改
speed: 4
},
gifsicle: {
interlaced: false,
},
// the webp option will enable WEBP
webp: {
quality: 75
}
}
}]
}
]
}
})
}
(8)删除无用的css样式purgecss-webpack-plugin
安装cnpm i -D purgecss-webpack-plugin
插件
使用参考链接:https://www.npmjs.com/package/purgecss-webpack-plugin
5、前端性能监控
5.1 window.Performance
5.2 performanceObserve
尽早定义。
5.2.1 前端DOM加载性能:
(1)html标签里加入elementtiming属性
(2)监听dom