前端工程化概述

40 阅读32分钟

说到前端工程化,要先从前端模块化开始讲起

最近研究了前端模块化和,看了一些书籍和博客,写篇文章记录一下,写的不好的地方还请指出。

这篇文章比较基础,算是入门以及查漏补缺基础知识的文章,大佬可自行忽略,下一篇更新以webpack为例的构建工具概述,不过可能时间比较久一点

本文的内容大致如下:

  • 前端模块化
    • CommonJS、AMD、CMD、ES Module等模块化规范
    • 前端框架中的模块化
    • 构建工具中的模块化
    • npm包管理器
    • ...
  • 前端工程化

前端模块化

由于web应用发展的越来越复杂和庞大,web前端的应用范围也更加广泛。简单的 JavaScript、CSS、HTML 开发方式已经无法应对当前web应用的发展。

项目越来越复杂和庞大,只能对其进行拆解再融合

模块化就是将一个复杂庞大的系统拆分成若干个模块,以此方便编码。

就好比微前端(qiankun、wujie等)、monorepo等等,就是将项目拆分成若干个子项目,与模块化的关注点不一样

什么是模块?

模块可以理解为就是一个文件,一个js文件或者ts文件等,准确的说是实现了一部分功能的文件,并隐藏了自己的内部实现,同时提供了一些接口供其他模块使用

