十三、工程化相关面试题讲解

郁子大约 14 分钟约 4118 字笔记渡一教育笔面试题甄选袁进

(一)CMJ 和 ESM

1.CommonJS

1)关键词

  • 社区标准
  • 使用函数实现
  • 仅 node 环境支持
  • 动态依赖(需要代码运行后才能确定依赖)
  • 动态依赖是 同步执行

2)原理

// require函数的伪代码
function require(path) {
  if (该模块有缓存吗) {
    return 缓存结果;
  }
  function _run(exports, require, module, __filename, __dirname) {
    // 模块代码会放到这里
  }

  var module = {
    exports: {},
  };

  _run.call(module.exports, module.exports, require, module, 模块路径, 模块所在目录);

  // 把 module.exports 加入到缓存;
  return module.exports;
}

2.ES Module

1)关键词

  • 官方标准
  • 使用新语法实现
  • 所有环境均支持
  • 同时支持静态依赖和动态依赖
    • 静态依赖:在代码运行前就要确定依赖关系
  • 动态依赖是 异步执行
  • 符号绑定

2)关于符号绑定

  • 导入位置的符号和导出的符号并非赋值
  • 完全是一个东西
// module a.js
export var a = 1;
export function changeA() {
  a = 2;
}

// index.js
import { a, changeA } from "./a.js";
console.log(a); // 1
changeA();
console.log(a); // 2

3.面试题

1)commonjs 和 es6 模块的区别是什么?

  1. CMJ 是社区标准,ESM 是官方标准
  2. CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
  3. CMJ 仅在 node 环境中支持,ESM 各种环境均支持
  4. CMJ 是动态的依赖,同步执行。ESM 既支持动态,也支持静态,动态依赖是异步执行的
  5. ESM 导入时有符号绑定,CMJ 只是普通函数调用和赋值

2)export 和 export default 的区别是什么?

  1. export 为普通导出,又叫做具名导出,顾名思义,它导出的数据必须带有命名,如:变量定义、函数定义这种带有命名的语句。在导出的模块对象中,命名即为模块对象的属性名。在一个模块中可以有多个具名导出
  2. export default 为默认导出,在模块对象中名称固定为 default,因此无须命名,通常导出一个表达式或字面量。在一个模块中只能有一个默认导出

3)下面的模块导出了什么结果?

exports.a = "a";
module.exports.b = "b";
this.c = "c";
module.exports = {
  d: "d",
};
{
  d: "d";
}

4)下面的代码输出什么结果?

// module counter
var count = 1;
export { count };
export function increase() {
  count++;
}

// module main
import { count, increase } from "./counter";
import * as counter from "./counter";
const { count: c } = counter;
increase();
console.log(count);
console.log(counter.count);
console.log(c);

2

1

1

(二)npx

1.运行本地命令

  • 使用 npx 命令
  • 会首先从本地工程的 node_modules/.bin 目录中寻找是否有对应的命令
npx webpack

上面这条命令寻找本地工程的 node_modules/.bin/webpack

  • 如果将命令配置到 package.jsonscripts
  • 可以省略 npx

2.临时下载执行

  • 当执行某个命令时,如果无法从本地工程中找到对应命令
  • 会把命令对应的包下载到一个临时目录
  • 下载完成后执行
  • 临时目录中的命令会在适当的时候删除
npx prettyjson 1.json
  • npx 会下载 prettyjson 包到临时目录,然后运行该命令
  • 如果命令名称和需要下载的包名不一致时,可以手动指定包名
  • 如:@vue/cli 是包名,vue 是命令名,两者不一致,可以使用下面的命令
npx -p @vue/cli vue create vue-app

3.npm init

  • npm init 通常用于初始化工程的 package.json 文件
  • 有时也可以充当 npx 的作用
# 等效于 npx create-包名
npm init 包名

# 等效于 npx @命名空间/create
npm init @命名空间

# 等效于 npx @命名空间/create-包名
npm init @命名空间/包名

(三)ESLint

ESLint 官网open in new window

ESLint 民间中文网open in new window

