面试四 模块化组件化

模块与模块化

  • 理解

    模块就是向外暴露的一些具有特定功能的js程序。一般就是一个js文件。这个文件里面包含了一些数据和对数据的操作,最后在通过指定的方式将他们暴露出去供外部使用

  • 为什么要用模块?有什么好处

    当js代码更多更复杂时,把它拆分成多个模块,不仅可以简化js的编写,也可以实现对js代码复用,提高了js的运行效率

  • 模块化

    模块是用来形容js文件的

    模块化是用来形容项目或项目的编码方式的

    模块化的编码方式:在进行项目的开发时,是以一个模块一个模块的形式进行编写的

组件与组件化

  • 理解

    组件就是一个界面的局部功能,这个组件包含了这个功能中的所有资源(html、css、js、img等)

  • 为什么要用组件?有什么好处?

    当一个界面的功能很复杂时,把它拆分成多个组件,不仅可以简化项目的编码,还提高的编码的复用,提高的js的运行效率

  • 组件化

    组件是用来形容界面或局部的功能界面

    组件化是用来项目的:在项目开发时,是一个组件一个组件的进行开发,最后在将这些界面组成一个完整的功能界面

    工程化

    • 理解

      工程化是一种思想,而不是技术。就是将一个项目看做一个工程进行分析、组织和构建。

      可以简单的认为模块化和组件化都是工程化的表现形式

    • 工程化的三个阶段

      1. 技术选型
      2. 构建阶段
      3. 模块化和组件化开发
    • 为什么工程化开发?

      是项目结构更加清晰、分工明确、提高开发效率

    module的语法

参考链接

ES6之前的模块加载方案:

浏览器方面的模块化:AMD和CMD

服务器方面的模块化:CommonJS(Node是CommonJS一个具有代表性的实现)

ES6提出的模块加载方案:(服务器和浏览器通用)

ES6的模块:编译时就能确定模块的依赖关系,以及输入和输出的变量。

CommonJS 和 AMD 模块:只能在运行时确定这些东西

ES6的模块思想:尽量静态化,在编译时就可以确定模块的依赖关系,以及输入和输出的变量。

ES6的加载实质:从模块中引入指定的方法,而不是整个文件作为一个模块进行加载

这种加载也称编译时加载或静态加载,即ES6在编译时就可以完成模块的加载

CommonJS加载实质:将一个文件整体加载为一个模块,并生成一个对象,然后通过这个对象来获取里面的方法

这种加载也称为运行时加载,因为只有运行时才可以得到这个对象

CommonJS

Node中模块的引入和导出就是CommonJS的一个具体代表

  • 导出模块:exports或module.exports
  • 导入模块:require

require导入模块的细节:https://blog.csdn.net/qq_43952245/article/details/106602068

缺点:是同步加载模块

只有等到对应的模块加载完毕,当前模块中的内容才能被运行;

AMD和CMD

CommonJS加载模块是同步的,但是在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;

如果将它应用于浏览器呢?

浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行;那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;

所以在浏览器中,我们通常不使用CommonJS规范:在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:

  • ADM(即Asynchronous Module Definition 异步模块定义)

    AMD实现的比较常用的库是require.js和curl.js;这里我们以require.js为例讲解:

    https://mp.weixin.qq.com/s/6Cc5RMw3pAHEUM58KLB-MQ

    1. 下载require.js
    2. 引入
    3. 加载模块:require(['foo'], function(foo) {... })使用require开始加载执行模块的代码
    4. 定义模块:如果一个模块不依赖其他,那么直接使用define(function)即可
  • CMD(即Common Module Definition通用模块定义)

    SeaJS就是CMD的优秀实现方案

    1. 下载SeaJS

    2. 引入

      // index.js文件中:引入foo模块
      define(function(require, exports, module) {
        const foo = require('./modules/foo');
      })
      // bar.js 暴露指定内容
      define(function(require, exports, module) {
        const name = 'lilei';
        const age = 20;
        const sayHello = function(name) {
          console.log("你好 " + name);
        }
        module.exports = {
          name,
          age,
          sayHello
        }
      })
      // foo.js 引入bar模块,并使用里面的内容
      define(function(require, exports, module) {
        const bar = require('./bar');
        console.log(bar.name);
        console.log(bar.age);
        bar.sayHello("韩梅梅");
      })
      

ES6和CommonJS的不同

ES6:

  • 静态加载,在编译时就完成了模块的加载
  • 从模块中引入指定的方法或属性,是一种静态定义,在代码静态解析阶段就会生成
  • export输出的接口与模块内变量的对应关系是动态的,通过该接口可以获取模块内部实时的值
  • import命令是异步加载(有一个独立的模块依赖的解析阶段)
  • 顶层的this指向undefined