核心要素隐藏暴露

  1. 隐藏:隐藏的是自己内部的实现

  2. 暴露:暴露的是提供给外部使用的接口(这里的接口是指外部可以通过某种方式调用内部功能

**重要概念:**导出和导入模块

导出:暴露接口的过程就是模块的导出

导入:当通过某种语法或api去使用一个模块时,这个过程就叫做模块的导入

为什么要使用模块?

当前端达到一定规模后,就会出现以下问题:

  1. 全局变量污染
  2. 依赖混乱

导致了代码文件难以细分,模块化就是为了解决上面两个问题的

模块化的好处在于,可以将一个大型项目根据功能进行细分,每一个模块只关注一个功能,符合单一职责原则设计模式,如果项目中整体用到的比较多,可以抽取为公共模块,如果别的地方也能用到,那可以发布为npm包等等,比较方便。

历史上,js一直没有模块体系,无法将一个大程序分成互相依赖的小文件,再用简单的方法拼装起来。ruby,python以及css都有模块化体系,但是js没有,这对于开发大型的、复杂的项目形成了障碍。nodejs刚刚发布的时候,前端没有统一的、官方的模块化规范,因此选择使用社区提供的CommonJS作为模块化规范

CommonJS

在nodejs中,有且仅有一个入口文件,而开发一个应用肯定会涉及到多个文件配合,因此nodejs对模块化的需求是比浏览器更大的

广泛的js模块化规范,核心思想是通过require方法来同步加载依赖的其他模块,通过module.exports导出需要暴露的接口。CommonJS规范的流行得益于Node.js采用了这种方式,后来这种方式被引入到了网页开发中。

// 导入
// nodejs中导入模块,使用相对路径,并且以./或../开头
// 如果去掉相对路径,那么就是别的含义,查找不到util模块
const util = require('./util')
// 执行导出函数
util.func()

// util.js 导出
module.exports = func

CommonJS还可以细分为CommonJS1和CommonJS2,区别在于CommonJS1只能通过exports.xx = xx

Node.js如何对CommonJS进行实现

比方说CommonJS隐藏功能,如何隐藏的呢?通过哪种方式隐藏的

nodejs执行模块时,会对模块中的所有代码放置到一个函数中执行,以保证不污染全局变量

(function() {
    // 模块中的代码
})()

如何导出的呢?如何导出模块内容

首先初始化一个值,module.exports = {}

module.exports即模块的导出值

在初始化module.exports后,又声明了一个变量exports, 这个变量的值是module.exports

// 作为简易理解
(function(module) {
    module.exports = {};
    var exports = module.exports;
    
    function func() {
        console.log('方法执行...')
    }
    
    // 暴露给外部的方法
    exports.func = func
    
    return module.exports
})()

上述代码中,可以看出module.exportsexports实际上是一个内容 但是如果在模块中给module.exports重新赋值,那么就不相等了

module.exports = {
    count: 0
}

console.log(module.exports === exports) // false

还有一点需要注意的是模块实际导出的内容是module.exports,并不是exports

// util.js
module.exports = {
    count: 0
}
exports.count1 = 0

// 使用util模块
const util = require('./util')
console.log(util.count1) // undefined

输出undefined是因为module.exports里面并没有count1属性

使用时还是建议使用module.exports进行模块导出,因为更加灵活,功能更强大

而且还可以对module.exports进行重新赋值

// util.js
module.exports = 'abc'

// 使用
const util = require('./util')
console.log(util) // 'abc'


// 如果对exports进行重新赋值为字符串,使用使用到的时候是空对象
// util.js
exports = 'abc'

// 使用
const util = require('./util')
console.log(util) // {}

nodejs为了避免反复加载同一个模块,默认开启了模块缓存,如何加载的模块已经加载过了,则会使用之前的导出结果.

优势:

  • 代码可复用于Node.js环境下并运行
  • 通过NPM发布的很多第三方模块都采用了CommonJS规范

缺点:

  • 无法直接运行在浏览器环境下,必须通过工具转换成标准的ES5
  • CommonJS是同步的,必须要等到加载完文件并执行完代码后才能继续向后执行

当我们想把CommonJS放到浏览器时,就遇到了一些问题

  1. 浏览器需要从远程服务器读取并加载js文件,传输效率低下,并且由于CommonJS是同步的,也会降低运行性能
  2. 需要浏览器支持,因为CommonJS是非官方标准

也并不是说模块化不能在浏览器中实现,只需要解决以上两个问题就行了

  1. 远程加载js浪费时间,那么就做成异步就行了,加载完成后调用一个回调函数
  2. 模块中的代码需要放置到函数中执行,编写模块时直接放函数中即可

所以就出现了AMD、CMD规范

AMD

全称Asynchronous Module Definition,异步模块加载机制

同样也是一种js模块化规范,与CommonJS最大的不同在于它采用异步的方式去加载依赖的模块。AMD规范主要是为了解决针对浏览器环境的模块化问题,最具代表性的实现是requirejs.

在AMD中,导入和导出模块的代码,都必须放置在define函数中,代码如下:

// 导出数字
define(123)

// 定义字符串
define('abc')

// 导入一个模块
// 导入的模块最好是与文件名一致
define('moduleA', function (moduleA) {
     //等a.js加载完成后运行该函数
     //模块内部的代码
     console.log(moduleA)
})

// 导入多个模块
define(["moduleA", "moduleB"], function (moduleB, moduleA) {
     //等b.js加载完成后运行该函数
     //模块内部的代码
     console.log(moduleB, moduleA)
})

// 同样提供了与CommonJS一样的用法
define((require, exports, module) => {
    var a = require("a"),
        b = require("b");
    console.log(b, a)
})

// 要导入的模块内部也可以使用此种方式
// moduleA.js
define(function (require, exports, module) {
    //模块内部的代码
    console.log("moduleA模块的内部代码")
    module.exports = {
        name: "moduleA模块",
        data: "moduleA模块的数据"
    }
})
// 如果要使用html测试的话,需要指定入口文件,然后引入require.js
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script data-main="./js/index.js" src="./js/require.js"></script>
</body>
</html>

data-main就是指定入口文件的属性

require.js是下载到本地的require.js第三方库

优点:

  • 可在不转换代码的情况下直接在浏览器中运行
  • 可异步加载依赖
  • 可并行加载多个依赖
  • 代码可运行在浏览器环境和Node.js环境下

缺点:

js运行环境没有原生支持AMD,需要先导入实现了AMD的库后才能使用

CMD

全称是Common Module Definition,公共模块定义规范

sea.js实现了CMD规范

CMD中,导入和导出模块的代码,都必须放置在define函数中

// index.js
// 导入模块,可以采用回调的方式实现异步
define(function(require, exports, module){
    require.async('moduleA', function(moduleA) {
        console.log('moduleA')
    })
})

html中使用CMD,不需要指定data-main属性,需要先引入sea.js,然后在之后的script脚本中写入模块代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <script src="./js/sea.js"></script>
    <script>
        seajs.use("./js/index")
    </script>
</body>
</html>

ES6模块化

ECMA参考了众多社区模块化标准,在2015年发布了ES6官方的模块化标准之后,后来成为了ES6模块化,在语言层面尚实现了模块化,浏览器和Node.js都宣布要原生支持该规范,逐渐取代了CommonJSAMD规范,实现起来也比较简单,成为浏览器和服务器通用的模块解决方案。

特点:

  1. 使用依赖预声明的方式导入模块

    依赖延迟声明:某些时候可以提高效率,但是无法在一开始确定模块依赖关系(比较模糊)

    依赖预声明:在一开始就可以确定依赖关系,但是某些时候效率较低

  2. 灵活的多种导入导出方式

  3. 规范的路径表示法,所有路径必须以./或者../开头

如何在html中使用es6模块化

// 非官方标准
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script src="./module/index.js" type="module"></script>
</body>
</html>

基本导入导出

基本导出必须具有名称,所以要求内容必须跟上声明表达式或具名符号

export 声明表达式

或者

export { 具名符号 }

示例代码如下:

// 导出
// 单个导出
// util.js
export const count = 0
export function test() {}

// 要是声明语句
export 3 // 不行

// 或者
// 多个导出
const count = 0
const gender = 'male'

// 这个大括号虽然是对象的形式,但不是对象, 意思就是分开导出
// 是将count的名称作为导出的名称,count变量的值作为导出的值
export {
	count,
    gender
}

// 导出的时候也可以使用 as 更换变量名称
export {
	count as count1,
    gender as gender1
}

基本导入

由于使用的是依赖预加载,因此导入任何其他模块必须放在所有代码之前

对于基本导出,如果要进行导入,使用下面的方式

import { 导入的符号列表 } from '模块路径'

注意:

  • 导入时,可以通过关键字as队导入的符号进行重命名
  • 导入时使用的符号是常量,不可修改
  • 可以使用 * 号导入所有的基本导出,形成一个对象
// 依赖预加载
// 如果util文件中依赖了别的模块,那么就会把所有的模块都导入进来之后,才会执行代码
import { count, gender } fomr './util.js'
console.log(count, gender)

// 对导入模块进行重命名
import { count as countUtil } from './util.js'
console.log(countUtil)

// 导入的内容不可修改
import { count } from './util.js'
count = 3 // ×

// 使用 * 导出util中的所有
import * as util from './util.js'
console.log(util.count, util.gender)

有的文件只需要运行一次,那么就不需要导出模块,可以直接导入

比方说全局样式文件,只需要初始化一次即可,就能使用下面的方式导入即可

import './global.css'

默认导入导出

每个模块,除了允许有多个基本导出之外,还允许有一个默认导出

默认到处类似于CommonJS中的module.exports,由于只有一个,因此无需具名

默认导出只允许有一个

const count = 0

function test() {
    console.log('test方法')
}
export default test // 这种写法实际上是 export { test as default } 这种写法的语法糖,简写形式

// 或者
export default function test() {
    console.log('test方法')
}

// 或者
export {
	test as default, // 作为默认导出
    count as count1 // 作为count1导出,导入时使用count1,但是实际上使用的还是count的值
}

默认导入

// 写法
import 接收变量名 from "模块路径"
// 类似于CommonJS中的
var 接收变量名 = require('模块路径')


// 用法
import test from './util.js'

// 也可以将默认导出和基本导出同时导出
import test, { count1 } from './util.js'

// 等同于下面这种形式
import { count1, default as test } from './util.js'

只有导出的内容才会进行加载,其他内容不加载,这种加载称为编译时加载或者静态加载

ES6模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,而CommonJSAMD只能在运行时确定这些东西。

细节

  1. 尽量导出不可变值

    // util.js
    export var name = 'util name'
    
    export default function() {
        name = 'change util name'
    }
    
    // index.js 
    import method, { name }  from './util.js'
    
    console.log(name) // util name
    method()
    console.log(name) // change util name
    

    这样的方式虽然不报错,但是不推荐,因为改变了模块内部的值

    // 可以使用const声明变量,来避免
    // util.js
    export const name = 'util name'
    

    这样改的话,在index.js文件中使用method改变name的值的时候就会报错来提示

  2. 可以使用无绑定的导入用于执行一些初始化代码

    // 如果模块只用于初始化,那么就可以用此种方式
    // 上文中也提到过,例如全局的css文件
    import './global.css'
    
  3. 可以使用绑定再导出,来重新导出来自另一个模块的内容

    有的时候,我们可能需要用一个模块封装多个模块,然后有选择的将多个模块的内容分别导出,可以使用下面的语法

    // 语法
    export { 绑定的标识符 } from '模块路径'
    
    // 小例子
    // number.js
    export const number = 0
    export default 'number module'
    
    // string.js
    export const str = 'string'
    export const str2 = 'string2'
    export default 'string module' 
    
    // util.js
    export { number, default } from './number.js'
    export { str, str2 as stringStr2, default } from './string.js'
    export const util = 'util module'
    
    
    // index.js
    // 全量导入util.js模块的内容
    import * as util from './util.js'
    console.log(util) // util里面就会包含util.js模块的内容以及util.js中导入的其他的模块内容
    

上面代码中,util.js 文件就是重新导出了 string.jsnumber.js 两个文件的内容,自身也导出了内容,相当于是中转了一下

样式文件中的模块化

以Less为例

Less是一种更加简洁的样式代码,是css的预编译语言,让编写样式变得更加容易

Less代码虽然好,但是无法被浏览器识别,因此需要一个工具将其转换为css

由于node具有读写文件的能力,于是在node环境中可以轻松的完成文件的转换

less代码通过npm下载一个less的包(这个包是运行在node环境中的),通过这个less的包将less代码转换为css代码运行在浏览器中,并不是直接运行,而是通过包进行转换为能运行的代码。

node环境在前端工程化中,充当了一个辅助的角色,它并不直接运行前端代码,而是让我们编写前端代码更加舒适便利

转换代码,称之为编译(compile),转换代码的工具,称之为编译器(compiler)

/* 例如下面这样一段less代码 */
@green: #008c8c;
.list {
    display: flex;
    flex-wrap: wrap;
    color: @green;
    
    li {
        margin: 1em;
        
        &:hover {
            background: @green;
            color: #fff;
        }
    }
}
/* 上面的less代码,就会被转译为下面这样一段css代码 */
.list {
    display: flex;
    flex-wrap: wrap;
    color: #008c8c;
}

.list li {
    margin: 1em;
}

.list li:hover {
    background: #008c8c;
    color: #fff;
}

如何下载包呢

可以通过npm下载less

less包提供了一个cli工具lessc, 可以通过以下两种方法使用lessc

  1. 全局安装less,这种方案可以让你在任何终端目录使用lessc命令,但是不利于版本控制

    # 这里是将index.less文件编译为index.css
    lessc index.less index.css
    

    这里的不利于版本控制,并不是说全局安装不太好切换版本,是不利于针对某个项目进行版本控制

  2. 本地安装less,这种方法会把less安装到工程目录的node_modules目录中,无法全局使用lessc命令,但是可以在当前工程目录中使用npx lessc 运行该命令

    这个npx是随着node一起安装下来的,作用就是可以在当前工程目录下,运行一个node_modules里面的命令

Less的核心语法

这里只写一些简单的,详细的可以查看Less官网 Less中文网

  • 变量

    以@符号开头

    @width: 10px;
    @height: @width + 10px;
    
    #header {
        width: @width;
        height: @height;
    }
    

    编译后的结果就是

    #header {
        width: 10px;
        height: 20px;
    }
    
  • 嵌套

    .list {
        li {
            color: #008c8c;
            
            &.active {
                color: #f40;
            }
            
            &::after {
                content: '';
            }
            
            > a {
                text-decoration: none;
            }
        }
    }
    

    编译后的结果

    .list li {
        color: #008c8c;
    }
    
    .list li.active {
        color: #f40;
    }
    
    .list li::after {
        content: '';
    }
    
    .list li > a {
        text-decoration: none;
    }
    
  • 混合

    .round {
        border-radius: 50%;
    }
    
    .a {
        .round();
    }
    

    上面代码也可以实现混合,编译之后类名为a的样式也拥有了圆角样式

    如何只是让round类作为一个混合来考虑的话,可以声明样式的时候就给它加上小括号,这样编译之后不会出现round的类样式

    .round() {
        border-radius: 50%;
    }
    

    作为混合的时候,还可以指定参数以及默认值

    .round(@r: 5px) {
        border-radius: @r;
    }
    

