带你复习前端工程化Webpack—Babel—Rollup—Vite【万字总结】

394 阅读19分钟

Webpack

概念 | webpack 中文文档 | webpack 中文文档 | webpack 中文网 (webpackjs.com)

跨域解决方案

Mode配置项

mode配置项可以告知webpack使用相应模式的内置优化

默认production

可选:'none'|'development'|'production'

  • development会将 DefinePluginprocess.env.NODE_ENV 的值设置为 development. 为模块和 chunk 启用有效的名。
    production会将 DefinePluginprocess.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPluginTerserPlugin
    none不使用任何默认优化选项

devtool

Source-map

我们编写的代码运行到浏览器上时,是通过打包压缩的。

但代码报错需要调试时,浏览器总能定位到源代码(未打包的文件)的错误行。这是为什么呢?

原因:Source-map

Source-map是从已转换的代码,映射到原始的源文件

使浏览器可以重构源文件并再调试器中显示重建的源代码

如何使用source-map

第一步:webpack.config.js中设置:devtool:"source-map"

module.exports = {
	mode: "production",
	devtool: "source-map",
	entry: "./src/main.js",
	output: {
		path: path.resolve(__dirname, "./build"),
		filename: "bundle.js",
	},
};

第二步:

在打包后的文件中,最后加入一个注释,它指向sourcemap

//#sourceMappingURL=common.bundle.js.map

浏览器会根据我们的注释,查找相应的source-map,并根据source-map还原我们的代码,方便我们调试

注意:需要在浏览器开启JavaScript源映射,浏览器默认开启

分析source-map文件

//bundle.map.js


uTools_1677317748748

Source Map的原理探究 | Fundebug博客 - 一行代码搞定BUG监控 - 网站错误监控|JS错误监控|资源加载错误|网络请求错误|小程序错误监控|Java异常监控|监控报警|Source Map|用户行为|可视化重现

Devtool

此选项控制是否生成,以及如何生成 source map。

共有26个可选项,来处理source-map,不同值,生产的source-map

Devtool | webpack 中文文档 | webpack 中文文档 | webpack 中文网 (webpackjs.com)

"production"模式下,devtool默认为"none",也就是说不会生成source map.

下列几个值不会生成source map:

  • false
  • none
  • eval:不生成map,但也可以方便我们调试,可不准确

uTools_1677318521651

一般prod环境下设置devtool:"source-map"

dev环境下devtool:"eval"

不常见的值:

  • eval-source-map:将map文件拼接到bundle文件里的eval函数的最后
  • inline-source-map:添加到文件的后面
  • cheap-source-map:更加高效,因为它不会生成列映射,用于dev环境
  • cheap-module-source-map: 对源自loader的source map处理更好
  • nosource-source-map:会生成source map但生成的source map 只有错误信息提示,不会生成源代码文件

Webpack搭建本地服务器

本地服务server

为了完成自动编译,webpack提供了几种可选方式:

  • webpack watch mode
  • webpack-dev-server(常用)
  • webpack-dev-middleware

webpack-dev-server使用:

  1. 安装

    npm i webpack-dev-serve -D
    
  2. 修改配置文件:

    devServer:{
    
    }
    

    webpack-dev-serve在编译后不会写入到任何输出文件,而是将bundle文件保留到内存中。事实上其使用了一个叫memory-fs的库。

server的静态资源

在index.html中自己引入了静态文件(js,img,css,font)时

<script src="./abc.js"></script>

webpack不会将这些静态文件打包到内存中,所以在运行服务时会找不到这些文件。

解决方法:设置static,在webpack找不到文件时,会自动到static里声明的文件下面去找

devServer: {
		static: ["public", "content"],
	},

static默认值为:"plublic"

build打包时,public文件夹会直接复制过去,而不会被打包

server的其他配置

hotOnly:

代码编译失败,修复后,是否刷新整个页面,不刷新则为true

host:

设置主机地址,默认值为localhost

设置0.0.0.0时,同网段下的主机可以通过ip地址访问

port:

端口

open:

true:编译成功后,自动打开浏览器

compress:

true: 开启gzip压缩 30-50%

server的proxy代理

解决跨域问题

proxy: {
			"/api": {
				target: "http://localhost:8080", //代理到服务器地址
				pathReWrite: {
					"^/api": "",//将客户端以/api开头的请求替换为http://localhost:8080
				},
                changeOrigin: true,
			},
		},

因为webpack是node端,而跨域问题只有浏览器才有限制,所以,本地node服务器请求api服务端,不会存在跨域问题

前端请求——》devServer开启本地服务器 代理——》api服务器

changeOrigin作用

是否更新 代理后请求头的host地址

使用代理后,请求头的host仍是前端的主机地址,而不是devServer代理的地址,当请求后端api接口时,如果后端做了验证,如 if(req.host!==后端服务器的地址):反爬措施 。则前端拿不到后端数据。此时开启changeOrigin,会将req.host更改。

historyApiFallback

解决SPA页面在路由跳转后,进行页面刷新时,返回404的错误

在hash路由下,路由跳转不会刷新网页,而是更新组件DOM。但当手动点击浏览器刷新时,浏览器会认为你在请求服务器的目录里的文件(静态资源),如:www.baidu.com/about,此时肯定是…

默认为false

true:刷新时,返回404错误时,会自动返回index.html的内容

原理:connect-history-api-fallback库

Babel

什么是Babel?

Babel是一个工具链,将TS/JSX/ES6+ ——》普通的js代码 es5

功能包括:语法转换,源代码转换,Polyfil来实现目标环境缺少的功能等

Babel命令行的使用

npm i @babel/core @babel/cli -D
npx babel --version

安装babel插件

npm i @babel/plugin-transform-block-scoping -D

将./src下的文件使用plugin-transform-block-scoping插件转化到./build目录下:

npx babel ./src --out-dir ./build --plugins=@babel/plugin-transform-block-scoping

Babel的预设Preset

预设 · Babel 中文文档 (docschina.org)

因为需要转化的内容过多,一个个设置比较麻烦,所以我们可以使用预设

安装:

npm i @babel/preset-env -D

Babel底层原理

可以把Babel看出是一个编译器

Babel也具有编译器的工作流程:

  1. 解析阶段
  2. 转换阶段
  3. 生成阶段

uTools_1677339549821

Webpack和Babel结合的使用

安装babel-loader

npm i babel-loader @babel/core -D
  • clean:重新打包时,先将之前打包的文件删除掉(新版本特性),旧版则使用CleanWebpackPlugin()插件。
module.exports = {
	mode: "production",
	devtool: "source-map",
	entry: "./src/main.js",
	output: {
		path: path.resolve(__dirname, "./build"),
		filename: "bundle.js",
		clean: true,
	},
	module: {
		rules: [
			{
				test: /\.js$/,
               	//exclude: /node_modules/,
				use: {
					loader: "babel-loader",
					options: {
						plugins: [
							"@babel/plugin-transform-arrow-functions",
							"@babel/plugin-transform-block-scoping",
						],
					},
				},
			},
		],
	},
};

遇到.js结尾的文件时使用babel-loader,并使用Babel里的plugin-transform-arrow-functions,plugin-transform-block-scoping插件

由于要用的插件太多,上面只列举了一部分,所以一般使用presets预设

use: {
					loader: "babel-loader",
					options: {
						// plugins: [
						// 	"@babel/plugin-transform-arrow-functions",
						// 	"@babel/plugin-transform-block-scoping",
						// ],
						presets: ["@babel/presets-env"],
					},
				},

浏览器兼容性配置

css兼容处理:

  • postcss : css新特性/浏览器前缀——自动转换

js兼容性处理:

  • babel : js新特性,ES6-ES13——自动转换

代码是否要自动转换,取决于要适配的浏览器,否则会消耗性能

查询浏览器市场占有率

