Webpack
概念 | webpack 中文文档 | webpack 中文文档 | webpack 中文网 (webpackjs.com)
跨域解决方案
Mode配置项
mode配置项可以告知webpack使用相应模式的内置优化
默认production
可选:'none'|'development'|'production'
-
development会将 DefinePlugin中process.env.NODE_ENV的值设置为development. 为模块和 chunk 启用有效的名。production会将 DefinePlugin中process.env.NODE_ENV的值设置为production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin和TerserPlugin。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
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,但也可以方便我们调试,可不准确
一般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使用:
-
安装
npm i webpack-dev-serve -D -
修改配置文件:
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也具有编译器的工作流程:
- 解析阶段
- 转换阶段
- 生成阶段
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编写规则:
-
defaults: browserslist的默认浏览器(市场占有率>0.5%,last 2 versions,not dead)。
-
5%:市场占有率>5%的浏览器
-
dead:24个月内没有官方更新或维护的浏览器
-
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)
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
-
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", }, ], ], }; - useBuiltIns:设置以什么方式来使用polyfill.可选值:
React和TS解析
Babel解析React
在编写react代码时,react使用的语法是jsx,jsx是可以直接使用babel来转换的,使用如下插件处理:
- @babel/plugin-syntax-jsx
- @babel/plugin-transform-react-jsx
- @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
-
安装loader
npm i ts-loader -
创建ts.config.json文件
tsc --init -
webpack.config.js中使用loader
{ test: /\.ts$/, use: "ts-loader", },
使用babel解析ts(推荐使用):
可以添加polyfill,但在打包时不会检测类型错误
-
webpack.config.js中使用babel-loader处理ts代码
{ test: /\.ts$/, use: "babel-loader", }, -
安装预设@babel/preset-typescript
npm i @babel/preset-typescript -
babel.config.js
module.exports = { presets: [ [ "@babel/presets-env", { corejs: 3, useBuiltIns: false, }, ], ["@babel/presets-react"], ["@babel/presets-typescript"], ], };
TS处理最佳实践
使用 babel里的转换和polyfill + ts-loader里的类型校验
-
运行scripts脚本:package.json
"ts-check-watch":"tsc --noEmit --watch"
Webpack性能优化
分类:
- 打包后的结果,上线时的性能优化
- 优化打包速度,开发或者构建时优化打包速度
只打包到一个文件:
- 所有东西放到一个包,不方便管理
- 包体积非常大,首屏渲染速度降低
代码分离(分包)
目的:将代码分离到不同的bundle中,之后可以按需加载,或者并行加载这些文件。
代码分离的三种方式:
- 入口起点(多入口加载):使用entry配置手动分离代码
- 防止重复:使用Entry Dependencies或SplitChunkPlugin去重和分离代码
- 动态导入:通过模块内嵌函数调用来分离代码
Webpack多入口依赖
手动分包:
-
webpack.config.js配置entry多入口
// entry:"./src/main.js" entry: { index: "./src/index.js", main: "./src/main.js", }, //多入口 index.js里可以写react代码,main.js里可以写vue代码。以此可以窥见微前端的雏形。 -
修改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(预加载):当前导航下可能需要的资源
不同点:
- preload chunk会在父chunk加载时,以并行方式开始加载。prefetch chunk会在父chunk加载结束后加载
- preload具有中等优先级,并立即下载。prefetch在浏览器闲置时下载
- preload chunk在父chunk中立即请求,用于当下时刻。prefetch chunk会用于将来某个时刻
使用方法:魔法注释
import(
/* WebpackChunkName:"about" */
/* WebPrefetch:true */
"./router/about.js"
).then((res) => {
res.about();
res.default();
});
CDN加速服务配置
开发中使用CDN的两种方式:
- 打包的所有静态资源,都放到CDN服务器
- 一些第三方资源放到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-loader。webpack使用loader顺序是从后往前
loader是链式传递的,对⽂件资源从上⼀个loader传递到下⼀个
MiniCssExtractPlugin
npm i mini-css-extract-plugin -D
将css打包到单独的文件
- 使用插件
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(推荐):
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里
Compress的unused: 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文件进行解压,再执行里面的代码
压缩流程:
- http数据再服务器发送前已经被压缩了(可在webpack中完成)
- 服务器发送请求时,告知服务器自己支持哪些压缩格式:
Accept-Encoding:gzip,deflate - 服务器再浏览器支持压缩的格式下,直接返回对应压缩后的文件,并在响应头里告知浏览器
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 -Dconst { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); //对打包结果进行分析 new BundleAnalyzerPlugin(),打包后会自动打开分析网页
Webpack源码分析
总体流程:
- 通过
webpack(config)创建Compiler对象 - 执行
Compiler.run(),开始对代码编译打包
- before-run 清除缓存
- run 注册缓存数据钩子
- before-compile
- compile 开始编译
- make 从入口分析依赖以及间接依赖模块,创建模块对象
- build-module 模块构建
- seal 构建结果封装, 不可再更改
- after-compile 完成构建,缓存数据
- emit 输出到dist目录
CreateCompiler(options)
-
通过
new创建compiler实例:const compiler=new Compiler(option.context,iptions) -
注册所有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()监听编译事件 -
调用钩子enviroment/afterEnviroment函数:
complier.hooks.enviroment.call() -
使用
process函数处理其他options:如entry/output/devtool(module不是):将这些选项new成插件,注册成插件:new SomePlugin().apply()。process函数执行完,webpack将所有它关心的hook消息都注册完成,等待后续编译过程中挨个触发。 -
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)
- 创建
compiliation - 执行到
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对象,负责此次更新的构建过程。
- 将入口添加到模块树中:
addModuleTree
doBuild
- 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
-
同步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
- 创建hooks:
new SyncHook(["args"]) - 注册Hook中的事件:
this.hooks.syncHook.tap("event",(args)=>{}) - 触发事件:
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生命周期中
- createCompiler()中,注册所有插件
- 注册插件时,会调用插件对象里的apply()方法
- 插件方法接收compiler对象,我们可以通过complier对象注册Hook事件
- 某些插件也会传入compilation对象,我们也可监听compilation的Hook事件
案例:静态文件自动上传服务器插件
- 创建AutoUploadWebpackPlugin类;
- 编写apply方法:
- ✓ 通过ssh连接服务器;
- ✓ 删除服务器原来的文件夹;
- ✓ 上传文件夹中的内容;
- 在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():并行任务组合;
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()],
};
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(),
]
}
搭建本地服务器
- 第一步:使用rollup-plugin-serve搭建服务
npm install rollup-plugin-serve -D
-
第二步:当文件发生变化时,自动刷新浏览器
npm install rollup-plugin-livereload -D -
第三步:启动时,开启文件监听
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由两部分组成:
- 一个开发服务器,它基于原生ES模块提供了丰富的内建功能,HMR的速度非常快速;
- 一套构建指令,它使用rollup打开我们的代码,并且它是预配置的,可以输出生成环境的优化过的静态资源;
浏览器原生支持模块化
在html文件中引入js时,声明type="module。
缺点:
- 文件内部引入其他文件必须明确写上写
.js文件后缀 - 浏览器不识别JSX,Vue代码
- 如果某模块,加载了很多其他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 -Dpostcss.config.js
module.exports={ plugins:[ require("postcss-preset-env") ] } -
Vite对Vue的支持:
-
安装vue,以及vite支持vue的插件
npm i vue npm install @vitejs/plugin-vue -Dvite.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);
})//异步