1

模块化

从本文你将了解到

  • 什么是模块化
  • 模块化的进化史
  • 当下常用的模块化规范 CommonJS,ES Module
  • ES Module特性
  • ES Module使用in Browsers , Node.js
  • ES Modules in Node.js - 与 CommonJS 交互
  • ES Modules in Node.js - 与 CommonJS 差异

模块化

  • 前端开发范式 是一种思想
  • 根据功能不同将代码划分不同模块,从而提高开发效率,降低维护成本

模块化的进化史

stage-1文件划分方式

<body>
  <h1>模块化演变(第一阶段)</h1>
  <h2>基于文件的划分模块的方式</h2>
  <p>
    具体做法就是将每个功能及其相关状态数据各自单独放到不同的文件中,
    约定每个文件就是一个独立的模块,
    使用某个模块就是将这个模块引入到页面中,然后直接调用模块中的成员(变量 / 函数)
  </p>
  <p>
    缺点十分明显:
    所有模块都直接在全局工作,没有私有空间,所有成员都可以在模块外部被访问或者修改,
    而且模块一段多了过后,容易产生命名冲突,
    另外无法管理模块与模块之间的依赖关系
  </p>
  <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')
}

stage-2命名空间方式

<body>
  <h1>模块化演变(第二阶段)</h1>
  <h2>每个模块只暴露一个全局对象,所有模块成员都挂载到这个对象中</h2>
  <p>
    具体做法就是在第一阶段的基础上,通过将每个模块「包裹」为一个全局对象的形式实现,
    有点类似于为模块内的成员添加了「命名空间」的感觉。
  </p>
  <p>
    通过「命名空间」减小了命名冲突的可能,
    但是同样没有私有空间,所有模块成员也可以在模块外部被访问或者修改,
    而且也无法管理模块之间的依赖关系。
  </p>
  <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')
  }
}

stage-3 IIFE方式

<body>
  <h1>模块化演变(第三阶段)</h1>
  <h2>使用立即执行函数表达式(IIFE:Immediately-Invoked Function Expression)为模块提供私有空间</h2>
  <p>
    具体做法就是将每个模块成员都放在一个函数提供的私有作用域中,
    对于需要暴露给外部的成员,通过挂在到全局对象上的方式实现
  </p>
  <p>
    有了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问。
    可以通过IIFE向模块内部传参
  </p>
  <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')
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleA = {
    method1: method1,
    method2: method2
  }
})()

stage-4 AMD规范

<body>
  <h1>模块化规范的出现</h1>
  <h2>Require.js 提供了 AMD 模块化规范,以及一个自动化模块加载器</h2>
  <script src="lib/require.js" data-main="main"></script>
</body>
module1.js

// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
// define API 参数:模块名,依赖项(可选参数),函数(函数参数与依赖项对应)为当前模块提供私有空间
define('module1', ['jquery', './module2'], function ($, module2) {
  return {  // 私有空间向外导出成员通过return
    start: function () {
      $('body').animate({ margin: '200px' })
      module2()
    }
  }
})
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>')
  }
})

当前模块化形势

image.png

模块化规范 CommonJS,ES Module

  • CommonJS规范

    • nodejs提出的一套标准
    • nodejs代码必须遵循CommonJS规范
    • 一个文件就是一个模块
    • 每个模块都有单独的作用域
    • 通过module.exports导出成员
    • 通过require函数载入模块
  • CommonJS是以同步模式加载模块
  • node端: node的执行机制是在启动时加载模块。执行过程中不需要加载,只会使用到模块,因此node环境下使用commonjs没有问题
  • 浏览器端: 必然导致效率低下,每次页面加载都会导致大量同步模式请求出现,因此早期前端模块化中并没有使用commonjs规范,而是根据浏览器特点设计了专门用于浏览器端的规范AMD(Asynchronous Module Definition),和库Require.js(实现了AMD规范),Require也是个非常强大的模块加载器
  • 目前绝大多数第三方库都支持AMD规范,但使用起来相对复杂,另外如果项目模块划分过细,那么同一个页面对js文件的请求次数就会特别多,从而导致页面效率低下
  • 同期出现了淘宝Sea.js库+CMD规范,当然后来也被Require.js兼容了

