import{_ as s,o as n,c as a,R as l}from"./chunks/framework.a304f0f7.js";const F=JSON.parse('{"title":"前端工程化","description":"","frontmatter":{},"headers":[],"relativePath":"note/Front-end Engineering.md","lastUpdated":1682669214000}'),o={name:"note/Front-end Engineering.md"},e=l(`

前端工程化

Node.js

什么是Node.js

Node.js是一个基于V8 JavaScript引擎JavaScript运行时环境

可以简单总结出Node.js和浏览器的区别

![The Node.js System](Front-end Engineering.assets/The Node.js System.jpeg)

Node.js的应用场景

Node.js的参数传递

process.argv

process.argv返回一个数组

js
// sum.js
const x = process.argv[2]
const y = process.argv[3]
console.log(x + y)
sh
# 通过命令行运行node执行脚本 并传入参数
node sum.js 5 10 # 15

console

REPL

在浏览器的控制台选项卡中,我们可以通过输入JS代码与之交互,在Node.js中同样提供了类似的功能

Node中的全局对象

在浏览器中,我们可以在JS代码中访问全局对象window,代表当前标签窗口

在Node.js中的全局对象名为global,在控制台输出global对象:

sh
> global
<ref *1> Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  performance: Performance {
    nodeTiming: PerformanceNodeTiming {
      name: 'node',
      entryType: 'node',
      startTime: 0,
      duration: 2245.9675999991596,
      nodeStart: 1.7120999991893768,
      v8Start: 7.749699998646975,
      bootstrapComplete: 56.47019999846816,
      environment: 28.44789999909699,
      loopStart: 97.62589999847114,
      loopExit: -1,
      idleTime: 2070.0206
    },
    timeOrigin: 1675854922619.539
  },
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  }
}

常见的全局对象

特殊的全局对象

__dirname __filename exports module require()

global对象

global是一个全局对象

这无异于增加了开发者的心智负担,所以在最新的ECMA标准中出现了globalThis,指向全局对象

两个全局对象的区别:在浏览器中通过var定义的变量会被放到window对象上,而Node.js不会

模块化开发

模块化的初衷

js
// moduleA.js
const moduleA = (function(){
  const name = "Ziu"
  const age = 18
  const run = () => {
    console.log(name + age + 'is running.')
  }
  return {
    name,
    age,
    run
  }
})()

// moduleB.js
console.log(moduleA.name) // 在其他模块中调用

CommonJS

CommonJS是一种规范,当初命名为ServerJS,旨在浏览器以外的地方使用,后为体现其广泛性,改名为CommonJS,简称CJS

规范 是用来指导 实现的

所以,Node.js对CommonJS进行了支持和实现,让JavaScript在Node上运行时可以实现模块化开发

js
// env.js
exports.name = 'Ziu'
exports.age = 18
js
// utils.js
module.exports = {
  sum: function(x, y) {
    return x + y
  }
}
js
// index.js
const utils = require('utils.js')
utils.sum(1, 2) // 3

const { sum } = require('utils.js')
sum(1, 2) // 3

const { name, age } = require('env.js')
console.log(name, age) // Ziu 18

exports的本质

exportsrequire在Node中的本质

js
// utils.js
exports.a = 0

// 1s后修改a值
setTimeout(() => {
  exports.a = 1
}, 1000)

// 2s后检查a值
setTimeout(() => {
  console.log(exports.a) // 2
}, 2000)
js
// index.js
const utils = require('./utils')

console.log(utils.a) // 0

setTimeout(() => {
  console.log(utils.a) // 1
  utils.a = 2 // 反过来修改a值
}, 1500)

在上述代码中,utils对象中的属性a在一秒后被赋值为1,因此在index.js中输出utils.a得到了两次不同的结果

反过来,在index.js中修改导入的utils.a的值后,修改结果也会反映在exports.a上,输出的值为2

实际开发中不要修改导入模块中的变量,改变原模块中变量的值并不规范

module.exports

在Node.js中,真正常用的导出方式是module.exports

js
const name = 'Ziu'
const run = () => console.log(name + 'is running.')

module.exports = {
  name,
  run
}

二者的区别

既然如此,为什么还要存在exports这个概念呢?

如果module.exports不再引用exports对象了,修改exports对象也就没有意义了

js
// utils.js
module.exports = {
  name: 'Ziu'
}
exports.age = 18
js
// index.js
const utils = require('utils.js')
console.log(utils.name) // Ziu
console.log(utils.age) // undefined

当使用module.exports = { ... }后,模块中原有的exports不再被导入识别,导入的内容将变为module.exports指定的对象内容

require的本质

require是一个函数,可以帮助我们导入一个文件(模块)中导出的对象

这涉及到require在匹配路径后的查找规则:

分为三种情况:内置模块、自定义路径、包名

模块的加载过程

CommonJS的缺点

ESModule

exportimport用法概览

ESModule借助exportimport导入导出内容,需要注意的是导入导出的并不是对象

export定义的是当前模块导出的接口import可以导入来自其他不同模块的接口

js
// utils.js
export function sum(a, b) {
  return a + b
}
export function sub(a, b) {
  return a - b
}
export default function log(...args) {
  console.log(...args)
}
export {
  name: 'Ziu',
  age: 18
}
export const ENV_VARIABLE = 'Hello, World!'
js
// index.js
import { sum, sub, name, age, ENV_VARIABLE } from './utils'
import log from './utils.js'

sum(1, 2) // 3
sub(2, 3) // -1
log(name, age, ENV_VARIABLE) // 'Ziu' 18 'Hello, World!'

需要注意的是,在浏览器中要使用ESModule,需要为<script>标签添加module标记:

<script src="index.js" type="module"></script>

另外,exportimport必须位于模块的顶层,如果位于作用域内会报错,因为这就无法对代码进行静态分析优化了

export详解

export有两种导出方式:

值的动态绑定

我们援引之前介绍CJS时的案例,将后缀名改为mjs即可在Node中运行ESModule模块代码

初始获得的a值为0,经过1s后,在utils.mjs中修改了a的值,这时导入utils.mjs模块的其他模块可以获取到a最新的值

js
// utils.mjs
export let a = 0

// 1s后修改a值
setTimeout(() => {
  a = 1
}, 1000)
js
// index.mjs
import { a } from './utils.mjs'

console.log(a) // 0

setTimeout(() => {
  console.log(a) // 1
}, 1500)

拓展阅读:CommonJS与ESModule加载模块的异同

import详解

检查下述代码:

js
foo()

import { foo } from 'foo'
js
import 'lodash'
import 'lodash'
js
import * from 'utils'
add(1, 2)

export * from 'utils'

import()函数

通过import命令导入的模块是静态的,会被提升到模块顶部,并不支持条件导入

ES2020引入了import()函数,可以通过import()函数实现条件导入,动态加载ESModule模块

js
const main = document.querySelector('main');

import(\`./section-modules/\${someVariable}.js\`)
  .then(module => {
    module.loadPageInto(main);
  })
    .catch(err => {
    main.textContent = err.message;
  })

通过.then函数处理导入的模块时,行为和import是相同的:

应用场景

按需加载:按钮点击后才加载相关的JS文件

js
btn.addEventListener('click', () => {
  import('./dialogBox.js')
    .then(dialogBox => {
      dialogBox.open()
    })
    .catch(err => console.log(err))
})

条件加载:根据主题色加载不同JS文件

js
if(darkMode) {
  import('dark.js').then(() => ...)
} else {
  import('light.js').then(() => ...)
}

传入动态值

js
let moduleName = () => ['Home', 'History', 'User'][0]
import(\`./\${moduleName()}.js\`)

import.meta

ES2020引入了import.meta,它仅能在模块内部使用,包含一些模块自身的信息,即模块元信息

规范中并未规定import.meta中包含哪些属性,一般包括上面两个属性

深入理解模块加载

ESModule的解析过程

ESModule的解析过程可以分为三个阶段:

![ESModule解析过程](Front-end Engineering.assets/esmodule-phases.png)

文章推荐:ES modules: A cartoon deep-dive

MJS和CJS的区别

CJS中的循环加载

设想有以下两文件 a.jsb.js

js
// a.js
exports.done = false
const b = require('./b.js')
console.log('在 a.js 之中,b.done = %j', b.done)
exports.done = true
console.log('a.js 执行完毕')
js
// b.js
exports.done = false
const a = require('./a.js')
console.log('在 b.js 之中,a.done = %j', a.done)
exports.done = true
console.log('b.js 执行完毕')
js
// main.js
const a = require('./a')
const b = require('./b')
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done)

执行脚本main.js,先执行a.js

b.js中:

回到a.js中:

最终输出:

sh
 b.js 之中,a.done = false
b.js 执行完毕
 a.js 之中,b.done = true
a.js 执行完毕
 main.js 之中, a.done=true, b.done=true

总结:

MJS中的循环加载

ESModule的导入和导出与CommonJS有本质不同:

js
// a.mjs
import { bar } from './b.mjs'
console.log('a.mjs')
console.log(bar)
export let foo = 'foo'
js
// b.mjs
import { foo } from './a.mjs'
console.log('b.mjs')
console.log(foo)
export let bar = 'bar'

执行a.mjs后发现报错了:ReferenceError: Cannot access 'foo' before initialization,变量foo未定义

要实现预期效果,可以将foobar改写为取值函数,这时执行就不会报错了:

js
// a.mjs
import { bar } from './b.mjs'
console.log('a.mjs')
console.log(bar())
export function foo() {
  return 'foo'
}
js
// b.mjs
import { foo } from './a.mjs'
console.log('b.mjs')
console.log(foo())
export function bar() {
  return 'bar'
}

这是因为函数function具有提升作用,在a.mjs中执行import { bar } from './b.mjs'之前,foo就有定义了。

因此在进入b.mjs执行console.log(foo())时可以取到foo,代码可以顺利执行

另:如果将foo定义为函数表达式export const foo = () => 'foo',由于没有变量提升,代码仍然会报错

内部变量差异

ESModule和CommonJS另一个重要区别就是:

ESModule模块是在浏览器与服务端通用的,之前在解读CommonJS时介绍了它拥有的一些内部变量(模块变量):

这些变量在ESModule模块中都是不存在的,且顶层的this不再指向当前模块,而是undefined

拓展内容

在Node.js中使用ESModule

在Node.js中,普通的.js文件会被默认解析为CommonJS,要使用ESModule有两种方式:

解读package.json中的字段

包管理工具

npm

全局安装、局部安装、开发依赖、生产依赖

package.json

package.json用来记录项目的配置信息,包括项目名、版本、项目入口、脚本、依赖项

通过npm init -y初始化项目,会为我们生成一个package.json文件

强烈建议阅读官方文档对package.json的解读:package.json

mainexports字段

参考:package.json 的 exports 字段

dependenciesdevDependencies的区别

官方对两个字段的定义:

dependencies: Dependencies are specified in a simple object that maps a package name to a version range. The version range is a string which has one or more space-separated descriptors. Dependencies can also be identified with a tarball or git URL.

devDependencies: If someone is planning on downloading and using your module in their program, then they probably don't want or need to download and build the external test or documentation framework that you use.

总结:将与代码运行时无关的依赖放入devDependencies,可以让他人在使用你开发的库时,少安装一些不必要的依赖。

依赖版本号中的~1.2.0^1.2.0有什么区别

版本号简要说明:[major 主要版本].[minor 次要版本].[patch 补丁版本]-[alpha | beta 测试阶段].[测试版本号]

参考:node-semver

Allows patch-level changes if a minor version is specified on the comparator. Allows minor-level changes if not.

devDependenciesAllows changes that do not modify the left-most non-zero element in the [major, minor, patch] tuple.

允许对版本 1.0.0 及更高版本进行补丁和次要更新,对版本 0.X >=0.1.0 进行补丁更新,对版本 0.0.X 不进行更新。

npx

npx命令用来在项目路径下执行node_modules/.bin下的命令,默认这些命令都位于node_modules/.bin中,如果不cd进去shell找不到它们,在项目根目录调用它们自然会报未知命令的错误。

以webpack为例:

解读package.json中的bin字段

.bin目录下的可执行文件从何处来?由npm官方文档中对package.json/bin字段的介绍可以知道:

A lot of packages have one or more executable files that they'd like to install into the PATH. npm makes this pretty easy (in fact, it uses this feature to install the "npm" executable.)

To use this, supply a bin field in your package.json which is a map of command name to local file name. When this package is installed globally, that file will be either linked inside the global bins directory or a cmd (Windows Command File) will be created which executes the specified file in the bin field, so it is available to run by name or name.cmd (on Windows PowerShell). When this package is installed as a dependency in another package, the file will be linked where it will be available to that package either directly by npm exec or by name in other scripts when invoking them via npm run-script.

如果第三方包在package.json中声明了bin字段:命令名称 -> 本地文件名的映射,如bin: { "webpack": "bin/webpack.js" }

在执行安装npm i xxx时,会由包管理工具创建.bin目录,并创建一个可执行命令并将其软链接到目标文件

推荐阅读:三面面试官:运行 npm run xxx 的时候发生了什么?

npm包的发布

如果你希望发布自己的npm包,主要涉及到的命令有:

sh
npm init -y # 初始化项目 生成package.json
npm login # 登录自己的npm账号
npm publish # 将包发布到npm上 后续版本更新也使用此命令
npm unpublish # 取消发布 删除发布的npm包
npm deprecate # 让发布的npm包过期

执行npm init -y后,为我们生成的默认package.json如下:

json
{
  "name": "my-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {},
  "devDependencies": {},
  "scripts": {
    "test": "echo \\"Error: no test specified\\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

其中keywords author license都会展示在此包的npm网站上,除此之外还可以定义homepage repository字段

npm包的开发调试

在npm包的开发过程中,如果要调试代码,那我们不可能每次修改代码都通过提交到npm后,重新下载到本地来调试代码

一般来讲有三种解决方案:

PNPM

解决了哪些痛点

传统包管理工具如npm yarn都有以下两个痛点:

PNPM(performant npm)有以下优点:

硬链接和软链接

![hard-link and soft-link](Front-end Engineering.assets/hard-link-and-soft-link.jpg)

操作系统使用不同的文件系统对真实的硬盘读写操作做了一层抽象,借由文件系统,我们得以方便地操作和访问文件的真实数据

实操硬链接和软链接

打开符号链接文件,会直接跳转到原文件,且符号链接文件不可写

PNPM的工作细节

硬链接

这样,多个项目之间可以方便地共享相同版本的依赖包,减小了硬盘的压力。

以安装axios为例,我们在某个项目中执行了pnpm add axios

非扁平的node_modules

![how pnpm works](Front-end Engineering.assets/how-pnpm-works.jpg)

常用命令

存储Store的位置

可以通过命令行,获取到这个目录:

sh
pnpm store path # 获取到硬链接store的路径

如果当前目录创建了很多项目,尽管PNPM已经节省了空间,但是由于所有依赖都通过硬链接存放在同一个store中

随着文件数量增长,导致store越来越大,可以通过命令行,移除冗余文件造成的体积占用

sh
pnpm store prune # prune修剪 从store中删除当前未被引用的包 以释放store的空间

系统学习Webpack

Node内置模块 path

可以通过Node的path模块来抹平不同平台的差异

常用API

js
const path = require('path')
const p1 = 'abc/cba'
const p2 = '../why/kobe/james.txt'
console.log(path.join(p1, p2)) // \\\\abc\\\\why\\\\kobe\\\\james.txt

初识Webpack

path模块在webpack的使用中有较大作用,所以预先介绍了一下,下面正式进入Webpack的学习

`,254),p=[e];function c(r,t,i,d,y,D){return n(),a("div",null,p)}const u=s(o,[["render",c]]);export{F as __pageData,u as default};