前端工程化(二)

模块化开发

--- 当下最重要的前端开发范式之一
所谓模块化,只是思想或者理论,不是具体的某个特定的实现

模块化的演变过程

  • 第一阶段:文件划分方式

    早起的模块化完全依赖约定

    • 缺点

      • 污染全局作用域

      • 命名冲突

      • 无法管理模块的依赖关系

    <!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 1</title>
      </head>
      <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>
    </html>
    
    // module a 相关状态数据和功能函数
    var name = 'module-a';
    
    function method1() {
      console.log(name + '#method1');
    }
    
    function method2() {
      console.log(name + '#method2');
    }
    
    // module a 相关状态数据和功能函数
    var name = 'module-a';
    
    function method1() {
      console.log(name + '#method1');
    }
    
    function method2() {
      console.log(name + '#method2');
    }
    
    // module b 相关状态数据和功能函数
    var name = 'module-b';
    
    function method1() {
      console.log(name + '#method1');
    }
    
    function method2() {
      console.log(name + '#method2');
    }
    
  • 第二阶段:命名空间方式

每个模块挂载到对象上

  • 缺点

    • 内部的所有成员任然可以被修改和访问

    • 无法管理模块的依赖关系

<!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 2</title>
  </head>
  <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>
</html>
// module a 相关状态数据和功能函数

var moduleA = {
  name: 'module-a',

  method1: function () {
    console.log(this.name + '#method1');
  },

  method2: function () {
    console.log(this.name + '#method2');
  },
};
// module b 相关状态数据和功能函数

var moduleB = {
  name: 'module-b',

  method1: function () {
    console.log(this.name + '#method1');
  },

  method2: function () {
    console.log(this.name + '#method2');
  },
};
  • 第三阶段:立即执行函数

每个模块放在一个立即执行函数中,将外部要用到的对象挂载到全局边梁上
优点:

  • 避免了大量的对象被挂载到全局,防止私有成员被访问

  • 可以通过参数传递依赖

缺点

  • 挂载的时候还是会有命名冲突
<!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 3</title>
  </head>
  <body>
    <h1>模块化演变(第三阶段)</h1>
    <h2>
      使用立即执行函数表达式(IIFE:Immediately-Invoked Function
      Expression)为模块提供私有空间
    </h2>
    <p>
      具体做法就是将每个模块成员都放在一个函数提供的私有作用域中,
      对于需要暴露给外部的成员,通过挂在到全局对象上的方式实现
    </p>
    <p>
      有了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问。
    </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>
</html>
// module a 相关状态数据和功能函数

(function () {
  var name = 'module-a';

  function method1() {
    console.log(name + '#method1');
  }

  function method2() {
    console.log(name + '#method2');
  }

  window.moduleA = {
    method1: method1,
    method2: method2,
  };
})();
// module b 相关状态数据和功能函数

(function () {
  var name = 'module-b';

  function method1() {
    console.log(name + '#method1');
  }

  function method2() {
    console.log(name + '#method2');
  }

  window.moduleB = {
    method1: method1,
    method2: method2,
  };
})();

模块化规范

历史

早期的模块化规范,Commonjs 规范不适合浏览器,在 node 中同步加载模块依赖。

- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过 module.exports 导出模块
- 通过 require 函数载入模块

于是在浏览器中又提出了 AMD(Asyncchronous Module Definition)

Require.js 实现了这个规范。目前大多数的对三方库支持 AMD 规范缺缺点:

  • 使用起来很复杂

  • 当我们模块划分很细的时候 js 文件就会请求的很频繁

AMD 规范是前端模块化演进道路上的一步,的历史长河中进了一步,是一种妥协的实现方式,不算是最终的姐解决方案。除此之外,同一时期,淘宝推出了 Sea.js 库,实现的是 CMD 标准,但是后面让 Require.js 兼容了

<!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>
require.config({
  paths: {
    // 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
    // 所以使用时必须通过 'jquery' 这个名称获取这个模块
    // 但是 jQuery.js 并不一定在同级目录下,所以需要指定路径
    jquery: './lib/jquery',
  },
});

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

最佳实践

随着技术的发展,JavaScript 标准逐渐完善,模块化被统一成了浏览器端的 ES Module,nodejs 中遵循 CommonJs 规范

<!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>ES Module - 模块的特性</title>
  </head>
  <body>
    <!-- 通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了 -->
    <script type="module">
      console.log('this is es module');
    </script>

    <!-- 1. ESM 自动采用严格模式,忽略 'use strict' -->
    <script type="module">
      console.log(this);
    </script>

    <!-- 2. 每个 ES Module 都是运行在单独的私有作用域中 -->
    <script type="module">
      var foo = 100;
      console.log(foo);
    </script>
    <script type="module">
      console.log(foo);
    </script>

    <!-- 3. ESM 是通过 CORS 的方式请求外部 JS 模块的 -->
    <!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->

    <!-- 4. ESM 的 script 标签会延迟执行脚本 等同于defer属性 -->
    <script type="module" src="demo.js"></script>
    <p>需要显示的内容</p>
  </body>
