【模块化开发】之 模块化概述

项目说明

模块化开发,是当下最重要的前端开发范式之一。模块化只是一个思想、一个理论。

笔记来源:拉勾教育 大前端高薪训练营
阅读建议:内容较多,建议通过左侧导航栏进行阅读

模块化过程

早期模块化完全依靠约定。

文件划分方式

缺点:

  • 1,污染全局作用域;
  • 2,命名冲突问题;
  • 3,无法管理模块依赖关系。

命名空间方式

命名空间方式,将所需变量包裹进全局对象内。

IIFE

IIFE,自执行函数,将变量设为私有成员。

模块化规范

模块化规范,是指 模块化标准 + 模块加载器。

CommonJs 规范

CommonJs 是针对 NodeJs 的规范,是以同步模式加载模块,约定如下:

  • 1,一个文件就是一个模块;
  • 2,每个模块都有单独的作用域;
  • 3,通过 module.exports 导出成员;
  • 4,通过 require 函数载入模块。

AMD + Require.js

AMD(Asynchronous Module Definition),异步的模块定义规范,是针对浏览器的规范,它是通过 Require.js 进行实现的,Require.js 是一个非常强大的模块加载器。

缺点

  • 1,AMD 使用起来相对复杂;
  • 2,模块 JS 文件请求频繁

Require.js API

define()

每一个模块都要通过 define() 函数进行定义。

传递三个参数

  • 第一个参数,是一个字符串,表示模块名,方便后期通过模块名进行使用;

  • 第二个参数,是一个数组,用来声明这个模块的一些依赖项,此参数按需添加,可有可无;

  • 第三个参数,是一个函数,函数的参数与前面数组中的依赖项一一对应,每一项分别为依赖项导出的成员,用来为当前模块提供私有空间,通过 return 向外部导出成员

    语法示例如下:

      define('module_name', [], function() { return ... })
    
require()

require() 函数,用来载入一个模块,内部会自动创建script标签,执行相应的代码。

传入两个参数

  • 第一个参数,是一个数组,表示所引入模块的组合

  • 第二个参数,是一个函数,函数的参数与前面数组中的模块一一对应,可以使用 模块名.xx 的方式访问模块中导出的成员

    语法示例如下:

      require(['module_name', ...], function(module_name){
         // module_name.xx 
      })
    

CMD + Sea.js

CMD(Common Module Definition),通用的模块定义规范,类似 CommonJs 规范。Sea.js 是由淘宝推出的库,遵循CMD规范。

ES Modules

ES Modules 规范 是在 ECMAScript2015(ES6)中提出的 Module 模块标准。通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码。

基本特性

  • 1,ESM 自动采用严格模式,忽略 ‘use strict’

      <script type="module">
          console.log(this); // undefined this在严格模式下,为 undefined
      </script>
    
  • 2,每个 ES Module 都是运行在单独的私有作用域中

      <script type="module">
          var foo = 100   // 不会造成全局作用域污染问题
          console.log(foo) // 100
      </script>
      <script type="module">
          console.log(foo) // Uncaught ReferenceError: foo is not defined
      </script>
    
  • 3,ESM 是通过 CORS 的跨域请求方式请求外部 JS 模块的

      <script type="module" src="https://umpkg.com/jquery@3.4.1/dist/jquery.min.js"> 		
      	// 需要服务端支持 CORS ,否则会出现跨域问题
      </script>
    
  • 4,ESM 的 script 标签会延迟执行脚本,相当于 script 的 defer 属性

      <script type="module" src="./demo.js">
          // 等待网页的渲染过后,再去执行脚本,不会阻碍页面的显示
      </script>
      <p>需要显示的内容</p>
    

导入和导出

导出 (export)

export 是在模块内去对外暴露接口。

导出语法

  • 1,直接成员导出,变量、函数、类等都可以导出。

    导出方式如下:

      export var name = 'foo module'   // 导出变量
      
      export function hello () { }     // 导出函数
      
      export class Person { }          // 导出类  
    
  • 2,成员集中导出,可以更加直观的看到向外部导出了哪些成员

    导出方式如下:

      var name = 'foo module'
      function hello () { }
      class Person { }
      
      export { name, hello, Person }
    
  • 3,别名导出,使用 as 进行重命名

    导出方式如下:

      var name = 'foo module'
      function hello () { }
      class Person { }
      
      export { name as fooName, hello as fooHello, Person as fooPerson }
    
  • 4,默认导出,设置某一个成员的别名是 default

    导出方式如下:

      var name = 'foo module'
      
      export { name as default }
      // <==> 推荐下方书写方式
      export default name
    
