IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 前端模块化开发知识梳理 -> 正文阅读

[JavaScript知识库]前端模块化开发知识梳理


前言

前端的模块化开发经过长时间的演变现在已经趋于稳定,但是模块化的演变过程对于前端开发者的学习来说也是十分重要的,所以我总结了模块化的演变过程和当下最流行的模块化方案的基本使用,希望大家能有所收获


模块化演变过程

  1. 基于文件的划分模块的方式

    <body>
      <script src="module-a.js"></script>
      <script src="module-b.js"></script>
      <script>
        // 命名冲突
        method1()
        // 模块成员可以被修改
        name = 'foo'
      </script>
    </body>
    
    // module-a.js
    var name = 'module-a'
    
    function method1 () {
      console.log(name + '#method1')
    }
    
    function method2 () {
      console.log(name + '#method2')
    }
    

    命名冲突,污染全局作用域,模块成员可以被修改,无法管理模块依赖关系;

  2. 命名空间方式

    每个模块只暴露一个全局对象,所有模块成员都挂载到这个对象中

    <body>
      <script src="module-a.js"></script>
      <script src="module-b.js"></script>
      <script>
        moduleA.method1()
        moduleB.method1()
        // 模块成员可以被修改
        moduleA.name = 'foo'
      </script>
    </body>
    
    // module-a.js
    
    var moduleA = {
      name: 'module-a',
    
      method1: function () {
        console.log(this.name + '#method1')
      },
    
      method2: function () {
        console.log(this.name + '#method2')
      }
    }
    

    通过命名空间的方式会减小命名冲突的可能,但是模块成员仍然可以被修改,仍然无法管理模块依赖关系;

  3. IIFE

    具体做法就是将每个模块成员都放在一个函数提供的私有作用域中。

    有了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问。

    对于管理模块依赖关系,可以利用自执行函数的参数声明去传入依赖的模块,这样可以很清晰的看见,当前模块依赖了哪些模块。

    <body>
      <!-- 引入jquery,这样在其他IIFE模块可以直接传入jQuery -->
      <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
      <script src="module-a.js"></script>
      <script src="module-b.js"></script>
      <script>
        moduleA.method1()
        moduleB.method1()
        // 模块私有成员无法访问
          
        console.log(moduleA.name) // => undefined
      </script>
    </body>
    
    // module-a.js
    ;(function ($) {
      var name = 'module-a'
      
      function method1 () {
        console.log(name + '#method1')
        $('body').animate({ margin: '200px' })
      }
      
      function method2 () {
        console.log(name + '#method2')
      }
    
      window.moduleA = {
        method1: method1,
        method2: method2
      }
    })(jQuery)
    

模块化规范的出现

commonJS是以同步模块加载模块,node是在启动的时候加载模块,而在使用的时候不需要去加载模块,所以在node环境中不会有问题,但是在浏览器端必然会引起效率低下,每次页面加载都会导致大量的同步请求出现;

所以在浏览器端,并没有选择commonJS规范,而是专门为浏览器端重新设计了规范AMD(异步的模块定义规范),同期推出了Require.js,它实现AMD规范,同时它还是强大的模块加载器;

<!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>Modular evolution stage 5</title>
</head>
<body>
  <h1>模块化规范的出现</h1>
  <h2>Require.js 提供了 AMD 模块化规范,以及一个自动化模块加载器</h2>
  <script src="lib/require.js" data-main="main"></script>
</body>
</html>
// mian.js
// AMD规范,相当于入口文件
require.config({
  paths: {
    // 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
    // 所以使用时必须通过 'jquery' 这个名称获取这个模块
    // 但是 jQuery.js 并不一定在同级目录下,所以需要指定路径
    jquery: './lib/jquery'
  }
})

require(['./modules/module1', 'jquery'], (module1) => {
  console.log(module1)
  module1.start()
})
// module1.js
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
define(['jquery', './module2'], function ($, module2) {
  return {
    start: function () {
      $('body').animate({ margin: '200px' })
      module2()
    }
  }
})

但是AMD使用起来相对复杂,模块JS文件请求频繁,一个模块就会请求一次,个人认为AMD只能算是前端模块化发展的一步,是一种妥协的方案,同期淘宝推出了Sea.js + CMD,设计的想法是希望能与CommonJS规范类似,减轻开发者的负担,算是一个重复的轮子;

// module2.js
// 兼容 CMD 规范(类似 CommonJS 规范)
define(function (require, exports, module) {
	// 通过 require 引入依赖
  var $ = require('jquery')
  // 通过 exports 或者 module.exports 对外暴露成员
  module.exports = function () {
    console.log('module 2~')
    $('body').append('<p>module2</p>')
  }
})

模块化的最佳实践

CommonJS in Node.js

ES Modules in Browers

ES Modules基本特性

  • 通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了
  • ESM 自动采用严格模式,忽略 ‘use strict’
  • 每个 ES Module 都是运行在单独的私有作用域中
  • ESM 是通过 CORS 的方式请求外部 JS 模块的,请求的地址必须支持CORS
  • ESM 的 script 标签会延迟执行脚本,等同于defer属性,异步加载,加载完成以后等待DOM树构建好,构建完成以后在执行;

ES Modules 导出

// 导出
var name = 'foo module'

function hello () {
  console.log('hello')
}

class Person {}

export { name as newName, hello as default, Person }
// export default hello

// 导入

import { newName, default as hello, Person } from './module.js'
console.log(name, hello, Person)

// import hello from '...'

