import{_ as s,o as n,c as a,a as l}from"./app.f4e2d6d1.js";const F=JSON.parse('{"title":"前端工程化","description":"","frontmatter":{},"headers":[{"level":2,"title":"Node.js","slug":"node-js","link":"#node-js","children":[{"level":3,"title":"什么是Node.js","slug":"什么是node-js","link":"#什么是node-js","children":[]},{"level":3,"title":"Node.js的应用场景","slug":"node-js的应用场景","link":"#node-js的应用场景","children":[]},{"level":3,"title":"Node.js的参数传递","slug":"node-js的参数传递","link":"#node-js的参数传递","children":[]},{"level":3,"title":"Node中的全局对象","slug":"node中的全局对象","link":"#node中的全局对象","children":[]}]},{"level":2,"title":"模块化开发","slug":"模块化开发","link":"#模块化开发","children":[{"level":3,"title":"模块化的初衷","slug":"模块化的初衷","link":"#模块化的初衷","children":[]},{"level":3,"title":"CommonJS","slug":"commonjs","link":"#commonjs","children":[]},{"level":3,"title":"ESModule","slug":"esmodule","link":"#esmodule","children":[]},{"level":3,"title":"深入理解模块加载","slug":"深入理解模块加载","link":"#深入理解模块加载","children":[]},{"level":3,"title":"拓展内容","slug":"拓展内容","link":"#拓展内容","children":[]}]}],"relativePath":"note/Front-end Engineering.md","lastUpdated":1676137882000}'),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解析过程

文章推荐: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中的字段

参考:package.json 的 exports 字段

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