1. inline loader、pre loader、 post loader和normal loader执行的先后顺序是什么? #

1.1 loader 运行的总体流程 #

webpackflowloader

loader-runner2

1.2.loader-runner #

1.2.1 loader 类型 #

1.2.2 特殊配置 #

符号 变量 含义
-! 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)

1.2.3 查找并执行 #

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);

commonloaderexec

1.2.4 pitch #

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

loader_pitch

1.2.1 loaders\loader1.js #

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;

1.2.2 loaders\loader2.js #

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;

1.2.3 loaders\loader3.js #

loaders\loader3.js

function loader(source) {
  console.log("loader3");
  return source + "//loader3";
}
loader.pitch = function () {
  console.log("pitch3");
};
module.exports = loader;

1.3.4 webpack.config.js #

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']
      }
    ]
  }
}

pitchloaderexec

2. 是否写过Loader?描述一下编写loader的思路? #

属性
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']
}

3. 是否写过Plugin?描述一下编写plugin的思路? #

3.1. plugin #

插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。创建插件比创建 loader 更加高级,因为你将需要理解一些 webpack 底层的内部特性来做相应的钩子

3.1.1 为什么需要一个插件 #

3.1.2 可以加载插件的常用对象 #

对象 钩子
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

3.2. 创建插件 #

webpack 插件由以下组成:

3.3. Compiler 和 Compilation #

在插件开发中最重要的两个资源就是compilercompilation对象。理解它们的角色是扩展 webpack 引擎重要的第一步。

3.4. 基本插件架构 #

3.4.1 使用插件代码 #

if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    plugin.apply(compiler);
  }
}

3.4.2 Compiler 插件 #

3.4.2.1 同步 #
class DonePlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    compiler.hooks.done.tap("DonePlugin", (stats) => {
      console.log("Hello ", this.options.name);
    });
  }
}
module.exports = DonePlugin;
3.4.2.2 异步 #
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;

3.4.3 使用插件 #

const DonePlugin = require("./plugins/DonePlugin");
module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve("build"),
    filename: "bundle.js",
  },
  plugins: [new DonePlugin({ name: "zhufeng" })],
};

4. webpack打包的原理是什么?聊一聊babel和抽象语法树吧 #

4.1 index.js #

let title = require('./title.js');
console.log(title);

4.2 title.js #

module.exports = 'title';

4.3 packer.js #

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);

5. tree-shaking了解过么?它的实现原理说一下 #

var babel = require("@babel/core");
let { transform } = require("@babel/core");

5.1 实现按需加载 #

import { flatten, concat } from "lodash";

treeshakingleft

转换为

import flatten from "lodash/flatten";
import concat from "lodash/flatten";

treeshakingright

5.2 webpack 配置 #

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从右往左

5.3 babel 插件 #

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,
  };
};

6. webpack的热更新是如何做到的?说明其原理? #

6.1. 什么是HMR #

6.2. 搭建HMR项目 #

6.2.1 安装依赖的模块 #

cnpm i webpack webpack-cli webpack-dev-server mime html-webpack-plugin express socket.io events -S

6.2.2 package.json #

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"
  }
}

6.2.3 webpack.config.js #

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'
        })
    ]
}

6.2.4 src\index.js #

src\index.js

let root = document.getElementById('root');
function render(){
   let title = require('./title').default;
   root.innerHTML= title;
}
render();

6.2.5 src\title.js #

src\title.js

export default 'hello';

6.2.6 src\index.html #

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>

6.3.流程图 #

webpackhmr.png

6.4.实现 #

6.4.1 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: {
    hot: true,
    contentBase: path.join(__dirname, "dist"),
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      filename: "index.html",
    }),
    new webpack.HotModuleReplacementPlugin()
  ]
};

6.4.2 index.js #

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();
  });
}

6.4.3 src\title.js #

src\title.js

module.exports = 'title7';

6.4.4 src\index.html #

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>

6.4.5 webpack-dev-server.js #

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);

6.4.6 webpackHotDevClient.js #

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;
}

7.从零实现Webpack5模块联邦原理并实现微前端 #