webpack专题
一、编译
编译命令
直接cmd下执行webpack 即可,但是配置成为node项目,就可以使用npm的快捷脚本了,
那么我再提供一个在npm下好用的script命令(只支持windows)
"scripts": {
"build":"PowerShell.exe rm ./dist/* && webpack"
},
普通编译
源代码
console.log(123);
编译后代码
(() => {
eval("console.log(123);\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");
})();
静态导入编译
源代码
// index.js
import util from './util';
console.log(123);
// util.js
export default{
say(){
console.log('hello')
}
}
编译后代码
(() => {
// 所有的模块
var __webpack_modules__ = ({
"./src/index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./util */ \"./src/util.js\");\n\r\nconsole.log(123);\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");
}),
"./src/util.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => __WEBPACK_DEFAULT_EXPORT__\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({\r\n say(){\r\n console.log('hello')\r\n }\r\n });\n\n//# sourceURL=webpack://webpack-demo/./src/util.js?");
})
});
// 模块缓存
var __webpack_module_cache__ = {};
// require 方法定义
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
})();
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
// 入口函数
__webpack_require__("./src/index.js");
})();
动态导入编译
源代码
// index.js
import('./util');
console.log(123);
// util.js
export default{
say(){
console.log('hello')
}
}
编译后代码
// main.js webpackBootstrap即启动入口文件
(() => {
// 模块和模块缓存
var __webpack_modules__ = ({});
var __webpack_module_cache__ = {};
// require 方法定义
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
__webpack_require__.m = __webpack_modules__;
(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
(() => {
__webpack_require__.f = {};
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
})();
(() => {
__webpack_require__.u = (chunkId) => {
return "" + chunkId + ".js";
};
})();
(() => {
__webpack_require__.g = (function () {
if (typeof globalThis === 'object') return globalThis;
try {
return this || new Function('return this')();
} catch (e) {
if (typeof window === 'object') return window;
}
})();
})();
(() => {
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
})();
(() => {
var inProgress = {};
var dataWebpackPrefix = "webpack-demo:";
__webpack_require__.l = (url, done, key) => {
if (inProgress[url]) { inProgress[url].push(done); return; }
var script, needAttach;
if (key !== undefined) {
var scripts = document.getElementsByTagName("script");
for (var i = 0; i < scripts.length; i++) {
var s = scripts[i];
if (s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
}
}
if (!script) {
needAttach = true;
script = document.createElement('script');
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.setAttribute("data-webpack", dataWebpackPrefix + key);
script.src = url;
}
inProgress[url] = [done];
var onScriptComplete = (prev, event) => {
script.onerror = script.onload = null;
clearTimeout(timeout);
var doneFns = inProgress[url];
delete inProgress[url];
script.parentNode && script.parentNode.removeChild(script);
doneFns && doneFns.forEach((fn) => fn(event));
if (prev) return prev(event);
}
;
var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
needAttach && document.head.appendChild(script);
};
})();
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
(() => {
var scriptUrl;
if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
var document = __webpack_require__.g.document;
if (!scriptUrl && document) {
if (document.currentScript)
scriptUrl = document.currentScript.src
if (!scriptUrl) {
var scripts = document.getElementsByTagName("script");
if (scripts.length) scriptUrl = scripts[scripts.length - 1].src
}
}
if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
__webpack_require__.p = scriptUrl;
})();
(() => {
var installedChunks = {
"main": 0
};
__webpack_require__.f.j = (chunkId, promises) => {
var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
if (installedChunkData !== 0) {
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
if (true) {
var promise = new Promise((resolve, reject) => {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
var error = new Error();
var loadingEnded = (event) => {
if (__webpack_require__.o(installedChunks, chunkId)) {
installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) installedChunks[chunkId] = undefined;
if (installedChunkData) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
installedChunkData[1](error);
}
}
};
__webpack_require__.l(url, loadingEnded, "chunk-" + chunkId);
} else installedChunks[chunkId] = 0;
}
}
};
// 异步加载脚本回调
var webpackJsonpCallback = (data) => {
var [chunkIds, moreModules, runtime] = data;
var moduleId, chunkId, i = 0, resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
if (runtime) runtime(__webpack_require__);
parentChunkLoadingFunction(data);
while (resolves.length) {
resolves.shift()();
}
}
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
var parentChunkLoadingFunction = chunkLoadingGlobal.push.bind(chunkLoadingGlobal);
chunkLoadingGlobal.push = webpackJsonpCallback;
})();
// 入口函数(这个我处理过,方便阅读)
__webpack_require__.e(/*! import() */ "src_util_js").then(__webpack_require__.bind(__webpack_require__, /*! ./util */ "./src/util.js"));
console.log(123);
//# sourceURL=webpack://webpack-demo/./src/index.js?;
})();
// src_util_js.js
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([["src_util_js"], {
"./src/util.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => __WEBPACK_DEFAULT_EXPORT__\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({\r\n say(){\r\n console.log('hello')\r\n }\r\n });\n\n//# sourceURL=webpack://webpack-demo/./src/util.js?");
})
}]);
看到没,总的来说玩的花样越多,编译后的代码就越长
推荐阅读 文章一
编译的几个特性
支持多种模块化方案
无论是commonJS和是esm都支持,而且混合使用也没问题
万物皆可打包
本身只支持js,但是通过三方扩展的loader,延伸至万物皆可打包
Code Splitting
支持代码切割,就是把代码分成很多很多块( chunk )。对比gulp
然后实现模块的动态加载。
触发这样的场景有这几种情况:commonJS、import()和require.ensure()。
意义:在以前,为了减少 HTTP 请求,通常地,我们都会把所有的代码都打包成一个单独的 JS 文件。但是,如果这个 JS 文件体积很大的话,那就得不偿失了。这时,我们不妨把所有代码分成一块一块,需要某块代码的时候再去加载它;还可以利用浏览器的缓存,下次用到它的话,直接从缓存中读取。很显然,这种做法可以加快我们网页的加载速度
tree-shaking
即移除 JavaScript 上下文中的未引用代码(dead-code),给一张流传甚广的图,你品
ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。
但是通过require和import()动态引入的,依然无法进行tree-shaking。
定义一个工具 util.js
export const add = (x, y)=> {
return x + y
}
export const reduce= (x, y)=> {
return x - y
}
// 或者(上下都行)
export default {
add(x, y) {
return x + y
},
reduce(x, y) {
return x - y
}
}
入口index.js
import {add} from './util';
console.log(add);
编译后的文件
// 可以看到reduce因为没有使用,已经被去除了
(()=>{"use strict";console.log(((o,s)=>o+s))})();
二、多页面入口
有时候一个项目可能会有多个入口,非单一的spa应用,有可能是多个组合成一个
那么这个时候就用到了webpack的多入口了
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const config = {
mode: 'none',
entry: { // 多入口
index: './src/index.js',
second: './src/second.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
plugins: []
};
for (const key in config.entry) {
config.plugins.push(new HtmlWebpackPlugin({ // 多页面
template: './public/index.html', // 生成页面模板
filename: key + '.html', //生成页面文件名
chunks: [key] // 引用的模块编译后的js
}))
}
module.exports = config;
编译后的会生成如下结构
dist
--index.html
--index.js
--second.html
--second.js
second.html代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="second.js"></script>
</body>
</html>
HtmlWebpackPlugin插件文档可以参考:官方文档、某人博客
三、一些不重要的概念
module,chunk 和 bundle 。
其实就是同一份逻辑代码在不同转换场景下的取了三个名字:
我们直接写出来的是 module,
webpack 处理时是 chunk---比如import()、require()动态引入,导致切割的文件名
最后生成浏览器可以直接运行的 bundle()---比如(多)入口文件
如果HtmlWebpackPlugin不指定chunks,那么entry的元素都会被打到html中。
四、自定义loader
webpack只能处理js文件,其他文件无法处理
比如我自定义了配置文件,但是文件的格式不是.js而是 .env
如果直接引入进项目中,直接会报错。提示无法解析
所以需要自定义一个loader,比如起个名字叫 env-loader
我的配置文件 .env
appName=测试app
appVersion=1.0
webpack.config.js增加loader配置
const path = require('path');
module.exports = {
module: {
rules: [{
test: /\.env$/,
use: {
loader: path.resolve(__dirname, './env-loader.js'),
options: {
name: 'env-loader'
}
}
}]
}
};
编写该loader env-loader.js
const fs = require('fs');
const path = require('path');
const loaderUtils = require('loader-utils');
// 读取环境变量的文件把它转化成对象
const envCompiler = (data) => { // flie为文件路径
let d = data.replace(/\r/g, ',').replace(/\n/g, '') // 把换行和回车替换
let arr = d.split(',').map(item => {
return item.split('=')
}) // [ [ 'a', '1' ], [ 'b', '2' ] ]
let obj = {}
arr.forEach(item => {
obj[item[0]] = item[1]
})
return obj //{ a: '1', b: '2' }
// 可以接着处理
/* 像vue-cli3 新版create-react-app 一样规定环境变量的Key必须以(VUE_APP_) (REACT_APP_) 开头 */
}
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
const result = JSON.stringify(envCompiler(source));
return `module.exports = ${result}`;
}
使用
import all from './.env';
console.log(all); // {appName:'测试app',appVersion:'1.0'}
五、自定义插件
在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。远比loader强大
自定义插件 my-plugin.js
const fs = require('fs');
const pluginName = 'MyPlugin';
class MyPlugin {
constructor(option = {}) {
this.option = option;
}
apply(compiler) {
// 监听异步compiler的run 钩子
compiler.hooks.emit.tapAsync(pluginName, (compilation, callBack) => {
for (const key in compilation.assets) {
// 干预编译时,产出的代码
compilation.assets[key]._value = 'var dsh=123;' + compilation.assets[key]._value;
}
// 将编译后的文件信息写入
let result = '';
for (const key in compilation.assets) {
result += `${key}=${compilation.assets[key].size()}b \n`;
}
console.log(result);
fs.writeFile('./compiler-file-info.properties', result, { encoding: 'utf8' }, err => { })
callBack();
});
}
}
module.exports = MyPlugin;
使用插件webpack.config.js
const MyPlugin = require('./my-plugin.js')
module.exports = {
plugins: [
new MyPlugin({fileType: 'properties'})
]
};
看看效果
1、编译后的每个js文件,都被插入了 var dsh=123 这端代码,比如mian.js
var dsh = 123; (() => { var e, t, r, o, ...
2、根目录下生成了个配置文件 compiler-file-info.properties
main.js=3051b
532.js=154b
645.js=17050b
index.html=207b
关于插件中的核心tapable
tapable 是一个类似于 Node.js 中的 EventEmitter的库,但更专注于自定义事件的触发和处理。
webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的形式存在。
同样的类库还有facebook家的emitter
tapble这个类库可以直接用在前端
import { SyncHook } from 'tapable';
// 创建一个同步钩子对象
const hook = new SyncHook(['name']);
hook.tap('hello', (name) => {
console.log(`hello ${name}`);
});
hook.call('ahonn');
import { AsyncSeriesHook } from 'tapable';
// 创建一个异步回调的钩子对象
const hook = new AsyncSeriesHook(['name']);
hook.tapAsync('hello', (name, cb) => {
setTimeout(() => {
console.log(`hello ${name}`);
cb();
}, 2000);
});
// 2s之后打印
hook.callAsync('ahonn', (name) => {
console.log('done');
});
六、webpack的未来
唯一不变的是变化。
模块加载在未来大概率会被浏览器原生支持,到那时会不会有新的优化方案?
打包这件事,怎么看都像一个补丁,未来的HTTP2会不会彻底使其成为伪需求?
IE8迟早淘汰,Vue/React的市场会无限增大吗,SEO怎么做,Nodejs服务端渲染是答案吗?
这依旧是一个动荡的年代