</html>

ES Module 核心功能

import export 注意

  • 默认导出的是字面量对象,非默认导出则不是,且必须为{}中的成员

  • 导入的语法中的{}为固定的语法,并非解构

  • 导出到外部的都是内存地址的引用,并非值的拷贝

  • 导出的成员都是只读的

<!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>ES Module 导出与导入 - 注意事项</title>
  </head>
  <body>
    <script type="module" src="app.js"></script>
  </body>
</html>
// CommonJS 中是先将模块整体导入为一个对象,然后从对象中结构出需要的成员
// const { name, age } = require('./module.js')

// ES Module 中 { } 是固定语法,就是直接提取模块导出成员
import { name, age } from './module.js';

console.log(name, age);

// 导入成员并不是复制一个副本,
// 而是直接导入模块成员的引用地址,
// 也就是说 import 得到的变量与 export 导入的变量在内存中是同一块空间。
// 一旦模块中成员修改了,这里也会同时修改,
setTimeout(function () {
  console.log(name, age);
}, 1500);

// 导入模块成员变量是只读的
// name = 'tom' // 报错

// 但是需要注意如果导入的是一个对象,对象的属性读写不受影响
// name.xxx = 'xxx' // 正常
var name = 'jack';
var age = 18;

// var obj = { name, age }

// export default { name, age }

// 这里的 `{ name, hello }` 不是一个对象字面量,
// 它只是语法上的规则而已
export { name, age };

// export name // 错误的用法

// export 'foo' // 同样错误的用法

setTimeout(function () {
  name = 'ben';
}, 1000);

import 额外注意

  • 导入模块的时候 import from 后的路径必须带文件后缀,不支持自动定位 index

  • 导入本地模块的时候 import from 后面的相对路径必须是'./' or '../'; 绝对路径支持从项目跟目录查找 以'/'开头; 或者完整的 url,不然会被按照导入三方模块处理

  • 加载这个模块并不提取任何成员

import {} from './module.js';
// 简写
import './module.js';
  • 全部导出成员
import * as mod from './module.js';
  • import from 后面不支持变量
var modulePath = './module.js'
import {name} from modulePath; // 报错
  • 块级作用域下不能使用 import
if (true) {
  import { name } from './module.js'; // 报错
}
  • 动态导入需要使用全局提供的 import 函数
import('./module.js').then(module => {
  console.log(module);
});
  • 直接导入导出
export { for, bar } from './module.js';
  • ES Module 浏览器环境 Polyfill
<!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>ES Module 浏览器环境 Polyfill</title>
  </head>
  <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>
</html>

注意:script 标签的 nomodule 属性可以在没有 ES Module 的环境中执行,动态编译,不推荐直接在生产环境使用

  • 在 node 中使用 ES module

    • 在 node 环境中运行 ES module,把 js 文件后缀改为'.mjs',在命令行中执行 node --experimental-modules index.mjs
    // 第一,将文件的扩展名由 .js 改为 .mjs;
    // 第二,启动时需要额外添加 `--experimental-modules` 参数;
    
    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.mjs
    import mod from './commonjs.js'
    console.log(mod) // 正常打印
    
    // commonJS 始终只会导出一个默认成员
    // commonjs.js
    module.export = {
      foo: 'commonjs exports value'
    }
    // 该语法为上面语法的简写
    export.foo = 'commonjs exports value'
    
  • 不能直接提取 commonjs 的成员,import 不是解构导出对象

import { foo } from './commonjs.js';
console.log(foo); // 报错
  • 不能在 CommonJS 模块中通过 require 载入 ES Module
// es-module.js
export const foo = 'es module export value';
const mod = require('./es-module.mjs');
console.log(mod); // 报错
  • commonjs 和 ES Module 在 node 中区别

common js

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

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

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

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

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

ES Module

// ESM 中没有模块全局成员了

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

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

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

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

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

// -------------

// 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);
  • 新版本 node 中进一步支持 ESModule
// 在 package.json 中设置type,默认以ES Module方式工作,不用在改扩展名了 .mjs=>.js
{
  "type": "module"
}
//  如果要继续支持commonjs规范,则可以把js文件后缀改为'.cjs'

  • 旧版本 node 中也可以通过 babel-node 支持 ES Module 特性
$ yarn add @babel/node @babel/core @babel/preset-env  -D
// or
$ yarn add @babel/node @babel/core @babel/plugin-transform-modules-commonjs  -D
$ yarn babel-node [文件名]
// .babelrc
{
  "plugins": [
    "@babel/plugin-transform-modules-commonjs"
  ]
}
// or
{
  "presets": ["@babel/preset-env"]
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342