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
面试题
-
commonjs 和 es6 模块的区别是什么?
参考答案:
- CMJ 是社区标准,ESM 是官方标准
- CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
- CMJ 仅在 node 环境中支持,ESM 各种环境均支持
- CMJ 是动态的依赖,同步执行。ESM 既支持动态,也支持静态,动态依赖是异步执行的。
- ESM 导入时有符号绑定,CMJ 只是普通函数调用和赋值
-
export 和 export default 的区别是什么?
参考答案:
export 为普通导出,又叫做具名导出,顾名思义,它导出的数据必须带有命名,比如变量定义、函数定义这种带有命名的语句。在导出的模块对象中,命名即为模块对象的属性名。在一个模块中可以有多个具名导出
export default 为默认导出,在模块对象中名称固定为 default,因此无须命名,通常导出一个表达式或字面量。在一个模块中只能有一个默认导出。
-
下面的模块导出了什么结果?
exports.a = 'a'; module.exports.b = 'b'; this.c = 'c'; module.exports = { d: 'd' }
参考答案:
{ d: 'd' }
-
下面的代码输入什么结果?
// 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.json
的scripts
中,可以省略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的意义主要有以下几点:
- 理解前端开发中出现的常见问题,以及对应的解决办法
- 帮助理解常见的脚手架,如vue-cli、create-react-app、umi-js等
- 可以脱离脚手架搭建工程,甚至自己完成脚手架开发
- 应对工程化方面的进阶面试题
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-loader
、url-loader
、raw-loader
进行处理
由于大部分前端项目都会用到资源型文件,因此webpack5
原生支持了资源型模块
详见:https://webpack.docschina.org/guides/asset-modules/
解释一下 npm 模块安装机制是什么?
参考答案:
- npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
- npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装
- 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。
npm 缓存相关命令:
# 清除缓存 npm cache clean -f # 获取缓存位置 npm config get cache # 设置缓存位置 npm config set cache "新的缓存路径"
模块联邦
在大型项目中,往往会把项目中的某个区域或功能模块作为单独的项目开发,最终形成「微前端」架构
在微前端架构中,不同的工程可能出现下面的场景
这涉及到很多非常棘手的问题:
- 如何避免公共模块重复打包
- 如何将某个项目中一部分模块分享出去,同时还要避免重复打包
- 如何管理依赖的不同版本
- 如何更新模块
......
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, // 全局唯一
},
},
}),
],
};