["Can I use" usage table

Browserslist:

在不同前端工具(babel,postcss)间,共享浏览器版本和node版本 的配置

Browserslist编写规则:

  1. defaults: browserslist的默认浏览器(市场占有率>0.5%,last 2 versions,not dead)。

  2. 5%:市场占有率>5%的浏览器

  3. dead:24个月内没有官方更新或维护的浏览器

  4. last 2 version: 每个浏览器的最新两个版本

配置Browserslist

npx browserslist ">1%,last 2 version,not dead"
  • 方法1:package.json中配置
"browserslist":[
    "> 1%",
    "last 2 version",
    "not dead"
]
  • 方法2: .browserslistrc文件里配置

    > 1%
    last 2 version
    not dead
    
    

Babel配置浏览器兼容

target配置:会覆盖browserslistrc里的配置,在开发中一般不使用。其只会设置babel里的兼容代码,而不会设置postcss等工具

						presets: [
							[
								"@babel/presets-env",
								{
									target: ">5%",
								},
							],
						],

在没有presets-env前(babel7前),babel使用的是stage-xx

如何区分Babel中的stage-0,stage-1,stage-2以及stage-3(一) - {前端开发} - 博客园 (cnblogs.com)

uTools_1677397016494

presets: [
							[
								"stage-1",
								{
									target: ">5%",
								},
							],
						],

Babel的配置文件

  • babel.config.json(.js):可以直接作用于Monorepos项目的子包,推荐

    //babel.config.js
    module.exports = {
    	presets: [
    		[
    			"@babel/presets-env",
    			{
    				target: ">5%",
    			},
    		],
    	],
    };
    
  • .**babelrc.json **(旧写法)

Babel和Polyfill

作用:

ES6+的其他特殊语法(Promise、Symbol)、特殊的API(includes,reduce) ——》ES5普通语法

使用方法:

npm i core-js regenerator-runtime
  1. babel.config.js

    • useBuiltIns:设置以什么方式来使用polyfill.可选值:
      • false:不使用polyfill,
      • "useage":自动检测源代码所需要的polyfill,推荐,
      • "entry":处理第三方库的代码,并且需要在入口文件添加"import 'core-js/stable';import 'regenerator-runtime;'"
    • corejs:设置corejs版本
    module.exports = {
    	presets: [
    		[
    			"@babel/presets-env",
    			{
    				corejs: 3,
    				useBuiltIns: "useage",
    			},
    		],
    	],
    };
    

React和TS解析

Babel解析React

在编写react代码时,react使用的语法是jsx,jsx是可以直接使用babel来转换的,使用如下插件处理:

  1. @babel/plugin-syntax-jsx
  2. @babel/plugin-transform-react-jsx
  3. @babel/plugin-transform-react-display-name

可以直接使用预设:@babel/preset-react

npm i @babel/preset-react -D
//babel.config.js
module.exports = {
	presets: [
		[
			"@babel/presets-env",
			{
				corejs: 3,
				useBuiltIns: false,
			},
		],
		["@babel/presets-react"],
	],
};

文件后缀名在resolve中声明后可以不写:

//webpack.config.js
resolve: {
		extensions: [".js", ".json", ".jsx"],
	},

Babel解析TS

使用loader解析ts:

可以将ts转换为js,可以在打包时检测类型错误,但不能使用polyfill

  1. 安装loader

    npm i ts-loader
    
  2. 创建ts.config.json文件

    tsc --init
    
  3. webpack.config.js中使用loader

    {
    				test: /\.ts$/,
    				use: "ts-loader",
    			},
    

使用babel解析ts(推荐使用):

可以添加polyfill,但在打包时不会检测类型错误

  1. webpack.config.js中使用babel-loader处理ts代码

    {
    				test: /\.ts$/,
    				use: "babel-loader",
    			},
    
  2. 安装预设@babel/preset-typescript

    npm i @babel/preset-typescript
    
  3. babel.config.js

    module.exports = {
    	presets: [
    		[
    			"@babel/presets-env",
    			{
    				corejs: 3,
    				useBuiltIns: false,
    			},
    		],
    		["@babel/presets-react"],
    		["@babel/presets-typescript"],
    	],
    };
    

TS处理最佳实践

使用 babel里的转换和polyfill + ts-loader里的类型校验

  1. 运行scripts脚本:package.json

    "ts-check-watch":"tsc --noEmit --watch"
    

Webpack性能优化

分类

  1. 打包后的结果,上线时的性能优化
  2. 优化打包速度,开发或者构建时优化打包速度

只打包到一个文件

  1. 所有东西放到一个包,不方便管理
  2. 包体积非常大,首屏渲染速度降低

代码分离(分包)

目的:将代码分离到不同的bundle中,之后可以按需加载,或者并行加载这些文件。

代码分离的三种方式:

  • 入口起点多入口加载):使用entry配置手动分离代码
  • 防止重复:使用Entry Dependencies或SplitChunkPlugin去重和分离代码
  • 动态导入:通过模块内嵌函数调用来分离代码

Webpack多入口依赖

手动分包:

  1. webpack.config.js配置entry多入口

    // entry:"./src/main.js"
    	entry: {
    		index: "./src/index.js",
    		main: "./src/main.js",
    	}, //多入口
            
            
        index.js里可以写react代码,main.js里可以写vue代码。以此可以窥见微前端的雏形。
    
  2. 修改output的文件:filename前使用[name]-占位符,以此来使不同入口(entry)文件输出到不同bundle文件

	output: {
		path: path.resolve(__dirname, "./build"),
		filename: "[name]-bundle.js",
		clean: true,
	},

手动分包的缺点:

​ 如果两个入口文件使用了相同的依赖,如都引用了axios。那么,打包时,每个bundle都会对axiso进行打包。此时产生重复打包依赖(index-bundle和main-bundle里都有一分axios的源码)。

解决方法:配置共享依赖

entry: {
		index: {
			import: "./src/index.js",
			dependOn: "shared1",
		},
		main: "./src/main.js",
		shared1: ["axios"],//依赖
		shared2: ["dayjs"],
		shared3: ["redux"],
	}, //多入口

这样,webapck打包时,会将shared单独打包到一个文件:sharde1-bundle.js,shared2-bundle.js

Webpack的动态导入

路由懒加载:import(组件),懒加载的组件会被webpack单独打包到一个分包里。依赖的是webpack的动态导入。

动态导入的两种方式:

  • import() 推荐:注意是import("../")函数而不是直接import "../" ,可以通过import.then(res=>{ res.default() })取得结果
  • require.ensure

动态导入的文件命名:

chunkFilename: "[id]-[name]-chunk.js"

output: {
		path: path.resolve(__dirname, "./build"),
		filename: "[name]-bundle.js",
		chunkFilename: "[id]-[name]-chunk.js",
		clean: true,
	},
  • [name]:占位符,打包时默认为文件名,在import()函数中可以通过魔法注释自己设置name
import(/* webpackChunkName:"about" */,'./about').then()

Optimization.SplitChunk Plugin

SplitChunksPlugin | webpack 中文文档 | webpack 中文文档 | webpack 中文网 (webpackjs.com)

该插件webpack默认安装和集成,直接使用即可

	//优化配置
	optimization: {
		splitChunks: {
			chunks: "all",
			//当一个包大于指定大小时,继续继续拆包
			maxSize: 20000, //20kb
			//包的最小体积
			minSize: 10000,
		},
	},
  • 默认配置:chunks仅针对异步(async)请求

  • "all":将第三方库全部打包到一个文件

自定义分包

	optimization: {
		splitChunks: {
			chunks: "all",
			//当一个包大于指定大小时,继续继续拆包
			// maxSize: 30000, //30kb
			//包的最小体积
			// minSize: 10000,//默认20kb
		},
		cacheGroups: {
			vendors: {
				test: /[\\/]node_modules[\\/]/,//匹配从node_module下引入的文件
				filename: "[name]_vendors.js",
			},
			utils: {
				test: /utils/,//匹配utils文件
				filename: "[name]_utils.js",
			},
		},
	},

TerserPlugin (js解释、压缩工具集)