ES Modules导入导出的注意事项

  • export{}的语法与对象的简写 var obj = { name, age }不相同

  • 导入成员并不是复制一个副本,而是直接导入模块成员的引用地址,也就是说 import 得到的变量与 export 导入的变量在内存中是同一块空间,一旦模块中成员修改了,这里也会同时修改

  • 导入模块成员变量是只读的,name = ‘tom’ // 报错

  • 但是需要注意如果导入的是一个对象,对象的属性读写不受影响,会修改原模块的值,name.xxx = ‘xxx’ // 正常

ES Module导入用法

  • 路径的.js后缀不可以省略,使用webpack可以省略 // import { name } from ‘./module.js’

  • 路径的index.js不可以省略,使用webpack可以省略 // import { lowercase } from ‘./utils/index.js’

  • 可以使用绝对路径,相对路径,URL // import { name } from ‘http://localhost:3000/04-import/module.js’

  • 当导入的成员比较多的时候,// import * as mod from ‘./module.js’

  • 不可以导入的路径为一个变量,也不可以出现在if判断中,如果有这样的需求,可以使用全局Import函数动态导入

    import('./module.js').then(function (module) {
      	console.log(module.default) // 默认导出的时候在default上
      	console.log(module.name) // 导出多个的时候
    })
    
  • 可以同时导入默认成员和其他成员 // import abc, { name, age } from ‘./module.js’

ES Module导入导出对象

当一个js要导入的模块特别多的时候,导入的语句会越来越多,那么可以用一个文件来集中管理,只需要从这个文件来导入就行了

// 导出
export { foo, bar } from './module.js'

export { default as Button } from './button.js'
export { Avatar } from './avatar.js'

// 导入
import { Button, Avatar } from './components/index.js'

这样做有它的好处,但是缺点是引入了所有的模块,但是我们使用的不一定是所有的模块,如果有副作用那么文件打包的时候就不会被tree shaking掉;

ES Module浏览器环境 Polyfill

低版本的IE浏览器不兼容ES Module

script有个nomodule属性,当不支持ES Module时运行

<body>
    <script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
    <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
    <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
    <script type="module">
        import { foo } from './module.js'
        console.log(foo)
    </script>
</body>

上述只限于在开发环境下使用,对于线上环境应该将这些兼容性代码提前编译出来;

ES Module在Node.js下支持情况

要在Node.js中使用ES Module规范有如下步骤

  1. 将.js文件后缀名改成.mjs后缀名

    注意: 也可以通过在package.json中添加"type":“module”,这样就不需要添加.mjs的后缀名了,但是如果这样加的话,对于使用CommonJS规范的文件需要将后缀名改为.cjs(好无语!)

  2. 命令行运行文件时添加参数node --exprimental-modules index.mjs(实测不加也没关系0-0)

import { foo, bar } from './module.mjs'

console.log(foo, bar)

// 此时我们也可以通过 esm 加载内置模块了
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')

// 也可以直接提取模块内的成员,内置模块兼容了 ESM 的提取成员方式
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')

// 对于第三方的 NPM 模块也可以通过 esm 加载
import _ from 'lodash'
_.camelCase('ES Module')

// 不支持,因为第三方模块都是导出默认成员
// import { camelCase } from 'lodash'
// console.log(camelCase('ES Module'))

ES Module与CommonJS交互

  • ES Module 中可以导入 CommonJS 模块

  • CommonJS 中不能导入ES Module 模块

  • CommonJS始终只会导出一个默认对象

    注意: 实测有个特殊情况,当CommonJS规范文件(只使用一个)exports.foo = xxx导出时,在ES Module规范文件中可以使用import { foo } from './commonjs.js’获取到导出的foo

    // common.js
    exports.foo = '111'
    
    // es-module.mjs
    import name, { foo } from './commonjs.js'
    console.log(foo, name) // 111, { foo: 111 }
    
    /*  但是当
    **	exports.foo1 = '111'
    **	exports.foo2 = '222'
    **
    **	import { foo1, foo2 } from './commonjs.js'不可用
    */ 
    
  • 注意import不是结构导出对象

ES Module与CommonJS差异

// CommonJS提供的五个参数

// 加载模块函数
console.log(require)

// 模块对象
console.log(module)

// 导出对象别名
console.log(exports)

// 当前文件的绝对路径
console.log(__filename)

// 当前文件所在目录
console.log(__dirname)

__filename 和 __dirname 通过 import 对象的 meta 属性获取

// require, module, exports 自然是通过 import 和 export 代替

// __filename 和 __dirname 通过 import 对象的 meta 属性获取
// const currentUrl = import.meta.url
// console.log(currentUrl)

// 通过 url 模块的 fileURLToPath 方法转换为路径
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)
console.log(import.meta.url)

__filename (绝对路径文件名): C:\Users\Administrator\Desktop\codes\es-module-in-node\03-differences\esm.mjs

import.meta.url: file:///C:/Users/Administrator/Desktop/codes/es-module-in-node/03-differences/esm.mjs

__dirname(绝对路径目录名): C:\Users\Administrator\Desktop\codes\es-module-in-node\03-differences

使用Babel兼容方案

当我们在低版本的node环境中运行ES Module时,有很多兼容性的问题,这个时候我们需要依靠babel;

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

  • presets其实就是一组plugin的集合
// .babelrc
{
  // "presets": ["@babel/preset-env"],
  "plugins": [
    "@babel/plugin-transform-modules-commonjs"
  ]
}

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

如果写了配置文件则后面的参数可以不需要

npx babel-node index.js

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-08 22:20:01  更:2022-03-08 22:20:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 9:44:10-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码