ESModule特性

  • 安装 yarn global add serve
  • 执行 serve ./src/xxx 查看文件
  • 给script 添加type=module 属性,就可以以 ESModule 的标准执行其中的 JS代码

    <script type="module">
    console.log('this is es module');
    </script>

    相比于普通script标签,esm的特点

  • 自动采用严格模式,忽略 'use strict'

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

    <script type="module">
    var foo = 100;
    console.log(foo);
    </script>
    
    <script type="module">
    console.log(foo); //报错
    </script>
  • ESM 是通过 CORS 的方式请求外部 js模块的

意味着如果请求的js不在同源目录下。请求的服务端地址,它在响应的响应头中需要提供有效的CORS标头,也就是请求地址需要支持CORS

//请求成功
<script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>

//请求失败
<script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> 
 
  1. ESM 的script标签会延迟执行脚本 等同于defer属性

默认按执行顺序,script立即执行,页面的渲染会等待脚本执行后再继续渲染

添加type后,执行顺序并不等于引入顺序

<script type="module">
  alert("Hello")
</script>
<p>需要显示的内容</p>

ESM 导入和导出

每个模块拥有私有作用域,因此外部无法访问

通过export ,import进行模块暴露和载入

export,import

  • export可以导出变量,函数,类

    export var foo = {}
    export var fn = function(){}
    export class Person{}
  • 单独使用export,更直观描述导出成员

    var foo = {}
    var fn = function(){}
    
    export {foo,fn}
  • 导出重命名

    var foo = {}
    var fn = function(){}
    
    export {foo as baz,fn} //引入时通过baz引入
  • 重命名为default,默认导出

    var foo = {}
    var fn = function(){}
    
    export {foo as default,fn} //默认导出必须重命名 import {default as baz} from ''
  • 默认导出写法2

    export default {foo, fn} //接收对象成员 import mod from '' | mod.fn使用

    导出注意事项

  • export {}不是字面量对象,import引入的也不是解构,都是固定语法,因此 export foo 也是不被允许的,要使用export var foo,而export default {} 默认导出,导出的是字面量对象

    # a.js
    var foo = {}
    var fn = function(){}
    
    export {foo,fn}
    
    ...
    
    # b.js
    import {foo,fn} from ''
  • 通过export导出的不是值,而是值的地址,外部取值会受到内部值修改的影响

    # a.js
    export {foo,fn}
    setTimeout(function(){foo="ben"},1000)
    
    ...
    # b.js
    import {foo,fn} from ''
    console.log(foo)
    setTimeout(function(){
    console.log(foo)
    },1500)
  • 外部导入的成员属于只读成员(常量),无法修改

    #b.js
    import {foo,fn} from ''
    console.log(foo)
    foo = "bau" //报错

    导入注意事项

  • import xx from './module.js' 路径名称必须完整不能省略,省略会报错
  • import xx from '/xx/module.js' 导入内部文件使用相对路径或者绝对路径,'./'不能省略,否则会认为是加载模块
  • import xx from 'http://localhost:3000/xx/module.js' 可以使用完整的url访问
  • import {} from './module.js' 只会执行模块,不会提取成员
  • import './module.js' 上面可以简写成 import '模块路径',导入一些不需要控制的子功能模块非常好用
  • import * as mod from './module.js' 当导出内容很多,用* as mod 提取出来,通过mod.xx使用成员
  • import() 动态导入模块
使用动态导入模块解决
//无法用变量
var modulePath = "./module.js"
import { name } from modulePath;

//无法条件判断,只能放在最顶层作用域
if(true){
  import {name} from './module.js'
}

//全局的import函数,用来动态导入模块,返回promise
import('./module.js').then(function(module){
  console.log(module); //导入的模块对象通过参数拿到
})
  1. import title,{foo,fn} from './module.js' 对于导出的默认成员可以将其提取出来,也可以写成 import {foo,fn, default as title} from './module.js'

导入导出成员

export {name, age} from '' 所有导入成员作为当前模块的导出成员,那么导入的成员无法访问

# index.js 临时文件
import { Button } from './xxx'
import { Avatar } from './xxx1'
export { Button,Avatar }

//改写成
export { Button } from './xxx'
export { Avatar } from './xxx1'

ESM in Browsers

ie下用Polyfill,添加CDN加速,解决esm用不了的问题

Polyfill 是一块代码(通常是 Web 上的 JavaScript),用来为旧浏览器提供它没有原生支持的较新的功能,可以理解为是一种兼容方案,参考地址

配置运行环境
安装yarn global add browser-sync