webpack打包为bundle其实默认情况下未进行代码压缩,因为底层使用了TerserPlugin

早期使用uglify-js来压缩、丑化代码

作用:帮助我们压缩、丑化我们的代码,使bundle体积减小:如 变量message压缩(丑化)成ab

Terser为独立的工具,可以独立安装:

npm i terser -D

命令行使用:参数可以多选

  • -o :源文件

  • -c :compress(压缩)

    • arrows=true: class或者obj中的函数,转换成箭头函数
    • argiments=true:将函数中使用arguments[0]这种代码,直接转换成参数名称
    • dead_code=true:移除不可达的代码:if(false){}
  • -m :mangle(丑化)

    • toplevel=true:将变量丑化命名

    • keep_fnames=true:保留函数名字

npx terser abc.js -o adc.min.js -c xxx -m xxx

Terser在Webpack中配置

npm install terser-webpack-plugin --save-dev
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
   //Terser
		minimize: true, //告知webpack使用TerserPlugin压缩代码
		minimizer: [
			new TerserPlugin({
				extractComments: false, //注释是否需要提取到一个单独的文件中。	
                parallel: true, //使用多进程并发运行提高构建速度,默认为true
                //并行运行的默认数:os.cpus().length-1
				terserOptions: {
					compress: {
						arguments: true,
						arrows: true,
						unused: true,//没用到的函数删除
					},
					mangle: true,
					toplevel: true,
					keep_fnames: true,
				},
			}),
		],
  },
};

Optimization.ChunkIds

优化(Optimization) | webpack 中文文档 (docschina.org)

设置生成[id]的算法

  • “deterministic”:在生产模式中默认开启,在不同的编译中不变的短数字 id。有益于浏览器长期缓存。
  • "named": dev下默认开启,按name构建id。
  • “natural":按照数字顺序使用id

Prefetch和Preload

​ 在使用import()函数时,使用下面这些指令,来告知浏览器

  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要的资源

不同点:

  1. preload chunk会在父chunk加载时,以并行方式开始加载。prefetch chunk会在父chunk加载结束后加载
  2. preload具有中等优先级,并立即下载。prefetch在浏览器闲置时下载
  3. preload chunk在父chunk中立即请求,用于当下时刻。prefetch chunk会用于将来某个时刻

使用方法:魔法注释

import(
	/* WebpackChunkName:"about" */
	/* WebPrefetch:true */
	"./router/about.js"
).then((res) => {
	res.about();
	res.default();
});

CDN加速服务配置

开发中使用CDN的两种方式:

  1. 打包的所有静态资源,都放到CDN服务器
  2. 一些第三方资源放到CDN服务器上

publicPath:打包时使用指定cdn服务器上的资源

module.exports={
	output: {
		path: path.resolve(__dirname, "./build"),
		filename: "[name]-bundle.js",
		chunkFilename: "[id]-[name]-chunk.js",
		clean: true,
		publicPath:"https://www.baidu.cdn/"
	},
}

将第三方库放入CDN服务器:

1.打包时不再对第三方库打包

module.exports={
	//排除某些包不需要打包
	externals: {
		react: "React",
      //key属性名:排除的框架的名称
      //value:CDN中第三方库提供的名称
		axios: "axios",
	},
}

2.在html模块中,加入对应的cdn服务器地址

index.html

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Document</title>
	</head>
	<body>
		<div id="root"></div>
        //手动引入cdn
		<script src="cdn.cdn.cdn.axios"></script>
		<script src="cdn.cdn.cdn.axios"></script>
	</body>
</html>

Shimming(垫片)

例如:我们现在依赖一个第三方库,而第三方库依赖于lodash,但第三方库默认没有对lodash进行导入(因为它认为我们项目里默认有lodash),我们可以用ProvidePlugin实现shimming效果。

const { ProvidePlugin } = require("webpack");

module.exports={
    	...
    	plugins: [
		new ProvidePlugin({
			axios: ["axios",'default'],
            _:'lodash'
		}),
	],
}

PovidePlugin能够帮我们在每个模块在,通过一个变量获取package;

如果webpack看到这个模块,它最终在bundle中引入这个模块;

其为webpack默认插件,无需专门安装;

注意:axios导出默认为default .axios export as default

CSS样式的单独提取

npm i style-loader css-loader -D
  • css-loader: 加载css文件
  • style-loader:使css生效
module: {
		//处理js,jsx代码
		rules: [
			{
				test: /\.jsx?$/,
				use: {
					loader: "babel-loader",
				},
			},
			{
				test: /\.ts$/,
				use: "babel-loader",
			},
			//处理css代码
			{
				test: /\.css$/,
				use: ["style-loader", "css-loader"],
			},
		],
	},

注意先用css-loader,再用style-loaderwebpack使用loader顺序是从后往前

loader是链式传递的,对⽂件资源从上⼀个loader传递到下⼀个

MiniCssExtractPlugin

npm i mini-css-extract-plugin -D

将css打包到单独的文件

  1. 使用插件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");



plugins: [
		new ProvidePlugin({
			axios: "axios",
			_: "lodash",
		}),
		new MiniCssExtractPlugin({
			filename: "css/[name].css",
			chunkFilename: "css/[name]_chunk.css",
		}),
	],

​ 2.将style-loader替换成MiniCssExtractPlugin.loader

{
				test: /\.css$/,
				use: [
					// "style-loader"//开发阶段
					MiniCssExtractPlugin.loader, //生产阶段
					"css-loader",
				],
			},

CSS压缩

css-minimizer-webpack-plugin:去除无用的空格,底层使用cssnano来优化、压缩CSS

安装:

npm i css-minimizer-webpack-plugin -D
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports={
    new CssMinimizerPlugin({}),
}

Hash值的设置

hash:通过md4散列函数处理后,生成的128位(32位16进制)的hash值

分类:

  • hash:项目源代码不变,hash值不变。项目源代码变化则hash变化
  • ChunkHash
  • ContentHash(推荐):

uTools_1677683779649

DDL库(动态链接库)

能够共享,并且不经常改变的代码,抽取到一个共享的库;这个库在之后编译的过程在,会被引入到其他项目的代码中

Webpack多环境配置

Webpack可以指定配置文件

方法:在scripts中使用--config指定配置文件, --env xxxx指定环境,使用webpack-merge合并配置

package.json

"scripts": {
		"test": "echo \"Error: no test specified\" && exit 1",
		"serve": "webpack serve --config ./config/common.config.js --env development",
		"build": "webpack --config ./config/common.config.js --env production"
	},

Common.config.js

将配置文件导出为一个函数,接收一个参数isProd

/**
 * @type {import('webpack').Configuration}
 */
const path = require("path");
const { ProvidePlugin } = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { merge } = require("webpack-merge");
const devConfig = require("./dev.config");
const ProdConfig = require("./prod.config");
const prodConfig = require("./prod.config");
const CommonConfig = function (isProd) {
	return {
		devtool: "eval",
		// entry:"./src/main.js"
		entry: {
			main: "./src/main.js",
		}, //多入口
		output: {
			path: path.resolve(__dirname, "../build"),
			filename: "js/[name]-bundle.js",
			chunkFilename: "js/[id]-[name]-chunk.js",
			clean: true,
			// publicPath: "www.baidu.cdn",
		},
		resolve: {
			extensions: [".js", ".json", ".jsx", ".ts"],
		},
		module: {
			//处理js,jsx代码
			rules: [
				{
					test: /\.jsx?$/,
					use: {
						loader: "babel-loader",
					},
				},
				{
					test: /\.ts$/,
					use: "babel-loader",
				},
				//处理css代码
				{
					test: /\.css$/,
					use: [
						isProd ? MiniCssExtractPlugin.loader : "style-loader",
						"css-loader",
					],
				},
			],
		},
		plugins: [
			new HtmlWebpackPlugin({
				template: "./index.html",
			}),
			new ProvidePlugin({
				axios: ['axios,"default'],
				_: "lodash",
			}),
		],
	};
};

