webpack 最出色的功能之一就是,除了
JavaScript
,还能够经过loader
引入任何其余类型的文件。css
Entry
(入口):Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。Output
(出口):指示 webpack 如何去输出、以及在哪里输出Module
(模块):在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出全部依赖的模块。Chunk
(代码块):一个 Chunk 由多个模块组合而成,用于代码合并与分割。Loader
(模块转换器):用于把模块原内容按照需求转换成新内容。Plugin
(扩展插件):在 Webpack 构建流程中的特定时机会广播出对应的事件,插件能够监听这些事件,并改变输出结果entry: {
a: "./app/entry-a",
b: ["./app/entry-b1", "./app/entry-b2"]
},
复制代码
多入口能够经过 HtmlWebpackPlugin
分开注入html
plugins: [
new HtmlWebpackPlugin({
chunks: ['a'],
filename: 'test.html',
template: 'src/assets/test.html'
})
]
复制代码
修改路径相关前端
publicPath
:并不会对生成文件的目录形成影响,主要是对你的页面里面引入的资源的路径作对应的补全filename
:能修改文件名,也能更改文件目录导出库相关vue
library
: 导出库的名称libraryTarget
: 通用模板定义方式webpack 一切皆模块,配置项 Module,定义模块的各类操做,node
Module 主要配置:jquery
loader
: 各类模块转换器extensions
:使用的扩展名alias
:别名、例如:vue-cli 经常使用的 @
出自此处plugins
: 插件列表devServer
:开发环境相关配置,譬如 proxy
externals
:打包排除模块target
:包应该运行的环境,默认 web
webpack从启动到结束会依次执行如下流程:webpack
Compiler
实例apply
方法,给插件传入compiler
实例的引用,插件经过compiler调用Webpack提供的API,让插件能够监听后续的全部事件节点。loader
将文件解析成抽象语法树 AST
chunk
ps:因为 webpack 是根据依赖图动态加载全部的依赖项,因此,每一个模块均可以明确表述自身的依赖,能够避免打包未使用的模块。git
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript
语法,以便可以运行在当前和旧版本的浏览器或其余环境中:github
Babel 内部所使用的语法解析器是 Babylonweb
主要功能
Polyfill
方式在目标环境中添加缺失的特性 (经过 @babel/polyfill
模块)codemods
)主要模块
@babel/parser
:负责将代码解析为抽象语法树@babel/traverse
:遍历抽象语法树的工具,咱们能够在语法树中解析特定的节点,而后作一些操做@babel/core
:代码转换,如ES6的代码转为ES5的模式在使用 webpack 构建的典型应用程序或站点中,有三种主要的代码类型:
library
或 "vendor
" 代码。webpack
的 runtime
使用 manifest
管理全部模块的交互。runtime
:在模块交互时,链接模块所需的加载和解析逻辑。包括浏览器中的已加载模块的链接,以及懒加载模块的执行逻辑。
manifest
:当编译器(compiler)开始执行、解析和映射应用程序时,它会保留全部模块的详细要点。这个数据集合称为 "Manifest", 当完成打包并发送到浏览器时,会在运行时经过 Manifest 来解析和加载模块。不管你选择哪一种模块语法,那些 import 或 require 语句如今都已经转换为 webpack_require 方法,此方法指向模块标识符(module identifier)。经过使用 manifest 中的数据,runtime 将可以查询模块标识符,检索出背后对应的模块。
其中:
import
或 require
语句会转换为 __webpack_require__
require.ensure
(在Webpack 4 中会使用 Promise 封装)gulp
是任务执行器(task runner):就是用来自动化处理常见的开发任务,例如项目的检查(lint)、构建(build)、测试(test)webpack
是打包器(bundler):帮助你取得准备用于部署的 JavaScript 和样式表,将它们转换为适合浏览器的可用格式。例如,JavaScript 能够压缩、拆分 chunk 和懒加载,loader
就是一个js文件,它导出了一个返回了一个 buffer
或者 string
的函数;
譬如:
// log-loader.js
module.exports = function (source) {
console.log('test...', source)
return source
}
复制代码
在 use 时,若是 log-loader
并无在 node_modules
中,那么可使用路径导入。
plugin: 是一个含有 apply
方法的 类
。
譬如:
class DemoWebpackPlugin {
constructor () {
console.log('初始化 插件')
}
apply (compiler) {
}
}
module.exports = DemoWebpackPlugin
复制代码
apply 方法中接收一个 compiler
参数,也就是 webpack实例。因为该参数的存在 plugin 能够很好的运用 webpack 的生命周期钩子,在不一样的时间节点作一些操做。
Webpack 加快打包速度的方法
include
或 exclude
加快文件查找速度HappyPack
开启多进程 Loader
转换ParallelUglifyPlugin
开启多进程 JS 压缩DllPlugin
+ DllReferencePlugin
分离打包
库
和 项目代码
分离打包cache-loader
)Webpack 加快代码运行速度方法
prefetch
) || 预加载(preload
)webpack-bundle-analyzer
代码分析动态导入
import(/* webpackChunkName: "lodash" */ 'lodash')
// 注释中的使用webpackChunkName。
// 这将致使咱们单独的包被命名,lodash.bundle.js
// 而不是just [id].bundle.js。
复制代码
预取(prefetch
):未来可能须要一些导航资源
chunk
加载完成,webpack
就会添加 prefetch
import(/* webpackPrefetch: true */ 'LoginModal');
// 将<link rel="prefetch" href="login-modal-chunk.js">其附加在页面的开头
复制代码
预加载(preload
):当前导航期间可能须要资源
preload
chunk 会在父 chunk 加载时,以并行方式开始加载webpackPreload
会有损性能,import(/* webpackPreload: true */ 'ChartingLibrary');
// 在加载父 chunk 的同时
// 还会经过 <link rel="preload"> 请求 charting-library-chunk
复制代码
为了极大减小构建时间,进行分离打包。
DllReferencePlugin 和 DLL插件DllPlugin 都是在_另外_的 webpack 设置中使用的。
DllPlugin
这个插件是在一个额外的独立的 webpack 设置中建立一个只有 dll 的 bundle(dll-only-bundle)。 这个插件会生成一个名为 manifest.json 的文件,这个文件是用来让 DLLReferencePlugin
映射到相关的依赖上去的。
webpack.vendor.config.js
new webpack.DllPlugin({
context: __dirname,
name: "[name]_[hash]",
path: path.join(__dirname, "manifest.json"),
})
复制代码
webpack.app.config.js
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require("./manifest.json"),
name: "./my-dll.js",
scope: "xyz",
sourceType: "commonjs2"
})
复制代码
经过将公共模块拆出来,最终合成的文件可以在最开始的时候加载一次,便存到缓存中供后续使用。这个带来速度上的提高,由于浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。
若是把公共文件提取出一个文件,那么当用户访问了一个网页,加载了这个公共文件,再访问其余依赖公共文件的网页时,就直接使用文件在浏览器的缓存,这样公共文件就只用被传输一次。
entry: {
vendor: ["jquery", "other-lib"], // 明确第三方库
app: "./entry"
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
// filename: "vendor.js"
// (给 chunk 一个不一样的名字)
minChunks: Infinity,
// (随着 entry chunk 愈来愈多,
// 这个配置保证没其它的模块会打包进 vendor chunk)
})
]
// 打包后的文件
<script src="vendor.js" charset="utf-8"></script>
<script src="app.js" charset="utf-8"></script>
复制代码
基本上脚手架都包含了该插件,该插件会分析JS代码语法树,理解代码的含义,从而作到去掉无效代码、去掉日志输入代码、缩短变量名等优化。
const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
//...
plugins: [
new UglifyJSPlugin({
compress: {
warnings: false, //删除无用代码时不输出警告
drop_console: true, //删除全部console语句,能够兼容IE
collapse_vars: true, //内嵌已定义但只使用一次的变量
reduce_vars: true, //提取使用屡次但没定义的静态值到变量
},
output: {
beautify: false, //最紧凑的输出,不保留空格和制表符
comments: false, //删除全部注释
}
})
]
复制代码
ExtractTextPlugin 从 bundle 中提取文本(CSS)到单独的文件,PurifyCSSPlugin纯化CSS(其实用处没多大)
module.exports = {
module: {
rules: [
{
test: /\.css$/,
loader: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: {
localIdentName: 'purify_[hash:base64:5]',
modules: true
}
}
]
})
}
]
},
plugins: [
...,
new PurifyCSSPlugin({
purifyOptions: {
whitelist: ['*purify*']
}
})
]
};
复制代码
DefinePlugin可以自动检测环境变化,效率高效。
在前端开发中,在不一样的应用环境中,须要不一样的配置。如:开发环境的API Mocker、测试流程中的数据伪造、打印调试信息。若是使用人工处理这些配置信息,不只麻烦,并且容易出错。
使用DefinePlugin
配置的全局常量
注意,由于这个插件直接执行文本替换,给定的值必须包含字符串自己内的实际引号。一般,有两种方式来达到这个效果,使用 ' "production" '
, 或者使用 JSON.stringify('production')
。
new webpack.DefinePlugin({
// 固然,在运行node服务器的时候就应该按环境来配置文件
// 下面模拟的测试环境运行配置
'process.env':JSON.stringify('dev'),
WP_CONF: JSON.stringify('dev'),
}),
复制代码
测试DefinePlugin
:编写
if (WP_CONF === 'dev') {
console.log('This is dev');
} else {
console.log('This is prod');
}
复制代码
打包后WP_CONF === 'dev'
会编译为false
if (false) {
console.log('This is dev');
} else {
console.log('This is prod');
}
复制代码
当使用了DefinePlugin
插件后,打包后的代码会有不少冗余。能够经过UglifyJsPlugin
清除不可达代码。
[
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false, // 去除warning警告
dead_code: true, // 去除不可达代码
},
warnings: false
}
})
]
复制代码
最后的打包打包代码会变成console.log('This is prod')
附Uglify文档:github.com/mishoo/Ugli…
使用DefinePlugin区分环境 + UglifyJsPlugin清除不可达代码,以减轻打包代码体积
HappyPack能够开启多进程Loader转换,将任务分解给多个子进程,最后将结果发给主进程。
使用
exports.plugins = [
new HappyPack({
id: 'jsx',
threads: 4,
loaders: [ 'babel-loader' ]
}),
new HappyPack({
id: 'styles',
threads: 2,
loaders: [ 'style-loader', 'css-loader', 'less-loader' ]
})
];
exports.module.rules = [
{
test: /\.js$/,
use: 'happypack/loader?id=jsx'
},
{
test: /\.less$/,
use: 'happypack/loader?id=styles'
},
]
复制代码
ParallelUglifyPlugin能够开启多进程压缩JS文件
import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';
module.exports = {
plugins: [
new ParallelUglifyPlugin({
test,
include,
exclude,
cacheDir,
workerCount,
sourceMap,
uglifyJS: {
},
uglifyES: {
}
}),
],
};
复制代码
webpack打包结果分析插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
复制代码
减少文件搜索范围,从而提高速度
示例
{
test: /\.css$/,
include: [
path.resolve(__dirname, "app/styles"),
path.resolve(__dirname, "vendor/styles")
]
}
复制代码
这玩意不是插件,是wenpack的配置选项
externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所建立的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。此功能一般对 library 开发人员来讲是最有用的,然而也会有各类各样的应用程序用到它。
entry: {
entry: './src/main.js',
vendor: ['vue', 'vue-router', 'vuex']
},
externals: {
// 从输出的 bundle 中排除 echarts 依赖
echarts: 'echarts',
}
复制代码
Hot Module Replacement(简称 HMR)
包含如下内容:
webpack-dev-middleware 调用 webpack 的 api 对文件系统 watch,当文件发生改变后,webpack 从新对文件进行编译打包,而后保存到内存中。
webpack 将 bundle.js 文件打包到了内存中,不生成文件的缘由就在于访问内存中的代码比访问文件系统中的文件更快,并且也减小了代码写入文件的开销。
这一切都归功于memory-fs,memory-fs 是 webpack-dev-middleware 的一个依赖库,webpack-dev-middleware 将 webpack 本来的 outputFileSystem 替换成了MemoryFileSystem 实例,这样代码就将输出到内存中。
webpack-dev-middleware 中该部分源码以下:
// compiler
// webpack-dev-middleware/lib/Shared.js
var isMemoryFs = !compiler.compilers &&
compiler.outputFileSystem instanceof MemoryFileSystem;
if(isMemoryFs) {
fs = compiler.outputFileSystem;
} else {
fs = compiler.outputFileSystem = new MemoryFileSystem();
}
复制代码
在启动 devServer 的时候,sockjs 在服务端和浏览器端创建了一个 webSocket 长链接,以便将 webpack 编译和打包的各个阶段状态告知浏览器,最关键的步骤仍是 webpack-dev-server 调用 webpack api 监听 compile的 done 事件,当compile 完成后,webpack-dev-server经过 _sendStatus 方法将编译打包后的新模块 hash 值发送到浏览器端。
// webpack-dev-server/lib/Server.js
compiler.plugin('done', (stats) => {
// stats.hash 是最新打包文件的 hash 值
this._sendStats(this.sockets, stats.toJson(clientStats));
this._stats = stats;
});
...
Server.prototype._sendStats = function (sockets, stats, force) {
if (!force && stats &&
(!stats.errors || stats.errors.length === 0) && stats.assets &&
stats.assets.every(asset => !asset.emitted)
) { return this.sockWrite(sockets, 'still-ok'); }
// 调用 sockWrite 方法将 hash 值经过 websocket 发送到浏览器端
this.sockWrite(sockets, 'hash', stats.hash);
if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); }
else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); }
};
复制代码
webpack-dev-server 修改了webpack 配置中的 entry 属性,在里面添加了 webpack-dev-client 的代码,这样在最后的 bundle.js 文件中就会接收 websocket 消息的代码了。
webpack-dev-server/client 当接收到 type 为 hash 消息后会将 hash 值暂存起来,当接收到 type 为 ok 的消息后对应用执行 reload 操做。
在 reload 操做中,webpack-dev-server/client 会根据 hot 配置决定是刷新浏览器仍是对代码进行热更新(HMR)。代码以下:
// webpack-dev-server/client/index.js
hash: function msgHash(hash) {
currentHash = hash;
},
ok: function msgOk() {
// ...
reloadApp();
},
// ...
function reloadApp() {
// ...
if (hot) {
log.info('[WDS] App hot update...');
const hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
// ...
} else {
log.info('[WDS] App updated. Reloading...');
self.location.reload();
}
}
复制代码
首先 webpack/hot/dev-server(如下简称 dev-server) 监听第三步 webpack-dev-server/client 发送的 webpackHotUpdate
消息,调用 webpack/lib/HotModuleReplacement.runtime(简称 HMR runtime)中的 check 方法,检测是否有新的更新。
在 check 过程当中会利用 webpack/lib/JsonpMainTemplate.runtime(简称 jsonp runtime)中的两个方法 hotDownloadManifest 和 hotDownloadUpdateChunk。
hotDownloadManifest 是调用 AJAX 向服务端请求是否有更新的文件,若是有将发更新的文件列表返回浏览器端。该方法返回的是最新的 hash 值。
hotDownloadUpdateChunk 是经过 jsonp 请求最新的模块代码,而后将代码返回给 HMR runtime,HMR runtime 会根据返回的新模块代码作进一步处理,多是刷新页面,也多是对模块进行热更新。该 方法返回的就是最新 hash 值对应的代码块。
最后将新的代码块返回给 HMR runtime,进行模块热更新。
附:为何更新模块的代码不直接在第三步经过 websocket 发送到浏览器端,而是经过 jsonp 来获取呢?
个人理解是,功能块的解耦,各个模块各司其职,dev-server/client 只负责消息的传递而不负责新模块的获取,而这些工做应该有 HMR runtime 来完成,HMR runtime 才应该是获取新代码的地方。再就是由于不使用 webpack-dev-server 的前提,使用 webpack-hot-middleware 和 webpack 配合也能够完成模块热更新流程,在使用 webpack-hot-middleware 中有件有意思的事,它没有使用 websocket,而是使用的 EventSource。综上所述,HMR 的工做流中,不该该把新模块代码放在 websocket 消息中。
这一步是整个模块热更新(HMR)的关键步骤,并且模块热更新都是发生在HMR runtime 中的 hotApply 方法中
// webpack/lib/HotModuleReplacement.runtime
function hotApply() {
// ...
var idx;
var queue = outdatedModules.slice();
while(queue.length > 0) {
moduleId = queue.pop();
module = installedModules[moduleId];
// ...
// remove module from cache
delete installedModules[moduleId];
// when disposing there is no need to call dispose handler
delete outdatedDependencies[moduleId];
// remove "parents" references from all children
for(j = 0; j < module.children.length; j++) {
var child = installedModules[module.children[j]];
if(!child) continue;
idx = child.parents.indexOf(moduleId);
if(idx >= 0) {
child.parents.splice(idx, 1);
}
}
}
// ...
// insert new code
for(moduleId in appliedUpdate) {
if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
// ...
}
复制代码
模块热更新的错误处理,若是在热更新过程当中出现错误,热更新将回退到刷新浏览器,这部分代码在 dev-server 代码中,简要代码以下:
module.hot.check(true).then(function(updatedModules) {
if(!updatedModules) {
return window.location.reload();
}
// ...
}).catch(function(err) {
var status = module.hot.status();
if(["abort", "fail"].indexOf(status) >= 0) {
window.location.reload();
}
});
复制代码
当用新的模块代码替换老的模块后,可是咱们的业务代码并不能知道代码已经发生变化,也就是说,当 hello.js 文件修改后,咱们须要在 index.js 文件中调用 HMR 的 accept 方法,添加模块更新后的处理函数,及时将 hello 方法的返回值插入到页面中。代码以下
// index.js
if(module.hot) {
module.hot.accept('./hello.js', function() {
div.innerHTML = hello()
})
}
复制代码