webpack底层学习总结
在开发中webpack是我们经常使用到的工具 在创建脚手架的时候框架会自动帮我们配置到webpack 但是我们还是需要学习一下底层 以防开发中遇到一些恶性的需求
soure-map
soure-map用于对我们的报错进行优化,为啥优化,当我们的一个项目有很多代码的时候很难保证不报错,如果你基于webpack打包以后 他执行的是你打包后的代码 ,他给你的保持就算准确你也看不懂打包后的代码
webpack打包后的代码
(()=>{"use strict";console.log(count),console.log("Hello World"),console.log(address),console.log("foo function exec~"),console.log(50),console.log(-10)})();
这还是mode为production打包的如果是development模式更加复杂 这里我故意写了一行报错代码
源码
//main.js
import { add, sub } from './utils/math'
const message = "Hello World"
console.log(message)
console.log(address)
const foo = () => {
console.log("foo function exec~")
}
foo()
console.log(add(20, 30))
console.log(sub(20, 30))
//math.js
function add(num1, num2) {
return num1 + num2
}
function sub(num1, num2) {
return num1 - num2
}
console.log(count) //不存在的定义
export {
add,
sub
}
他这里给我的报错就很离谱(不不,只能怪我看不懂)
那么我们如果想准确的直到报错位置呢我们就需要使用soure-map
- source-map是从已转换的代码,映射到原始的源文件;
- 使浏览器可以重构原始源并在调试器中显示重建的原始源;
使用步骤
你只需要在你的webpack.config.js中加上 devtool: 'source-map'
就行 他会生成一个map文件用来映射使用
module.exports = {
mode: 'production',
// 1developmen和eval搭配 也就是 devtool:'eval'
// 2production和source-map搭配
// devtool不常见的值:
// 1.eval-source-map: 添加到eval函数的后面
// 2.inline-source-map: 添加到文件的后面
// 3.cheap-source-map(dev环境): 低开销, 更加高效
// 4.cheap-module-source-map(这个方案也是推荐使用 react用的就是他): 和cheap-source-map比如相似, 但是对来自loader的source-map处理的更好说白了删除一些不需要的空格 注释等等
// 5.hidden-source-map: 会生成sourcemap文件, 但是不会对source-map文件进行引用 如果你要使用该map你就自己加上引用的注释前面有说过
// 6.nosources-source-map:只会生成报错信息 不会生成文件内容只会生成文件结构
// 发布的时候都给干掉哦 黑客会来个迷之微笑
devtool: 'source-map',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './build'),
filename: 'bundle.js'
},
}
报错也就正常了
需要注意的是在你的源文件下最好加个注释 表示指向哪里 因为我们后期会有很多map和源文件
//# sourceMappingURL=bundle.js.map
如果你使用的谷歌浏览器 他会自动帮我们打开souremaps调试模式如果没打开你需要打开一下
大大提高了开发效率
bable
bable低层原理
bable用于把es6+转换为es5方便浏览器解析这个过程是如何完成的呢?
这个转换过程其实就是编译器工作 事实上我们可以把bable看成一个编译器
- 口语化:
Babel编译原理可以简单解释为以下几个步骤:首先,Babel会将你写的最新版本的JavaScript代码解析成一种叫做抽象语法树的数据结构。然后,Babel会对这个语法树进行遍历和修改,将新版本的语法转换为旧版本的语法。最后,Babel会将修改后的语法树重新生成为代码字符串,这样就可以在不支持新语法和特性的浏览器中运行了。所以,Babel的作用就是让你可以使用最新的JavaScript语法,同时保证代码在各种浏览器和环境中的兼容性。
- 标准化:
Babel是一个广泛使用的JavaScript编译器,用于将新版本的JavaScript代码转换为向后兼容的旧版本,以便在不支持新语法和特性的浏览器中运行。Babel编译原理可以简单概括为以下几个步骤:
-
解析(Parsing):Babel首先将输入的源代码解析成抽象语法树(AST),这是一种以对象形式表示代码结构的数据结构。解析器会根据语法规则将源代码转换为AST。
-
转换(Transformation):在转换阶段,Babel会对AST进行遍历和修改,将新版本的语法转换为旧版本的语法。Babel使用插件机制,每个插件负责实现一项特定的转换规则,可以根据需要配置多个插件,以完成不同的转换任务。
-
生成(Generation):在生成阶段,Babel会将经过转换的AST重新生成为代码字符串。这个过程会将AST中的每个节点转换为相应的代码片段,并按照原始代码的格式进行排版。
Babel的编译原理基于以上三个步骤,通过解析源代码、转换AST和生成目标代码,实现了将新版本的JavaScript转换为向后兼容的旧版本。这样,开发者就可以使用最新的JavaScript语法和特性,同时保证代码在不同浏览器和环境中的兼容性。
我们使用bable对写完的es6代码进行编译 需要使用命令 很不银杏 所以我们需要配置webpack编译对我们的es6代码进行编辑转化
任务一:配置webpack实现es6转换为es5
在配置转换的时候要么你用预设要么你用插件
-
比如你箭头函数转换需要使用@babel/plugin-transform-arrow-functions
-
比如你cost转换为es5需要使用@babel/plugin-transform-block-scoping
我还是使用预设吧 配置webpack.config.js就行 使用到的插件自己安装
注意你如果使用插件解析是在module>rules>use>options>plugins写插件
注意你如果使用预设解析是在module>rules>use>options>presets写预设
const path = require('path');
module.exports = {
// 使用dev环境 不用丑化代码方便我们查看
mode: 'development',
// 不生成eval 方便我们查看
devtool: false,
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './build'),
filename: 'bundle.js',
// 用于在下一次打包以后删除上一次打包的东西
clean: true,
},
module: {
// 这里是匹配规则 用于匹配文件的处理方案
rules: [
{
// 当遇到.js结尾的文件时候使用
test: /\.js$/,
use: {
// 配置我们需要对js转换所需要使用的loader
loader: 'babel-loader',
options: {
// plugins: [
// // 箭头函数转换需要的插件
// '@babel/plugin-transform-arrow-functions',
// // const解析需要使用的插件
// '@babel/plugin-transform-block-scoping',
// ],
// 我们如果不想写插件解析我们就使用预设进行解析
presets: ['@babel/preset-env'],
},
},
},
],
},
};
任务二:配置浏览器兼容性
作为一个web开发者我们开发中需要考虑各种浏览器的兼容性 这很蛋疼我的建议是不如使用webpack实现浏览器的兼容
要不你就手动写js实现浏览器兼容
我们的代码要不要自动转换取决于要适配的浏览器 我需要告诉webpack你要给我兼容的浏览器 如果每一个都要自己写一下浏览器代号 很不银杏 我们可以使用.browserslistrc配置文件 这个文件就是用来告诉webpack 我们是否需要兼容该浏览器
browserslist他是不用安装的 我们在安装bable的时候就会依赖安装browserslist的所以我们可以直接使用
我们可以通过两个方案实现浏览器兼容
- 在pack.json中编写 我不喜欢
- 单独创建.browserslistrc文件来编写webpack会自动读取到该文件来适配浏览器
>1% 市场占有率
last 2 versions 最后两个版本
not dead 没有死亡的 还在维护的浏览器
建议开发环境写大于2% 打包环境写大于0.2%
不过还是看你公司的需求
你也可以针对于某个模块去做适配
options: {
// plugins: [
// // 箭头函数转换需要的插件
// '@babel/plugin-transform-arrow-functions',
// // const解析需要使用的插件
// '@babel/plugin-transform-block-scoping',
// ],
// 我们如果不想写插件解析我们就使用预设进行解析 预设需要在 presets中写 插件需要在plugins中写
presets: [
'@babel/preset-env',
{
// 配置兼容的目标浏览器
// 在开发中我们一般使用 一般还是配置browserslist 因为他可以共享兼容性 比如css js 等等都会共享该兼容性
targets: '>5%',
},
],
},
任务三:配置webpack解析react中的jsx代码
我们这里编写的react代码不可能就去看源码有没有编译了,我们需要跑到浏览器里看一下运行结果 这时候就需要html出现了 但是htmlwebpack他不会帮你打包到build中所以我们需要使用插件帮我们一起打包html文件供我们验证代码
其实我们的jsx解析和e6——>es5解析一样要么你用插件 要么你用预设 设置方法都一样
安装编译html的插件
npm install html-webpack-plugin --save-dev
html-webpack-plugin
默认将会在 output.path
的目录下创建一个 index.html
文件, 并在这个文件中插入一个 script
标签,标签的 src
为 output.filename
。
当配置多个入口文件
entry
时, 生成的将都会使用script
引入。
如果
webpack
的输出中有任何CSS资源 (例如,使用mini-css-extract-plugin
提取的CSS
),那么这些资源将包含在HTML
头部的link
标记中。
template String 空 生成 filename
文件的模版, 如果存在src/index.ejs
, 那么默认将会使用这个文件作为模版。 重点:与filename
的路径不同, 当匹配模版路径的时候将会从项目的跟路径开始属性名 类型 默认值 说明
more:
https://juejin.cn/post/6844903853708541959
react代码
import React, { useState } from 'react';
const App = function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>App Count:{count}</h1>
<button onClick={(e) => setCount(count + 1)}>+1</button>
</div>
);
};
export default App;
index.js
import React from 'react';
import ReactDom from 'react-dom/client';
import App from './react/App.jsx';
const obj = { name: 'wangfeng', age: 19 };
const { name, age } = obj;
console.log(name, age);
const foo = () => {
console.log('name');
};
const myname = 'wangfeng';
const mynamehasw = myname.includes('w');
console.log(mynamehasw);
const root = ReactDom.createRoot(document.querySelector('#app'));
root.render(<App />);
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './build'),
filename: 'bundle.js',
clean: true,
},
// 配置别名 就是我们在引入App的时候可以不写App.jsx
resolve: {
extensions: ['.js', '.json', '.wasm', '.jsx', '.ts'],
},
module: {
// 这里是匹配规则 用于匹配文件的处理方案
rules: [
{
// 我们需要改变匹配规则匹配jsx
test: /\.jsx?$/, //?代表0或者1个 即jsx或者js
use: {
// 配置我们需要对js转换所需要使用的loader
loader: 'babel-loader',
options: {
presets: [
// 解析js的预设 你也可以使用插件
['@babel/preset-env'],
// 解析jsx的预设 你也可以使用插件
['@babel/preset-react'],
],
},
},
},
],
},
// 打包html文件
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
],
};
任务四:配置webpack解析ts代码
在项目开发中,我们会使用TypeScript来开发,那么TypeScript代码是需要转换成JavaScript代码
首先你需要安装ts npm install typescript -D
对于ts的解析有两种方案 都不是最完美的在最后我会给出一个最佳实践
- 方案一:使用ts-loader 也就是 TypeScript Compiler
- 在编译之前我们需要生成ts的编译配置信息ypeScript的编译配置信息我们通常会编写一个tsconfig.json文件,我们可以用这个命令生成编译配置信息tsc --init
我们需要安装一下ts-loader npm install ts-loader -D
配置webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './build'),
filename: 'bundle.js',
clean: true,
},
// 配置别名 就是我们在引入App的时候可以不写App.jsx
resolve: {
extensions: ['.js', '.json', '.wasm', '.jsx', '.ts'],
},
module: {
//使用ts-loader来编译文件
rules: [
{
test: /\.ts$/,
use: ['ts-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
],
};
- 方案二:使用babel-loader 来编译ts文件 他还可以帮我们实现polyfill的功能
- 除了可以使用TypeScript Compiler来编译TypeScript之外,我们也可以使用Babel:
Babel是有对TypeScript进行支持;
我们可以使用插件: @babel/tranform-typescript;
但是更推荐直接使用preset: @babel/preset-typescript;
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './build'),
filename: 'bundle.js',
clean: true,
},
resolve: {
extensions: ['.js', '.json', '.wasm', '.jsx', '.ts'],
},
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'babel-loader',
options: {
presets: [
// 解析ts需要的
['@babel/preset-typescript'],
],
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
],
};
- 最佳实践
- 使用ts-loader(TypeScript Compiler)
来直接编译TypeScript,那么只能将ts转换成js;
如果我们还希望在这个过程中添加对应的polyfill,那么ts-loader是无能为力的;
我们需要借助于babel来完成polyfill的填充功能; - 使用babel-loader(Babel)
来直接编译TypeScript,也可以将ts转换成js,并且可以实现polyfill的功能;
但是babel-loader在编译的过程中,不会对类型错误进行检测;
解决方案:
我们使用Babel来完成代码的转换,使用tsc来进行类型的检查
我们可以在scripts中添加了两个脚本,用于类型检查;
我们执行 npm run type-check可以对ts代码的类型进行检测;
我们执行 npm run type-check-watch可以实时的检测类型错误;
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"ts-check": "tsc --noEmit",
"ts-check-watch": "tsc --noEmit --watch"
},
npm run ts-check
PS C:\Users\WangFeng\Desktop\WebPack\02_bable> npm run ts-check
> babel_core_demo@1.0.0 ts-check
> tsc --noEmit
src/ts/index.ts:4:15 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
4 const res=sum("1","1")
~~~
Found 1 error in src/ts/index.ts:4
npm run ts-check-watch
他可以持续进行监听你的代码结果会一直让你改到没有报错为止
polyfill
该模块用于填充比如我们使用一些比较新的语法
转换前代码
const obj = { name: 'wangfeng', age: 19 };
const { name, age } = obj;
console.log(name, age);
const foo = () => {
console.log('name');
};
const myname = 'wangfeng';
const mynamehasw = myname.includes('w');
转换后代码
var foo = function foo() {
var _console2;
/* eslint-disable */(_console2 = console).log.apply(_console2, _toConsumableArray(oo_oo("8d027165_1", 'name')));
};
var myname = 'wangfeng';
var mynamehasw = myname.includes('w');
可以发现includes方法并没有进行转换 那么如果你需要运行到老版浏览器 这个代码肯定会报错
这时候我们可以使用polyfill 进行填充
比如我们使用了includes 他会把字符串上的String.prototype.includes方法给你填充进来供你使用
使用方法 第一步安装
npm install core-js regenerator-runtime --save
第二步在webpack.config.js里面 配置一下
options: {
// plugins: [
// // 箭头函数转换需要的插件
// '@babel/plugin-transform-arrow-functions',
// // const解析需要使用的插件
// '@babel/plugin-transform-block-scoping',
// ],
// 我们如果不想写插件解析我们就使用预设进行解析
presets: [
[
'@babel/preset-env',
{
// 你的corejs版本在pack.json中查看
corejs: 3,
// 表示需要处理特殊语法
useBuiltIns: 'usage',
},
],
],
},
在useBuiltIns里面我们还有一个选择是 entry 表示如果我们依赖一个库比如dayjs 等等一些第三方库 如果这些库中也使用了一些特殊的语法 如果你还是使用 useBuiltIns: ‘usage’ 用户的浏览器可能会报错 所以我们可以把他设置为entry 即使第三方库也用了特殊语法他还是会帮我们解析这些语法 他会根据browserslist所需要兼容的浏览器 去导入所有的polyfill
当你把useBuiltIns设置为entry后你还需要在你的index中引入一下两个库
import 'core-js/stable'
import 'regenerator-runtime/runtime'
在开发中我们推荐使用usage 如果用了还是报错在用entry
WebpackServer
在之前的代码写完之后我们都是通过 npm run build来编译相关代码 很不银杏
为了完成自动编译, webpack提供了几种可选的方式:
webpack watch mode;
webpack-dev-server(常用);
webpack-dev-middleware;
webpack-dev-server 在编译之后不会写入到任何输出文件,而是将 bundle 文件保留在内存中
我们在pack.json中添加上
"serve": "webpack serve "
在webpack.config.js中添加上配置
devServer: {
static: ['public', 'content'], //静态资源目录
// host: '0.0.0.0',
port: 3000,//开启端口
open: true, //编译完成是否打开浏览器
compress: true, //是否开启压缩
//配置代理服务器和node中基本一致 用于解决开发过程中的跨域问题
proxy: {
//只要是以api开头的请求我们都给他转换为 target目标的请求地址
'/api': {
target: 'http://localhost:9000',
//通配/api接口目的是替换掉/api
//比如请求是 /api/user/list
//请求服务器地址 http://localhost:9000/user/list
//我们就需要吧/api替换掉
//替换后结果 http://localhost:9000/user/list
pathRewrite: {
'^/api': ''
},
changeOrigin: true
//这个在官网讲的比较模糊 实际的意义是这样的 我们通过axios.get("/api/user/list") 如果把地址写全就是通过 http://localhost:3000/api/user/list 实际上拿到的是这个地址 webpack其实也就是通过这个接口去请求数据那么如果后端的大哥如果开启了服务器校验你通过3000端口向9000端口去请求数据你是拿不到数据的 所以我们开启changeOrigin换源在请求的时候你把我的请求给我换掉换成http://localhost:9000/user/list 就可以了
}
},
//用于调试页面刷新
//historyApiFallback是开发中一个非常常见的属性,它主要的作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误
//boolean值:默认是false如果设置为true,那么在刷新时,返回404错误时,会自动返回 index.html 的内容;
//object类型的值,可以配置rewrites属性:可以配置from来匹配路径,决定要跳转到哪一个页面;
historyApiFallback: true
},
webpack性能优化
这个话题在面试中问的很多 你都可以切入到webpack中来和面试官聊
问题:
-
前端有哪些常见的性能优化?
-
可以配置哪些属性来进行webpack性能优化?
webpack的性能优化较多,我们可以对其进行分类:
- 优化一: 打包后的结果,上线时的性能优化。(比如分包处理、减小包体积、 CDN服务器等)
- 优化二: 优化打包速度,开发或者构建时优化打包速度。(比如exclude、 cache-loader等)
大多数情况下,我们会更加侧重于优化一,这对于线上的产品影响更大
在大多数情况下webpack都帮我们做好了该有的性能优化:
- 比如配置mode为production或者development时,默认webpack的配置信息;
- 但是我们也可以针对性的进行自己的项目优化;
webpack代码分包处理
如果你不做代码分离 那么webpack默认将你所有的代码都打包到bulid文件夹中 如果一次性加载所有的代码会导致用户的浏览器首屏渲染速度变慢 代码分离的话就是把不同的代码 打包的不同的包中 当用户用到的时候在去加载一些文件 或者在浏览器闲置的时候去下载我们代码需要的模块
webpack代码分离有三种方案
- 入口起点
- 防止重复
- 动态导入
webpack性能优化-分包处理-入口起点
就是通过配置webpack.config.js实现多入口 多输出这种方式用的不多但是可以和面试官聊 比如现在有两个js文件 index.js放的是vue代码 main.js放的是react代码 然后这两个文件都依赖于axios 在你没有做额外的配置下 你的axios会打包两次 很不银杏 接下来会一次解决这些问题
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
// 配置一:配置多个入口
entry: {
// 需要导入的文件一
index: {
import: './src/index.js',
// 需要使用某个共享库
dependOn: 'shared',
},
// 需要导入的文件二
main: {
import: './src/main.js',
// 需要使用某个共享库
dependOn: 'shared',
},
// 需要共享的库一
shared: ['axios'],
// 需要共享的库二
// shared1:[xxxx]
// shared2:[xxxx2]
},
output: {
path: path.resolve(__dirname, './build'),
// 在输出配置中我们需要配置一下placeholder
// filename 会把文件分开输出因为我们有多个文件 name相当于通配符 他会取到我们的文件name 比如 index.js打包后就是 index-bundle.js
filename: '[name]-bundle.js',
clean: true,
},
resolve: {
extensions: ['.js', '.json', '.wasm', '.jsx', '.ts'],
},
devServer: {
static: ['public', 'content'],
port: 3000,
compress: true,
proxy: {
'/api': {
target: 'http://localhost:9000',
pathRewrite: {
'^/api': '',
},
changeOrigin: true,
},
},
historyApiFallback: true,
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.ts$/,
use: 'babel-loader',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
],
};
webpack性能优化-分包处理-动态导入
在我们的代码中我们需要使用某个模块我们就使用import去导入 完了如果在你的首页中你没有第一次渲染页面没有用到这个模块那么这个包是不会下载的,只有等到你用到的时候才会下载同样的在打包的时候webpack对于分包导入的东西也是不会一起打包,会分开打包
在webpack.config.js中你可以增加一个配置项 chunkFilename: '[name]_chunk.js'
这个配置项主要就是给单独打包的文件进行命名的 他的命名方式取决于你在导入的时候有没有传魔法注释 比如下面的代码就是传了魔法注释,如果没有webpack会帮你生成name
魔法注释:
他是import的第一个参数 也是作为打包后文件的命名项
/* webpackChunkName: “about” */
// index.js作为入口
const btn1 = document.createElement('button')
const btn2 = document.createElement('button')
btn1.textContent = '关于'
btn2.textContent = '分类'
document.body.append(btn1)
document.body.append(btn2)
btn1.onclick = function() {
//传递魔法注释那么打包出来的文件名是 about_chunk.js
import(/* webpackChunkName: "about" */'./router/about').then(res => {
res.about()
res.default()
})
}
btn2.onclick = function() {
//传递魔法注释那么打包出来的文件名是 about_chunk.js
import(/* webpackChunkName: "category" */'./router/category')
}
webpack.config.js
output: {
clean: true,
path: path.resolve(__dirname, './build'),
// placeholder
filename: '[name]-bundle.js',
// 单独针对分包的文件进行命名
chunkFilename: '[name]_chunk.js'
},
webpack性能优化-分包处理-自定义分包
这种优化方案我们只需要通过配置一下就可以实现自定义分包 配置一下optimization很细节
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'production',
devtool: false,
// entry: './src/index.js',
entry: './src/main.js',
output: {
clean: true,
path: path.resolve(__dirname, './build'),
// placeholder
filename: '[name]-bundle.js',
// 单独针对分包的文件进行命名
chunkFilename: '[name]_chunk.js',
// publicPath: 'http://coderwhycdn.com/'
},
resolve: {
extensions: ['.js', '.json', '.wasm', '.jsx', '.ts']
},
devServer: {
static: ['public', 'content'],
port: 3000,
compress: true,
proxy: {
'/api': {
target: 'http://localhost:9000',
pathRewrite: {
'^/api': ''
},
changeOrigin: true
}
},
historyApiFallback: true
},
// 自定义分包的优化配置
optimization: {
// 设置生成的chunkId的算法
// development: named
// production: deterministic(确定性)
// webpack4中使用: natural
// 最佳实践:
// 开发过程中,我们推荐使用named;
// 打包过程中,我们推荐使用deterministic;
chunkIds: 'named',
// 分包插件: SplitChunksPlugin
splitChunks: {
chunks: "all",
// 当一个包大于指定的大小时, 继续进行拆包
// maxSize: 20000,
// // 将包拆分成不小于minSize的包
// minSize: 10000,
minSize: 10,
// 自己对需要进行拆包的内容进行分包
cacheGroups: {
// 只要是引用了utils下的代码你就给我单独打包
utils: {
test: /utils/,
// 生产的文件的名称 --这里的id是由chunkIds的值来决定的
filename: "[id]_utils.js"
},
vendors: {
// 只要是引入了node_modules下的代码你就给我单独打包
test: /[\\/]node_modules[\\/]/,
// 生产的文件的名称 --这里的id是由chunkIds的值来决定的
filename: "[id]_vendors.js"
}
}
},
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: "babel-loader",
}
},
{
test: /\.ts$/,
use: 'babel-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
})
]
}
上面的配置中 chunkIds 有啥用的呢?
如果你是顺序id你在改了一个文件 他全都要重新打包重新生成很不银杏
如果你是随机的不变的id 那么他去改变的就是你改的文件那么你么有动的文件他不去动
而且在用户浏览器也是会对比id 当id不没变浏览器就不会重新下载 当id变了浏览器就会去下载 所以在打包的时候我们设置为不变的id上面有写最佳实践
这也是webpack5 的新特性也可以和面试官聊一下
预加载和预获取
- prefetch(预获取):将来某些导航下可能需要的资源
- preload(预加载):当前导航下可能需要资源
比如当前有个页面 home,在home中有 一些子页面 (手机,电脑,箱包,家具,电器)等等,当用户访问我们的网站时候他可能并不会第一时间去点击子页面可能会在主页面home中闲逛一会那么这时候就是浏览器闲置下载时间这个时候我们可以去预加载一下子页面方便用户点击子页面的时候能快速访问到我们的子页面
想要实现预加载的功能我们需要在需要预加载的文件中添加一个魔法注释 /*webpackPreload:true*/
那么这个文件就会在父包下载完成后去下载
与 prefetch 指令相比, preload 指令有许多不同之处:
preload chunk 会在父 chunk 加载时,以并行方式开始加载。 prefetch chunk 会在父 chunk 加载结束后开始加载。
preload chunk 具有中等优先级,并立即下载。 prefetch chunk 在浏览器闲置时下载。
preload chunk 会在父 chunk 中立即请求,用于当下时刻。 prefetch chunk 会用于未来的某个时刻
CDN引入
比如在你的index.js中有两个库 react和axios 这两个包我们实际上可以使用他的cdn地址 这样的话可以大大优化我们的网站性能
首先你需要在你的webpack.config.js中 把这两个包给排除掉就是在打包的时候不要给我打包这两个包
这两个包我需要从cdn服务器上拿
index.js代码
import React from 'react';
import axios from 'axios'
webpack.config.js
externals: {
React: 'React',
axios: 'axios',
},
在这里写上你需要忽略的库 比如react 我在引入的时候是大写引入React 所以你这里的key 就是大写react 那么value呢这个value取决于你的cdn的名字 比如cdn是大写就就写大写 不能乱写的
webpack提取css
你编写的css需要在你的index.js中进行引入一下要不然我估计不会编译
首先我们需要安装一下处理css的插件
npm i css-loader
第二步安装提取css的插件
npm install mini-css-extract-plugin -D
配置webpack来对.css文件进行处理
{
test: /\.css$/,
use: [
// 'style-loader', 开发阶段
MiniCssExtractPlugin.loader, // 生产阶段
'css-loader',
],
},
第二步你需要引入该插件在webpack.config.js中用来提取css文件
const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’)
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
// 完成css的提取
new MiniCssExtractPlugin({
filename: 'css/[name].css',
chunkFilename: 'css/[name]_chunk.css',
}),
],
这样我们就能把我们的css文件提取到一个单独的文件夹中方便我们进行管理
Terser
当你面试被问到webpack优化你也可以聊到Teser
什么是Terser呢?
Terser是一个JavaScript的解释(Parser)、 Mangler(绞肉机) 、Compressor(压缩机)的工具集;
早期我们会使用 uglify-js来压缩、丑化我们的JavaScript代码,但是目前已经不再维护,并且不支持ES6+的语法;
Terser是从 uglify-es fork 过来的,并且保留它原来的大部分API以及适配 uglify-es和uglify-js@3等;
也就是说, Terser可以帮助我们压缩、丑化我们的代码,让我们的bundle变得更小。
因为Terser是一个独立的工具,所以它可以单独安装:
# 全局安装
npm install terser -g
# 局部安装
npm install terser -D
我们可以在命令行中使用Terser来压缩js:
terser [input files] [options]
# 举例说明
terser js/file1.js -o foo.min.js -c -m
Compress option (-c的选项)
arrows: class或者object中的函数,转换成箭头函数;
arguments:将函数中使用 arguments[index]转成对应的形参名称;(如果你使用了arguments[1]来获取参数他会直接转换为具体的比变量名)
dead_code:移除不可达的代码(tree shaking); (死代码移除)
Mangle option (-m的选项)
toplevel:默认值是false,顶层作用域中的变量名称,进行丑化(转换)比如var name 他会给你转换为 var o等等;
keep_classnames:默认值是false,是否保持依赖的类名称 ;
keep_fnames:默认值是false,是否保持原来的函数名称;
terser abc.js -o foo.min.js -c arrows=true,arguments=true,dead_code=true -m toplevel=true,keep_classnames=true,keep_fnames=true
其他属性可以查看文档
- https://github.com/terser/terser#compress-options
- https://github.com/terser/terser#mangle-options
我们可以配置webpack来支持Terser压缩js
前提是你对于webpack处理的代码不太满意你可以自定义压缩代码
- 在webpack中有一个minimizer属性,在production模式下,默认就是使用TerserPlugin来处理我们的代码的;
- 如果我们对默认的配置不满意,也可以自己来创建TerserPlugin的实例,并且覆盖相关的配置;
我们首先要把环境切换为production 模式来调试代码
第二步在webpack.config.js中 配置optimization
在optimization追加配置项
// 优化配置
optimization: {
.....
// 代码压缩配置
minimize: true, //你需要打开minimize该配置在production环境下是打开的 但是在Development模式下是不开启的
// 自定义压缩的配置
minimizer: [
// type Array
// js压缩配置
new TerserPlugin({
extractComments: false,
terserOptions: {
compress: {
arrows: true,
arguments: true,
dead_code: true,
},
// 丑化代码配置
mangle: true,
toplevel: true,
keep_classnames: true,
keep_fnames: true,
},
}),
// css压缩配置
],
},
配置解析
- extractComments:默认值为true,表示会将注释抽取到一个单独的文件中;
✓ 在开发中,我们不希望保留这个注释时,可以设置为false; - parallel:使用多进程并发运行提高构建的速度,默认值是true
✓ 并发运行的默认数量: os.cpus().length - 1;
✓ 我们也可以设置自己的个数,但是使用默认值即可; - terserOptions:设置我们的terser相关的配置
✓ compress:设置压缩相关的选项;
✓ mangle:设置丑化相关的选项,可以直接设置为true;
✓ toplevel:顶层变量是否进行转换;
✓ keep_classnames:保留类的名称;
✓ keep_fnames:保留函数的名称;
我们可以配置webpack来支持Teser压缩css
css压缩一般是去除空格
npm install css-minimizer-webpack-plugin -D
在optimization.minimizer中配置
// css压缩配置
// CSS压缩的插件: CSSMinimizerPlugin
new CSSMinimizerPlugin({
// parallel: true 开不开无所谓 好像他默认就开了
}),
webpack.config.js的抽取分离
首先我们需要对pack.json进行配置
Development —开发环境
Production —生成环境也就是上线环境
对于两种环境我们对于代码的调试也是不同的所以我们首先就要对pack.json进行配置让他在进行不同的打包的时候携带不同的表示
"build": "webpack --config ./config/comm.config.js --env production",---上线环境
"serve": "webpack serve --config ./config/comm.config.js --env development",---开发环境
接下来对webpack.config.js进行抽取
我们在项目根目录下创建一个config文件夹在该文件夹下有三个文件 comm.config.js(公共的配置文件) dev.config.js(开发的配置文件) pron.config.js(上线的配置文件)
comm.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { merge } = require('webpack-merge');
const devConfig = require('./dev.config');
const pronConfig = require('./pron.config');
const getCommonConfig = (isProdution) => {
return {
entry: './src/index.js',
output: {
clean: true,
path: path.resolve(__dirname, '../build'),
// placeholder
filename: '[name]-bundle.js',
// 单独针对分包的文件进行命名
chunkFilename: '[name]_chunk.js',
// publicPath: 'http://coderwhycdn.com/'
},
resolve: {
extensions: ['.js', '.json', '.wasm', '.jsx', '.ts'],
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.ts$/,
use: 'babel-loader',
},
{
test: /\.css$/,
use: [
// 'style-loader', 开发阶段
// MiniCssExtractPlugin.loader, // 上线阶段
isProdution ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
],
};
};
module.exports = function (env) {
const isProduction = env.production;
if (isProduction) {
console.log('上线环境');
} else {
console.log('开发环境');
}
let mergeConfig = isProduction ? pronConfig : devConfig;
return merge(getCommonConfig(isProduction), mergeConfig);
};
该公共配置文件需要提供入口、出口、resolve、plugins(主要是单独打包html的模块)、module(无论是开发还是上线我们都需要,它里面的css比较特殊开发和上线用的是不同的模块所以我们可以把该公共模块的文件重构为一个函数 该函数接口一个值用来判断是开发环境还是上线环境),注意该公共模块在暴露的时候也写成一个函数来暴露 这样我们可以做很多操作比如判断环境合并环境等等
dev.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const devConfig = {
mode: 'development',
devServer: {
static: ['public', 'content'],
port: 3000,
compress: true,
proxy: {
'/api': {
target: 'http://localhost:9000',
pathRewrite: {
'^/api': '',
},
changeOrigin: true,
},
},
historyApiFallback: true,
},
plugins: [],
};
module.exports = devConfig;
在开发环境中我们不需要做太多的优化配置只需要配置一个跨域以及静态文件目录就行
pron.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const pronConfig = {
mode: 'production',
externals: {
React: 'React',
axios: 'axios',
},
// 优化配置
optimization: {
// 设置生成的chunkId的算法
// development: named
// production: deterministic(确定性)
// webpack4中使用: natural
// 最佳实践:
// 开发过程中,我们推荐使用named;
// 打包过程中,我们推荐使用deterministic;
chunkIds: 'named',
// 分包插件: SplitChunksPlugin
runtimeChunk: {
name: 'runtime',
},
splitChunks: {
chunks: 'all',
// 当一个包大于指定的大小时, 继续进行拆包
// maxSize: 20000,
// // 将包拆分成不小于minSize的包
// minSize: 10000,
minSize: 10,
// 自己对需要进行拆包的内容进行分包
cacheGroups: {
// 只要是引用了utils下的代码你就给我单独打包
utils: {
test: /utils/,
// 生产的文件的名称 --这里的id是由chunkIds的值来决定的
filename: '[id]_utils.js',
},
vendors: {
// 只要是引入了node_modules下的代码你就给我单独打包
test: /[\\/]node_modules[\\/]/,
// 生产的文件的名称 --这里的id是由chunkIds的值来决定的
filename: '[id]_vendors.js',
},
},
},
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].css',
chunkFilename: 'css/[name]_chunk.css',
}),
],
};
module.exports = pronConfig;
上线配置文件就需要很多配置比如项目代码的优化、提取css,以及对于一些文件名他也是依赖于我们的环境的我们也可以写成一个函数用来判断环境
Tree Shaking
-
Tree Shaking是一个术语,在计算机中表示消除死代码(dead_code)
-
最早的想法起源于LISP,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求我们在进行函数式
编程时,尽量使用纯函数的原因之一) -
后来Tree Shaking也被应用于其他的语言,比如JavaScript、 Dart;
-
比如当前有个index.js文件里面导出两个函数
webpack实现Tree Shaking (处理js)
事实上webpack实现Tree Shaking采用了两种不同的方案:
- usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的;
- sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用;
方案一:usedExports
搭建环境 index.js负责引入函数调用函数 main.js用于输出函数 在main.js中输出了两函数 export { sum, sub };
,在index.js中引入了sum函数并且进行了调用 这时候你如果不配置treeshakingwebpack会把所有的代码都打包,很不银杏
使用这个方案前你要把你的环境设为development 应为production 我们很难看到代码是否优化
首先打开你的webpack.config.js 在optimization中设置usedExports为true(该设置在development 是默认关闭的 在production环境下是开启的),当你打开他以后你去打包会发现,打包后的文件会有一段注释: unused harmony export mul;这段注释的意义是什么呢?告知Terser在优化时,可以删除掉这段代码;然而他并没有帮我们删除没有用到的代码现在你还需要去开启minimize:true要不然他不会帮你删除
当minimize:true时
- usedExports设置为false时,没有用的函数没有被移除掉;
- usedExports设置为true时, 没有用的函数有被移除掉;
所以, usedExports实现Tree Shaking是结合Terser来完成的。
方案二:sideEffects
sideEffects用于告知webpack compiler哪些模块时有副作用的:
副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过export来判断这段代码的意义;
副作用的问题,在讲React的纯函数时是有讲过的;
◼ 在package.json中设置sideEffects的值:
如果我们将sideEffects设置为false,就是告知webpack可以安全的删除未用到的exports;
如果有一些我们希望保留,可以设置把sideEffects设置为数组;
"sideEffects": ["./src/myfunction.js","*.css"],//表示css文件给我们保留即使你没有用浏览器也要用所以我们需要保留一下css文件或者一些js文件你当前文件并没有使用但是你导入了 如果你设置为false他会把没有用的导入都给删掉 如果我们需要保留呢你就在数组中给我写上文件路径保留一下就行
◼ 比如我们有一个format.js、 style.css文件:
该文件在导入时没有使用任何的变量来接受;
那么打包后的文件,不会保留format.js、 style.css相关的任何代码;
最佳实践
- 在optimization中配置usedExports为true,来帮助Terser进行优化;
- 在package.json中配置sideEffects,直接对模块进行优化;
webpack实现Tree Shaking (处理css)
第一步:安装glob
npm i glob (windows建议安装7.X版本要不然有问题)
"glob": "7.*"
第二步:在你的webpack.config.js中引入glob
const glob = require(‘glob’)
第三步:在你的webpack.config.js中的plugins中添加该配置即可实现
// 对CSS进行TreeShaking
new PurgeCSSPlugin({
paths: glob.sync(`${path.resolve(__dirname, '../src')}/**/*`, { nodir: true }),
safelist: function() {
return {
standard: ["body"]
}
}
})
paths:表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob;
默认情况下, Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性;
purgecss也可以对less文件进行处理(所以它是对打包后的css进行tree shaking操作);
Scope Hoisting
什么是Scope Hoisting呢?
- Scope Hoisting从webpack3开始增加的一个新功能;
- 功能是对作用域进行提升,并且让webpack打包后的代码更小、运行更快;
默认情况下webpack打包会有很多的函数作用域,包括一些(比如最外层的) IIFE:
- 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数;
- Scope Hoisting可以将函数合并到一个模块中来运行;
使用Scope Hoisting非常的简单, webpack已经内置了对应的模块:
- 在production模式下,默认这个模块就会启用;
- 在development模式下,我们需要自己来打开该模块;
首先在webpack.config.js中引入const webpack = require(‘webpack’)
第二步在webpack.config.js中的plugins中写上这个代码
// 作用域提升
new webpack.optimize.ModuleConcatenationPlugin()
HTTP压缩
当浏览器在请求我们的代码的时候我们事实上可以让浏览器去请求我们的压缩后的代码事实上我们在之前的代码优化中我们已经做了很多优化压缩了,其实我们可以在前面的优化中继续优化继续压缩 压缩率一般可以维持在40%~50%之间
第一步安装:
npm install compression-webpack-plugin -D
第二步在webpack.config.js中引入CompressionPlugin
const CompressionPlugin = require(“compression-webpack-plugin”)
第三步配置webpack.config.js中的plugins
// 对打包后的文件(js/css)进行压缩
new CompressionPlugin({
test: /\.(js|css)$/,
algorithm: 'gzip'
压缩index.html 配置
new HtmlWebpackPlugin({
template: './index.html',
cache: true,// 只有我们第二次打包后的html有改变才重新打包
minify: isProdution? {
// 移除注释
removeComments: true,
// 移除属性
removeEmptyAttributes: true,
// 移除默认属性
removeRedundantAttributes: true,
// 折叠空白字符
collapseWhitespace: true,
// 压缩内联的CSS
minifyCSS: true,
// 压缩JavaScript
minifyJS: {
mangle: {
toplevel: true
}
}
}: false
}),
minify:默认会使用一个插件html-minifier-terser
自定义Loader
自定义loader的步骤
首先你自定义的loader需要是导出一个函数
wangfeng-loader01.js
module.exports = function (content, map, meta) {
//content:资源文件的内容;
//map: sourcemap相关的数据;
//meta:一些元数据;
console.log('wf_loader01:');
return content;
};
然后在你的webpack.config.js应用你自定义的loader
- 你需要在resolveLoader中定义你自定的loader所在的位置要不然他找不到
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './build'),
filename: 'bundle.js',
},
resolveLoader: {
// 定义一下你的loader所在的目录
modules: ['node_modules', './wf-loader'],
},
module: {
rules: [
{
test: /\.js$/,
use: ['wf-loader01', 'wf-loader02', 'wf-loader03'],
},
],
},
};
自定义loader之PitchLoader
module.exports = function (content, map, meta) {
console.log('wf_loader03:');
return content;
};
module.exports.pitch = function () {
console.log('wf_loader03-pitch');
};
自定义loader的执行顺序
在不改变任何配置的情况下 你自定义的loader如果没有pitch他是根据你在webpack.config.js中的应用顺序去反向执行
wf_loader03:
wf_loader02:
wf_loader01:
当你的自定义loader中有pitch时他是先顺序执行pitch函数再逆向执行loader函数
wf_loader01-pitch
wf_loader02-pitch
wf_loader03-pitch
wf_loader03:
wf_loader02:
wf_loader01:
其实webpack首先一上来会先扫描你的自定义loader(从前往后),然后会依次执行每个loader中的pitch,再然后去从最后一个执行到第一个loader
能不能改变执行顺序呢?
首先你需要明白一个概念 :
默认我们自定义的和官方提供的loader都是normal
如果你在module.exorts上加上了pitch 这个叫Pitching loader
别看他俩在一个文件其实是一个文件有两个loader
行内设置的loader叫做inline
-
在你引入某个加载的文件的时候在该引入的前面加上某个loader就能实现优先执行(inline方式)
import 'loader1!loader2!.....!loadern!./test.js'
-
也可以通过enforce设置 pre 和 post;
怎么做? 打开你的webpack.config.js 改成这种写法
rules: [ // { // test: /\.js$/, // use: ['wf-loader01', 'wf-loader02', 'wf-loader03'], // }, { test: /\.js$/, use: ['wf-loader01'], }, { test: /\.js$/, use: ['wf-loader02'], // 配置优先级 enforce:"pre" }, { test: /\.js$/, use: ['wf-loader03'], }, ],
通过把loader匹配分开 就能单独设置优先级了
优先级的执行顺序
Pitching :post, inline, normal, pre;
Normal : pre, normal, inline, post;
同步loader和异步loader
什么是同步loader呢?
-
默认创建的Loader就是同步的Loader;
-
这个Loader必须通过 return 或者 this.callback来返回结果,交给下一个loader来处理。(什么意思?就是你在编写loader的时候是不是返回了一个content叫做共享资源,这个东西就是用来传递个下一个loader来用的比如你在loader3的content中加上了一个aaa 传递给下一个,下一个打印content的时候就会带着aaa一起输出 以此类推);
我在03loader的content中加上了111111111那么第二个就可以拿到这个content
wf_loader03: wf_loader02: const message = 'wangfeng'; console.log(message); 111111111111111111111111111 wf_loader01:
-
通常在有错误的情况下,我们会使用 this.callback (待定);
module.exports = function (content, map, meta) { console.log('wf_loader03:'); this.callback(null,"你好02") //(this.callbacl也是可以返回结果的) }; module.exports.pitch = function () { console.log('wf_loader03-pitch'); };
什么是异步loader呢?
-
有时候我们使用Loader时会进行一些异步的操作;
-
我们希望在异步操作完成后,再返回这个loader处理的结果;
-
这个时候我们就要使用异步的Loader了;
实现方法:
loader-runner已经在执行loader时给我们提供了方法,让loader变成一个异步的loader:
this.callback的用法如下:
- 第一个参数必须是 Error 或者 null;
- 第二个参数是一个 string或者Buffer;
module.exports = function (content, map, meta) {
const callback = this.async();//拿到异步函数
setTimeout(() => {
console.log('wf_loader03:');
callback(null, '异步执行中...');//调用异步函数,返回异步的结果
}, 2000);
};
module.exports.pitch = function () {
console.log('wf_loader03-pitch');
};
wf_loader01-pitch
wf_loader02-pitch
wf_loader03-pitch
wf_loader03:
wf_loader02:
异步执行中...
wf_loader01:
自定义loader之获取参数
在之前我们使用 babel-loader的时候我们是不是可以在options中传递一些参数,我们在自定义loader的时候也是需要获取这个参数,对这些参数进行解析进行后续的操作
{
loader:"babel-loader",
options:{
plugins:[],
presets:[]
}
}
早期你需要使用解析库 loader-utils 来获取参数,现在你可以直接获取
webpack.config.js
{
test: /\.js$/,
use: [
{
loader: 'wf-loader03',
options: {
name: 'wangfeng',
age: 19,
},
},
],
},
自定义loader
module.exports = function (content, map, meta) {
const opstions = this.getOptions();
console.log(opstions);
console.log('wf_loader03:');
return content;
};
module.exports.pitch = function () {
console.log('wf_loader03-pitch');
};
参数的校验
我们可以通过一个webpack官方提供的校验库 schema-utils,安装对应的库:
自定义的loader
const { validate } = require('schema-utils');//1.拿到校验函数
const loader03Scheam = require('./schema/loader-03_schema.json');//2.获取创建的校验规则
module.exports = function (content, map, meta) {
// 参数校验
// 第一个参数是校验的规则你需要创建一个json来编写校验规则
// 第二个参数是你需要校验的对象options
const options = this.getOptions();
validate(loader03Scheam, options);//3.把你获取到的options传递进来
console.log(options);
console.log('wf_loader03:');
return content;
};
module.exports.pitch = function () {
console.log('wf_loader03-pitch');
};
校验json
{
"type": "object", // opstion的类型
"properties": { // 属性
"name": {
"type": "string", // 属性类型
"description": "请输入名称, 并且是string类型" // 属性不对的的提示
},
"age": {
"type": "number",
"description": "请输入年龄, 并且是number类型"
}
}
}
自定义loader练习一
尝试手写babel-loader 解析js (es5->es6)
我们不可能去一点点实现js的解析,反正我不行,所以我这个选择用@babel/core里面的工具来实现js的js,间接实现js的解析,学习一下bable-loader的原理,你也可以自己写一些校验函数去校验插件
第一步安装@bable/core : npm i @babel/core
const bable = require('@babel/core');
module.exports = function (content, map, meta) {
// 1.这里需要使用异步的bable因为你不确定什么时候能转换完成
const callback = this.async();
// 2.获取options
let options = this.getOptions();
// 3.如果没有传递插件或者预设我们去根目录下找babel.config.js
if (!Object.keys(options).length) {
options = require('../babel.config');
}
// 1.传入的内容 2.传入的插件或者预设
bable.transform(content, options, (err, res) => {
if (err) {
callback(err);
} else {
callback(null, res.code);
}
});
};
module.exports.pitch = function () {
console.log('wf_loader03-pitch');
};
webpack.config.js
{
test: /\.js$/,
use: [
{
loader: 'wf-loader03',
options: {
// plugins: [
// '@babel/plugin-transform-block-scoping',
// '@babel/plugin-transform-arrow-functions',
// ],
presets: ['@babel/preset-env'],
},
},
],
},
自定义loader练习一
尝试编写一个解析md文件的loader
要求:
webpack能解析我们的写的md文件,并且能在网页中展示(这不就是blog需要的吗?)
首先我们去安装一个库用来解析md文件
npm i marked -D
index.js文件
import code from './mymd.md';//拿到你解析好的语句
document.body.innerHTML = code;//写入语句
loader文件
const { marked } = require('marked')
const hljs = require('highlight.js')//npm i highlight.js
module.exports = function(content) {
// 让marked库解析语法的时候将代码高亮内容标识出来
//你有了这个库 他会把你的代码比如const 单独用一个标签框起来起一个类名 你可以写一点css高亮显示你的代码
//highlight.js默认也会为我们提供一些css样式你可以你你的lib文件夹下面看看自己用
// 用法: 在你的index.js中引入css样式表就行 import "highlight.js/styles/default.css"
//当然你也要配置webpack去解析css
//我感觉bing、github上面应该有很多大神写了很多样式 可以copy一下
marked.setOptions({
highlight: function(code, lang) {
return hljs.highlight(lang, code).value
}
})
// 将md语法转化成html元素结构
const htmlContent = marked(content)
// console.log(htmlContent)
// 返回的结果必须是模块化的内容
const innerContent = "`" + htmlContent + "`"
const moduleContent = `var code = ${innerContent}; export default code;`
return moduleContent
}
你在配置解析一下html这样你的md文件就能在网页中展示了
自定义plugin
我们知道webpack有两个非常重要的类: Compiler和Compilation
他们通过注入插件的方式,来监听webpack的所有生命周期;
插件的注入离不开各种各样的Hook,而他们的Hook是如何得到的呢?
其实是创建了Tapable库中的各种Hook的实例;
所以,如果我们想要学习自定义插件,最好先了解一个库: Tapable
Tapable是官方编写和维护的一个库;
Tapable是管理着需要的Hook,这些Hook可以被应用到我们的插件中;
Tapable有哪些Hook呢?
Tapable的Hook分类
◼ 同步和异步的:
以sync开头的,是同步的Hook;
以async开头的,两个事件处理回调,不会等待上一次处理回调结束后再执行下一次回调;
◼ 其他的类别
bail:当有返回值时,就不会执行后续的事件触发了;
Loop:当返回值为true,就会反复执行该事件,当返回值为undefined或者不返回内容,就退出事件;
Waterfall:当返回值不为undefined时,会将这次返回的结果作为下次事件的第一个参数;
Parallel:并行,不会等到上一个事件执行结束,才执行下一次事件处理回调;(两个一起执行)
Series:串行,会等待上一是异步的Hook;
syn
const { SyncHook } = require('tapable')
class HYCompiler {
constructor() {
this.hooks = {
// 1.创建hooks
syncHook: new SyncHook(["name", "age"])
}
// 2.用hooks监听事件(自定义plugin)
this.hooks.syncHook.tap("event1", (name, age) => {
console.log("event1事件监听执行了:", name, age)
})
this.hooks.syncHook.tap("event2", (name, age) => {
console.log("event1事件监听执行了:", name, age)
})
}
}
const compiler = new HYCompiler()
// 3.发出去事件
setTimeout(() => {
compiler.hooks.syncHook.call("why", 18)
}, 2000);
bail
const { SyncBailHook } = require('tapable')
class HYCompiler {
constructor() {
this.hooks = {
// 1.创建hooks
// bail的特点: 如果有返回值, 那么可以阻断后续事件继续执行
bailHook: new SyncBailHook(["name", "age"])
}
// 2.用hooks监听事件(自定义plugin)
this.hooks.bailHook.tap("event1", (name, age) => {
console.log("event1事件监听执行了:", name, age)
return 123
})
this.hooks.bailHook.tap("event2", (name, age) => {
console.log("event1事件监听执行了:", name, age)
})
}
}
const compiler = new HYCompiler()
// 3.发出去事件
setTimeout(() => {
compiler.hooks.bailHook.call("why", 18)
}, 2000);
Loop
const { SyncLoopHook } = require('tapable')
let count = 0
class HYCompiler {
constructor() {
this.hooks = {
// 1.创建hooks
// bail的特点: 如果有返回值, 那么可以阻断后续事件继续执行
loopHook: new SyncLoopHook(["name", "age"])
}
// 2.用hooks监听事件(自定义plugin)
this.hooks.loopHook.tap("event1", (name, age) => {
if (count < 5) {
console.log("event1事件监听执行了:", name, age)
count++
return true
}
})
this.hooks.loopHook.tap("event2", (name, age) => {
console.log("event1事件监听执行了:", name, age)
})
}
}
const compiler = new HYCompiler()
// 3.发出去事件
setTimeout(() => {
compiler.hooks.loopHook.call("why", 18)
}, 2000);
Waterfall
const { SyncWaterfallHook } = require('tapable')
class HYCompiler {
constructor() {
this.hooks = {
// 1.创建hooks
// bail的特点: 如果有返回值, 那么可以阻断后续事件继续执行
waterfallHook: new SyncWaterfallHook(["name", "age"])
}
// 2.用hooks监听事件(自定义plugin)
this.hooks.waterfallHook.tap("event1", (name, age) => {
console.log("event1事件监听执行了:", name, age)
return {xx: "xx", yy: "yy"}
})
this.hooks.waterfallHook.tap("event2", (name, age) => {
console.log("event1事件监听执行了:", name, age)
})
}
}
const compiler = new HYCompiler()
// 3.发出去事件
setTimeout(() => {
compiler.hooks.waterfallHook.call("why", 18)
}, 2000);
Parallel
const { AsyncParallelHook } = require('tapable')
class HYCompiler {
constructor() {
this.hooks = {
// 1.创建hooks
// bail的特点: 如果有返回值, 那么可以阻断后续事件继续执行
parallelHook: new AsyncParallelHook(["name", "age"])
}
// 2.用hooks监听事件(自定义plugin)
this.hooks.parallelHook.tapAsync("event1", (name, age) => {
setTimeout(() => {
console.log("event1事件监听执行了:", name, age)
}, 3000);
})
this.hooks.parallelHook.tapAsync("event2", (name, age) => {
setTimeout(() => {
console.log("event2事件监听执行了:", name, age)
}, 3000);
})
}
}
const compiler = new HYCompiler()
// 3.发出去事件
setTimeout(() => {
compiler.hooks.parallelHook.callAsync("why", 18)
}, 0);
Series
const { AsyncSeriesHook } = require('tapable')
class HYCompiler {
constructor() {
this.hooks = {
// 1.创建hooks
// bail的特点: 如果有返回值, 那么可以阻断后续事件继续执行
seriesHook: new AsyncSeriesHook(["name", "age"])
}
// 2.用hooks监听事件(自定义plugin)
this.hooks.seriesHook.tapAsync("event1", (name, age, callback) => {
setTimeout(() => {
console.log("event1事件监听执行了:", name, age)
callback() //只有你调用callback才执行下面的事件 类似于koa中的next
}, 3000);
})
this.hooks.seriesHook.tapAsync("event2", (name, age, callback) => {
setTimeout(() => {
console.log("event2事件监听执行了:", name, age)
callback()
}, 3000);
})
}
}
const compiler = new HYCompiler()
// 3.发出去事件
setTimeout(() => {
compiler.hooks.seriesHook.callAsync("why", 18, () => {
console.log("所有任务都执行完成~")
})
}, 0);
自定义插件练习
尝试编写一个将静态文件自动上传到服务器
情景:
我们在开发项目过程中会有很对静态资源,打包后会放到static 、assets,那么这些资源我们是需要进行部署到静态资源服务器的,这点很重要。
如何部署呢?
-
手动部署:上传,拷贝u盘拷贝到服务器中,使用工具上传等等。
-
自动部署:Jenkins等等。
-
我们选择:使用插件部署 当我们打包代码后就给我直接上传。
自定义插件开发流程:
- 创建AutoUploadWebpackPlugin类;
- 编写apply方法:
- 通过ssh连接服务器;
- 删除服务器原来的文件夹;
- 上传文件夹中的内容;
- 在webpack的plugins中,使用AutoUploadWebpackPlugin类
我们这个插件的执行时机应该是在wbeapck编译完成后输出内容完成后的时候
参考文档可以发现我们应该在afterEmit事件中执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nq6b7j1j-1691301477381)(C:\Users\WangFeng\Desktop\webpack笔记\4.png)]
https://webpack.docschina.org/api/compiler-hooks/
阅读文档发现他是一个AsyncSeriesHook事件,那么我们就要通过tapAsync来监听通过callAsync来触发事件这点在前面Tapbale中我们有讲到过,继续阅读发现他的回调参数是compilation所以我们需要接收一下还有callback
上传插件主要代码
const { NodeSSH } = require('node-ssh');
const { PASSWORD } = require('./config');
class AutoUploadWebpackPlugin {
constructor(options) {
this.ssh = new NodeSSH();
this.options = options;
}
apply(compiler) {
// console.log("AutoUploadWebpackPlugin被注册:")
// 完成的事情: 注册hooks监听事件
// 等到assets已经输出到output目录上时, 完成自动上传的功能
compiler.hooks.afterEmit.tapAsync(
'AutoPlugin',
async (compilation, callback) => {
// 1.获取输出文件夹路径(其中资源)
const outputPath = compilation.outputOptions.path;
// 2.连接远程服务器 SSH
await this.connectServer();
// 3.删除原有的文件夹中内容
const remotePath = this.options.remotePath;
this.ssh.execCommand(`rm -rf ${remotePath}/*`);
// 4.将文件夹中资源上传到服务器中
await this.uploadFiles(outputPath, remotePath);
// 5.关闭ssh连接
this.ssh.dispose();
// 完成所有的操作后, 调用callback()
callback();
},
);
}
async connectServer() {
await this.ssh.connect({
host: this.options.host,
username: this.options.username,
password: this.options.password,
});
console.log('服务器连接成功');
}
async uploadFiles(localPath, remotePath) {
const status = await this.ssh.putDirectory(localPath, remotePath, {
recursive: true,
concurrency: 10,
});
if (status) {
console.log('文件上传服务器成功~');
}
}
}
module.exports = AutoUploadWebpackPlugin;
module.exports.AutoUploadWebpackPlugin = AutoUploadWebpackPlugin;
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AutoUploadWebpackPlugin = require('./plugins/AutoUploadWebpackPlugin')
const { PASSWORD } = require('./plugins/config')
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "./build"),
filename: "bundle.js"
},
plugins: [
new HtmlWebpackPlugin(),
new AutoUploadWebpackPlugin({
host: "123.207.32.32",
username: "root",
password: PASSWORD,
remotePath: "/root/test"
})
]
}
以上就是在前端开发中我们需要的一些配置以及优化,最后建议学习一下webapck源码对我们的开发很有帮助
ssh’);
const { PASSWORD } = require(‘./config’);
class AutoUploadWebpackPlugin {
constructor(options) {
this.ssh = new NodeSSH();
this.options = options;
}
apply(compiler) {
// console.log(“AutoUploadWebpackPlugin被注册:”)
// 完成的事情: 注册hooks监听事件
// 等到assets已经输出到output目录上时, 完成自动上传的功能
compiler.hooks.afterEmit.tapAsync(
‘AutoPlugin’,
async (compilation, callback) => {
// 1.获取输出文件夹路径(其中资源)
const outputPath = compilation.outputOptions.path;
// 2.连接远程服务器 SSH
await this.connectServer();
// 3.删除原有的文件夹中内容
const remotePath = this.options.remotePath;
this.ssh.execCommand(`rm -rf ${remotePath}/*`);
// 4.将文件夹中资源上传到服务器中
await this.uploadFiles(outputPath, remotePath);
// 5.关闭ssh连接
this.ssh.dispose();
// 完成所有的操作后, 调用callback()
callback();
},
);
}
async connectServer() {
await this.ssh.connect({
host: this.options.host,
username: this.options.username,
password: this.options.password,
});
console.log(‘服务器连接成功’);
}
async uploadFiles(localPath, remotePath) {
const status = await this.ssh.putDirectory(localPath, remotePath, {
recursive: true,
concurrency: 10,
});
if (status) {
console.log(‘文件上传服务器成功~’);
}
}
}
module.exports = AutoUploadWebpackPlugin;
module.exports.AutoUploadWebpackPlugin = AutoUploadWebpackPlugin;
webpack.config.js
```js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AutoUploadWebpackPlugin = require('./plugins/AutoUploadWebpackPlugin')
const { PASSWORD } = require('./plugins/config')
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "./build"),
filename: "bundle.js"
},
plugins: [
new HtmlWebpackPlugin(),
new AutoUploadWebpackPlugin({
host: "123.207.32.32",
username: "root",
password: PASSWORD,
remotePath: "/root/test"
})
]
}
以上就是在前端开发中我们需要的一些配置以及优化,最后建议学习一下webapck源码对我们的开发很有帮助