扩展:使用Volta管理工具链版本

以往,都是用nvm(node version manager)工具来管理node版本,通过这个工具,可以在一台计算机上安装多个版本的node,并且随时进行切换

# 安装某个版本的node
nvm install node版本号

# 切换node版本
nvm use node版本号

但是这个工具也存在一些问题,比如:版本切换引发的“工具丢失”

nvm install 16.17.1
nvm use 16.17.1
npm install -g express-generator

# 切换node版本
nvm install 22
nvm use 22

# 然后执行 express 命令会报错
express 

# command not found

因为每个node版本都有自己隔离的全局node_modules目录

image-20251110212328482.png

除此之外,还有一些问题:

  1. 每次手动执行nvm use,容易忘
  2. nvm是shell脚本实现,nvm use切换慢
  3. nvm在Windows上并不原生,需要nvm-windows替代版
  4. 等等...

面对nvm的诸多问题,需要一个更现代、更可靠的工具链管理器。

Volta就应运而生,Volta是一个用Rust编写的现代Node.js的工具链管理器,设计初衷就是为了解决nvm的所有痛点。

nvm只能管理node版本,Volta可以管理vite、express等等整个工具链的版本

下载

这里以windows为例,可以使用winget命令直接下载

winget install Volta.Volta

但是winget也需要下载