CommonJS:

  • 动态加载,只有运行时,才可以获得加载的模块对象
  • 加载的是一个对象(即module.exports属性),然后通过对象来使用里面的方法(该对象只有在脚本运行时才会生成)
  • CommonJS 模块输出的是值的缓存,不存在动态更新
  • require()是同步加载模块
  • 顶层的this指向当前模块

CommonJS加载原理

CommonJS的一个模块就是一个脚本文件。使用require命令第一次加载该脚本时,就会执行整个脚本,然后在内存中生成一个对象

{
  id: '...', // 模块名
  exports: { ... }, // 模块输出的各个接口。以后需要用到这个模块的时候,就会到exports属性上面取值
  loaded: true, // 表示该模块的脚本是否执行完毕
  ...
}

CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

ES6加载模块:ES6 模块是动态引用

使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用

export命令

用于规定模块的对外接口

注意:export命令就是来规定模块的对外接口,因此export暴露的接口名必须和模块内的变量建立一一对应的关系

// profile.js 方法一
export var firstName = 'Michael';
export function multiply(x, y) {
  return x * y;
};
// profile.js 方法二
export { firstName};

错误写法:

// 报错
export 1; // 直接输出1,1只是一个值,不是接口
// 报错
var m = 1;
export m; // 通过变量 m 直接输出1, 1只是一个值,不是接口

正确写法:

/**
  规定了对外的接口m,其他脚本可以通过这个接口,取到值1
  实质是:在接口名与模块内部变量之间,建立了一一对应的关系。
**/
// 写法一
export var m = 1; // 模块内部变量 m <=> 接口名m
// 写法二
var m = 1;
export {m}; // 模块内部变量 m <=> 接口名 m 暴露出一个对象作为接口
// 写法三
var n = 1;
export {n as m};  // 模块内部变量 n <=> 接口名 m

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。(接口名默认是输出变量的名字)

function v1() { ... }
function v2() { ... }
export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion //重命名后,v2可以用不同的名字输出两次。
};

import命令

引入其他模块提供的功能

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

// circle.js
export function area(radius) {
  return Math.PI * radius * radius;
}
export function circumference(radius) {
  return 2 * Math.PI * radius;
}

逐一加载

// main.js
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

整体加载

import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

export 和 export default

export:

  • 一个模块内可以有多个
  • import引入时需加{}

export default

  • 一个模块内最多只可以有一个
  • import引入时不加{}

因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。

// 正确
export var a = 1;
// 正确
var a = 1;
export default a; //将后面的值,赋给default变量
// 错误
export default var a = 1;
// 正确
export default 42; // 将2赋值给default
// 报错
export 42;

import()

import命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行

importexport命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)

否则会报句法错误,而不是执行时错误

优点:有利于编译器提高效率。

缺点:导致无法在运行时加载模块,在语法上,条件加载就不可能实现

如果import命令要取代 Node 的require方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能。

解决办法

引入import()函数,支持动态加载模块。

import(specifier):import函数的参数specifier,指定所要加载的模块的位置。import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。

  • import()返回一个 Promise 对象

  • import()函数可以用在任何地方

    不仅仅是模块,非模块的脚本也可以使用

  • 它是运行时执行

    什么时候运行到这一句,就会加载指定的模块

  • import()函数与所加载的模块没有静态连接关系

    import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

import()使用场合

  • 按需加载:import()可以在需要的时候,再加载某个模块。

    import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。

    button.addEventListener('click', event => {
    import('./dialogBox.js')
    .then(dialogBox => {
    dialogBox.open();
    })
    .catch(error => {
    /* Error handling */
    })
    });
    
  • 条件加载:import()可以放在if代码块,根据不同的情况,加载不同的模块。

  • 动态的模块路径:import()允许模块路径动态生成。

module的加载实现

浏览器加载

传统方法:HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本。

缺点:

浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。

如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞

解决办法:

浏览器允许脚本异步加载,下面就是两种异步加载的语法。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

defer中指的“页面的正常渲染结束”指得是:DOM 结构完全生成,以及其他脚本执行完成

浏览器加载ES6模块

加载规则:

也使用<script>标签,但是要加入type="module"属性。

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

浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

一旦使用了async属性,

node加载ES6模块

JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。

服务器端的模块加载机制:CommonJS 和 ES6的module

CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容

语法上:

CommonJS 模块使用require()module.exports

ES6 模块使用importexport

要想ES6的module在node中使用,就必须为ES6中的module和CommonJS规定好各自的加载方案

  • Node.js 要求 ES6 模块采用.mjs后缀文件名

    只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名

    如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。

    {
    "type": "module"
    }
    
  • 如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs

    如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。

总结:

>`.mjs`文件总是以 ES6 模块加载,`.cjs`文件总是以 CommonJS 模块加载,`.js`文件的加载取决于`package.json`里面`type`字段的设置。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值