//webpack允许导出一个函数
module.exports = function (env) {
	const isProd = env.production;
	const mergeConfig = isProd ? prodConfig : devConfig;
	if (isProd) {
		console.log("生产环境");
		return;
	} else {
		console.log("开发环境");
	}
	return merge(CommonConfig(isProd), mergeConfig);
};

dev.config.js

/**
 * @type {import('webpack').Configuration}
 */
const path = require("path");
module.exports = {
	mode: "development",
	devServer: {
		static: ["public", "content"],
		// hotOnly: true,
		// host: "localhost",
		proxy: {
			"/api": {
				target: "http://localhost:8080", //代理到服务器地址
				pathReWrite: {
					"^/api": "",
				},
				changeOrigin: true,
			},
		},
	},
	plugins: [],
};

prod.config.js

/**
 * @type {import('webpack').Configuration}
 */
const path = require("path");
const { ProvidePlugin } = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
	mode: "production",
	//排除某些包不需要打包

	//优化配置
	optimization: {
		//分包插件
		splitChunks: {
			chunks: "all",
			//当一个包大于指定大小时,继续继续拆包
			// maxSize: 20000, //20kb
			//包的最小体积
			// minSize: 10000,
		},
		cacheGroups: {
			vendors: {
				test: /[\\/]node_modules[\\/]/,
				filename: "js/[name]_vendors.js",
			},
			utils: {
				test: /utils/,
				filename: "js/[name]_utils.js",
			},
		},
		chunkIds: "",
		//Terser
		minimize: true, //告知webpack使用TerserPlugin压缩代码
		minimizer: [
			new TerserPlugin({
				extractComments: false, //注释是否需要提取到一个单独的文件中。
				terserOptions: {
					compress: {
						arguments: true,
						arrows: true,
						unused: true, //没用到的函数删除
					},
					mangle: true,
					toplevel: true,
					keep_fnames: true,
				},
				parallel: true, //使用多进程并发运行提高构建速度,默认为true
				//并行运行的默认数:os.cpus().length-1
			}),
			new CssMinimizerPlugin({}),
		],
	},
	plugins: [
		new MiniCssExtractPlugin({
			filename: "css/[name].css",
			chunkFilename: "css/[name]_chunk.css",
		}),
	],
};

Tree-Shaking

作用:删除无用代码

实现tree-shaking的两种方案

  • useExports:需要将mode设置为development模式,开启后,在导入模块时,分析模块中哪些函数有被使用,哪些没有被使用。

    它在bundle文件中,将无用函数前添加/*unsed harmony export xxx*/魔法注释,并且不会在/* harmony export */处exports。Terser在检测到/*unsed harmony export xxx*/魔法注释时,会删除标记的函数。

    需要配合Terser里Compressunused: true使用

    在production模式下自动开启useExports

    module.exports={
        //导入模块时,分析模块中哪些函数有被使用,哪些没有被使用
    	usedExports: true,
        mode: "development",
    }
    
  • sideEffects:告诉webpack compiler哪些模块有副作用,false:所有文件都无副作用

    package.json

    数组:可以指明哪些文件有副作用

    {
    	"name": "source-map",
    	"version": "1.0.0",
    	"sideEffects":  [
    		"*.css" //保留css文件
    	],// ["./src/demo.js"],false,true
    	"main": "index.js",
    }
    

    编写代码时,尽量编写纯模块(无副作用的模块)

CSS实现Tree-shaking

早期使用PurifyCss,已淘汰。

PurgeCss

npm i purgecss-webpack-plugin -D
npm i glob@7.* -D
const {PurgeCSSPlugin}=require("purgecss-webpack-plugin");
const glob = require("glob");



plugins: [
		new ProvidePlugin({
			axios: "axios",
			_: "lodash",
		}),
		new MiniCssExtractPlugin({
			filename: "css/[name].css",
			chunkFilename: "css/[name]_chunk.css",
		}),
			new PurgeCSSPlugin({
			paths: glob.sync(`${path.resolve(__dirname, "./src")}/**/*`, { nodir }),
            safelist: function () {
				return {
					standard: ["html,body,button"],
				};
			}, //白名单
		}),
	],

glob.sync(`${path.resolve(__dirname, "../src")}/**/*`, { nodir }):获取匹配src下的所有文件,nodir:不是文件夹

Scope Hoisting

对作用域进行提升,并让打包后的代码更小,运行更快

默认情况下,webpack打包会有很多函数作用域,包括IIFE(立即执行函数)

production模式下默认开启

const webpack = require("webpack");
module.exports={
    mode:"production"
	plugins:[		
		new webpack.optimize.ModuleConcatenationPlugin(),
	]
}

Http压缩

gzip,deflate,br压缩文件

浏览器自动.gz文件进行解压,再执行里面的代码

压缩流程

  1. http数据再服务器发送前已经被压缩了(可在webpack中完成)
  2. 服务器发送请求时,告知服务器自己支持哪些压缩格式:Accept-Encoding:gzip,deflate
  3. 服务器再浏览器支持压缩的格式下,直接返回对应压缩后的文件,并在响应头里告知浏览器Content-Encoding:gzip

Webpack压缩文件

npm i compression-webpack-plugin -D
new CompressionPlugin({
			test: /\.(css|js)$/,
			threshold: 500, //设置文件多大开始压缩
			minRatio: 0.7, //至少压缩比例
			algorithm: "gzip",//压缩算法
			//include
			//exclude:
		}),

压缩HTML文件

HtmlWebpackPlugin配置压缩html

new HtmlWebpackPlugin({
			template: "./index.html",
			cache: true, //当文件改变时,才生产新的文件,默认true
    		inject: "head",//打包的资源插入的位置
			minify: isProd
				? {
						removeComments: true, //移除注释
						removeEmptyAttributes: true, //移除空属性
						removeRedundantAttributes: true, //移除多余属性
						collapseWitheSpace: true, //移除空行
						minifyCSS: true, //压缩内联css
						minifyJS: {
							mangle: {
								toplevel: true,
							},
						}, //压缩JS
				  }
				: false,
		}),

打包分析

测量每个loader,插件消耗的时间

npm i speed-measure-webpack-plugin -D
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();



module.exports = function (env) {
	const isProd = env.production;
	const mergeConfig = isProd ? prodConfig : devConfig;
	if (isProd) {
		console.log("生产环境");
		return;
	} else {
		console.log("开发环境");
	}
	const config = merge(CommonConfig(isProd), mergeConfig);
	return smp.wrap(config);//测速
};

文件分析

  • 生成stats.json文件

    ​ 1.build加入--profile --json=stats.json命令

    "scripts": {
    		"test": "echo \"Error: no test specified\" && exit 1",
    		"serve": "webpack serve",
    		"build": "webpack --config ./config/common.config.js --env production --profile --json=stats.json"
    	}
    

    2.Page not found · GitHub Pages进入网站,上传stats.json文件

  • webpack-bundle-analyzer

    npm i webpack-bundle-analyzer -D
    
    const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
    
    
    		//对打包结果进行分析
    		new BundleAnalyzerPlugin(),
    

    打包后会自动打开分析网页

Webpack源码分析

总体流程:

  1. 通过webpack(config)创建Compiler对象
  2. 执行Compiler.run(),开始对代码编译打包
  • before-run 清除缓存
  • run 注册缓存数据钩子
  • before-compile
  • compile 开始编译
  • make 从入口分析依赖以及间接依赖模块,创建模块对象
  • build-module 模块构建
  • seal 构建结果封装, 不可再更改
  • after-compile 完成构建,缓存数据
  • emit 输出到dist目录