1.ESLint 的由来

  • JavaScript 是一个过于灵活的语言,因此在企业开发中,往往会遇到下面两个问题
    • 如何让所有员工书写高质量的代码?【代表代码的质量】
      • 如:使用 === 替代 ==
    • 如何让所有员工书写的代码风格保持统一?【代表代码的风格】
      • 如:字符串统一使用单引号
  • 如果纯依靠人工进行检查,不仅费时费力,而且还容易出错。
  • ESLint 由此诞生,是一个工具
  • 预先配置好各种规则,通过这些规则来自动化的验证代码,甚至自动修复

2.安装

npm i -D eslint

3.验证

# 验证单个文件
npx eslint 文件名

# 验证全部文件
npx eslint src/**

4.配置规则

  • ESLint 会自动寻找根目录中的配置文件
  • 支持三种配置文件
    • .eslintrc JSON 格式
    • .eslintrc.js JS 格式
    • .eslintrc.yml YAML 格式
  • .eslintrc.js 为例
// ESLint 配置
module.exports = {
  // 配置规则
  rules: {
    规则名1: 级别,
    规则名2: 级别,
    ...
  },
};
  • 每条规则由名称和级别组成
    • 规则名称决定了 要检查什么
    • 规则级别决定了 检查没通过时的处理方式

1)所有的规则名称

2)所有的级别

级别含义
0 或 'off'关闭规则
1 或 'warn'验证不通过提出警告
2 或 'error'验证不通过报错,退出程序

3)eslint-basic

module.exports = {
  rules: {
    eqeqeq: "error",
  },
};

5.在 VSCode 中及时发现问题

  • 每次都要输入命令发现问题非常麻烦
  • 可以安装 VSCode 插件 ESLint
    • 只要项目的 node_modules 中有 eslint
    • 就会按照项目根目录下的规则自动检测

6.使用继承

  • ESLint 的规则非常庞大,全部自定义过于麻烦
  • 一般继承其他企业开源的方案来简化配置

这方面做的比较好的是一家叫 Airbnb 的公司,他们在开发前端项目的时候自定义了一套开源规则,受到全世界的认可

只需要安装它即可

# 为了避免版本问题,不要直接安装eslint,直接安装下面的包,会自动安装相应版本的eslint
npm i -D eslint-config-airbnb
  • 稍作配置
module.exports = {
  // 配置继承自 airbnb
  extends: "airbnb",
};

7.在框架中使用

  • 一般我们使用脚手架搭建工程,在搭建工程时通常都可以直接设置 eslint
module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: ["plugin:vue/essential", "@vue/airbnb"],
  parserOptions: {
    parser: "babel-eslint",
  },
  rules: {
    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
  },
};

8.企业开发的实际情况

  • 安装好 VSCode 的 ESLint 插件
  • 学会查看 ESLint 错误提示

(四)关于 Webpack 的诸多问题

1.为什么要学习 Webpack?

  • 前端有很多打包工具,其中,Webpack 生态最完整、使用最广泛
  • 学习 Webpack 的意义
  1. 理解前端开发中出现的常见问题,以及对应的解决办法
  2. 帮助理解常见的脚手架,如 vue-cli、create-react-app、umi-js 等
  3. 可以脱离脚手架搭建工程,甚至自己完成脚手架开发
  4. 应对工程化方面的进阶面试题

2.Webpack 学习哪个版本?

  • 截止到 2022-01-04,Webpack 的版本是 Webpack5,但目前使用的最广泛的是 Webpack4
  • Webpack 的版本会不断更新,但核心原理是不变的,因此,学习 Webpack4 成为了最好的选择

3.如何学习 Webpack?

需要完整的学习课程「Webpack 详细版」

学习过程中,把重心放在第一章「Webpack 核心功能」和第五章「性能优化」

(五)Webpack Scope Hoisting

详细介绍open in new window

1.面试题

1)介绍一下 webpack scope hoisting?

  1. scope hoisting 是 webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启
  2. 在未开启 scope hoisting 时,webpack 会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰
  3. 而 scope hoisting 的作用恰恰相反,是把多个模块的代码合并到一个函数环境中执行。在这一过程中,webpack 会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名,这样做的好处是减少了函数调用,对运行效率有一定提升,同时也降低了打包体积
  4. 但 scope hoisting 的启用是有前提的,如果遇到某些模块多次被其他模块引用,或者使用了动态导入的模块,或者是非 ESM 的模块,都不会有 scope hoisting

(六)Webpack5 更新

1.清除输出目录

  • Webpack5 清除输出目录开箱可用
  • 无须安装 clean-webpack-plugin
module.exports = {
  mode: "production",
  output: {
    clean: true,
  },
};

2.top-level-await

  • Webpack5 现在允许在模块的 顶级代码 中直接使用 await
// src/index.js
const resp = await fetch("http://www.baidu.com");
const jsonBody = await resp.json();
export default jsonBody;
  • 目前,top-level-await 还未成为正式标准
  • 因此,对于 Webpack5 而言,该功能是作为 experiments 发布的
  • 需要在 webpack.config.js 中配置开启
// webpack.config.js
module.exports = {
  mode: "development",
  devtool: "source-map",
  entry: "./src/index.js",
  experiments: {
    topLevelAwait: true,
  },
};

3.打包体积优化

  • Webpack5 对模块的合并、作用域提升、tree shaking 等处理更加智能
module.exports = {
  mode: "production",
  devtool: "source-map",
  entry: {
    index1: "./src/index1.js",
    index2: "./src/index2.js",
  },
};

4.打包缓存开箱即用

  • 在 Webpack4 中,需要使用 cache-loader 缓存打包结果以优化之后的打包性能
  • 在 Webpack5 中,默认就已经开启了打包缓存,无须再安装 cache-loader
  • 默认情况下,Webpack5 是将模块的打包结果 缓存到内存
    • 可以通过 cache 配置进行更改
const path = require("path");

module.exports = {
  cache: {
    // 缓存类型,支持:memory、filesystem
    type: "filesystem",
    // 缓存目录,仅类型为 filesystem 有效
    cacheDirectory: path.resolve(__dirname, "node_modules/.cache/webpack"),
  },
};

关于 cache 的更多配置参考 https://webpack.docschina.org/configuration/other-options/#cacheopen in new window

5.资源模块

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  mode: "development",
  devtool: "source-map",
  entry: "./src/index.js",
  devServer: {
    port: 8080,
  },
  plugins: [new HtmlWebpackPlugin()],
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
    assetModuleFilename: "assets/[hash:5][ext]", // 在这里自定义资源文件保存的文件名
  },
  module: {
    rules: [
      {
        test: /\.png/,
        type: "asset/resource", // 作用类似于 file-loader
      },
      {
        test: /\.jpg/,
        type: "asset/inline", // 作用类似于 url-loader 文件大小不足的场景
      },
      {
        test: /\.txt/,
        type: "asset/source", // 作用类似于 raw-loader
      },
      {
        test: /\.gif/,
        type: "asset", // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
        generator: {
          filename: "gif/[hash:5][ext]", // 这里的配置会覆盖 assetModuleFilename
        },
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024, // 4kb以下使用 data uri
          },
        },
      },
    ],
  },
};

(七)npm 模块安装机制

1.npm 缓存相关命令

# 清除缓存
npm cache clean -f

# 获取缓存位置
npm config get cache

# 设置缓存位置
npm config set cache "新的缓存路径"

2.面试题

1)解释一下 npm 模块安装机制是什么?

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来

npm 缓存相关命令:

# 清除缓存
npm cache clean -f

# 获取缓存位置
npm config get cache

# 设置缓存位置
npm config set cache "新的缓存路径"

(八)模块联邦

1.背景

  • 在大型项目中,往往会把项目中的某个区域或功能模块作为单独的项目开发,最终形成 微前端 架构
  • 在微前端架构中,不同的工程可能出现下面的场景

  • 这涉及到很多非常棘手的问题
    • 如何避免公共模块重复打包
    • 如何将某个项目中一部分模块分享出去,同时还要避免重复打包
    • 如何管理依赖的不同版本
    • 如何更新模块
    • ......
  • Webpack5 尝试着通过 模块联邦 来解决此类问题

示例

现有两个微前端工程,它们各自独立开发、测试、部署,但有一些相同的公共模块,并有一些自己的模块需要分享给其他工程使用,同时又要引入其他工程的模块

2. 初始化工程

1)home 项目

  • 安装
# 初始化 package.json
npm init -y

# 安装依赖
npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
npm i jquery
  • 修改 package.json
"scripts": {
   "build": "webpack",
   "dev": "webpack serve"
 }
  • 配置 webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  entry: "./src/index.js",
  mode: "development",
  devtool: "source-map",
  devServer: {
    port: 8080,
  },
  output: {
    clean: true,
  },
  plugins: [new HtmlWebpackPlugin()],
};
  • 代码
// src/now.js
import $ from "jquery";

export default function (container) {
  const p = $("<p>").appendTo(container).text(new Date().toLocaleString());
  setInterval(function () {
    p.text(new Date().toLocaleString());
  }, 1000);
}

// src/bootstrap.js
import $ from "jquery";
import now from "./now";
// 生成首页标题
$("<h1>").text("首页").appendTo(document.body);
// 首页中有一个显示当前时间的区域
now($("<div>").appendTo(document.body));

// src/index.js
import("./bootstrap");

2)active 项目

  • 安装
# 初始化 package.json
npm init -y

# 安装依赖
npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
npm i jquery
  • 修改 package.json
"scripts": {
   "build": "webpack",
   "dev": "webpack serve"
 }
  • 配置 webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  entry: "./src/index.js",
  mode: "development",
  devtool: "source-map",
  devServer: {
    port: 3000,
  },
  output: {
    clean: true,
  },
  plugins: [new HtmlWebpackPlugin()],
};
  • 代码
// src/news.js
import $ from "jquery";
export default function (container) {
  const ul = $("<ul>").appendTo(container);
  let html = "";
  for (var i = 1; i <= 20; i++) {
    html += `<li>新闻${i}</li>`;
  }
  ul.html(html);
}

// src/bootstrap.js
import $ from "jquery";
import news from "./news";
// 生成活动页标题
$("<h1>").text("活动页").appendTo(document.body);
// 活动页中有一个新闻列表
news($("<div>").appendTo(document.body));

// src/index.js
import("./bootstrap");

3.暴露和引用模块

1)active 项目需要使用 home 项目的 now 模块

  • home 项目暴露 now 模块
// webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // 模块联邦的名称
      // 该名称将成为一个全部变量,通过该变量将可获取当前联邦的所有暴露模块
      name: "home",
      // 模块联邦生成的文件名,全部变量将置入到该文件中
      filename: "home-entry.js",
      // 模块联邦暴露的所有模块
      exposes: {
        // key:相对于模块联邦的路径
        // 这里的 ./now 将决定该模块的访问路径为 home/now
        // value: 模块的具体路径
        "./now": "./src/now.js",
      },
    }),
  ],
};
  • active 项目引入 now 模块
// webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // 远程使用其他项目暴露的模块
      remotes: {
        // key: 自定义远程暴露的联邦名
        // 比如为 abc, 则之后引用该联邦的模块则使用 import "abc/模块名"
        // value: 模块联邦名@模块联邦访问地址
        // 远程访问时,将从下面的地址加载
        home: "home@http://localhost:8080/home-entry.js",
      },
    }),
  ],
};

// src/bootstrap.js
// 远程引入时间模块
import now from "home/now";
now($("<div>").appendTo(document.body));

2)home 项目需要使用 active 项目的 news 模块

  • active 项目暴露 news 模块
// webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // 模块联邦的名称
      // 该名称将成为一个全部变量,通过该变量将可获取当前联邦的所有暴露模块
      name: "active",
      // 模块联邦生成的文件名,全部变量将置入到该文件中
      filename: "active-entry.js",
      // 模块联邦暴露的所有模块
      exposes: {
        // key:相对于模块联邦的路径
        // 这里的 ./news 将决定该模块的访问路径为 active/news
        // value: 模块的具体路径
        "./news": "./src/news.js",
      },
    }),
  ],
};
  • home 项目引入 news 模块
// webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // 远程使用其他项目暴露的模块
      remotes: {
        // key: 自定义远程暴露的联邦名
        // 比如为 abc, 则之后引用该联邦的模块则使用 import "abc/模块名"
        // value: 模块联邦名@模块联邦访问地址
        // 远程访问时,将从下面的地址加载
        active: "active@http://localhost:3000/active-entry.js",
      },
    }),
  ],
};

// src/bootstrap.js
// 远程引入新闻模块
import news from "active/news";
news($("<div>").appendTo(document.body));

4.处理共享模块

  • 两个项目均使用了 jquery
  • 为了避免重复,可以同时为双方使用 shared 配置共享模块
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // 配置共享模块
      shared: {
        // jquery为共享模块
        jquery: {
          singleton: true, // 全局唯一
        },
      },
    }),
  ],
};
上次编辑于: