符号 | 变量 | 含义 | |
---|---|---|---|
-! |
noPreAutoLoaders | 不要前置和普通 loader | Prefixing with -! will disable all configured preLoaders and loaders but not postLoaders |
! |
noAutoLoaders | 不要普通 loader | Prefixing with ! will disable all configured normal loaders |
!! |
noPrePostAutoLoaders | 不要前后置和普通 loader,只要内联 loader | Prefixing with !! will disable all configured loaders (preLoaders, loaders, postLoaders) |
let path = require("path");
let nodeModules = path.resolve(__dirname, "node_modules");
let request = "-!inline-loader1!inline-loader2!./index.js";
//首先解析出所需要的 loader,这种 loader 为内联的 loader
let inlineLoaders = request
.replace(/^-?!+/, "")
.replace(/!!+/g, "!")
.split("!");
let resource = inlineLoaders.pop(); //// 获取资源的路径
let resolveLoader = (loader) => path.resolve(nodeModules, loader);
//从相对路径变成绝对路径
inlineLoaders = inlineLoaders.map(resolveLoader);
let rules = [
{
enforce: "pre",
test: /\.css?$/,
use: ["pre-loader1", "pre-loader2"],
},
{
test: /\.css?$/,
use: ["normal-loader1", "normal-loader2"],
},
{
enforce: "post",
test: /\.css?$/,
use: ["post-loader1", "post-loader2"],
},
];
let preLoaders = [];
let postLoaders = [];
let normalLoaders = [];
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
if (rule.test.test(resource)) {
if (rule.enforce == "pre") {
preLoaders.push(...rule.use);
} else if (rule.enforce == "post") {
postLoaders.push(...rule.use);
} else {
normalLoaders.push(...rule.use);
}
}
}
preLoaders = preLoaders.map(resolveLoader);
postLoaders = postLoaders.map(resolveLoader);
normalLoaders = normalLoaders.map(resolveLoader);
let loaders = [];
//noPrePostAutoLoaders 忽略所有的 preLoader / normalLoader / postLoader
if (request.startsWith("!!")) {
loaders = inlineLoaders; //只保留inline
//noPreAutoLoaders 是否忽略 preLoader 以及 normalLoader
} else if (request.startsWith("-!")) {
loaders = [...postLoaders, ...inlineLoaders]; //只保留post和inline
//是否忽略 normalLoader
} else if (request.startsWith("!")) {
loaders = [...postLoaders, ...inlineLoaders, ...preLoaders]; //保留post inline pre
} else {
loaders = [...postLoaders, ...inlineLoaders, ...normalLoaders, ...preLoaders];
}
console.log(loaders);
pitch 与 loader 本身方法的执行顺序图
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
loaders\loader1.js
function loader(source) {
console.log("loader1", this.data);
return source + "//loader1";
}
loader.pitch = function (remainingRequest, previousRequest, data) {
data.name = "pitch1";
console.log("pitch1");
};
module.exports = loader;
loaders\loader2.js
function loader(source) {
console.log("loader2");
return source + "//loader2";
}
loader.pitch = function (remainingRequest, previousRequest, data) {
console.log("remainingRequest=", remainingRequest);
console.log("previousRequest=", previousRequest);
console.log("pitch2");
//return 'console.log("pitch2")';
};
module.exports = loader;
loaders\loader3.js
function loader(source) {
console.log("loader3");
return source + "//loader3";
}
loader.pitch = function () {
console.log("pitch3");
};
module.exports = loader;
module.exports = {
resolveLoader: {
alias: {
'a-loader': path.resolve(__dirname, 'loaders/a.js')
},
modules: [path.resolve(__dirname, 'node_modules'),path.resolve(__dirname, 'loader')]
},
module: {
rules: [
{
test: /\.js$/,
use: ['loader1', 'loader2', 'loader3']
}
]
}
}
属性 | 值 |
---|---|
this.request | /loaders/babel-loader.js!/src/index.js |
this.userRequest | /src/index.js |
this.rawRequest | ./src/index.js |
this.resourcePath | /src/index.js |
$ cnpm i @babel/preset-env @babel/core -D
const babel = require("@babel/core");
function loader(source, inputSourceMap,data) {
//C:\webpack-analysis2\loaders\babel-loader.js!C:\webpack-analysis2\src\index.js
const options = {
presets: ["@babel/preset-env"],
inputSourceMap: inputSourceMap,
sourceMaps: true, //ourceMaps: true 是告诉 babel 要生成 sourcemap
filename: this.request.split("!")[1].split("/").pop(),
};
//在webpack.config.js中 增加devtool: 'eval-source-map'
let { code, map, ast } = babel.transform(source, options);
return this.callback(null, code, map, ast);
}
module.exports = loader;
resolveLoader: {
alias: {//可以配置别名
"babel-loader": resolve('./build/babel-loader.js')
},//也可以配置loaders加载目录
modules: [path.resolve('./loaders'), 'node_modules']
},
{
test: /\.js$/,
use:['babel-loader']
}
插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。创建插件比创建 loader 更加高级,因为你将需要理解一些 webpack 底层的内部特性来做相应的钩子
对象 | 钩子 |
---|---|
Compiler | run,compile,compilation,make,emit,done |
Compilation | buildModule,normalModuleLoader,succeedModule,finishModules,seal,optimize,after-seal |
Module Factory | beforeResolver,afterResolver,module,parser |
Module | |
Parser] | program,statement,call,expression |
Template | hash,bootstrap,localVars,render |
webpack 插件由以下组成:
在插件开发中最重要的两个资源就是compiler
和compilation
对象。理解它们的角色是扩展 webpack 引擎重要的第一步。
compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
}
class DonePlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.done.tap("DonePlugin", (stats) => {
console.log("Hello ", this.options.name);
});
}
}
module.exports = DonePlugin;
class DonePlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.done.tapAsync("DonePlugin", (stats, callback) => {
console.log("Hello ", this.options.name);
callback();
});
}
}
module.exports = DonePlugin;
const DonePlugin = require("./plugins/DonePlugin");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve("build"),
filename: "bundle.js",
},
plugins: [new DonePlugin({ name: "zhufeng" })],
};
let title = require('./title.js');
console.log(title);
module.exports = 'title';
const fs = require("fs");
const path = require("path");
const types = require("babel-types");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const baseDir = process.cwd().replace(/\\/g,path.posix.sep);
const entry = path.posix.join(baseDir, "src/index.js");
let modules = [];
function buildModule(absolutePath){
const body = fs.readFileSync(absolutePath, "utf-8");
const ast = parser.parse(body, {
sourceType: "module",
});
const moduleId = "./" + path.posix.relative(baseDir, absolutePath);
const module = { id: moduleId };
const deps = [];
traverse(ast, {
CallExpression({ node }) {
if (node.callee.name === 'require') {
node.callee.name = "__webpack_require__";
let moduleName = node.arguments[0].value;
const dirname = path.posix.dirname(absolutePath);
const depPath = path.posix.join(dirname, moduleName);
const depModuleId = "./" + path.posix.relative(baseDir, depPath);
node.callee.name = "__webpack_require__";
node.arguments = [types.stringLiteral(depModuleId)];
deps.push(buildModule(depPath));
}
}
});
let { code } = generate(ast);
module._source = code;
module.deps = deps;
modules.push(module);
return module;
}
let entryModule = buildModule(entry);
let content = `
(function (modules) {
function __webpack_require__(moduleId) {
var module = {
i: moduleId,
exports: {}
};
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
return module.exports;
}
return __webpack_require__("${entryModule.id}");
})(
{
${modules
.map(
(module) =>
`"${module.id}": function (module, exports,__webpack_require__) {${module._source}}`
)
.join(",")}
}
);
`;
fs.writeFileSync("./dist/bundle.js", content);
var babel = require("@babel/core");
let { transform } = require("@babel/core");
import { flatten, concat } from "lodash";
转换为
import flatten from "lodash/flatten";
import concat from "lodash/flatten";
cnpm i webpack webpack-cli -D
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
path: path.resolve("dist"),
filename: "bundle.js",
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
plugins: [["import", { library: "lodash" }]],
},
},
},
],
},
};
编译顺序为首先
plugins
从左往右,然后presets
从右往左
babel-plugin-import.js
放置在 node_modules 目录下let babel = require("@babel/core");
let types = require("babel-types");
const visitor = {
ImportDeclaration: {
enter(path, state = { opts }) {
const specifiers = path.node.specifiers;
const source = path.node.source;
if (
state.opts.library == source.value &&
!types.isImportDefaultSpecifier(specifiers[0])
) {
const declarations = specifiers.map((specifier, index) => {
return types.ImportDeclaration(
[types.importDefaultSpecifier(specifier.local)],
types.stringLiteral(`${source.value}/${specifier.local.name}`)
);
});
path.replaceWithMultiple(declarations);
}
},
},
};
module.exports = function (babel) {
return {
visitor,
};
};
live reload
刷新页面的方案,HMR的优点在于可以保存应用的状态,提高了开发效率cnpm i webpack webpack-cli webpack-dev-server mime html-webpack-plugin express socket.io events -S
package.json
{
"name": "zhufeng_hmr",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"webpack": "4.39.1",
"webpack-cli": "3.3.6",
"webpack-dev-server": "3.7.2"
}
}
webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode:'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.join(__dirname, 'dist')
},
devServer: {
contentBase:path.join(__dirname, 'dist')
},
plugins:[
new HtmlWebpackPlugin({
template:'./src/index.html',
filename:'index.html'
})
]
}
src\index.js
let root = document.getElementById('root');
function render(){
let title = require('./title').default;
root.innerHTML= title;
}
render();
src\title.js
export default 'hello';
src\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>webpack热更新</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.join(__dirname, "dist"),
},
devServer: {
hot: true,
contentBase: path.join(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
filename: "index.html",
}),
new webpack.HotModuleReplacementPlugin()
]
};
src\index.js
import '../webpackHotDevClient';
let root = document.getElementById("root");
function render() {
let title = require("./title");
root.innerHTML = title;
}
render();
if(module.hot){
module.hot.accept(['./title'],()=>{
render();
});
}
src\title.js
module.exports = 'title7';
src\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>webpack热更新</title>
</head>
<body>
<div id="root"></div>
<script src="/socket.io/socket.io.js"></script>
</body>
</html>
webpack-dev-server.js
const path = require("path");
const fs = require("fs");
const express = require("express");
const mime = require("mime");
const webpack = require("webpack");
let config = require("./webpack.config");
let compiler = webpack(config);
//1. 创建webpack实例
//2. 启动webpack-dev-server服务器
class Server {
constructor(compiler) {
//4. 添加webpack的`done`事件回调,在编译完成后会向浏览器发送消息
let lastHash;
let sockets = [];
compiler.hooks.done.tap("webpack-dev-server", (stats) => {
lastHash = stats.hash;
sockets.forEach((socket) => {
socket.emit("hash", stats.hash);
socket.emit("ok");
});
});
let app = new express();
compiler.watch({}, (err) => {
console.log("编译成功");
});
//3. 添加webpack-dev-middleware中间件
const webpackDevMiddleware = (req, res, next) => {
if (req.url === "/favicon.ico") {
return res.sendStatus(404);
}
let filename = path.join(config.output.path, req.url.slice(1));
try {
let stats = fs.statSync(filename);
if (stats.isFile()) {
let content = fs.readFileSync(filename);
res.header("Content-Type", mime.getType(filename));
res.send(content);
} else {
next();
}
} catch (error) {
return res.sendStatus(404);
}
};
app.use(webpackDevMiddleware);
this.server = require("http").createServer(app);
//4. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接
//将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些`socket`消息进行不同的操作
//当然服务端传递的最主要信息还是新模块的`hash`值,后面的步骤根据这一`hash`值来进行模块热替换
let io = require("socket.io")(this.server);
io.on("connection", (socket) => {
sockets.push(socket);
if (lastHash) {
//5.发送hash值
socket.emit("hash", lastHash);
socket.emit("ok");
}
});
}
//9. 创建http服务器并启动服务
listen(port) {
this.server.listen(port, () => {
console.log(port + "服务启动成功!");
});
}
}
//3. 创建Server服务器
let server = new Server(compiler);
server.listen(8080);
webpackHotDevClient.js
let socket = io("/");
let currentHash;
let hotCurrentHash;
const onConnected = () => {
console.log("客户端已经连接");
//6. 客户端会监听到此hash消息
socket.on("hash", (hash) => {
currentHash = hash;
});
//7. 客户端收到`ok`的消息
socket.on("ok", () => {
hotCheck();
});
socket.on("disconnect", () => {
hotCurrentHash = currentHash = null;
});
};
//8.执行hotCheck方法进行更新
function hotCheck() {
if (!hotCurrentHash || hotCurrentHash === currentHash) {
return (hotCurrentHash = currentHash);
}
//9.向 server 端发送 Ajax 请求,服务端返回一个hot-update.json文件,该文件包含了所有要更新的模块的 `hash` 值和chunk名
hotDownloadManifest().then((update) => {
let chunkIds = Object.keys(update.c);
chunkIds.forEach((chunkId) => {
//10. 通过JSONP请求获取到最新的模块代码
hotDownloadUpdateChunk(chunkId);
});
});
}
function hotDownloadUpdateChunk(chunkId) {
var script = document.createElement("script");
script.charset = "utf-8";
script.src = "/" + chunkId + "." + hotCurrentHash+ ".hot-update.js";
document.head.appendChild(script);
}
function hotDownloadManifest() {
var url = "/" + hotCurrentHash + ".hot-update.json";
return fetch(url).then(res => res.json()).catch(error=>{console.log(error);});
}
//11. 补丁JS取回来后会调用`webpackHotUpdate`方法
window.webpackHotUpdate = (chunkId, moreModules) => {
for (let moduleId in moreModules) {
let oldModule = __webpack_require__.c[moduleId];//获取老模块
let { parents, children } = oldModule;//父亲们 儿子们
var module = (__webpack_require__.c[moduleId] = {
i: moduleId,
exports: {},
parents,
children,
hot: window.hotCreateModule(),
});
moreModules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
parents.forEach((parent) => {
let parentModule = __webpack_require__.c[parent];
parentModule.hot &&
parentModule.hot._acceptedDependencies[moduleId] &&
parentModule.hot._acceptedDependencies[moduleId]();
});
hotCurrentHash = currentHash;
}
};
socket.on("connect", onConnected);
window.hotCreateModule = () => {
var hot = {
_acceptedDependencies: {}, //接收的依赖
_acceptedDependencies: function (dep, callback) {
for (var i = 0; i < dep.length; i++) {
hot._acceptedDependencies[dep[i]] = callback;
}
},
};
return hot;
}