CreateCompiler(options)

  1. 通过new创建compiler实例:const compiler=new Compiler(option.context,iptions)

  2. 注册所有webpack插件数组NodeEnviromentPlugin({}).apply(compiler)

    循环遍历options.plugins

    如果plugin是函数则将this绑定为compiler,并传入compliler参数.call(complier,complier)

    如果plugin是对象:则调用对象内的apply方法并传入complier。所以plugin对象需要有apply方法,且传参为complier:apply(compiler){

    }

    拿到complier后,可以通过complier.hook.compile.tap()监听编译事件

  3. 调用钩子enviroment/afterEnviroment函数:complier.hooks.enviroment.call()

  4. 使用process函数处理其他options:如entry/output/devtool(module不是):将这些选项new成插件,注册成插件:new SomePlugin().apply()process函数执行完,webpack将所有它关心的hook消息都注册完成,等待后续编译过程中挨个触发。

  5. return complier

完整代码

const createCompiler = options => {
    const compiler = new Compiler(options.context)
    // 注册所有的自定义插件
    if(Array.isArray(options.plugins)){
        for(const plugin of options.plugins){
            if(typeof plugin === 'function'){
                plugin.call(compiler, compiler)
            }else{
                plugin.apply(compiler)
            }
        }
    }
    compiler.hooks.environment.call()
    compiler.hooks.afterEnvironment.call()
    // process中注册所有webpack内置的插件
    compiler.options = new WebpackOptionsApply().process(options, compiler)  
    return compiler
}

Compile(callback)

  1. 创建compiliation
  2. 执行到this.hooks.make.callAsync(compilation,cb)时,立马执行EntryPlugin的apply函数中,complier.hooks.make.tapAsync("EntryPlugin",(compilation,cb)=>{回调})里的回调函数:compiliation.addEntry()
compile(callback){
    const params = this.newCompilationParams()  // 初始化模块工厂对象
    this.hooks.beforeCompile.callAsync(params, err => {
        this.hooks.compile.call(params)
        // compilation记录本次编译作业的环境信息 
        const compilation = new Compilation(this)
        this.hooks.make.callAsync(compilation, err => {
            compilation.finish(err => {
                compilation.seal(err=>{
                    this.hooks.afterCompile.callAsync(compilation, err => {
                        return callback(null, compilation)
                    })
                })
            })
        })
    })
}

make就是我们关心的编译过程

Compilation

compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。

  1. 将入口添加到模块树中:addModuleTree

doBuild

  1. runLoaders():根据loader执行模块的转换,底层使用Loader-Runner

Webpack源码解读:理清编译主流程 - 掘金 (juejin.cn)

编写自定义webpack插件从理解Tapable开始 - 掘金 (juejin.cn)

Geekhyt/simple-pack: 简易版Webpack实现 (github.com)

自定义Loader

  • Loader本质上是一个导出为函数的JavaScript模块(遵循CommonJS规范);
  • loader runner库会调用这个函数,然后将上一个loader产生的结果或者资源文件传入进去;

Loader接收三个参数:

  • content:资源文件的内容;
  • map:sourcemap相关的数据;
  • meta:一些元数据;

Loader必须通过 return 或者 this.callback 来返回结果,交给下一个loader来处理;

this.callback

  • 第一个参数必须是 Error(报错信息) 或者 null;
  • 第二个参数是一个 string或者Buffer(要传给下一个loader的数据);

同步Loader

默认编写的loader都是同步loader,webpack会按照顺序执行这些loader

异步Loader

当Loader内部进行异步操作时,如setTimeout,我们希望在异步操作完后,再返回loader处理的结果。此时就需要异步Loader.

通过 this.async()获取异步callback

module.exports = function (content) {
	// 获取异步callback
	const callback = this.async();
	//获取同步callback
	// const callback = this.callback;
	//进行耗时操作
	setTimeout(() => {
		console.log("my-loader02");
		callback(null, content + "哈哈哈哈");
	}, 2000);
};

获取和传入参数(options)

获取config中传入的options参数

use: [
					"./my-loaders/my_loader_03.js",
					"./my-loaders/my_loader_02.js",
					{
						loader: "./my-loaders/my_loader_01.js",
						options: {
							name: "zxs",
							age: 18,
						},
					},
				],

早期时使用 loader-utils

npm install loader-utils -D

现在可以通过this.getOptions()直接获取options参数

module.exports = function (content, map, meta) {
	console.log("my-loader01");
	const options = this.getOptions();//获取参数
	console.log(options);
	return content;
};

校验参数

webpack官方提供的校验库 schema-utils

npm install schema-utils -D

loader-schema.json

{
	"type": "object",
	"properties": {
		"name": {
			"type": "string",
			"description": "请输入名称,string"
		},
		"age": {
			"type": "number",
			"description": "请输入age,number"
		}
	}
}

my_loader01.js

const { validate } = require("schema-utils");
const loader01Schema = require("./schema/loader01-schema.json");
module.exports = function (content, map, meta) {
	console.log("my-loader01");
	//获取参数
	const options = this.getOptions();
	console.log(options);
	// 校验参数
	validate(loader01Schema, options);//(校验规则,要校验的options)
	return content;
};

自定义babel-loader

const babel = require("@babel/core");
module.exports = function (content, map, meta) {
	console.log("babel-loader");
	const callback = this.async();
	const options = this.getOptions();
	if (!Object.keys(options).length) {
		options = require("../babel.config");
	}
	//利用babel转换代码
	babel.transform(content, options, (err, res) => {
		if (err) {
			callback(err);
		} else {
			callback(null, res.code);
		}
	});
	return content;
};

自定义markdown文件的loader

marked库将md语法转化为html结构

npm i marked -D

安装hightlight.js解析高亮代码

npm i hightlight.js -D

md.loader.js

const { marked } = require("marked");
const hljs = require("highlight.js");
module.exports = function (content) {
	// marked让代码高亮
	marked.setOptions({
		highlight: function (code, lang) {
			return hljs.highlight(lang).value;
		},
	});
	//将md语法转化为html结构
	const htmlContent = marked(content);
	console.log(htmlContent);
	//最终返回的结果必须是模块化内容
	const innerContent = "`" + htmlContent + "`";
	const moduleContent = `var code=${innerContent};export default code`;
	return moduleContent;
};

给md文件添加自定义样式

md中的代码,会添加hljs-xxx的类名

code.css

pre {
	background-color: #f2f2f2;
	padding: 15px;
	margin: 20px;
}
.hljs-keyword {
	color: red;
}
.hljs-string {
	color: skyblue;
}

main.js

import code from "./test.md";
import "highlight.js/styles/default.css";//使用highlight里的默认样式
import "./css/code.css";//使用自定义样式
document.body.innerHTML = code;

Tapable

Tapable的Hook

uTools_1677935137164

  • 同步Sync:

    • bail:当有返回值时,就不会执行后续的事件触发了; 使用场景:如果发生错误,直接return

      const { SyncBailHook } = require("tapable");
      class MyCompiler {
      	constructor() {
      		this.hooks = {
      			bailHook: new SyncBailHook(["name", "age"]),
                  }; //里面的["name", "age"]是预计要接收参数
      
      		this.hooks.bailHook.tap("event1", (name, age) => {
      			console.log("event1执行了", name, age);
      			return 123; //有返回值,阻断了后面事件的执行
      			//使用场景:如果有错误,直接return
      		});
      		this.hooks.bailHook.tap("event2", (name, age) => {
      			console.log("event1执行了", name, age);
      		});
      	}
      }
      
      const compiler = new MyCompiler();
      
      compiler.hooks.bailHook.call("zxs", 18);
      
      
    • Loop:当返回值为true,就会反复执行该事件,当返回值为undefined或者不返回内容,就退出事件;

    • Waterfall:当返回值不为undefined时,会将这次返回的结果作为下次事件的第一个参数

  • 异步Async:

    • Parallel:并行,不会等到上一个事件回调执行结束,才执行下一次事件处理回调,而是同时执行事件

      const { AsyncParallelHook } = require("tapable");
      class MyCompiler {
      	constructor() {
      		this.hooks = {
      			AsyncParallelHook: new AsyncParallelHook(["name", "age"]),
      		};
      
      		this.hooks.AsyncParallelHook.tapAsync("event1", (name, age) => {
      			setTimeout(() => {
      				console.log("event1执行了", name, age);
      			}, 3000);
      		});
      		this.hooks.AsyncParallelHook.tapAsync("event2", (name, age) => {
      			setTimeout(() => {
      				console.log("event2执行了", name, age);
      			}, 3000);
      		});
      	}
      }
      
      const compiler = new MyCompiler();
      
      compiler.hooks.AsyncParallelHook.callAsync("zxs", 18);
      
      

      三秒后,控制台同时打印:

      event1执行了 zxs 18 event2执行了 zxs 18

    • Series:串行,会等待上一个异步的Hook;

    const { AsyncSeriesHook } = require("tapable");
    class MyCompiler {
    	constructor() {
    		this.hooks = {
    			AsyncSeriesHook: new AsyncSeriesHook(["name", "age"]),
    		};
    
    		this.hooks.AsyncSeriesHook.tapAsync("event1", (name, age, cb) => {
    			setTimeout(() => {
    				console.log("event1执行了", name, age);
    				cb();
    			}, 3000);
    		});
    		this.hooks.AsyncSeriesHook.tapAsync("event2", (name, age, cb) => {
    			setTimeout(() => {
    				console.log("event2执行了", name, age);
    				cb();
    			}, 3000);
    		});
    	}
    }
    
    const compiler = new MyCompiler();
    
    compiler.hooks.AsyncSeriesHook.callAsync("zxs", 18, () => {
    	console.log("所有任务执行完成了,执行该回调");
    });
    
    

    event1执行了 zxs 18 event2执行了 zxs 18 所有任务执行完成了,执行该回调

    参数cb()类似于koa里的next(),当前cb()执行时,执行下一个任务

