说到前端工程化,要先从前端模块化开始讲起
最近研究了前端模块化和,看了一些书籍和博客,写篇文章记录一下,写的不好的地方还请指出。
这篇文章比较基础,算是入门以及查漏补缺基础知识的文章,大佬可自行忽略,下一篇更新以webpack为例的构建工具概述,不过可能时间比较久一点
本文的内容大致如下:
- 前端模块化
- CommonJS、AMD、CMD、ES Module等模块化规范
- 前端框架中的模块化
- 构建工具中的模块化
- npm包管理器
- ...
- 前端工程化
前端模块化
由于web应用发展的越来越复杂和庞大,web前端的应用范围也更加广泛。简单的 JavaScript、CSS、HTML 开发方式已经无法应对当前web应用的发展。
项目越来越复杂和庞大,只能对其进行拆解再融合
模块化就是将一个复杂庞大的系统拆分成若干个模块,以此方便编码。
就好比微前端(qiankun、wujie等)、monorepo等等,就是将项目拆分成若干个子项目,与模块化的关注点不一样
什么是模块?
模块可以理解为就是一个文件,一个js文件或者ts文件等,准确的说是实现了一部分功能的文件,并隐藏了自己的内部实现,同时提供了一些接口供其他模块使用
核心要素:隐藏和暴露
-
隐藏:隐藏的是自己内部的实现
-
暴露:暴露的是提供给外部使用的接口(这里的接口是指外部可以通过某种方式调用内部功能)
**重要概念:**导出和导入模块
导出:暴露接口的过程就是模块的导出
导入:当通过某种语法或api去使用一个模块时,这个过程就叫做模块的导入
为什么要使用模块?
当前端达到一定规模后,就会出现以下问题:
- 全局变量污染
- 依赖混乱
导致了代码文件难以细分,模块化就是为了解决上面两个问题的
模块化的好处在于,可以将一个大型项目根据功能进行细分,每一个模块只关注一个功能,符合单一职责原则设计模式,如果项目中整体用到的比较多,可以抽取为公共模块,如果别的地方也能用到,那可以发布为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.exports和exports实际上是一个内容
但是如果在模块中给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放到浏览器时,就遇到了一些问题
- 浏览器需要从远程服务器读取并加载js文件,传输效率低下,并且由于CommonJS是同步的,也会降低运行性能
- 需要浏览器支持,因为CommonJS是非官方标准
也并不是说模块化不能在浏览器中实现,只需要解决以上两个问题就行了
- 远程加载js浪费时间,那么就做成异步就行了,加载完成后调用一个回调函数
- 模块中的代码需要放置到函数中执行,编写模块时直接放函数中即可
所以就出现了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都宣布要原生支持该规范,逐渐取代了CommonJS和AMD规范,实现起来也比较简单,成为浏览器和服务器通用的模块解决方案。
特点:
-
使用依赖预声明的方式导入模块
依赖延迟声明:某些时候可以提高效率,但是无法在一开始确定模块依赖关系(比较模糊)
依赖预声明:在一开始就可以确定依赖关系,但是某些时候效率较低
-
灵活的多种导入导出方式
-
规范的路径表示法,所有路径必须以./或者../开头
如何在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模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,而CommonJS和AMD只能在运行时确定这些东西。
细节
-
尽量导出不可变值
// 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的值的时候就会报错来提示 -
可以使用无绑定的导入用于执行一些初始化代码
// 如果模块只用于初始化,那么就可以用此种方式 // 上文中也提到过,例如全局的css文件 import './global.css' -
可以使用绑定再导出,来重新导出来自另一个模块的内容
有的时候,我们可能需要用一个模块封装多个模块,然后有选择的将多个模块的内容分别导出,可以使用下面的语法
// 语法 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.js 和 number.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
-
全局安装less,这种方案可以让你在任何终端目录使用
lessc命令,但是不利于版本控制# 这里是将index.less文件编译为index.css lessc index.less index.css这里的不利于版本控制,并不是说全局安装不太好切换版本,是不利于针对某个项目进行版本控制
-
本地安装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目录
除此之外,还有一些问题:
- 每次手动执行
nvm use,容易忘 - nvm是shell脚本实现,
nvm use切换慢 - nvm在Windows上并不原生,需要
nvm-windows替代版 - 等等...
面对nvm的诸多问题,需要一个更现代、更可靠的工具链管理器。
Volta就应运而生,Volta是一个用Rust编写的现代Node.js的工具链管理器,设计初衷就是为了解决nvm的所有痛点。
nvm只能管理node版本,Volta可以管理vite、express等等整个工具链的版本
下载
这里以windows为例,可以使用winget命令直接下载
winget install Volta.Volta
但是winget也需要下载
也可以直接下载Volta的exe文件
使用
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的代码可以更细粒度的划分,为了解决开发过程中遇到的问题,例如解密、模拟数据等等,出现了大量的第三方库,但是当下载使用这些第三方库的时候,出现了难以处理的问题:
- 下载过程繁琐:需要进入github或者官网找到相应的版本,拷贝到目录中,如果遇到同名的,还需要改名
- 如果依赖其他库,还需要按照要求先下载其他库
- 开发环境中安装的大量的库如何在生产环境中还原,又如何区分
- 更新一个库很麻烦
- 自己开发的库,如何在下一次开发使用
要解决以上问题,包管理器就出现了。
前端几乎所有的包管理器都是基于npm的,npm是一个包管理器,也是其他包管理的基石
npm全称node package manager,即node包管理器,运行在node环境中,让开发者可以用简单的方式完成包的查找、安装、更新、卸载、上传等操作
npm无法运行在浏览器环境是因为浏览器环境无法提供下载、删除、读取本地文件的功能,而node属于服务器环境,没有浏览器的种种限制,理论上可以完全掌控运行node的计算机
npm组成
-
registry:入口
相当于一个庞大的数据库;第三方库的开发者,将自己的库按照npm的规范,打包上传到数据库中;使用统一的地址下载第三方包
-
CLI: command-line interface 命令行接口,安装好npm后,通过CLI来使用npm的各种功能
npm安装包
- 本地安装
npm install 包名
# 简写形式
npm i 包名
# 例如
npm install lodash
npm install jquery
安装后的包会在项目目录下的node_modules文件夹中
如果本地安装的包带有CLI,npm会将它的CLI脚本文件放置到node_modules/.bin文件夹下,使用npx 命令名就可以进行调用
有时候下载一个包,这个包里面可能依赖了其他的包,这时候npm就会自动安装
-
全局安装
npm install --global 包名全局安装的目录可以通过
npm config get prefix命令查看
并不是很多项目都需要同一个包,所以把这个包安装在全局,那样的话会造成更新以及删除比较麻烦,而且如果共同依赖一个包,包的版本如果更新的话有可能造成很多项目都有问题
全局安装的包并非所有工程可用,它仅提供全局CLI工具
除非包的版本非常稳定,很少有大的更新;提供的CLI工具在各个工程中使用的非常频繁;CLI工具仅为开发环境提供支持,而非部署环境
包配置
npm将每一个使用npm的工程本身都看作是一个包,包的信息需要通过一个名称固定的配置文件来描述
配置文件的名称固定为package.json
可以手动创建,也可以通过命令npm init创建
配置文件中包含大量信息,包括:name、version、description、author、main、keywords
还有一个配置也可以写入到文件中,比方说eslint、browserslist的一些配置
此文件还可以记录当前工程的依赖
以我平时写的一个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命令是有默认值的
但是当前目录下如果没有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属性就包含了当前计算机中的所有系统变量
通过系统变量 NODE_ENV 的值,来判定node程序处于何种环境
- 永久性设置,windows系统可以在计算机环境变量中进行设置
-
临时设置,可以通过在当前工程中
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:关键级别
-
-
为什么安装一个包,哪些包会用到它
yarn whyyarn 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
新兴的包管理器
-
目前,安装效率高于npm和yarn的最新版
-
非常简洁的node_modules目录
-
避免了开发时使用间接依赖的问题
例如项目的package.json文件里只有mocha一个依赖,但是使用npm和yarn安装的时候是会把mocha的依赖包也安装进来的,项目中的文件同样可以引入间接依赖,但是实际上这些间接依赖是不在package.json文件中的,这就导致了一些开发时的不好的习惯
pnpm就避免了这个问题,项目中引入间接安装的依赖,就会报错
-
能极大的降低磁盘空间的占用
使用起来也比较简单,原来使用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的内容放到下一篇文章中进行分享,每篇文章更新的时间都比较久,有的是一些整理性的文章,有的是一些深入原理的文章,所以感谢大家的支持,也多谢大家能喜欢我写的文章,你们的支持就是我的动力,有什么不足的地方还请指出。