使用browser-sync . --files **/*.js

或者vscode 安装个live Server启用

// 通过unpkg网站提供的cdn服务拿到js文件
// https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/ 
// https://unpkg.com/browse/promise-polyfill@8.2.0/dist/
<body>
  <!-- nomodule布尔值 不支持模块化的浏览器会执行,不然chrome执行两次,只适合开发阶段 -->
  <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>

ESM in Node.js

nodejs中使用ESM需要修改文件名从 .js => .mjs

node启动文件使用 node --experimental-modules xxx.mjs

案例

# module.mjs

export const foo = 'hello'
export const bar = 'world'
# index.mjs

import { foo, bar } from "./module.mjs";

console.log(foo,bar);

//node 中使用esm 需要修改文件名为 .mjs
/**
 * node --experimental-modules index.mjs
    (node:9696) ExperimentalWarning: The ESM module loader is experimental.
    hello world
 * 
*/

//通过ESM载入原生模块
import fs from "fs";
fs.writeFileSync('./foo.txt','es module working')

//系统内置模块,可以通过这种方式导入,内置模块兼容了ESM的提取成员方式
import { writeFileSync } from "fs";
writeFileSync('./bar.txt','es module working~ bar')


//通过ESM载入第三方模块
import _ from "lodash" //需要yarn add loadsh
console.log(_.camelCase('ES MODULE'))

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

ESM 与 CommonJS交互

# commonjs.js

//CommonJS模块始终只会导出一个默认成员
//这也就意味着只能通过import载入默认成员方式引入

module.exports = {
  foo: 'commonjs exports value foo'
}

exports.baz = 'commonjs exports value baz'


console.log("---------------------------------------");

// node --experimental-modules commonjs.js
//不能在 CommonJS模块中通过require 载入 ESM
const mod = require('./es-module.mjs')
console.log(mod);//报错
# es-module.mjs

// node --experimental-modules es-module.mjs

//ESM中可以导入 Commonjs模块
import mod from "./commonjs.js";
console.log(mod); //ok

//不能直接提取成员,注意import不是解构导出对象
import { baz } from "./commonjs.js";
console.log(baz); //报错
import baz from './commonjs.js' //ok


console.log("---------------------------------------------");

export const foo = 'es module export value'

总结

  • ES Modules中可以导入CommonJS模块
  • CommonJS中不能导入ES Modules模块
  • CommonJS始终只会导出一个默认成员
  • 注意import 不是解构导出对象

ESM 与 CommonJS差异

运行环境nodemon --experimental-modules xxx.mjs

在commonjs中的对象在ESM中会报错,解决办法是用其他方法代替,报错方法有

# cjs.js

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

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

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

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

//当前文件所在目录
console.log(__dirname);
# esm.mjs

// 前三个可以使用export import代替
// 后两个
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url)
console.log(__filename); //d:\Users\admin\Desktop\part2-2\differences\esm.mjs

import { dirname } from 'path'
const __dirname = dirname(__filename)
console.log(__dirname); //d:\Users\admin\Desktop\part2-2\differences

ESM in Nodejs 进一步支持

将package.json中添加type = 'module',这样不用修改.js为.mjs

# package.json

{
  "type":"module"
}

// 运行命令`nodemon --experimental-modules xxx.js`

如果添加了type的项目中还想使用CommonJS

# common.js

const path = require('path')
console.log(path.join(__dirname,'foo'))

会报错,解决办法将commonjs文件修改为 common.cjs

ESM in Nodejs Babel兼容方案

早期node版本8.0.0,通过babel来让node运行esm

安装babel-mode yarn add @babel/node @babel/core @babel/preset-env --dev

babel是基于插件机制去实现的,核心core和preset-env并不会转换代码,具体转换特性通过插件

image.png

一个插件转化一个特性,preset-env是插件集合,这个集合包含了js标准中所有新特性

实际帮我们转换的是集合里的插件

可以通过yarn babel-node index.js --presets=@babel/preset-env

或者放入配置文件.babelrc json格式文件

{
  "presets":["@babel/preset-env"]
}

执行yarn babel-node index.js

或者移除集合使用插件

通过 yarn remove @babel/preset-env

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

{
  "plugins":[
    "@babel/plugin-tranform-modules-commonjs"
  ]
}

mcgee0731
60 声望4 粉丝

不会做饭的程序猿不是一个好厨子


下一篇 »
webpack笔记

引用和评论

0 条评论