使用Hook

  1. 创建hooks:new SyncHook(["args"])
  2. 注册Hook中的事件:this.hooks.syncHook.tap("event",(args)=>{})
  3. 触发事件:compiler.hooks.syncHook.call(args);
const { SyncHook } = require("tapable");
class MyCompiler {
	constructor() {
		this.hooks = {
			//1.创建hooks
			syncHook: new SyncHook(["name", "age"]),
		};

		//2.使用hooks的tap()监听事件
		this.hooks.syncHook.tap("event1", (name, age) => {
			console.log("event1执行了", name, age);
		});
	}
}

const compiler = new MyCompiler();
//3.发送事件
compiler.hooks.syncHook.call("zxs", 18); //只要调用call()就可以被监听到,并且发送传参

自定义Plugin

Plugin如何被注册到webpack生命周期中

  1. createCompiler()中,注册所有插件
  2. 注册插件时,会调用插件对象里的apply()方法
  3. 插件方法接收compiler对象,我们可以通过complier对象注册Hook事件
  4. 某些插件也会传入compilation对象,我们也可监听compilation的Hook事件

案例:静态文件自动上传服务器插件

  1. 创建AutoUploadWebpackPlugin类;
  2. 编写apply方法:
    1. ✓ 通过ssh连接服务器;
    2. ✓ 删除服务器原来的文件夹;
    3. ✓ 上传文件夹中的内容;
  3. 在webpack的plugins中,使用AutoUploadWebpackPlugin类
const { NodeSSH } = require("node-ssh");
class AutoUploadWebpackPlugin {
	constructor() {
		this.ssh = new NodeSSH();
	}
	apply(compiler) {
		console.log("插件被注册");
		//等到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.uploadFile(outputPath, remotePath);
				//关闭连接
				this.ssh.dispose();
				callback();
			}
		);
	}
	async connectServer() {
		await this.ssh.connect({
			host: this.options.host,
			username: this.options.username,
			password: this.options.password,
		});
		console.log("服务器连接成功");
	}
	async uploadFile(outputPath, remotePath) {
		const status = await this.ssh.putDirectory(outputPath, remotePath, {
			recursive: true, //递归上传
			concurrency: 10, //并发数:10
		});
		if (status) {
			console.log("文件上传成功");
		}
	}
}
module.exports = AutoUploadWebpackPlugin;

自动化工具Gulp

在Javascript的开发过程中,经常会遇到一些重复性的任务,比如合并文件、压缩代码、检查语法错误、将Sass代码转成CSS代码等等。通常,我们需要使用不同的工具,来完成不同的任务,既重复劳动又非常耗时。grunt,gulp都是为了解决这个问题而发明的工具,可以帮助我们自动管理和运行各种任务。

gulp的核心理念是task runner :

  • 可以定义自己的一系列任务,等待任务被执行;
  • 基于文件Stream的构建流;
  • 我们可以使用gulp的插件体系来完成某些任务;
npm install gulp -g

在gulpfile.js编写任务

const foo = (callback) => {
	console.log("第一个gulp任务");
	callback();
};

//导出任务
module.exports = {
	foo,
};

npx gulp foo

每个gulp任务都是一个异步的JavaScript函数:

  • 此函数可以接受一个callback作为参数,调用callback函数那么任务会结束;
  • 或者是一个返回stream、promise、event emitter、child process或observable类型的函数;

任务可以是public或者private类型的:

  • 公开任务(Public tasks): 从 gulpfile 中被export,可以通过 gulp 命令直接调用;
  • 私有任务(Private tasks): 被设计为在内部使用,通常作为 series() 或 parallel() 组合的组成部分;

gulp提供了两个强大的组合方法:

  • series():串行任务组合;
  • parallel():并行任务组合;

uTools_1678003780018

gulp基本操作

gulpfile.js

const { series, parallel, src, dest, watch } = require("gulp");
const babel = require("gulp-babel");
const terser = require("gulp-terser");

const foo = (callback) => {
	console.log("第一个gulp任务");
	callback();
};
//异步任务
const bar = (cb) => {
	setTimeout(() => {
		console.log("bar");
		cb();
	}, 1000);
};


//串行任务:等到foo,执行完,再执行bar
const seriesTask = series(foo, bar);
//并行任务:
const parallelTask = parallel(foo, bar);


//读取和写入文件:src()读取,dest()写入
const copyFile = (cb) => {
	// 1.读取,写入文件
	// 将/file/main.js拷贝到file_move文件夹下
	return src("./file/main.js").pipe(dest("./file_move"));
	// src("./file/**/*")拷贝file下的全部文件
};


//js文件的转换与压缩
// npm i @babel/core gulp-babel
// npm i @babel/preset-env -D //es6--es5
// npm i gulp-terser -D
const jsTransform = () => {
	return src("./file/**/*")
		.pipe(
			babel({
				presets: ["@babel/preset-env"],
			})
		)
		.pipe(
			terser({
				mangle: { toplevel: true },
			})
		)
		.pipe(dest("./build"));
};


//默认任务
module.exports.default = (cb) => {
	console.log("默认任务");
	cb();
};


//监听文件变化,并执行任务
watch("./file/**/*", jsTransform);
//导出任务
module.exports = {
	foo,
	seriesTask,
	parallelTask,
	copyFile,
	jsTransform,
};

Gulp案例:开启本地服务和打包

const browserSync = require("browser-sync");
const { src, dest, parallel, series, watch } = require("gulp");
const babel = require("gulp-babel");
const htmlmin = require("gulp-htmlmin");
const inject = require("gulp-inject");
const less = require("gulp-less");
const terser = require("gulp-terser");

//打包html
//npm i gulp-htmlmin -D
const htmlTask = () => {
	return src("./src/**/*.html")
		.pipe(
			htmlmin({
				collapseWhitespace: true,
			})
		)
		.pipe(dest("./dist"));
};

//js文件的转换与压缩
// npm i @babel/core gulp-babel
// npm i @babel/preset-env -D //es6--es5
// npm i gulp-terser -D
const jsTransform = () => {
	return src("./src/**/*.js")
		.pipe(
			babel({
				presets: ["@babel/preset-env"],
			})
		)
		.pipe(
			terser({
				mangle: { toplevel: true },
			})
		)
		.pipe(dest("./dist"));
};

//打包less文件
// npm i gulp-less -D
const lessTask = () => {
	return src("./src/**/*.less").pipe(less()).pipe(dest("./dist"));
};