导入 (import)

import 是在模块内导入其他模块所提供的接口。

导入语法

对应上面的导出语法。

  • 1,对应 直接成员导出 和 成员集中导出

    导入方式如下:

      // .js 不可以省略,完整路径
      import { name, hello, Person } from 'module_path' 
    
  • 2,对应 别名导出

    导入方式如下(app.js):

      import { fooName, fooHello, fooPerson } from 'module_path'
    
  • 3,对应 默认导出

    导入方式如下(app.js):

      import { default as name }  from 'module_path' // default 是关键字
      // 简写为
      import name from 'module_path'
    
  • 4,导入模块时,模块路径的三种写法

    模块路径如下(app.js):

      // ./ 不可以省略,相对路径
      import { name } from './module.js'
      
      // / 绝对路径
      import { name } from '/04-import/module.js'
      
      // 完整 url 路径
      import { name } from 'htto://localhost:3000//04-import/module.js'
    
  • 5,加载模块,但不提取模块内的成员,一般用于导入一些不需要外部控制的子功能模块

    导入示例如下(app.js):

      import {} from './module.js'
      // 简写为
      import './module.js'
    
  • 6,提取模块导出的所有成员, 使用 as 将所有成员作为一个对象的属性

    导入示例如下(app.js):

      import * as mod from './module.js'
      console.log(mod.name, mod.age);
    
  • 7,动态导入模块, 返回一个 Promise对象

    导入示例如下(app.js):

      import('./module.js').then(function (module) {
          console.log(module);
      })
    
  • 8,导出时,同时导出命名成员和默认成员,如何导入

    导入示例如下(app.js):

      import { name, age, default as d } from './module.js'
      // 简写为, 默认成员的名字可以随意命名
      import d, { name, age } from './module.js'
    
导出导入成员

当前 module.js 模块的导出成员,将直接作为 app.js 模块的导出成员使用,一般用于集中导出分散的子模块成员

  • 1,index.js ,集中导出成员文件

      export { Button } from './button.js'
      
      // 默认成员的导出,必须重命名,以别名的形式导出
      export { default as Avatar} from './avatar.js'
    
  • 2,button.js ,分散子模块,导出 Button 成员

      export var Button = 'Button Component'
    
  • 3,avatar.js ,分散子模块,导出 Avatar 默认成员

      var  Avatar  = 'Avatar Component'
      export default Avatar
    
注意事项
  • 1,export 单独使用时, {} 是固定语法,导出的不是对象字面量

    代码示例如下:

      var name = 'jack'
      var age = 18
      
      export { name, age } 
    

    导入模块,{} 固定语法,不是对象的解构

    代码示例如下:

      import { name, age } from './module.js'
    
  • 2,export default 组合使用时,{} 代表导出的是对象字面量

    代码示例如下:

      var name = 'jack'
      var age = 18
      
      export default { name, age }
    

    导入模块,不可以使用 {} 写法

    代码示例如下:

      // module_obj 自定义名字,最好和模块名保持一致
      import module_obj from './module.js' 
      
      // 访问导出的成员
      console.log(module_obj.name, module_obj.age)
    
  • 3,export 导出的是值的内存地址

    代码示例如下(module.js):

      var name = 'jack'
      var age = 18
      
      export { name, age } // 导出的是值的内存地址
      
      setTimeout(function () {
          name = 'ben'
      }, 1000)
    

    代码示例如下(app.js):

      import { name, age } from './module.js'
      
      console.log(name, age);  // jack 18
      
      setTimeout(function () {
          console.log(name, age); // ben 18
      }, 1500)
    
  • 4,导出的值是只读的,无法在模块外部修改成员

    代码示例如下(app.js):

      import { name, age } from './module.js'
      
      name = 'tom' // Uncaught TypeError: Assignment to constant variable.
    

Polyfill

