常用的Loader使用
raw-loader:加载文件原始内容(utf-8)file-loader:将一个文件输入到另外一个文件中 在代码中相当于url的引用输出文件(一般都是处理大图片和大字体用的)url-loader:与file-loader类似 区别就是可以设置一个阀值 大于阀值交给file-loader 小于的时候 返回文件base64形势编码(一般都是处理小的图片和小的字体用的)source-map-loader:一般都是用于加载source-map生成的.map文件 方便断点svg-inline-loader:将压缩后的svg内容注入代码中(处理svg的)image-loader: 处理加载并压缩图片文件json-loader: 用于加载处理json文件(默认是自动包含的)Handlebars-loader:用于将Handlebars模版编译成为函数并且返回(Handlebars是全球比较流行的一种模版方式 一个高效的构建语义化模版 Handlebars和Mustache语法相结合 更加高效的执行速度)babel-loader:将es6代码转换成es5ts-loader:将 TypeScript 转换成 JavaScriptawesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
这里着重说明下 TS编译工具有三种方式
-
TypeScript官方的编译器
tsc它可以吧TypeScript代码编译成JavaScript代码 他是依赖于tsconfig,json配置文件中配置项 -
ts-loader他的内部其实调用了TypeScript官方的编译器 --tsc所以 他和tsc是共享tsconfig.json的
module.exports = {
...
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true // 只做语言转换,而不做类型检查
}
}
]
}
]
}
}
经过实验结论 当
transpileOnly=true打包文件 仅仅语言转换=需要598ms当
transpileOnly=false打包文件 语言转换+类型检测=需要3290ms当我们使用上
fork-ts-checker-webpack-plugin(可以补全类型检查的功能) 配合上transpileOnly=true一起使用const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); module.exports = { ... plugins:[ ... new ForkTsCheckerWebpackPlugin() ] }语言转换+类型检测=需要2289ms
3.awesome-typescript-loader 这个loader主要为了加快项目中的编译速度
他与ts-loader主要区别
- 更适合于babel集成 使用的是babel中的转义和缓存
- 不需要安装其他插件 就可以吧类型检测放在独立的进程中
- babel
- 在babel7之前的 都是不支持TypeScript的 他的编译流程是这样的
TS->TS 的编辑器 ->JS->Babel->JS(新的) - 在babel7之后 使用
@babel/preset-typescript他只做了转移 但是还得需要TypeScript的tsc来补齐类型检测功能 但是有两种语法他无法编译 1.常量枚举 2.历史遗留风格的import/export语法
如何选择TypeScript编辑器
- 如果项目中使用了babel 那就使用
@babel/preset-typescript+tsc类型检测- 如果没有使用babel 那就使用自带的便器(配合ts-loader)
sass-loader:将SCSS/SASS代码转换成csscss-loader:加载CSS 支持模块化 压缩 文件导入等特性style-loader:把css代码注入到javaScript 通过DOM操作去加载CSS中postcss-loader:扩展CSS语法 使用下一代CSS 可以配合插件autoprefixer来给CSS添加浏览器的前缀eslint-loader:通过 ESLint 检查 JavaScript 代码tslint-loader:通过 TSLint检查 TypeScript 代码mocha-loader:加载 Mocha 测试用例的代码coverjs-loader:计算测试的覆盖率vue-loader:加载 Vue.js 单文件组件i18n-loader: 国际化cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里
rules: [
{
test: /.js$/,
use: [
'cache-loader',
'babel-loader'
],
include: path.resolve('src')
}
]
常用的plugin总结
-
define-plugin:定义环境变量最常用的process.env``用途就是处理我们开发环境和生产环境不同 用一个变量可以去开发环境和生产环境(Webpack4 之后指定 mode 会自动配置) -
ignore-plugin:忽略三方包的指定目录,指定目录不会被打包进去new webpack.IgnorePlugin(/\.\/locale/,/monent/), -
html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
new HtmlWebpackPlugin({
title: 'My App',
filename: 'assets/admin.html' // 在 output.path 目录下生成 assets/admin.html 文件
})
// html
<title><%= htmlWebpackPlugin.options.title %></title>
web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
new WebPlugin({
filename: 'index.html',
// html template file path(full path relative to webpack.config.js)
template: './template.html',
requires: ['A', 'B'],
}),
-
uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前基本已经废弃) -
terser-webpack-plugin: 支持压缩 ES6 (Webpack4) -
webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度(目前没人维护 issue也没人处理)
uglifyJS: {
output: {
comments: false,//是否保留代码中的注释,默认为保留
},
warnings: true,//是否在UglifyJS删除没有用到的代码时输出警告信息,默认为false
compress:{
drop_console: true,//是否删除代码中所有的console语句,默认为false
collapse_vars: true,//是否内嵌虽然已经定义了,但是只用到一次的变量, 默认值false
reduce_vars: true,//是否提取出现了多次但是没有定义成变量去引用的静态值,默认为false
}
},
cacheDir: '',//用作缓存的可选绝对路径。如果未提供,则不使用缓存。
sourceMap: config.build.productionSourceMap,//可选布尔值。是否为压缩后的代码生成对应的Source Map(浏览器可以在调试代码时定位到源码位置了),这会减慢编译速度。默认为false
}),
uglifyJS: {},
test: /.js$/g,
include: [],
exclude: [],
cacheDir: '',
workerCount: '',
sourceMap: false
test: 使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩,默认是 /.js$/.
include: 使用正则去包含被 ParallelUglifyPlugin 压缩的文件,默认为 [].
exclude: 使用正则去不包含被 ParallelUglifyPlugin 压缩的文件,默认为 [].
cacheDir: 缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回,
cacheDir 用于配置缓存存放的目录路径。默认不会缓存,想开启缓存请设置一个目录路径。
**workerCount:**开启几个子进程去并发的执行压缩。默认是当前运行电脑的 CPU 核数减去1。
**sourceMap:**是否为压缩后的代码生成对应的Source Map, 默认不生成,
开启后耗时会大大增加,一般不会将压缩后的代码的
sourceMap发送给网站用户的浏览器。
uglifyJS:用于压缩 ES5 代码时的配置,Object 类型,直接透传给 UglifyJS 的参数。
uglifyES:用于压缩 ES6 代码时的配置,Object 类型,直接透传给 UglifyES 的参数。
mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,为每个包含css的js文件创建一个css文件 支持按需加载 (替代extract-text-webpack-plugin 缺点:1.额外的http请求2.更长的编译时间3.没有运行时的公共路径修改4.没有热替换)
filename: 'css/[name].[contenthash].css', // 设置导出css名称,[name]占位符对应chunkName
chunkFilename: (pathData) => {
// 此选项决定了非入口的 chunk 文件名称
return (
pathData.chunk.name !== 'main' &&
'css/[id]/[id].[contenthash].css'
)
},
// 一个官方维护人员的回复如下,简单的说,就是在js里css的引入顺序导致的问题,
多个css的在js里的引入顺序不同,就会提示这个警告。例如,在1.js 里,引入的顺序是a.css, b.css; 在2.js里,引入顺序是b.css,a.css, 出现了这种引入顺序不同,就导致了警告。在两个js里把引入顺序调成一致,就没问题了。在1.js和2.js里的引入顺序都调整成a.css, b.css 就没有那个警告了。
ignoreOrder: true // 对于通过使用 scoping 或命名约定来解决 css order 的项目,可以通过将插件的 ignoreOrder 选项设置为 true 来禁用 css order 警告。
})
-
serviceworker-webpack-plugin:为网页应用增加离线缓存功能 -
clean-webpack-plugin: 目录清理 -
ModuleConcatenationPlugin: 开启 Scope Hoisting -
speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时) -
webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)
Loader和Plugin区别
Loader 本质上是一个函数 在该函数中对接收到其他的内容进行转换 返回的转换后的结果 因为webpack只认识javaScript 所以loader就成为了翻译官 对其它类型的资源进行了转译的预处理工作
plugin 它是基于Tapable事件流 插件可以扩展Webpack的功能 因为在webpack的生命周期中会广播出来许多事件,Plugin可以来监听广播出来的事件 在合适的时机通过Webpack暴露出来的api改变输出结果
Loader 他在配置文件中的module.rules中配置 并且他接收一个数组 每一项每一个Loader都是一个对象 里面有text(正则匹配文件) use(使用loader) options(参数)
Plugin 属于配置文件中一个单独的plugin的数组 每一项都是一个plugin实例 参数可以通过构造函数传入
webpack开发的高效插件
webpack-dashboard可以友好地展示相关打包信息 包括构建过程 状态 日志 模块列表 打印在了控制台中
首先导入Dashboard和对应插件 创建一个实体
const Dashboard = require('webpack-dashboard');
const DashboardPlugin = require('webpack-dashboard/plugin');
const dashboard = new Dashboard();
plugins: [ new DashboardPlugin(dashboard.setData) ]
再启动WebpackDevServer中配置quiet: true过滤掉多余的日志
{
publicPath: settings.output.publicPath,
hot: true,
quiet: true, // lets WebpackDashboard do its thing
historyApiFallback: true,
}
也可以在shell脚本中增加上这个参数
"scripts": { "start": "webpack-dev-server --quiet" },
webpack-merge:提取公共配置 减少重复配置代码speed-measure-webpack-plugin:简称SMP 它是一个可以分析Webpack打包过程中 Loader和Plugin的耗时操作 有助于找到构建过程中性能瓶颈
const smp = new SpeedMeasurePlugin();
smp.wrap({配置config})
size-plugin:监控资源体积变化HotModuleReplacementPlugin:热更新模块替换 其实在devServer设置了hot:true 并且在shell脚本中"start": "webpack-dev-server --hot --open"这个时候webpack 自动引入 HotModuleReplacementPlugin 插件,而不用我们再手动引入了
文件监听的原理
在发现源码发生变化的时候 自动重新构建出新的输出文件 webpack 开启监听模式有两种
- 启动webpack命令时候加上
--watch - 在配置文件中加入
watch:true
缺点: 需要每次手动刷新浏览器
原理:轮询判断文件的最后编辑时间是否发生了变化 如果发生变化 他不会立刻告诉监听者 而是先缓存起来 然后等aggregateTimeout 后再执行。
module.export = {
// 默认false,也就是不开启
watch: true,
// 只有开启监听模式时,watchOptions才有意义
watchOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout:300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll:1000
}
}
webpack热更新的原理
webpack 的热更新也称热替换(Hot Module Replacement)建成HMR 这个机制可以达到不用刷新浏览器而将新的模块替换旧的模块
HMR的核心就是当代码发生了改变 客户端会从服务端拉去更新后的代码 准确说应该是chunk diff(chunk需要更新的部分也就是修改的部分) 实际上是webpack-dev-server(WDS)与浏览器之间维护了一个websocket 当本地资源发生改变的时候 WDS会想浏览器推送更新 并且会携带上一次hash 让客户端与上一次资源进行了对比 客户端对比出结果以后 会向WDS发起Ajax的请求来回去更新的内容(文件列表和hash)这样客户端就可以借助这些信息继续向WDS发起jsonp请求获取chunk的增加进行更新
拿到更新的chunk 由HotModulePlugin来完成 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用 像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。
文件指纹是什么
文件指纹就是打包后输出的文件名和后缀
Hash和整个项目构建有关 项目文件只要修改 hash就会发生改变 (图片指纹一般用它)Chunkhash和webpack打包的chunk有关 不同的entry会有不同的chunkhash (JS文件指纹用它)Contenthash根据文件内容定义hash 文件内容不变 则他不变 (css指纹用)
代码分割的本质和意义
代码分割的本质其实就是源代码直接上线和打包成唯一脚本main.bundle.js 这两种极端方案之间的一种更适合实际场景的中间状态
在开发多页面应用的时候 如果不对webpack打包进行优化 当某个模块被多个入口模块引用的时候 他就会被打包多次都是相同的代码 当项目复杂起来 打包出来的文件体积就会非常庞大 大体积的文件会增加编译时间 影响开发效率 并且当上线的时候 也会拉长请求时间和加载过长 造成首屏渲染速度过慢 影响网站体验
个人的拆分原则为
业务代码和第三方库分离打包 实现代码分割业务代码中的公共业务模块提取打包到一个模块-第三方库也可能会累积非常大 把大的三方库单独打包一个 剩下的打包一起
webpack提供了一个内置插件optimization.splitChunks
splitChunks: {
minSize: 30, //提取出的chunk的最小大小
cacheGroups: {
default: {
name: 'common', 提取出来的bundle的重命名
chunks: 'initial', 制定哪些chunk参与拆分 (all为所有模块 async 异步加载的 initial为初始化所获取的模块)
minChunks: 2, //模块被引用2次以上的才抽离
priority: -20 , // 如果有重复的地方 根据这个判断优先级
minSize:1111 // 生成文件的最小大小 但是为字节
},
vendors: { //拆分第三方库(通过npm|yarn安装的库)
test: /[\\/]node_modules[\\/]/, // 通过正则来进一步缩小模块范围
maxInitialRequests: 5, 入口并行加载最大数量 为了对拆分的数量进行限制 不至于拆分太多模块导致数量过多
name: 'vendor',
chunks: 'initial',
priority: -10
},
locallib: { //拆分指定文件
test: /(src\/locallib\.js)$/,
name: 'locallib',
chunks: 'initial',
priority: -9
}
common: {
chunks: 'all',
test: /[\\/]src[\\/]js[\\/]/,
name: 'common',
minChunks: 1,
minSize: 30000,
priority: 60
},
styles: {
name: 'styles',
test: /\.(sa|sc|c)ss$/,
chunks: 'all',
enforce: true
},
}
}
}
babel原理
大多数javaScript 的parser都遵循着estree规范 babel最初基于是acorn项目(一个js的解析器)
babel大概分为三大部分
解析:将代码解析为AST-
词法分析:将代码(字符串)分割成token流 就是语法单元成的数组 数组里面有多个对象 没个对象里面有type和value
-
语法分析:分析token流(数组)并生成AST
转换:访问AST的节点进行变换操作生产新的AST生成:以新的AST为基础生成代码
webpack的优化项
开启多线程打包
- thread-loader 他是放在其他loader上面 他下面的loader都会放在一个单独的
worker线程池中运行 一个worker就是一个nodeJS进程 每个单独的进程处理事件上线600ms 2.注意:thread-loader要放在style-loader之后 因为链式调用 他后面的loader无法存取文件也没法获取webpack的选项设置 - 官方为了防止启动worker时的高延迟 还提供了对worker池的预热优化
// ...
const threadLoader = require('thread-loader');
const jsWorkerPool = {
// options
// 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
// 当 require('os').cpus() 是 undefined 时,则为 1
workers: 2,
// 闲置时定时删除 worker 进程
// 默认为 500ms
// 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
poolTimeout: 2000
};
const cssWorkerPool = {
// 一个 worker 进程中并行执行工作的数量
// 默认为 20
workerParallelJobs: 2,
poolTimeout: 2000
};
threadLoader.warmup(jsWorkerPool, ['babel-loader']);
threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
options: jsWorkerPool
},
'babel-loader'
]
},
{
test: /\.s?css$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'thread-loader',
options: cssWorkerPool
},
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]--[hash:base64:5]',
importLoaders: 1
}
},
'postcss-loader'
]
}
// ...
]
// ...
}
// ...
}
HappyPack(作者已经不开始维护他 所以不推荐用)
在webpack构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中,HappyPack 可利用多进程对文件进行打包(默认cpu核数-1),对多核cpu利用率更高
合理利用缓存机制(缩短连续构建时间,增加初始构建事件)
每次项目都没必要需要进行初始构建的话,缓存会大大缩短你的二次构建时间
- cache-loader 仅仅在性能消耗大的loader前面加上就行 它会把后面的loader添加到磁盘里面 显著提升二次构建 请注意:因为保存和读取缓存文件需要一些时间开销 所以只对性能开销大的loader使用
module: {
rules: [
{
test: /\.ext$/,
use: ['cache-loader', ...loaders],
include: path.resolve('src'),
},
],
},
};
- HardSourceWebpackPlugin
- 第一次构建将花费正常的时间
- 第二次构建将显着加快(大概提升90%的构建速度) 他是在高速缓存内写入 默认情况下时在node_modules目录中 configHash在启动webpack实例时转换webpack配置, 并用于cacheDirectory为不同的webpack配置构建不同的缓存
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
const clientWebpackConfig = {
// ...
plugins: [
new HardSourceWebpackPlugin({
// cacheDirectory是在高速缓存写入。默认情况下,将缓存存储在node_modules下的目录中
// 'node_modules/.cache/hard-source/[confighash]'
cacheDirectory: path.join(__dirname, './lib/.cache/hard-source/[confighash]'),
// configHash在启动webpack实例时转换webpack配置,
// 并用于cacheDirectory为不同的webpack配置构建不同的缓存
configHash: function(webpackConfig) {
// node-object-hash on npm can be used to build this.
return require('node-object-hash')({sort: false}).hash(webpackConfig);
},
// 当加载器、插件、其他构建时脚本或其他动态依赖项发生更改时,
// hard-source需要替换缓存以确保输出正确。
// environmentHash被用来确定这一点。如果散列与先前的构建不同,则将使用新的缓存
environmentHash: {
root: process.cwd(),
directories: [],
files: ['package-lock.json', 'yarn.lock'],
},
// An object. 控制来源
info: {
// 'none' or 'test'.
mode: 'none',
// 'debug', 'log', 'info', 'warn', or 'error'.
level: 'debug',
},
// Clean up large, old caches automatically.
cachePrune: {
// Caches younger than `maxAge` are not considered for deletion. They must
// be at least this (default: 2 days) old in milliseconds.
maxAge: 2 * 24 * 60 * 60 * 1000,
// All caches together must be larger than `sizeThreshold` before any
// caches will be deleted. Together they must be at least this
// (default: 50 MB) big in bytes.
sizeThreshold: 50 * 1024 * 1024
},
}),
new HardSourceWebpackPlugin.ExcludeModulePlugin([
{
test: /.*\.DS_Store/
}
]),
]
}
优化搜索时间 缩小文件范围 减少不必要的编译工作
- 优化loader配置 可以通过
test,include,exclude配置寻找规则 - 优化
resolve.module配置 从webpack那些目录下面寻找第三方模块 因为会在当前目录寻找./node_modules然后会一层一层的去寻找 知道最终的./node_modules 还是没有找到就会报错 - 优化
resolve.alias配置 给项目路径起个别名 把原导入路径映射成一个新的导入路径 减少耗时递归解析 - 优化
resolve.extensions配置 给文件名自动添加后缀 把高频的放在前面 - 优化
module.noParse忽略没采用模块化的文件递归处理 提高构建性能 例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准
// 编译代码的基础配置
module.exports = {
// ...
module: {
// 项目中使用的 jquery 并没有采用模块化标准,webpack 忽略它
noParse: /jquery/,
rules: [
{
// 这里编译 js、jsx
// 注意:如果项目源码中没有 jsx 文件就不要写 /\.jsx?$/,提升正则表达式性能
test: /\.(js|jsx)$/,
// babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
use: ['babel-loader?cacheDirectory'],
// 排除 node_modules 目录下的文件
// node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: /node_modules/,
},
]
},
resolve: {
// 设置模块导入规则,import/require时会直接在这些目录找文件
// 可以指明存放第三方模块的绝对路径,以减少寻找
modules: [
path.resolve(`${project}/client/components`),
path.resolve('h5_commonr/components'),
'node_modules'
],
// import导入时省略后缀
// 注意:尽可能的减少后缀尝试的可能性
extensions: ['.js', '.jsx', '.react.js', '.css', '.json'],
// import导入时别名,减少耗时的递归解析操作
alias: {
'@compontents': path.resolve(`${project}/compontents`),
}
},
};
- 压缩图片 配置 image-webpack-loader