也可以直接下载Volta的exe文件

image-20251110220242803.png

使用

volta install express-generator
volta install node@16.17.1
volta install node@22.8.0

# 不论当前Node是哪个版本
express new my-app # 永远可用

只需要在项目中绑定所需Node版本:

volta pin node@16.17.1

这个绑定信息会写入到package.json文件的volta字段中,例如:

{
    "volta": {
        "node": "16.17.1",
        "npm": "10.2.4",
        "yarn": "1.22.19"
    }
}

之后进入这个项目目录,volta会自动切换到对应node版本,无需额外操作。

包管理器

模块 --> 通常以单个文件形式存在的功能片段,入口文件通常被称之为入口模块或主模块

库 --> 以一个或多个模块组成的完整功能块,为开发中某一方面的问题提供完整的解决方案

包 --> 包含元数据的库,这些元数据包括:名称、描述、许可证协议等

在node环境下js的代码可以更细粒度的划分,为了解决开发过程中遇到的问题,例如解密、模拟数据等等,出现了大量的第三方库,但是当下载使用这些第三方库的时候,出现了难以处理的问题:

  1. 下载过程繁琐:需要进入github或者官网找到相应的版本,拷贝到目录中,如果遇到同名的,还需要改名
  2. 如果依赖其他库,还需要按照要求先下载其他库
  3. 开发环境中安装的大量的库如何在生产环境中还原,又如何区分
  4. 更新一个库很麻烦
  5. 自己开发的库,如何在下一次开发使用

