ES Module使用-原理-包管理工具npm
(理解)前端使用模块化的方案解析
- es modules
- ECMA2015提出模块化规范
- 必须浏览器本身支持es modules才能够使用
- webpack 模块化打包工具
- 如果浏览器不支持的话我们可以通过webpack进行打包,这样就算浏览器不支持也能够进行使用,因为webpack内部已经做出处理了。后续我们也会写webpack内部是怎么实现的
认识 ES Module
- JavaScript没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD等,所以在ECMA推出自己的模块化系统时,大家也是兴奋异常
- ES Module和CommonJS的模块化有一些不同之处:
- 一方面它使用了import和export关键字
- 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式
- ES Module模块采用export和import关键字来实现模块化:
- export负责将模块内的内容导出;
- import负责从其他模块导入内容;
- 了解:采用ES Module将自动采用严格模式:use strict
(掌握)ESModule的基本的导入导出
案例代码结构组件
- 这里我在浏览器中演示ES6的模块化开发:
<script src="./modules/foo.js" type="module"></script> <script src="main.js" type="module"></script>
- 如果直接在浏览器中运行代码,会报如下错误:(不开启本地服务)
- 这个在MDN上面有给出解释:
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
- 你需要注意本地测试 — 如果你通过本地加载Html 文件 (比如一个 file:// 路径的文件), 你将会遇到 CORS 错误,因为Javascript 模块安全性需要
- 你需要通过一个服务器来测试
- 我这里使用的VSCode插件:Live Server
exports关键字
- export关键字将一个模块中的变量、函数、类等导出;
- 我们希望将其他中内容全部导出,它可以有如下的方式:
- 方式一:在语句声明的前面直接加上export关键字
- 方式二:将所有需要导出的标识符,放到export后面的 {}中
- 注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的
- 所以: export {name: name},是错误的写法;
- 方式三:导出时给标识符起一个别名
- 通过as关键字起别名
//方式2 //定义的时候就直接导出了,这种方式不能起别名,但有时候反而更加方便 export const name = "小余" export function sayHello(){ console.log("你好啊") } export class Person{ }
//方式3 //导出取别名 exports { name as fname//要导出本文件的name,取别名为fname } //导入 import {fname} from "./xxx" //为什么这么做?因为导入怕原先变量名跟当前文件的变量名有冲突,而直接修改导入文件的变量名又会突兀又麻烦,所以在导出的时候就采用取别名的方式
import关键字
- import关键字负责从另外一个模块中导入内容
- 导入内容的方式也有多种:
- 方式一:import {标识符列表} from '模块';
注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容
import {} from "./xxx"//常用
- 方式二:导入时给标识符起别名
通过as关键字起别名
//刚刚导出的第二种方式无法在导出的时候起别名,那就可以在导入的时候起别名 import {name as myName} from "./xxx"//常用 //这种起别名方式更常用
- 方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上
//*就全部导入的意思,as foo就是给*起别名 import * as foo from "./foo.js"//特殊情况使用
(掌握)ESModule的导入和导出方式扩展export和import结合使用
- 补充:export和import可以结合使用
//原写法 import {sum} from "./bar" export{sum} //优化写法1,两步省略到一步,随着使用次数的增加,省略的就会越明显,也更简洁(阅读性强),推荐这种 export { sum as barSum} from "./bar.js"//导出来自./bar.js的sum,并起别名为barSum //优化写法2,导入多个内容直接超级简写(简洁性强) 有总结工具文档的话使用这种 export * form "./bar.js"//导出来自./bar.js的所有内容
- 为什么要这样做呢?
- 在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中(比如放到index.js里面)
- 这样方便指定统一的接口规范,也方便阅读
- 这个时候,我们就可以使用export和import结合使用
而且我们专门拿出一个文件用来存放导入的东西的时候,就不用到处翻来翻去的了,使用的都是统一在index.js里面,甚至文件名都可以直接省略掉,而是填入文件夹名,文件夹名就会自动推导去找自身里面的index.js,非常的科学规范
(理解)ESModule的导入和导出结合使用
default用法
- 前面我们学习的导出功能都是有名字的导出(named exports):
- 在导出export时指定了名字
- 在导入import时需要知道具体的名字
其实也可以不指定名字的,后续到webpack的时候再来进行讨论
- 还有一种导出叫做默认导出(default export)
- 默认导出export时可以不需要指定名字
- 在导入时不需要使用 {},并且可以自己来指定名字
- 它也方便我们和现有的CommonJS等规范相互操作
- 注意:在一个模块中,只能有一个默认导出(default export)
//xiaoyu文件 function parseLyric(){ return ["歌词"] } const name = "小余" //1.默认导出 export default parseLyric //2.定义标识符直接作为默认导出(一个模块只能有一个默认导出,这里主要是为了记录所以才写在一起) export default function(){//直接默认导出甚至可以不起函数名字,而是由导出的那方去决定名字 return ["十万八千梦"] }
//在另一个文件中导入 import aaaa from "./xiaoyu"//这个aaaa是随便起的名字,由于是默认导出的方式,所以取名是什么在我们导入的时候可以随便起
(掌握)ESModule的默认导出和导入
import函数
模块要放在js最顶层的,不允许在逻辑代码中编写import导入。
而且顶层的import引入的路径是不能够拼接的
- 因为顶层模块是最先执行的,优于代码的执行,所以当模块开始执行的时候,字符串是还没有拼起来的,所以会报错
//错误做法 import {name,age} from ("./foo"+".js")
- 通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
- 为什么会出现这个情况呢?
- 这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系
- 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况
- 甚至拼接路径的写法也是错误的:因为我们必须到运行时能确定path的值
- 但是某些情况下,我们确确实实希望动态的来加载某一个模块:
- 如果根据不懂的条件,动态来选择加载模块的路径(就是逻辑成立,我们才导入某个模块)
- 这个时候我们需要使用 import() 函数来动态加载
√
import函数返回一个Promise,可以通过then获取结果
const flag = "作者是xiaoYu" if(flag){//满足逻辑 // const importPromise = import("./foo.js") // importPromise.then(res => { // //import()返回的是一个Promise,所以我们可以这样来决定处理 // }) //简写 import("xiaoyu").then(res =>{ console.log("你说对了,作者是:",res); }) }
import meta
- import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。
- 它包含了这个模块的信息,比如说这个模块的URL
- 在ES11(ES2020)中新增的特性
(理解)ESModule的解析过程和原理
ES Module的解析流程
- ES Module是如何被浏览器解析并且让模块之间可以相互引用的呢?
- ES Module的解析过程可以划分为三个阶段:
- 阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record)
- 阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址(模块环境记录Module environment record)
- 阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中
阶段一:构建阶段
- 首先从服务器下载下来js文件,然后script的src进行fetch(获取),使用type="module"当作模块去解析
- 解析成Module Record,然后在这中途这个js文件解析出来的里面引入了counter.js和display.js文件,然后就又从去fetch(获取)下来,然后解析(parse)生成两个Mudule Record
- 然后还会继续检查这两个有没有继续引入,有的话就重复上面的操作,没有的话就结束
MODULE MAP:映射关系
阶段二和三:实例化阶段 – 求值阶段
- 查看模块Module Record有没有对这个模块进行导出东西,也就是LocalExport
- 有导出就会针对导出的东西生成Module Environment Record(模块环境记录),这里面就记录着我导出的东西,比如图中的count或者render
- 然后对count或者render赋值具体值5或者是函数
- 然后我们这时候再获取这值就有具体的信息了
(模块环境记录):不是由我们创建出来的,而是由浏览器(JS引擎)生成的,这也就是说我们下面这个不是对象
export{//这个括号不是对象,而是导出的特殊语法,是为了告诉JS引擎我们想要导出哪些内容 xxx }
包管理工具详解npm、yarn、cnpm、npx、pnpm
(了解)包管理工具-内容概述
- npm包管理工具
- package配置文件
- npm install原理
- yarn,cnpm,npx
- 发布自己的开发包
- pnpm使用和原理
(掌握)包管理工具-代码共享和npm基本操作
代码共享方案
- 我们已经学习了在JavaScript中可以通过模块化的方式将代码划分成一个个小的结构:
- 在以后的开发中我们就可以通过模块化的方式来封装自己的代码,并且封装成一个工具
- 这个工具我们可以让同事通过导入的方式来使用,甚至你可以分享给世界各地的程序员来使用
- 如果我们分享给世界上所有的程序员使用,有哪些方式呢?
- 方式一:上传到GitHub上、其他程序员通过GitHub下载我们的代码手动的引用
- 缺点是大家必须知道你的代码GitHub的地址,并且从GitHub上手动下载
- 需要在自己的项目中手动的引用,并且管理相关的依赖
- 不需要使用的时候,需要手动来删除相关的依赖
- 当遇到版本升级或者切换时,需要重复上面的操作
- 显然,上面的方式是有效的,但是这种传统的方式非常麻烦,并且容易出错;
- 方式二:使用一个专业的工具来管理我们的代码
- 我们通过工具将代码发布到特定的位置(npm (npmjs.com))
- 其他程序员直接通过工具来安装、升级、删除我们的工具代码
- 专业的工具就是npm,yarn,cnpm,npx,pnpm这类
- 显然,通过第二种方式我们可以更好的管理自己的工具包,其他人也可以更好的使用我们的工具包
包管理工具npm
- 包管理工具npm:
- Node Package Manager,也就是Node包管理器;
- 但是目前已经不仅仅是Node包管理器了,在前端项目中我们也在使用它来管理依赖的包
- 比如vue、vue-router、vuex、express、koa、react、react-dom、axios、babel、webpack等等
- 如何下载和安装npm工具呢?
- npm属于node的一个管理工具,所以我们需要先安装Node
- node管理工具:https://nodejs.org/en/,安装Node的过程会自动安装npm工具
- npm管理的包可以在哪里查看、搜索呢?
- https://www.npmjs.org/(上面也有写一遍)
- 或者看你要下载那个包的官网,例如dayjs:Day.js中文网 (fenxianglu.cn)
- 在GitHub上找包名
- npm管理的包存放在哪里呢?
- 我们发布自己的包其实是发布到registry上面的
- 当我们安装一个包时其实是从registry上面下载的包
3.24小时整集内容
(掌握)项目的配置文件
npm的配置文件(包工具的使用)
- 那么对于一个项目来说,我们如何使用npm来管理这么多包呢?
- 事实上,我们每一个项目都会有一个对应的配置文件,无论是前端项目(Vue、React)还是后端项目(Node)
- 这个配置文件会记录着你项目的名称、版本号、项目描述等
- 也会记录着你项目所依赖的其他库的信息和依赖库的版本号
- 这个配置文件就是package.json
- 那么这个配置文件如何得到呢?
- 方式一:手动从零创建项目,npm init –y
- 方式二:通过脚手架创建项目,脚手架会帮助我们生成package.json,并且里面有相关的配置(都配置好了)
常见的配置文件
- npm init #创建时填写信息
npm init -y # 所有信息使用默认的(比较简单) - Vue CLI4创建的Vue3项目(Vue的脚手架)
- create-react-app创建的react17项目(react的脚手架)
(掌握)项目配置文件-基础字段
常见的属性
- 必须填写的属性:name、version
属性 |
意思 |
name |
项目的名称 |
version |
当前项目的版本号 |
description |
描述信息,很多时候是作为项目的基本描述 |
author |
作者相关信息(发布时用到) |
license |
开源协议(发布时用到) |
- private属性:
- private属性记录当前的项目是否是私有的;
- 当值为true时,npm是不能发布它的,这是防止私有项目或模块发布出去的方式
- main属性:
- 设置程序的入口
√
比如我们使用axios模块 const axios = require('axios');√
如果有main属性,实际上是找到对应的main属性查找文件的(就不会默认找到index.js、json、node文件去了) - 就引入的时候直接输入main的名字,而不是完整路径了(更加简洁,也是基本的操作)
(掌握)项目配置文件-项目依赖
- scripts属性
- scripts属性用于配置一些脚本命令,以键值对的形式存在;
- 配置后我们可以通过 npm run 命令的key来执行这个命令
- npm start和npm run start的区别是什么?
√
它们是等价的√
对于常用的 start、 test、stop、restart可以省略掉run直接通过 npm start等方式运行(你自己定义的就不能省略run)
"scripts": { "start":"运行文件的路径" } //运行快捷键 npm run start
- dependencies属性
- dependencies属性是指定无论开发环境还是生产环境都需要依赖的包
- 通常是我们项目实际开发用到的一些库模块vue、vuex、vue-router、react、react-dom、axios等等
- 打包npm run build还有vue代码是因为我们还需要依赖vue来操作DOM
- 与之对应的是devDependencies
- devDependencies属性
- 一些包在生产环境是不需要的,比如webpack、babel等(打包完就不需要了)
- 这个时候我们会通过 npm install webpack --save-dev,将它安装到devDependencies属性中
- devDependencies = development Dependencies(开发依赖)
//开发环境依赖 npm install xxx --save-dev//全写 npm install xxx --D//简写
- peerDependencies属性
- 还有一种项目依赖关系是对等依赖,也就是你依赖的一个包,它必须是以另外一个宿主包为前提的
- 比如element-plus是依赖于vue3的,ant design是依赖于react、react-dom
//在element-plus中就能看到 "perrDependencies":{ "vue":"版本" }//这表示必须有Vue才能够使用
ES Module使用-原理-包管理工具npm(二)https://developer.aliyun.com/article/1470452