webpack四个核心概念:
入口(entry),输出 (output),loader,插件(plugins)
1.webpack工作流程
- 参数解析
- 找到入口文件
- 调用Loader编译文件
- 遍历AST,收集依赖
- 生成Chunk
- 输出文件
2.loader的配置和使用
// webpack.config.js
module.exports = {
module: {
rules: [
{ test: /\.js$/, use: 'babel-loader' },
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'postcss-loader' },
]
}
]
}
};
use的类型 string|array|object|function:
- sting:只用一个Loader时,直接声明Loader,比如 babel-loader
- array:声明多个Loader时,使用数组形式声明,比如.css的loader
- object:只有一个Loader时,需要有额外的配置项时
- function: use也支持回调函数形式
当use是通过数组形式声明Loader时,Loader的执行顺序是从右到左,从下到上,
potscss-loader -> css-loader -> style-loader
styleLoader(cssLoader(postcssLoader(content)))
2.1内联引入
可以在import等语句里指定Loader,使用!来将Loader分开
import style from 'style-loader!css-loader?modules!./styles.css'
内联时,通过query来传递参数,例如?key=value
一般来说,推荐同意config的形式来配置Loader,内联形式多出现在Loader内部,比如style-loader会在自身代码里引入css-loader
require("!!../../node_modules/css-loader/dist/cjs.js!./styles.css")
2.2loader类型
同步loader
module.exports = function(source){
const result = someSyncOperation(source)
return result
}
一般来说,loader都是同步的,通过return或者this.callback来同步地返回source转换后的结果。
异步loader
有的时候,我们需要在loader里做些异步的事情,比如需要发送网络请求,如果同步地等着,网络请求就会阻塞整个构建过程,这个时候我们需要异步Loader
module.exports = function (source){
// 告诉webpack这次是异步转换
const callback = this.async()
someAsyncOperation(content,function(err,result){
if(err) return callback(err)
// 通过 callback来返回异步处理的结果
callback(null,result,map,meta)
})
}
piching loader
{
test: /\.js$/,
use: [
{ loader: 'aa-loader' },
{ loader: 'bb-loader' },
{ loader: 'cc-loader' },
]
}
loader总是从右到左被调用, cc-loader ->bb-loader ->aa-loader
每个loader都支持一个pitch属性,通过module.exports.pitch 声明,如果该loader声明了pitch,则该方法会优先于loader的实际方法先执行,
|- aa-loader `pitch`
|- bb-loader `pitch`
|- cc-loader `pitch`
|- requested module is picked up as a dependency
|- cc-loader normal execution
|- bb-loader normal execution
|- aa-loader normal execution
也就是会先从左向右执行一次每个loader的pitch方法,再按照从右向左的顺序执行其实际方法
2.3 raw loader
我们在url-loader 里和file-loader最后都见过这样一句代码
export const raw = true
默认情况下,webpack会把文件进行utf-8编码,然后传给loader,通过设置raw,loader就可以接受原始的buffer数据。
2.4 loader几个重要的api
所谓loader,也只是一个符合commonjs规范的node模块,它会导出一个可执行函数。loader runner会调用这个函数,将文件的内容或者上一个loader处理的结果传递进去。同时,webpack还为loader提供了一个上下文this
2.4.1 this.callback()
在loader中,通常使用return 来返回一个字符串或者buffer。如果需要返回多个结果值时,就需要使用 this.callback,定义
this.callback(
// 无法转换时返回 Error,其余情况都返回 null
err: Error| null,
// 转换结果
content: string | Buffer,
// source map 方便调试用
sourceMap?: SourceMap,
// 可以时任何东西
meta?: any
)
一般来说如果调用该函数的话,应该手动return ,告诉webpack返回的结果在this.callback中,以避免含糊不清的结果
module.exports = function (source){
this.callback(null, source, sourceMaps)
return
}
2.4.2 this.async()
同上异步loader
2.4.3 this.cacheable()
3. webpack常用的三种JS压缩插件
UglifyJS,webpack-parallel-uglify-plugin,terser-webpack-plugin
3.1 UglifyJS
支持: babel、present2015、webpack3
缺点:它使用的是单线程压缩代码,也就是说多个js文件需要被压缩,它需要一个个文件进行压缩。所以在正式环境打包压缩代码速度非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,再去应用各种规则分析和处理AST,导致这个过程耗时非常大)
优点:老项目支持(兼容IOS10)
3.2 webpack-parallel-uglify-plugin
支持: babel7、webpack4
缺点:老项目不支持(不兼容IOS10)
优点:parallelUglifyPlugin插件则会开启多个子进程,把多个文件压缩的工作分给多个子进程去完成,但是每个子进程还是通过UglifyJS去压缩代码。无非就是变成了并行处理该压缩了,并行处理多个子任务,效率会更加的高。
3.3 terser-webpack-plugin
支持:babel7、webpack4
缺点:老项目不支持(不兼容IOS10)
有点:和ParallelUglifyPlugin一样,并行处理多个子任务,效率会更高。webpack4官方推荐,有人维护。
4. webpack面试题
- 说一说 loader和 plugin 的区别
- webpack 构建流程是怎样的
- 编写 webpack loader 的思路
- 编写 webpack plugin 的思路
webpack打包原理:
1. 识别入口文件
2. 通过逐层识别模块依赖(Commonjs、amd或者es6的import,webpack都会对其进行分析,来获取代码的依赖)
3. webpack做的就是分析代码,转换代码,编译代码,输出代码
4. 最终形成打包后的代码
什么是loader:
loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
1. 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行
2. 第一个执行的loader接受源文件内容作为参数,其他loader接受前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码
什么是plugin
在webpack运行的生命周期中会广播出许多时间,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。
loader和plugin的区别
对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.less转换为A.css,单纯的将文件转换过程
plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。
如何自定义webpack plugins:
1. javascript 命名函数
2. 在插件函数prototype上定义一个apply方法
3. 定义一个绑定到webpack自身的hook
4. 处理webpack内部特定数据
5. 功能完成后调用webpack提供的回调
5.webpack优化
5.1缓存
大部分loader都提供了cache配置项,可以通过设置cacheDirectory来开启缓存或者使用cache-loader
module.exports = {
module:{
rules:[
{
test:/\.ext$/,
use:['cache-loader',...loaders],
include:path.resolve('src')
},
{
test:/\.js$/,
loader:require.resolve('bable-loader'),
options:{
cacheDirectory:true,
}
}
]
}
}
5.2多核
happypack 开启多核线程
const HappyPack = require('happypack')
const os = require('os')
// 开辟一个线程池
// 拿到系统CPU的最大核数,happypack 将编译工作灌满所有线程
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: 'happypack/loader?id=js',
},
],
},
plugins: [
new HappyPack({
id: 'js',
threadPool: happyThreadPool,
loaders: [
{
loader: 'babel-loader',
},
],
}),
],
}
5.3抽离
常用的静态依赖,或者工具库,使用Externals的方式使用CDN的方式引用他们
module.exports = {
...,
externals: {
// key是我们 import 的包名,value 是CDN为我们提供的全局变量名
// 所以最后 webpack 会把一个静态资源编译成:module.export.react = window.React
"react": "React",
"react-dom": "ReactDOM",
"redux": "Redux",
"react-router-dom": "ReactRouterDOM"
}
}
6. tree-shaking
tree-shaking的技术是因为ES6Module是一种可以做静态分析的模块机制,当前主流的tree-shaking技术依赖于ES6中的Import和export模块机制,打包器会检测代码中的模块时候被导出、导入,且被JavaScript文件使用。
// 导入并赋值给 JavaScript 对象,然后在下面的代码中被用到
// 这会被看作“活”代码,不会做 tree-shaking
import Stuff from './stuff';
doSomething(Stuff);
// 导入并赋值给 JavaScript 对象,但在接下来的代码里没有用到
// 这就会被当做“死”代码,会被 tree-shaking
import Stuff from './stuff';
doSomething();
// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
// 这会被当做“死”代码,会被 tree-shaking
import './stuff';
doSomething();
//// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
//// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理方式不同。
import 'my-lib';
doSomething();
用支持tree-shaking的方式写import
在编写支持tree-shaking的代码时,导入方式非常重要,避免将整个库导入到单个JavaScript对象中,当这样做时,webpack会认为你需要整个库,这个时候,webpack就不会摇它
比如:
// 全部导入 (不支持 tree-shaking)
import _ from 'lodash';
// 具名导入(支持 tree-shaking)
import { debounce } from 'lodash';
// 直接导入具体的模块 (支持 tree-shaking)
import debounce from 'lodash/lib/debounce';
基本的webpack配置
使用webpack进行tree-shaking第一步是编写webpack配置文件,
1.处于生产模式,webpack只有在压缩代码时才会tree-shaking
2.usedExports设置为true,
3.支持删除死代码的压缩器 terser-webpack-plugin,terserPlugin
// Base Webpack Config for Tree Shaking
const config = {
mode: 'production',
optimization: {
usedExports: true,
minimizer: [
new TerserPlugin({...})
]
}
};
副作用
全局样式表,或者全局配置的JavaScript文件,webpack认为这样的文件有副作用,具有副作用的文件不应该被tree-shaking。
但是我们可以配置我们的项目,告诉webpack它是没有副作用的,可以进行tree-shaking
如何告诉webpack你的代码无副作用
package.json有一个特殊的属性sideEffects,就是为此而存在的,它有三个值:
1.true是默认值,这意味着所有的文件都有副作用,也就是没有一个可以tree-shaking
2.false,所有的文件都可以进行tree-shaking
3.[...]文件路径数组,告诉webpack除了数组中的文件,其他的文件都可以进行tree-shaking
全局CSS与副作用
全局CSS直接导入到JavaScript文件中的样式表
import './myStyleSheet.css'
因此,如果你做了副作用的修改,那么在运行webpack构建时,导入的样式将会被删除。
解决方案:
webpack使用它的模块规则系统来控制各种类型文件的加载。每种文件类型的每个规则都有自己的sideEffects标志。这会覆盖之前为匹配规则的文件设置的所有sideEffects标志
// 全局 CSS 副作用规则相关的 Webpack 配置
const config = {
module: {
rules: [
{
test: /regex/,
use: [loaders],
sideEffects: true
}
]
}
};
webpack所有模块规则上都有这个属性,处理全局样式表的规则必须用上它。
什么是模块,模块为什么重要
多年来,javaScript已经发展出在文件之间以模块的方式有效导入/到处代码的能力。
// Commonjs
const stuff = require('./stuff');
module.exports = stuff;
// es2015
import stuff from './stuff';
export default stuff;
默认情况下,babel假定我们使用的es2015模块编写代码,并转换成commonJS模块,这样做是为了与服务器端JavaScript库的广泛兼容,JavaScript库通常构建在nodejs之上,但是webpack不支持使用commonJS模块来完成tree-shaking
为了进行tree-shaking我们需要将代码编译到es2015模块
es2015模块Babel配置
// es2015 模块的基本 Babel 配置
const config = {
presets: [
[
'[@babel/preset-env](http://twitter.com/babel/preset-env)',
{
modules: false
}
]
]
};
把modules设置为false,就是告诉Babel不要编译模块代码,这会让Babel保留我们现有的es215 import/export语句
**划重点:**所有可需要 tree-shaking 的代码必须以这种方式编译。因此,如果你有要导入的库,则必须将这些库编译为 es2015 模块以便进行 tree-shaking 。如果它们被编译为 commonjs,那么它们就不能做 tree-shaking ,并且将会被打包进你的应用程序中。许多库支持部分导入,lodash 就是一个很好的例子,它本身是 commonjs 模块,但是它有一个 lodash-es 版本,用的是 es2015模块。
此外,如果你在应用程序中使用内部库,也必须使用 es2015 模块编译。为了减少应用程序包的大小,必须将所有这些内部库修改为以这种方式编译。
7.code spliting
主要是将逻辑代码和第三方代码进行拆分,
自动化分离vendor需要引入minChunks,在配置中我们就可以对所有node_module下所引用的模块进行打包,
new webpack.optimize.CommonsChunkPlugin({
name:"vendor",
minChunks:({resource})=>(
resource && resource.indexOf('node_modules')>=0 && resource.match(/\.js$/)
)
})
// 使用async异步加载的就再加上这个优化
new webpack.optimize.CommonsChunkPlugin({
async: 'used-twice',
minChunks: (module, count) => (
count >= 2
),
}),
如果第三方库使用的是异步加载,就还是会导致重复加载第三方库
8.babel编译原理
- babylon 将ES6/ES7代码解析成AST
- babel-traverse对AST进行遍历转译,得到新的AST
- 新的AST通过babel-generator转换成ES5
9.webpack loader为什么是从右至左运行
js函数组合是函数式编程中非常重要的思想,有两种函数组合的实现方式,一种是pipe,一种是compose,前者从左向右组合函数,后者方向反之。
例如:let compose = (f,g)=>(...args)=>f(g(...args));
在webpack loader中就运用了compose的方式运行的。
10. css,js处理
css 处理 4步走
css less/scss...代码处理=> css => css兼容 => 代码提取到单独的css文件=> css文件压缩
module.exports = {
module:{
rules:[
{
test:/\.css$/,
use:[
MiniCssExtractPlugin.loader, // 将js文件中css抽出打包到一个css文件中
'css-loader',
{
loader:'postcss-loader', // css 兼容性处理在package.json对浏览器版本browserslist做配置
options:{
ident:'postcss',
plugins:()=>[
require('postcss-preset-env')()
]
}
},
'less-loader'// "scss-loader"
]
}
]
},
plugins:[
new MiniCssExtractPlugin({ // 将js文件中的css提取出到单独的css文件中
filename:"css/build.css"
}),
new OptimizeCssAssetsWebpackPlugin() // css代码压缩
]
}
js
开发中的代码通过eslint-loader做规范处理,错误预检,然后通过babel-loader做兼容性,将第三方代码和业务代码分开code-split,代码压缩
module.exports = {
// 一般在开发模式下做兼容处理
/*
js兼容性处理:babel-loader @babel-core
1.基本兼容性处理 @babel/preset-env
问题只能转换基本语法,promise不能处理
2.全部兼容性处理 @babel/polyfill
问题:只需要部分兼容就好了,但是将所有的兼容都引入了
3.按需加载做兼容性处理:core-js
*/
module:{
rules:[
{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:[
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs:{version:3},
targets:{
chrome: '60',
firefox:"60",
ie:"9",
safari:'10',
edge:'17'
}
}
]
]
}
},
/*
语法检查 : eslint-loader eslint
*/
{
test:/\.js$/,
exclude:/node_modules/,
enforce:'pre', // 一个文件同时只能进行一个loader的处理,加这个属性,就会使得语法检查最先执行
loader:"eslint-loader",
options:{
// 自动修复格式问题
fix:true
}
}
]
}
}
11.如何提高webpack的打包速度
- 利用缓存:利用Webpack的持久缓存功能,避免重复构建没有变化的代码
- 使用多进程/多线程构建 :使用thread-loader、happypack等插件可以将构建过程分解为多个进程或线程
- 使用DllPlugin和HardSourceWebpackPlugin: DllPlugin可以将第三方库预先打包成单独的文件,减少构建时间。HardSourceWebpackPlugin可以缓存中间文件,加速后续构建过程
- 使用Tree Shaking: 配置Webpack的Tree Shaking机制,去除未使用的代码,减小生成的文件体积
- 移除不必要的插件: 移除不必要的插件和配置,避免不必要的复杂性和性能开销
12. 如何减少打包的体积
- 代码分割(Code Splitting):将应用程序的代码划分为多个代码块,按需加载
- Tree Shaking:配置Webpack的Tree Shaking机制,去除未使用的代码
- 压缩代码:使用工具如UglifyJS或Terser来压缩JavaScript代码
- 使用生产模式:在Webpack中使用生产模式,通过设置mode: 'production'来启用优化
- 使用压缩工具:使用现代的压缩工具,如Brotli和Gzip,来对静态资源进行压缩
- 利用CDN加速:将项目中引用的静态资源路径修改为CDN上的路径,减少图片、字体等静态资源等打包
// 提高打包速度
// 1. 合理使用loader, exclude,include
// 2. 使用treeshaking, 删除无用代码
// 3. 开启多进程打包, thread-loader
// 4. 利用缓存,cache
// 5. DllPlugin可以将第三方库预先打包成单独的文件,减少构建时间
// 减少打包体积 将一个大的包拆分成多个小包,并提取公共代码
// 1. 代码压缩,js terser-webpack-plugin css css-minimizer-webpack-plugin,optimization, minimize:true
// 2. three-shaking 删除引用但是没用的代码 ,optimization usedExports:true
// 3. 组件可以采用 按需引用,babel-plugin-import 设置
// 4. dllPlugin 将不会改动的三方代码打包到一个js文件中