要解决以上问题,包管理器就出现了。

前端几乎所有的包管理器都是基于npm的,npm是一个包管理器,也是其他包管理的基石

npm全称node package manager,即node包管理器,运行在node环境中,让开发者可以用简单的方式完成包的查找、安装、更新、卸载、上传等操作

npm无法运行在浏览器环境是因为浏览器环境无法提供下载、删除、读取本地文件的功能,而node属于服务器环境,没有浏览器的种种限制,理论上可以完全掌控运行node的计算机

npm组成

  1. registry:入口

    相当于一个庞大的数据库;第三方库的开发者,将自己的库按照npm的规范,打包上传到数据库中;使用统一的地址下载第三方包

  2. 官网:www.npmjs.com,用来查询包,注册、登录、管理个人信息

  3. CLI: command-line interface 命令行接口,安装好npm后,通过CLI来使用npm的各种功能

npm安装包

  1. 本地安装
npm install 包名

# 简写形式
npm i 包名

# 例如
npm install lodash
npm install jquery

安装后的包会在项目目录下的node_modules文件夹中

如果本地安装的包带有CLI,npm会将它的CLI脚本文件放置到node_modules/.bin文件夹下,使用npx 命令名就可以进行调用

有时候下载一个包,这个包里面可能依赖了其他的包,这时候npm就会自动安装

  1. 全局安装

    npm install --global 包名
    

    全局安装的目录可以通过npm config get prefix命令查看

image-20251111205846522.png

并不是很多项目都需要同一个包,所以把这个包安装在全局,那样的话会造成更新以及删除比较麻烦,而且如果共同依赖一个包,包的版本如果更新的话有可能造成很多项目都有问题

全局安装的包并非所有工程可用,它仅提供全局CLI工具

除非包的版本非常稳定,很少有大的更新;提供的CLI工具在各个工程中使用的非常频繁;CLI工具仅为开发环境提供支持,而非部署环境

包配置

npm将每一个使用npm的工程本身都看作是一个包,包的信息需要通过一个名称固定的配置文件来描述

配置文件的名称固定为package.json

可以手动创建,也可以通过命令npm init创建

配置文件中包含大量信息,包括:name、version、description、author、main、keywords

还有一个配置也可以写入到文件中,比方说eslintbrowserslist的一些配置

此文件还可以记录当前工程的依赖

以我平时写的一个vue项目为例

{
   "dependencies": {
    "axios": "^1.7.0",
    "core-js": "^3.8.3",
    "dayjs": "1.11.11",
    "echarts": "^5.5.0",
    "element-plus": "^2.7.3",
    "hls.js": "^1.6.9",
    "js-md5": "^0.8.3",
    "lodash": "^4.17.21",
    "vue": "^3.2.13",
    "vue-router": "^4.0.3",
    "vuex": "^4.1.0"
  },
  "devDependencies": {
    "@babel/core": "^7.12.16",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-router": "~5.0.0",
    "@vue/cli-plugin-vuex": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "lint-staged": "^11.1.2",
    "postcss": "^8.4.38",
    "postcss-pxtorem": "^6.1.0",
    "sass": "^1.32.7",
    "sass-loader": "^12.0.0"
  },
}
  • dependencies:生产环境的依赖包

  • devDependencies:仅开发环境的依赖包

# 安装依赖到生产环境
npm i 包名 # 默认就是安装到生产环境
npm i --save 包名
npm i -S 包名 # 简写形式

## 安装依赖到开发环境
npm i --save-dev 包名
npm i -D 包名 # 简写形式
# 直接安装所有依赖
npm i
npm install

# 仅安装生产环境的所有依赖
npm install --production

npm包版本号

版本规范:主版本号.次版本号.补丁版本号

  • 主版本号:仅当程序发生了重大变化时才会增长,如新增了重要功能、新增了大量的API、技术架构发生了重大变化

  • 次版本号:仅当程序发生了一些小变化时才会增长,如新增了一些小功能、新增了一些辅助型的API

  • 补丁版本号:仅当解决了一些 bug 或 进行了一些局部优化时更新,如修复了某个函数的 bug、提升了某个函数的运行效率

语义版本的书写规则非常丰富,下面列出了一些常见的书写方式