//html资源注入
// npm i gulp-inject -D
// 注意:需要在被注入的文件内加入声明插槽
// <!-- inject:js -->
// <!-- endinject -->
const htmlInject = () => {
	return src("./src/**/*.html")
		.pipe(
			inject(src(["./dist/**/*.js", "./dist/**/*..css"]), { relative: true })
		)
		.pipe(dest("./dist"));
};

//开启本地服务器
// npm i browser-sync -D
const bs = browserSync.create();
const Server = () => {
	bs.init({
		port: 8080,
		open: true,
		files: "./dist/*",
		server: {
			baseDir: "./dist",
		},
	});
};

//构建项目打包任务
const build = series(parallel(htmlTask, jsTransform, lessTask), inject);

//开启监听
watch("./src/**", build);
const serverTask = series(build, Server);
module.exports = {
	serverTask,
	build,
};

Rollup:打包库的工具

Rollup是一个JavaScript的模块化打包工具,可以帮助我们编译小的代码到一个大的、复杂的代码中,比如一个库或者一个应用程序

Rollup更专注JS代码打包

通常在实际项目开发过程中,我们都会使用webpack(比如react、angular项目都是基于webpack的);

对库文件进行打包时,我们通常会使用rollup(比如vue、react、dayjs源码本身都是基于rollup的,Vite底层使用Rollup);

Rollup基本使用

npm install rollup -D

创建main.js,打包到bundle.js文件中

npx rollup ./lib/index.js -o dist/bundle.js

Rollup不同环境打包

# 打包浏览器的库
npx rollup ./src/main.js -f iife -o dist/bundle.js
# 打包AMD的库
npx rollup ./src/main.js -f amd -o dist/bundle.js
# 打包CommonJS的库
npx rollup ./src/main.js -f cjs -o dist/bundle.js
# 打包通用的库(必须跟上name)
npx rollup ./src/main.js -f umd --name mathUtil -o dist/bundle.js

常用UMD通用打包

Rollup的配置文件

rollup.config.js

module.exports = {
	input: "./lib/index.js",
	output: {
		format: "umd",
		name: "my-package",
		file: "./build/bundle.js",
	},
};

打包后会将name挂载到全局对象上(如window,golbal)。就像lodash里的_jquery里的$

使用第三方库

安装解决commonjs的库:

npm install @rollup/plugin-commonjs -D

安装解决node_modules的库:

npm install @rollup/plugin-node-resolve -D
// 适配自己的代码内的commonjs规范
const commonjs = require("@rollup/plugin-commonjs");
//适配第三方库里的commonjs规范
const nodeResolve = require("@rollup/plugin-node-resolve");
module.exports = {
	input: "./lib/index.js",
	output: {
		format: "umd",
		name: "my-package",
		file: "./build/bundle.js",
		globals: {
			lodash: "_",
		},
	},
	external: ["lodash"], //打包时排除的第三方库
	plugins: [commonjs(), nodeResolve()],
};

rollup的插件列表

Babel转换代码

npm install @babel/core @rollup/plugin-babel -D

rollup.config.js

const { babel } = require("@rollup/plugin-babel");
const { terser } = require("@rollup/plugin-terser"); ////Terser代码压缩
const postcss = require("@rollup/plugin-postcss");//处理css文件
const vue = require("@rollup/plugin-vue");//处理vue文件
module.exports = {
	plugins: [
		commonjs(),
		nodeResolve(),
		babel({
			exclude: /node_modules/,
			babelHelpers: "bundled",
		}),
		terser(),
		postcss({ plugins: [require("postcss-preset-env")] }),
		vue(),
	],
};

npm i @babel/preset-env //默认预设
npm install @rollup/plugin-terser -D //Terser代码压缩
npm install rollup-plugin-vue @vue/compiler-sfc -D //处理vue文件
npm install rollup-plugin-postcss postcss -D //处理css文件

babel.config.js

module.exports = {
	preset: ["@babel/preset-env"],
};

css加上浏览器前缀的两种方法:

  • 使用预设

    npm i postcss-preset-env -D
    //rollup.config.js
    postcss({ plugins: [require("postcss-preset-env")] })
    
  • 配置postcss.config.js

    module.exports = {
    	plugins: [require("postcss-preset-env")],
    };
    
    

打包vue文件报错process is not defined

这是因为在我们打包的vue代码中,用到 process.env.NODE_ENV,所以我们可以使用一个插件 rollup-plugin-replace 设置 它对应的值:

npm install @rollup/plugin-replace -D
module.exports={
	plugins:[
		replace({ "process.env.NODE_ENV":JSON.stringify("production") }),
		vue(),
	]
}

搭建本地服务器

  1. 第一步:使用rollup-plugin-serve搭建服务
 npm install rollup-plugin-serve -D
  1. 第二步:当文件发生变化时,自动刷新浏览器

    npm install rollup-plugin-livereload -D
    
  2. 第三步:启动时,开启文件监听

     npx rollup -c -w
    
// 适配自己的代码内的commonjs规范
const commonjs = require("@rollup/plugin-commonjs");
//适配第三方库里的commonjs规范
const nodeResolve = require("@rollup/plugin-node-resolve");
const { babel } = require("@rollup/plugin-babel");

const { terser } = require("@rollup/plugin-terser"); ////Terser代码压缩
const replace = require("@rollup/plugin-replace");
const serve = require("rollup-plugin-serve");
const livereload = require("rollup-plugin-livereload");
const vue = require("rollup-plugin-vue");
const postcss = require("rollup-plugin-postcss");

//根据环境配置插件
const isProd = process.env.NODE_ENV === "production";
const plugins = [
	commonjs(),
	nodeResolve(),
	babel({
		exclude: /node_modules/,
		babelHelpers: "bundled",
	}),
	postcss({ plugins: [require("postcss-preset-env")] }),
	replace({ "process.env.NODE_ENV": JSON.stringify("production") }),
	vue(),
];
if (isProd) {
	plugins.push(terser());
} else {
	const extractPlugin = [
		serve({
			port: 8888,
			open: true,
			contentBase: ".",
		}),
		livereload(),
	];
	plugins.push(...extractPlugin);
}
module.exports = {
	input: "./lib/index.js",
	output: {
		format: "umd",
		name: "my-package",
		file: "./build/bundle.js",
		globals: {
			lodash: "_",
		},
	},
	external: ["lodash"], //打包时排除的第三方库
	plugins: plugins,
};

多环境配置

设置scripts命令

	"scripts": {
		"test": "echo \"Error: no test specified\" && exit 1",
		"serve": "rollup -c --environment NODE_ENV:development -w",
		"build": "rollup -c --environment NODE_ENV:production"
	},
// 适配自己的代码内的commonjs规范
const commonjs = require("@rollup/plugin-commonjs");
//适配第三方库里的commonjs规范
const nodeResolve = require("@rollup/plugin-node-resolve");
const { babel } = require("@rollup/plugin-babel");

const { terser } = require("@rollup/plugin-terser"); ////Terser代码压缩
const vue = require("rollup/plugin-vue");
const replace = require("@rollup/plugin-replace");
const serve= require("rollup-plugin-serve");
const livereload= require("rollup-plugin-livereload");
const isProd = process.env.NODE_ENV === "production";
const plugins = [
	commonjs(),
	nodeResolve(),
	babel({
		exclude: /node_modules/,
		babelHelpers: "bundled",
	}),
	postcss({ plugins: [require("postcss-preset-env")] }),
	replace({ "process.env.NODE_ENV": JSON.stringify("production") }),
	vue(),
];
//根据环境加入插件
if (isProd) {
	plugins.push(terser());
} else {
	const extractPlugin = [
		serve({
			port: 8888,
			open: true,
			contentBase: ".",
		}),
		livereload(),
	];
	plugins.push(...extractPlugin);
}
module.exports = {
	input: "./lib/index.js",
	output: {
		format: "umd",
		name: "my-package",
		file: "./build/bundle.js",
		globals: {
			lodash: "_",
		},
	},
	external: ["lodash"], //打包时排除的第三方库
	plugins: plugins,
};