解决浏览器的兼容性问题。

  • 在 HTML 页面,手动引入 browser-es-module-loader

    js 文件查找地址,如下:browser-es-module-loader

    代码示例如下(app.js):

      <!-- 有的IE版本不支持 Promise,因此需要引入 Promise Polyfill -->
      <script nomodule src="https://unpkg.com/promise-polyfill@8.2.0/dist/polyfill.min.js"></script>
      <!-- babel 即时运行在浏览器上的版本 -->
      <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
      <!-- ES Modules Loader, 读取代码,将不识别的特性交给 babel 进行转换 -->
      <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
    

nomodule 属性,表示只在不支持 ES Modules 的浏览器中运行,避免支持的浏览器多次运行 。

不建议在生产版本中使用,影响效率。

in Node.js

  • 测试 ES Modules 在 node.js 环境的运行情况

    文件后缀名,设置为 xxx.mjs

    添加参数 – experimental-modules , 启动 ES Modules 的实验特性

      $ node --experimental-modules xxx.mjs # node 8.5+
    

    1)内置模块兼容了 ESM 的提取成员方式

    引入方式如下(index.mjs):

      // 方式一
      import fs from 'fs'
      fs.writeFileSync('./foo.txt', 'es module working') // 代码执行成功
      
      // 方式二
      import { writeFileSync } from 'fs'
      writeFileSync('./bar.txt', 'es module working~') // 代码执行成功
    

    2)第三方模块都是导出默认成员,不支持使用 {} 语法导入成员

    引入方式如下(index.mjs):

      import _ from 'lodash'
      console.log(_.camelCase('ES Module')); // 代码执行成功
      
      // import { camelCase } from 'lodash'
      // console.log(camelCase('ES Module')); // SyntaxError:...
    
  • ES Modules 与 CommonJS 交互

    1)ES Modules 中可以导入 CommonJS 模块

    代码示例如下(commonJs.js):

      module.exports = {
          foo: 'commonjs'
      }
      // exports 是 module.exports 的别名,二者是等价的
      exports.foo = 'commonjs'
    

    代码示例如下(es-modules.mjs):

      import mod from './common.js'
      console.log(mod);
    

    2)CommonJS 中不能导入 ES Modules 模块

    代码示例如下(commonJs.js):

      const mod = require('./es-module.mjs')
      console.log(mod); // Error [ERR_REQUIRE_ESM]: Must use import to load ES Module
    

    代码示例如下(es-modules.mjs):

      export const foo = 'es module export value'
    

    3)CommonJS 始终只会导出一个默认成员,不能直接提取成员

    代码示例如下(commonJs.js):

      exports.foo = 'commonjs'
    

    代码示例如下(es-modules.mjs):

      import { foo } from './common.js' // SyntaxError:...
      console.log(foo);
    

    4)注意 import 不是解构导出对象

  • ES Modules 与 CommonJs 的差异

    ES Modules 中没有 CommonJs 中的那些模块全局成员了,如:

    require(加载模块函数);
    module(模块对象);
    exports(导出对象别名);
    __filename(当前文件的绝对路径);
    __dirname(当前文件所在目录)

  • node.js 新版本进一步支持

    为了使项目中所有的js文件,都可以使用 ES Modules ,在 package.json 中添加属性 type 进行设置。

    代码示例如下(package.json):

      {
          type: 'module'
      }
    

    此时,无需再将 .js 改为 .mjs。但是要将 CommonJS 的 .js 改为 .cjs,保证兼容 CommonJS。

  • 低版本 Node.js,使用 Babel 进行兼容

    1)安装 babel 相关依赖模块

      $ yarn add @babel/node @babel/core @babel/preset-env --dev
    

    2)运行 ES Modules 的 JS 文件,需要添加特性转换的预设参数

      $ yarn babel-node index.js --presets=@babel/preset-env
    

    3)@babel/preset-env 只是一个插件集合,真正起作用的是 @babel/plugin-transform-modules-commonjs 插件

      $ yarn add @babel/plugin-transform-modules-commonjs --dev
    

    4)若不想在执行命令时添加参数,可以配置 .babelrc 文件,这是 babel 的配置文件

    代码示例如下(.babelrc):

      {
          "presets": ["@babel/preset-env"],
          "plugins": ["@babel/plugin-transform-modules-commonjs"]
      }
    

    5)运行命令,进行测试

      $ yarn babel-node index.js
    

总结

模块化的最佳实践:NodeJs 环境遵循 CommonJs,浏览器环境遵循 ES Modules 规范。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值