符号描述示例示例描述
大于某个版本>1.2.1大于1.2.1版本
>=大于等于某个版本>=1.2.1大于等于1.2.1版本
<小于某个版本<1.2.1小于1.2.1版本
<=小于等于某个版本<=1.2.1小于等于1.2.1版本
-介于两个版本之间1.2.1 - 1.4.5介于1.2.1和1.4.5之间
x不固定的版本号1.3.x只要保证主版本号是1,次版本号是3即可
~补丁版本号可增~1.3.4保证主版本号是1,次版本号是3,补丁版本号大于等于4
此版本和补丁版本可增^1.3.4保证主版本号是1,次版本号可以大于等于3,补丁版本号可以大于等于4
*最新版本*始终安装最新版本

npm 在安装包的时候,会自动生成一个 package-lock.json 文件,记录了安装包时的依赖关系

当移植工程时,如果移植了 package-lock.json 文件,恢复安装时,会按照 package-lock.json 文件中的确切依赖进行安装

不仅保证了package.json中的包安装正常,也保证了依赖包的版本正常,最大限度的避免了差异

npm脚本 scripts

脚本名称可以在package.json文件里面进行配置

{
  "name": "scan",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "npx nodemon index.js",
    "build": "webpack",
    "watch": "webpack --watch",
    "dev": "webpack-dev-server --inline --hot",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "axios": "^1.13.2",
    "cheerio": "^1.1.2"
  }
}

以上面的文件内容为例,脚本命令里面的npx可以省略

因为项目在执行脚本的时候,会将node_modules/.bin目录里面的命令添加到环境变量中,所以运行脚本的时候不需要npx也可以运行

# 运行脚本
npm run 脚本名称

以下脚本名称不需要使用run

  • start
  • stop
  • test

如果脚本里面没有start命令的话,那么默认运行当前目录下的server.js文件,也就是说start命令是有默认值的 image-20251112063915236.png 但是当前目录下如果没有server.js文件,那么就会报错

脚本名称如何配置?

比如说Windows系统,只要是DOS命令支持的,scripts中都可以配置

"scripts": {
    "start": "npx nodemon index.js",
    "build": "webpack",
    "watch": "webpack --watch",
    "dev": "webpack-dev-server --inline --hot",
    "test": "echo \"Error: no test specified\" && exit 1",
    "dir": "dir",
    "trick": "chrome https://www.baidu.com"
},

运行环境配置

node中有一个global的全局变量,该变量是一个对象,对象中的所有属性可以直接使用

global里面有一个属性process,process包含了当前运行node程序的计算机的很多信息,其中env属性就包含了当前计算机中的所有系统变量

image-20251112065617886.png

通过系统变量 NODE_ENV 的值,来判定node程序处于何种环境

  1. 永久性设置,windows系统可以在计算机环境变量中进行设置

image-20251112070030141.png

  1. 临时设置,可以通过在当前工程中package.json文件的脚本进行配置

    {
        "name": "chapter2",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
            "start": "set NODE_ENV=development&&node index.js",
            "build": "set NODE_ENV=production&&node index.js",
            "test": "set NODE_ENV=test&&node index.js"
        },
        "author": "",
        "license": "ISC"
    }
    

    这里是以windows系统为例,如果是macOS系统的话,将脚本中的set改为export即可

为了避免不同系统的设置方式的差异,可以使用第三方库cross-env对环境变量进行配置

通过npm install -D cross-env进行安装

{
    "name": "chapter2",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "start": "cross-env NODE_ENV=development node index.js",
        "build": "cross-env NODE_ENV=production node index.js",
        "test": "cross-env NODE_ENV=test node index.js"
    },
    "author": "",
    "license": "ISC",
    "devDependencies": {
        "cross-env": "^6.0.3"
    }
}

直接将set改为cross-env即可,中间也不需要 && 连接

npm的其他命令

npm的命令还有很多,但是平时用到的不多,下面说几个npm的其他命令

# 精确安装到最新包
npm install --save-exact 包名 
npm install -E 包名

# 查看包的安装路径
npm root [-g]

# 检查哪些包需要更新
npm outdated

# 卸载
npm uninstall [-g] 包名 # 加上 -g 就是卸载全局包

# 获取某个配置项
npm config get 配置项

# 删除某个配置项
npm config delete 配置项

# 设置某个配置项
npm config set 配置项=值

# 更新某个包
npm update [-g] 包名


npm发布包

使用npm init -y 初始化package.json文件,然后对默认值进行修改

配置如下:

{
  "name": "simple-date-formatter",
  "version": "1.0.0",
  "description": "A simple date formatting utility.",
  "main": "index.js",
  "type": "module",
  "keywords": ["date", "format", "utility"],
  "author": "labixiong",
  "license": "MIT"
}