Vite

Webpack与Vite

  • ​ Webpack

    graph LR
    ES6+,TS,JSX,Vue语法--Babel-->ES5
    
  • Vite

    ES6+不转换

    TS ==>ES6+

    JSX ==>简单转换

    Vue==>简单转换

    通过ESbuild转换

Vite由两部分组成

  1. 一个开发服务器,它基于原生ES模块提供了丰富的内建功能,HMR的速度非常快速;
  2. 一套构建指令,它使用rollup打开我们的代码,并且它是预配置的,可以输出生成环境的优化过的静态资源;

浏览器原生支持模块化

在html文件中引入js时,声明type="module

缺点:

  1. 文件内部引入其他文件必须明确写上写.js文件后缀
  2. 浏览器不识别JSX,Vue代码
  3. 如果某模块,加载了很多其他js文件,则这些js文件都需要被依次加载,那么浏览器需要吧这些文件都请求下来,发生了很多http请求,效率大大降低
<script src="./src/main.js" type="module"></script>

Vite安装

npm install vite –g
npm install vite -d

通过vite来启动项目:

npx vite

Vite的语法处理

  • vite默认支持CSS

  • Vite对TypeScript默认支持

    如果我们查看浏览器中的请求,会发现请求的依然是ts的代码:

    • 这是因为vite中的服务器Connect(vite2之前用的koa)会对我们的请求进行转发;

    • 获取ts编译后的代码,给浏览器返回,浏览器可以直接进行解析;

    • 简述就是 .ts=ESbuild==>.js=Connect转发>浏览器

  • Vite处理css预处理器:less,scss,postcss

    npm install less -D
    npm install postcss postcss-preset-env -D
    

    postcss.config.js

    module.exports={
    	plugins:[
    		require("postcss-preset-env")
    	]
    }
    
  • Vite对Vue的支持:

  • 安装vue,以及vite支持vue的插件

    npm i vue
    npm install @vitejs/plugin-vue -D
    

    vite.config.js

    import { defineConfig } from "vite";
    import vue from "@vitejs/plugin-vue";
    
    //方法1
    // export default {
    // 	plugins: [vue()],
    // };
    
    //方法2
    export default defineConfig({
    	plugins: [vue()],
    });
    
    

    其他vue&vite相关插件:

    • Vue 3 单文件组件支持:@vitejs/plugin-vue
    • Vue 3 JSX 支持:@vitejs/plugin-vue-jsx
    • Vue 2 支持:underfin/vite-plugin-vue2
  • Vite对React支持

    jsx 和 .tsx 文件同样开箱即用,它们也是通过 ESBuild来完成的编译

Vite打包

npx vite build

可以通过preview的方式,开启一个本地服务来预览打包后的效果:

npx vite preview

Vite脚手架

Vite实际上是有两个工具的:

  • Vite:相当于是一个构件工具,类似于webpack、rollup;
  • @vitejs/create-app:类似vue-cli、create-react-app;

脚手架创建项目:

npm create vite

ESBuild解析

ESBuild的特点:

  • 超快的构建速度,并且不需要缓存;
  • 支持ES6和CommonJS的模块化;
  • 支持ES6的Tree Shaking;
  • 支持Go、JavaScript的API;
  • 支持TypeScript、JSX等语法编译;
  • 支持SourceMap;
  • 支持代码压缩;
  • 支持扩展其他插件;

ESBuild的构建速度

为什么这么快:

  • 使用Go语言编写的,可以直接转换成机器代码,而无需经过字节码;
  • ESBuild可以充分利用CPU的多内核,尽可能让它们饱和运行;
  • ESBuild的所有内容都是从零开始编写的,而不是使用第三方,所以从一开始就可以考虑各种性能问题;

Vite生产环境用Rollup打包

为何不用 ESBuild 打包?

虽然 esbuild 快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些针对构建 应用 的重要功能仍然还在持续开发中 —— 特别是代码分割和 CSS 处理方面。就目前来说,Rollup 在应用打包方面更加成熟和灵活。尽管如此,当未来这些功能稳定后,我们也不排除使用 esbuild 作为生产构建器的可能。

脚手架开发

package.json

"bin":{
	"vite":"bin/vite.js"
}

原理:在敲vite xxx命令时,会去bin目录下找vite.js文件,这个vite.js就是这个脚手架的入口文件

添加--version查看版本号功能:

commander获取命令行参数

npm i commander
#!/usr/bin/env node
const { program } = require("commander");

//处理--version
const version = require("../package.json").version;
program.version(version, "-v --version");

//增加其他option操作
program.option("-z --zxs <arg>", "fuck you bitch,this is zxs!"); //(命令<参数>,命令描述)

//commander解析argv参数
program.parse(process.argv);

// 获取命令参数arg
console.log(program.opts().arg);

//监听命令
program.on("--help", () => {
	console.log("\n");
});

从github clone项目模板

npm i download-git-repo

模板引擎

npm i ejs
#!/usr/bin/env node
const { program } = require("commander");
const helpOptions = require("./core/help-options");
const download = require("download-git-repo");
const { spawn } = require("child_process");
const path = require("path");
const ejs = require("ejs");
const fs = require("fs");
helpOptions();

//增加其他功能:1.创建项目模板
program
	.command("create <project> [...others]")
	.description("创建项目,如zxscli create my-project")
	.action(async function (project) {
		console.log("创建了一个项目:", project);

		try {
			//从github clone项目模板
			await download(
				"direct:https://github.com/coderwhy/vue3_template.git#main",
				project,
				{
					clone: true,
				}
			);
			//执行npm i
			await execCommand("npm.cmd", ["install"], { cwd: `./${project}` });
			//执行npm run dev
			await execCommand("npm.cmd", ["run", "dev"], { cwd: `./${project}` });
		} catch (err) {
			console.log("github连接失败");
		}
	});

//2.通过命令创建组件
program
	.command("addcpn <cpn>")
	.description("创建组件:zxscli addcpn name -d src/components")
	.action(addComponentAction(cpn));

//commander解析argv参数
program.parse(process.argv);

//执行cmd命令
function execCommand(...args) {
	return new Promise((resolve) => {
		//开启子进程,并执行命令
		const childProcess = spawn(...args);
		//获取子进程输出及错误信息
		childProcess.stdout.pipe(process.stdout);
		childProcess.stderr.pipe(process.stderr);
		//监听子进程执行结束,关闭
		childProcess.on("close", () => {
			resolve();
		});
	});
}
//创建组件
async function addComponentAction(name) {
	//先写好组件的模板,再根据内容给模板填充数据
	//				用户是否指定目录,没有则使用默认目录
	const destPath = program.opts.dest || "src/components";
	const res = await compileEjs("component.vue.ejs", { name: "zxs" });
	//将模板写入到对应的文件夹中
	await writeFile(`${destPath}/${name}.vue`, res);
}

//给模板填充数据
function compileEjs(tempName, data) {
	return new Promise((resolve, reject) => {
		//获取模板路径
		const tempPath = `./template/${tempName}`;
		const absPath = path.resolve(__dirname, tempPath);
		ejs.renderFile(absPath, data, (err, res) => {
			if (err) {
				console.log("编译失败", err);
				reject(err);
				return;
			}
			resolve(res);
		});
	});
}
//写入文件
function writeFile(path, data) {
	return fs.promises.writeFile(path, data);
}

杂项

CommonJS与ESModule区别

1.CommonJS模块输出的是值的拷贝,ES6模块输出的是值的引用

2.CommonJS模块是运行是加载,ES6模块是编译时输出接口

3.CommonJS是单个值导出,ES6Module可以导出多个

4.CommonJS模块为同步加载,ES Module支持异步加载

import ('test.js').then(mod=>{
console.log(mod);
})//异步

5.CommonJS的this是当前模块,ES Module的this是undefined

6.CommonJS和ES Module的语法不同