十三、工程化相关面试题讲解
大约 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 模块的区别是什么?
- CMJ 是社区标准,ESM 是官方标准
- CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
- CMJ 仅在 node 环境中支持,ESM 各种环境均支持
- CMJ 是动态的依赖,同步执行。ESM 既支持动态,也支持静态,动态依赖是异步执行的
- ESM 导入时有符号绑定,CMJ 只是普通函数调用和赋值
2)export 和 export default 的区别是什么?
- export 为普通导出,又叫做具名导出,顾名思义,它导出的数据必须带有命名,如:变量定义、函数定义这种带有命名的语句。在导出的模块对象中,命名即为模块对象的属性名。在一个模块中可以有多个具名导出
- 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.json
的scripts
中 - 可以省略
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
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 的意义
- 理解前端开发中出现的常见问题,以及对应的解决办法
- 帮助理解常见的脚手架,如 vue-cli、create-react-app、umi-js 等
- 可以脱离脚手架搭建工程,甚至自己完成脚手架开发
- 应对工程化方面的进阶面试题
2.Webpack 学习哪个版本?
- 截止到 2022-01-04,Webpack 的版本是 Webpack5,但目前使用的最广泛的是 Webpack4
- Webpack 的版本会不断更新,但核心原理是不变的,因此,学习 Webpack4 成为了最好的选择
3.如何学习 Webpack?
需要完整的学习课程「Webpack 详细版」
学习过程中,把重心放在第一章「Webpack 核心功能」和第五章「性能优化」
(五)Webpack Scope Hoisting
1.面试题
1)介绍一下 webpack scope hoisting?
- scope hoisting 是 webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启
- 在未开启 scope hoisting 时,webpack 会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰
- 而 scope hoisting 的作用恰恰相反,是把多个模块的代码合并到一个函数环境中执行。在这一过程中,webpack 会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名,这样做的好处是减少了函数调用,对运行效率有一定提升,同时也降低了打包体积
- 但 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/#cache
5.资源模块
- 在 Webpack4 中,针对资源型文件通常使用
file-loader
、url-loader
、raw-loader
进行处理 - 由于大部分前端项目都会用到资源型文件,因此 Webpack5 原生支持了资源型模块
- 详见:https://webpack.docschina.org/guides/asset-modules/
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 模块安装机制是什么?
- npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
- npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装
- 如果本地和缓存中均不存在,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, // 全局唯一
},
},
}),
],
};