name就是包名称,author是作者,一般设置为npmjs.com网站你的用户名

name必须是npm上唯一的,可以使用npm view 包名 来查看名称是否已经被占用

在入口文件index.js中编写你的内容

// index.js
/**
 * Format a date to 'YYYY-MM-DD' string
 * @param {Date} date - The date to format
 * @returns {string} Formatted date string
 */
export function formatToDateString(date) {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

/**
 * Format a date to 'HH:MM:SS' string
 * @param {Date} date - The date to format
 * @returns {string} Formatted time string
 */
export function formatToTimeString(date) {
  const hours = String(date.getHours()).padStart(2, '0');
  const minutes = String(date.getMinutes()).padStart(2, '0');
  const seconds = String(date.getSeconds()).padStart(2, '0');
  return `${hours}:${minutes}:${seconds}`;
}

// Example usage within the file:
// const now = new Date();
// console.log('Current Date:', formatToDateString(now));
// console.log('Current Time:', formatToTimeString(now));

根目录创建README.md项目说明文档

# simple-date-formatter

A simple utility to format dates.

## Installation
`npm install simple-date-formatter`

## Usage```javascript
import { formatToDateString, formatToTimeString } from 'simple-date-formatter';

const now = new Date();
console.log(formatToDateString(now)); // e.g., "2023-11-12"
console.log(formatToTimeString(now)); // e.g., "14:05:30"

发布前,如果需要本地调试,本地可以通过npm link命令,把你的包链到全局

然后在本地项目中运行npm link 包名进行测试,这样无需发布远程即可验证包的功能是否正常

发布前要先检查npm官方源,需要切换到官方源,不能是淘宝或者其他的镜像源

# 检查npm当前源
npm config get registry

# 如果不是,通过下方命令切换到官方源
npm config set registry https://registry.npmjs.org/

登录npm账号

npm login

如果没有的话 需要前往https://npmjs.com官网注册一下

登录后,使用npm whoami命令验证是否登录成功

发布 publish

npm publish --access public

每次修复或更新后重新发布时,都需要使用 npm version <patch|minor|major> 或手动修改 package.json 中的 version 字段来更新版本号

如果不慎发布错误,可以使用 npm unpublish <包名> --force 删除。但请注意,此命令通常只能删除 72小时以内 发布的包,且删除后的包名在 24小时内 不允许重复使用

npm存在的问题

  • 依赖目录嵌套层次深:npm的依赖是嵌套的,这在windows系统上是一个极大的问题,因为windows系统无法支持太深的目录
  • 下载速度慢:由于嵌套层次的问题,所以npm对包的下载只能是串行的,即前一个包下载完后才会下载下一个包,导致带宽资源没有完全利用;还有就是多个相同版本的包会被重复的下载
  • 控制台输出繁杂:npm每安装一个依赖,就会输出依赖的详细信息,导致一次安装有大量的信息输出到控制台,遇到错误极难查看
  • 工程移植问题:由于npm的版本依赖可以是模糊的,可能会导致工程移植后,依赖的确切版本不一致。

针对以上问题,yarn从诞生那天就已经开始解决,使用了以下手段:

  • 使用扁平的目录结构,避免嵌套,造成文件目录层次过深
  • 并行下载,文件依赖同时下载
  • 使用本地缓存,再次下载相同依赖,不会从网络上重新下载,而是直接从本地拷贝
  • 控制台仅输出关键信息
  • 使用yarn.lock文件记录确切依赖

yarn还优化了以下内容:

  • 增加了某些功能强大的命令

  • 让既有的命令更加语义化

  • 本地安装的CLI工具可以使用yarn直接启动

    有些包是自带CLI工具的,比方说mocha、nodemon等等,之前npm是通过进入到 node_modules/.bin 目录下面运行 mocha 等命令,后来有了npx之后可以不用进入到此目录下运行,直接使用npx运行即可

    但是yarn是可以直接运行的

  • 将全局安装的目录当作一个普通的工程,生成package.json文件,便于全局安装移植

    npm全局安装的包,需要一个一个的进行移植,但是yarn将全局安装的目录作为一个项目,一个工程,在文件夹下创建一个package.json文件记录全局安装了哪些依赖,迁移的时候可以直接 install 即可

yarn的出现也给npm带来了不小的压力,在npm6版本出现之后,已经完全解决了上面的问题,可以说两个包管理器的功能非常接近。

yarn的核心命令

  • 初始化 yarn init [--yes/-y]

  • 安装

    添加指定包 yarn [global] add package-name [--dev/-D] [--exact/-E]

    安装package.json中的所有依赖:yarn install [--production/--prod]

  • 脚本和本地CLI:yarn run 脚本名 yarn run CLI名

    # 运行脚本
    yarn run 脚本名 
    # stop start test 可以省略run
    yarn start
    
    # 运行CLI
    yarn run mocha
    
  • 查询

    查询bin目录:yarn [global] bin

    查询包信息:yarn info 包名 [子字段] yarn info react README

    列举已安装的依赖:yarn [global] list [--depth=依赖深度]

    yarn的list命令输出更加丰富,更加详细,包括顶级目录结构、每个包的依赖版本号

  • 更新

    列举需要更新的包: yarn outdated

    更新包:yarn [global upgrade [包名] 如果不指定包,那么就是全部更新

  • 卸载 yarn remove 包名 卸载的时候要使用remove命令来卸载,这样操作可以改动yarn.lock文件

  • 检查 yarn check 验证package.json文件的依赖记录和lock文件是否一致,对于防止篡改(有的时候会手动篡改)非常有用

  • 检查漏洞 yarn audit 此命令可以检查本地安装的包有哪些已知漏洞,以表格的形式列出,漏洞级别分为以下几种:

    • INFO:信息级别

    • LOW:低级别

    • MODERATE:中级别

    • HIGH:高级别

    • CRITICAL:关键级别

image-20251121194851876.png

  • 为什么安装一个包,哪些包会用到它 yarn why yarn why terser

  • yarn create

    由于大部分脚手架工具都是以create-xxx的方式来命名的,比如react的官方脚手架名称为create-react-app

    使用create-react-app创建一个react项目,就需要全局安装脚手架create-react-app工具,然后使用全局命令create-react-app搭建脚手架

    可以使用yarn命令来一步完成安装和搭建

    # 使用yarn创建项目
    yarn create react-app my-app
    
    # 等同于下面两条命令
    yarn global add create-react-app
    create-react-app my-app
    

其他包管理器

  • cnpm 淘宝镜像源,同步频率目前为每10分钟一次保证尽量与官方服务同步。支持除了npm publish以外的所有命令,如今npm已经支持修改registry了,可能cnpm唯一的作用就是和npm共存

  • pnpm

    新兴的包管理器

    1. 目前,安装效率高于npm和yarn的最新版

    2. 非常简洁的node_modules目录

    3. 避免了开发时使用间接依赖的问题

      例如项目的package.json文件里只有mocha一个依赖,但是使用npm和yarn安装的时候是会把mocha的依赖包也安装进来的,项目中的文件同样可以引入间接依赖,但是实际上这些间接依赖是不在package.json文件中的,这就导致了一些开发时的不好的习惯

      pnpm就避免了这个问题,项目中引入间接安装的依赖,就会报错

    4. 能极大的降低磁盘空间的占用

    使用起来也比较简单,原来使用npm的命令替换为pnpm即可

    唯一不同的是,在使用pnpx执行一个需要安装的命令时,会使用pnpm进行安装

    例如npx mocha执行本地的mocha命令时,如果mocha没有安装,则npx会自动的、临时的安装mocha,安装好后,自动运行mocha命令

  • bower 在浏览器端搞定依赖关系,当时只是有npm,还没有官方的ESModule模块化,所以这个包管理器比较落后

    使用方式与现在的npm差不多

    但是使用起来需要手动引入包,如果安装的包过多就会出现依赖混乱的问题

    也可以结合CMD、AMD这些模块化来使用,但是非常麻烦,所以此包管理器已经过时了,只是了解

    # 安装bower
    npm install -g bower
    
    # 初始化项目
    # 初始化之后,项目中会有一个bower.json文件,类似于npm中的package.json文件
    bower init 
    
    # 安装依赖
    # 安装后的依赖以及依赖的版本号会在bower.json中,包文件会在bower_components文件夹下
    bower install --save jquery
    
    # 使用安装好的包
    # 以jquery为例
    <script src="bower_components/jquery/dist/jquery.min.js"></script>
    

前端工程化是一个循序渐进的过程,从模块化 --> 包管理 --> css工程化 --> JS工程化 --> 构建工具

如何将npm下载的包以及代码应用到浏览器端,这时候就需要构建工具了

构建其实就是把源代码转换为发布到线上的可执行js、css、html代码,是工程化、自动化思想在前端开发中的体现,把一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程。构建给前端开发注入了更大的活力,解放了我们的生产力。

构建包括但不限于以下内容:

  • 代码转换
  • 文件优化
  • 代码分割
  • 模块合并
  • 自动刷新
  • 代码校验
  • 自动发布
  • 等等...

本篇文章篇幅过长,所以关于Webpack的内容放到下一篇文章中进行分享,每篇文章更新的时间都比较久,有的是一些整理性的文章,有的是一些深入原理的文章,所以感谢大家的支持,也多谢大家能喜欢我写的文章,你们的支持就是我的动力,有什么不足的地方还请指出。

参考

  1. 深入浅出Webpack
  2. 阮一峰ECMAScript6 入门
  3. less官网
  4. less中文网
  5. volta官网