工程化分类面试题

CommonJS

关键词:

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

原理:

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

ES Module

关键词:

  • 官方标准

  • 使用新语法实现

  • 所有环境均支持

  • 同时支持静态依赖和动态依赖

    静态依赖:在代码运行前就要确定依赖关系

  • 动态依赖是异步的

  • 符号绑定

关于符号绑定:

// 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

面试题

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

    参考答案:

    1. CMJ 是社区标准,ESM 是官方标准
    2. CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
    3. CMJ 仅在 node 环境中支持,ESM 各种环境均支持
    4. CMJ 是动态的依赖,同步执行。ESM 既支持动态,也支持静态,动态依赖是异步执行的。
    5. 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);
    

运行本地命令

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

例如:

npx webpack

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

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

临时下载执行

当执行某个命令时,如果无法从本地工程中找到对应命令,则会把命令对应的包下载到一个临时目录,下载完成后执行,临时目录中的命令会在适当的时候删除

例如:

npx prettyjson 1.json

npx会下载prettyjson包到临时目录,然后运行该命令

如果命令名称和需要下载的包名不一致时,可以手动指定报名

例如@vue/cli是包名,vue是命令名,两者不一致,可以使用下面的命令

npx -p @vue/cli vue create vue-app

npm init

npm init通常用于初始化工程的package.json文件

除此之外,有时也可以充当npx的作用

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

ESLint官网:https://eslint.org/

ESLint民间中文网:https://eslint.bootcss.com/

ESLint的由来

JavaScript是一个过于灵活的语言,因此在企业开发中,往往会遇到下面两个问题:

  • 如何让所有员工书写高质量的代码?

    比如使用===替代==

  • 如何让所有员工书写的代码风格保持统一?

    比如字符串统一使用单引号

上面两个问题,一个代表着代码的质量,一个代表着代码的风格。

如果纯依靠人工进行检查,不仅费时费力,而且还容易出错。

ESLint由此诞生,它是一个工具,预先配置好各种规则,通过这些规则来自动化的验证代码,甚至自动修复

ESLint的基本使用

安装

npm i -D eslint

如何验证

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

配置规则

eslint会自动寻找根目录中的配置文件,它支持三种配置文件:

  • .eslintrc JSON格式
  • .eslintrc.js JS格式
  • .eslintrc.yml YAML格式

这里以.eslintrc.js为例:

// ESLint 配置
module.exports = {
  // 配置规则
  rules: {
    规则名1: 级别,
    规则名2: 级别,
    ...
  },
};

每条规则由名称和级别组成

规则名称决定了要检查什么

规则级别决定了检查没通过时的处理方式

所有的规则名称看这里:

  • 官方:https://eslint.org/docs/rules/
  • 中文:https://eslint.bootcss.com/docs/rules/

所有级别如下:

  • 0 或 ‘off’:关闭规则
  • 1 或 ‘warn’:验证不通过提出警告
  • 2 或 ‘error’:验证不通过报错,退出程序

在VSCode中及时发现问题

每次都要输入命令发现问题非常麻烦

可以安装VSCode插件ESLint,只要项目的node_modules中有eslint,它就会按照项目根目录下的规则自动检测

使用继承

ESLint的规则非常庞大,全部自定义过于麻烦

一般我们继承其他企业开源的方案来简化配置

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

我们只需要安装它即可

# 为了避免版本问题,不要直接安装eslint,直接安装下面的包,会自动安装相应版本的eslint
npm i -D eslint-config-airbnb

然后稍作配置

module.exports = {
	extends: 'airbnb' # 配置继承自 airbnb
}

在框架中使用

一般我们使用脚手架搭建工程,在搭建工程时通常都可以直接设置eslint

企业开发的实际情况

我们要做什么?

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

关于webpack的诸多问题

为什么要学习webpack?

前端有很多打包工具,其中,webpack生态最完整、使用最广泛。

学习webpack的意义主要有以下几点:

  1. 理解前端开发中出现的常见问题,以及对应的解决办法
  2. 帮助理解常见的脚手架,如vue-cli、create-react-app、umi-js等
  3. 可以脱离脚手架搭建工程,甚至自己完成脚手架开发
  4. 应对工程化方面的进阶面试题

webpack学习哪个版本?

截止到2022-01-04,webpack的版本是webpack5,但目前使用的最广泛的是webpack4。

webpack的版本会不断更新,但它的核心原理是不变的,因此,学习webpack4成为了最好的选择。

如何学习webpack?

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

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

webpack scope hoisting

详细介绍:https://webpack.docschina.org/plugins/module-concatenation-plugin/

面试题

介绍一下 webpack scope hoisting?

参考答案:

scope hoisting 是 webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启。

在未开启scope hoisting时,webpack 会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰。

而 scope hoisting 的作用恰恰相反,是把多个模块的代码合并到一个函数环境中执行。在这一过程中,webpack 会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名。

这样做的好处是减少了函数调用,对运行效率有一定提升,同时也降低了打包体积。

但 scope hoisting 的启用是有前提的,如果遇到某些模块多次被其他模块引用,或者使用了动态导入的模块,或者是非 ESM 的模块,都不会有 scope hoisting。

清除输出目录

webpack5清除输出目录开箱可用,无须安装clean-webpack-plugin,具体做法如下:

module.exports = {
  output: {
    clean: true
  }
}

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 = {
  experiments: {
    topLevelAwait: true,
  },
};

打包体积优化

webpack5对模块的合并、作用域提升、tree shaking等处理更加智能

打包缓存开箱即用

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

资源模块

webpack4中,针对资源型文件我们通常使用file-loaderurl-loaderraw-loader进行处理

由于大部分前端项目都会用到资源型文件,因此webpack5原生支持了资源型模块

详见:https://webpack.docschina.org/guides/asset-modules/

解释一下 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 "新的缓存路径"

模块联邦

在大型项目中,往往会把项目中的某个区域或功能模块作为单独的项目开发,最终形成「微前端」架构

在微前端架构中,不同的工程可能出现下面的场景

image-20210122172549530

这涉及到很多非常棘手的问题:

  • 如何避免公共模块重复打包
  • 如何将某个项目中一部分模块分享出去,同时还要避免重复打包
  • 如何管理依赖的不同版本
  • 如何更新模块
  • ......

webpack5尝试着通过模块联邦来解决此类问题

示例

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

初始化工程

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

暴露和引用模块

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

处理共享模块

两个项目均使用了jquery,为了避免重复,可以同时为双方使用shared配置共享模块

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // 配置共享模块
      shared: {
        // jquery为共享模块
        jquery: {
          singleton: true, // 全局唯一
        },
      },
    }),
  ],
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狡辉两门

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值