前端进阶-前端工程化一,webpack基本概念和使用
# Webpack
webpack 是一个用于现代 JavaScript 应用程序的_静态模块打包工具_。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph) (opens new window),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。
# webpack中的基本概念
1.入口:
__入口起点(entry point)__指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) (opens new window) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
默认值是 ./src/index.js
,但你可以通过在 webpack configuration (opens new window) 中配置 entry
属性,来指定一个(或多个)不同的入口起点。
2.输出:
可以通过配置 output
选项,告知 webpack 如何向硬盘写入编译文件。注意,即使可以存在多个 entry
起点,但只能指定一个 output
配置。
3.loader:
webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块 (opens new window),以供应用程序使用,以及被添加到依赖图中。
在 webpack 的配置中,loader 有两个属性:
(1)test
属性,识别出哪些文件会被转换。
(2)use
属性,定义出在进行转换时,应该使用哪个 loader。
4.插件。
loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
5.模式
# 配置的相关概念
1.解析(resolve):能设置模块如何被解析。webpack 提供合理的默认值,但是还是可能会修改一些解析的细节。
2.优化(Optimization)
3.开发服务器(devServer)
devServer配置跨域:
使用http-proxy-middleware实现跨域代理
module.exports = {
devServer:{
proxy:{
'/api':{
target:'http://www.baidu.com',
pathRewrite:{'^/api':''},
changeOrigin: true, //代理为域名是添加此配置才能生效
secure:false, //是否支持https协议的代理
},
'/api2':{
...
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
说明:
'/api':捕获api的标志,如果api中有这个字符串就会匹配代理,如/api/users会代理为 http://www.baidu.com/api/users
Target:代理的api地址,可以是域名或者ip地址
pathRewrite:
4.devtools:
此选项控制是否生成,以及如何生成 source map。
使用 SourceMapDevToolPlugin
(opens new window) 进行更细粒度的配置。查看 source-map-loader
(opens new window) 来处理已有的 source map。
source map 是有用的调试工具,可以在报错时直接查看压缩代码对应的原始代码文件。
常用配置
//webpack.config.js
module.exports = {
devServer:{
hot:true //热更新
}
}
2
3
4
5
6
# 代码分离
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
去掉重复
动态加载模块
# tree shaking
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。
# 外部扩展external
externals
配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。
防止将某些 import
的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies),比如CDN的方式引入。
比如在index.html中通过cdn的方式引入jquery而不是打包
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous"
></script>
2
3
4
5
在webpack中配置
module.exports = {
//...
externals: {
jquery: 'jQuery',
},
};
2
3
4
5
6
这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:
import $ from 'jquery';
$('.my-element').animate(/* ... */);
2
3
具有外部依赖(external dependency)的 bundle 可以在各种模块上下文(module context)中使用
另一方面,如果你想将一个符合 CommonJS 模块化规则的类库外部化,你可以提供外联类库的类型以及类库的名称。
如果你想将 fs-extra
从输出的 bundle 中剔除并在运行时中引入它,你可以如下定义:
module.exports = {
// ...
externals: {
'fs-extra': 'commonjs2 fs-extra',
},
};
2
3
4
5
6
则代码会变化为
//import fs from 'fs-extra';
const fs = require('fs-extra');
2
# 支持typescript
首先,执行以下命令安装 TypeScript compiler 和 loader:
npm install --save-dev typescript ts-loader
在webpack.config.js中配置
const path = require('path');
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建插件
一个webpack的插件有五部分构成:
1.一个JavaScript的命名函数;
2.在该函数上定义apply方法;
3.指定一个绑定到 webpack 自身的事件钩子。
4.处理 webpack 内部实例的特定数据。
5.功能完成后调用 webpack 提供的回调。
因此,一个最简单的插件可以写为
// 一个 JavaScript 命名函数。
function MyExampleWebpackPlugin(options) {
// 使用 options 设置插件实例……
};
// 在插件函数的 prototype 上定义一个 `apply` 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
// 指定一个挂载到 webpack 自身的事件钩子。
compiler.plugin('webpacksEventHook', function(compilation /* 处理 webpack 内部实例的特定数据。*/, callback) {
console.log("This is an example plugin!!!");
// 功能完成后调用 webpack 提供的回调。
callback();
});
};
//ES6 写法
class BasicPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options){
}
// Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler){
compiler.plugin('compilation',function(compilation) {
})
}
}
// 导出 Plugin
module.exports = BasicPlugin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
具体解释如下:
compiler
对象代表了完整的 webpack 环境配置,负责文件的监听和启动编译,全局只有一个compiler对象。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
compilation
对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
插件是由「具有 apply
方法的 prototype 对象」所实例化出来的。这个 apply
方法在安装插件时,会被 webpack compiler 调用一次。apply
方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。
使用 compiler 对象时,你可以绑定提供了编译 compilation 引用的回调函数,然后拿到每次新的 compilation 对象。这些 compilation 对象提供了一些钩子函数,来钩入到构建流程的很多步骤中。
上述插件在Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options)
初始化一个 BasicPlugin 获得其实例。 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler)
给插件实例传入 compiler 对象。 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数)
监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。
有一些编译插件中的步骤是异步的,这样就需要额外传入一个 callback 回调函数,并且在插件运行结束时,_必须_调用这个回调函数。
事件流
Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
Webpack 通过 Tapable (opens new window) 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。 Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,
/** 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params);
/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {
});
2
3
4
5
6
7
8
9
10
11
12
13
常用api
读取输出资源、代码块、模块及其依赖
有些插件可能需要读取 Webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。
在 emit
事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。
class Plugin {
apply(compiler) {
compiler.plugin('emit', function (compilation, callback) {
// compilation.chunks 存放所有代码块,是一个数组
compilation.chunks.forEach(function (chunk) {
// chunk 代表一个代码块
// 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块
chunk.forEachModule(function (module) {
// module 代表一个模块
// module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
module.fileDependencies.forEach(function (filepath) {
});
});
// Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
// 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
// 该 Chunk 就会生成 .js 和 .css 两个文件
chunk.files.forEach(function (filename) {
// compilation.assets 存放当前所有即将输出的资源
// 调用一个输出资源的 source() 方法能获取到输出资源的内容
let source = compilation.assets[filename].source();
});
});
// 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。
// 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
callback();
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
监听文件变化api
// 当依赖的文件发生变化时会触发 watch-run 事件
compiler.plugin('watch-run', (watching, callback) => {
// 获取发生变化的文件列表
const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
// changedFiles 格式为键值对,键为发生变化的文件路径。
if (changedFiles[filePath] !== undefined) {
// filePath 对应的文件发生了变化
}
callback();
});
2
3
4
5
6
7
8
9
10
默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件。 由于 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会重新触发新的 Compilation。 为了监听 HTML 文件的变化,我们需要把 HTML 文件加入到依赖列表中,
compiler.plugin('after-compile', (compilation, callback) => {
// 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
compilation.fileDependencies.push(filePath);
callback();
});
2
3
4
5
判断当前使用了哪些插件
// 判断当前配置使用使用了 ExtractTextPlugin,
// compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {
// 当前配置所有使用的插件列表
const plugins = compiler.options.plugins;
// 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null;
}
2
3
4
5
6
7
8
修改输出资源
有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit
事件,因为发生 emit
事件时所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此 emit
事件是修改 Webpack 输出资源的最后时机。
所有需要输出的资源会存放在 compilation.assets
中,compilation.assets
是一个键值对,键为需要输出的文件名称,值为文件对应的内容。
compiler.plugin('emit', (compilation, callback) => {
// 设置名称为 fileName 的输出资源
compilation.assets[fileName] = {
// 返回文件内容
source: () => {
// fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
return fileContent;
},
// 返回文件大小
size: () => {
return Buffer.byteLength(fileContent, 'utf8');
}
};
callback();
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建脚手架
# webpack多入口配置
通常使用webpack来打包单页应用,这个时候只需要配置一个入口文件,但是有时候会出现需要打包多个页面的项目,比如项目比较大或者项目需要多次的更新迭代时,都适合做成多页面程序;
又或者,公司经常开发一些活动页,几个活动页毫不相关,页面之间也没有共享的数据,但是页面都是用相同的react框架,使用相同的弹框组件,在这种情景下就需要webpack进行多页面打包。
这样既保留了单页应用的开发习惯,可以把每个页面看成单独的应用,又可以独立部署,解耦项目的复杂性,甚至可以在不同的页面选择不同的技术栈。
可以把多页面应用看成乞丐版的前端微服务。
手动配置
配置多个入口文件和htmlwebpackplugin
单页面
module.exports = {
entry:{
'page1':'./src/page/page1.js', //页面1
},
output:{
path:path.resolve(_dirname,'./dist'),
filename:'js/[name]/[name]-bundle.js', //filename不能写死,通过name获取bundle
},
}
2
3
4
5
6
7
8
9
多页面
module.exports = {
entry:{
'page1':'./src/page/page1.js', //页面1
'page2':'./src/page/page2.js' //页面2
},
output:{
path:path.resolve(_dirname,'./dist'),
filename:'js/[name]/[name]-bundle.js', //filename不能写死,通过name获取bundle
},
plugins:[
new HtmlWebpackPlugin(
{
template: './src/pages/page1/index.html',
chunks:['page1'],
}
),
new HtmlWebpackPlugin(
{
template: './src/pages/page2/index.html',
chunks:['page2'],
}
),
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
其他细节
1.把多个页面共用的第三方库(React、Fastclick)单独打包成一个vendor.js
2.把多个页面共用的逻辑代码和共用的全局css(字体、icon、全局cssreset)等单独打包common.js和common.css
3.把运行时代码单独提取出来manifest.js
4.把每个页面自己的业务代码打包出page1.js和page1.css
通过optimization实现
module.export = {
optimization:{
splitChunks:{
cacheGroups:{
common:{
name:"common",
chunks:"initial",
minSize:1,
priority:0,
minChunks:2 //引用了2次才打包
},
vendor:{
name:"vendor",
test:/[\\/]node_modules[\\/]/,
chunks:"initial",
priority:10,
minchunks:2 //引用了2次才打包
}
}
}
runtimeChunk:{name:'manifest'}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
动态添加
通过glob库动态添加
const getEntry = () =>{
const entry = {}
glob.sync('/src/js/*.js').forEach((name)=>{
const start = name.indexOf('/src/js')+8;
const end = name.length -3;
const eArr = [];
const n = name.slice(start,end);
eArr.push(name);
entry[n] = eArr;
})
return entry;
}
module.exports = {
entry:getEntry(),
output:{
path:path.resolve(_dirname,'./dist'),
filename:'./js/[name].[chunkhash:8].js' //包名
chunkFilename:'js/[name].[chunkhash:8].js'//公共块名
}
}
Object.keys(config.entry).forEach((name)=>{
config.plugins.push(new HtmlWebpackPlugin({
template:`./src/${name}.pug`
filename:`${name}.html`
chunks:['js',`${name`],
minify:{
collapseWhitespace:false;
}
}))
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# webpack 的缺点
# webpack优化
如果你的项目很小,构建很快,其实不需要特别关注性能方面的问题。
但是随着项目涉及到的页面越来越多,功能和业务代码也会越来越多,相应的 webpack
的构建时间也会越来越久,这个时候我们就不得不考虑性能优化的事情了。
因为这个构建时间与我们的日常开发是密切相关,当我们本地开发启动 devServer
或者 build
的时候,如果时间过长,会大大降低我们的工作效率。
试想一个场景,我们突然碰到一个紧急 bug
,项目启动需要花费 3/4
分钟,改完后项目 build
上线也要 3/4
分钟,这个时候脑瓜是不是 duang
、duang
、duang
...
体积压缩
优化构建速度
减小搜索范围
利用多线程、多进程打包、压缩:运行在 Node.js 之上的 Webpack 是单线程模型的,也就是说 Webpack 需要处理的任务需要一件件挨着做,不能多个事情一起做。由于 JavaScript 是单线程模型,要想发挥多核 CPU 的能力,只能通过多进程去实现,而无法通过多线程实现。HappyPack (opens new window) 就能让 Webpack 做到这点,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。
用过 UglifyJS 的你一定会发现在构建用于开发环境的代码时很快就能完成,但在构建用于线上的代码时构建一直卡在一个时间点迟迟没有反应,其实卡住的这个时候就是在进行代码压缩。
由于压缩 JavaScript 代码需要先把代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST,导致这个过程计算量巨大,耗时非常多。
ParallelUglifyPlugin (opens new window) 就做了这个事情。 当 Webpack 有多个 JavaScript 文件需要输出和压缩时,原本会使用 UglifyJS 去一个个挨着压缩再输出, 但是 ParallelUglifyPlugin 则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。 所以 ParallelUglifyPlugin 能更快的完成对多个文件的压缩工作。
使用 ParallelUglifyPlugin 也非常简单,把原来 Webpack 配置文件中内置的 UglifyJsPlugin 去掉后,再替换成 ParallelUglifyPlugin,
开启缓存,提升二次构建的速度。
合理使用sourcemap
- 使用
ES6 Modules
语法,以保证Tree-Shaking
起作用
因为 tree-shaking
只对 ES6 Modules
静态引入生效,对于类似于 CommonJs
的动态引入方式是无效的
- 合理使用
Ployfill
如果我们对于引入的 polyfill
不做处理的话,Webpack
会把所有的 Polyfill
都加载进来,导致产出文件过大。推荐使用 @babel/preset-env
的 useBuiltIns='usage'
方案,此配置项会根据浏览器的兼容性帮助我们按需引入所需的垫片;此外我们也可以使用动态 polyfill
服务,每次根据浏览器的 User Agent
,下发不同的 Polyfill
,具体可以参考 polyfill.io
(opens new window)。
- 预加载资源
webpackPrefetch
使用 webpackPrefetch
来提前预加载一些资源,意思就是 将来可能需要一些模块资源,在核心代码加载完成之后带宽空闲的时候再去加载需要用到的模块代码。
icon
类图片使用css Sprite
来合并图片
如果 icon
类图片太多的话,就使用雪碧图合成一张图片,减少网络请求,或者使用字体文件。
html-webpack-externals-plugin
此插件可以将一些公用包提取出来使用 cdn
引入,不打入 bundle
中,从而减少打包文件大小,加快打包速度。
- 合理配置
chunk
的哈希值
在生产环境打包,一定要配置文件的 hash
,这样有助于浏览器缓存我们的文件,当我们的代码文件没变化的时候,用户就只需要读取浏览器缓存的文件即可。一般来说 javascript
文件使用 [chunkhash]
、css
文件使用 [contenthash]
、其他资源(例如图片、字体等)使用 [hash]
。
# Webpack loader
# loader原理
module 一开始构建的过程中,首先会创建一个 loaderContext 对象,它和这个 module 是一一对应的关系,而这个 module 所使用的所有 loaders 都会共享这个 loaderContext 对象,每个 loader 执行的时候上下文就是这个 loaderContext 对象
一共有4种loader,同一种类型文件被多条rules匹配
上的话会按以下叠加顺序处理,通过enforce
声明类型
- post
- inline
- normal(AutoLoaders)(
默认
) - pre(前置loader)
Loader默认加载顺序 post(后置)->inline(内联)->normal(autoLoaders)->pre(前置)
控制loader加载(require路径)
# 常见loader
file-loader
File-loader就是在JavaScript代码里import/require一个文件时会将该文件生成到输出目录,一般是资源文件图片等
npm install file-loader
配置
module.exports = {
module: {
rules: [
{
test:/\.(jpg|png|gif)$/,
use: [
{
loader: 'file-loader',
options: {}
}
]
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss-loader
style-loader
css-loader
babel-loader
# 创建loader
一个loader
可以看做是一个node
模块,也可以看做一个loader
就是一个函数 (loader会导出一个函数),众所周知webpack
只能识别js
文件,loader
在webpack
中担任的角色就是翻译工作,它可以让其它非js
的资源(source)可以在webpack
中通过loader
顺利加载。
编写loader的原则:
- 单一职责,一个loader只做一件事
- 调用方式,loader是从右向左执行,链式调用
- 统一原则,loader输入和输出都字符串
loader导出尽量别使用箭头函数,loader内部属性都是靠this来获取的,如this.callback,this.sync
一个最简单的loader代码
module.exports = function(source) {
// source 为 compiler 传递给 Loader 的一个文件的原内容
// 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
return source;
};
2
3
4
5
由于 Loader 运行在 Node.js 中,你可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:
← 前端富文本 前端进阶(七)-单元测试 →