JavaScript模块化开发

Posted by Liber Sun on July 24, 2018

伴随着移动互联的大潮,当今越来越多的网站已经从网页模式进化到了 Webapp 模式。它们运行在现代的高级浏览器里,使用 HTML5、 CSS3、 ES6 等更新的技术来开发丰富的功能,网页已经不仅仅是完成浏览的基本需求,并且 webapp 通常是一个单页面应用,每一个视图通过异步的方式加载,这导致页面初始化和使用过程中会加载越来越多的 JavaScript 代码,这给前端开发的流程和资源组织带来了巨大的挑战。

前端开发和其他开发工作的主要区别,首先是前端是基于多语言、多层次的编码和组织工作,其次前端产品的交付是基于浏览器,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统,这个理想中的模块化系统是前端工程师多年来一直探索的难题。

什么是模块化,模块化就是以相同的方式来编写模块,需要解决的问题包括模块的定义、依赖和导出。

原始形式

<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>
<script src="d.js"></script>

这种原始的加载方式暴露了一些弊端:

  • 全局作用域下的变量冲突
  • 文件只能按照<script>的书写顺序进行加载,依赖关系由顺序决定
  • 大型项目中,难以维护代码。

CommonJS 规范

出发点:官方定义的 JS 只能构建基于浏览器的应用程序,没有模块系统、标准库较少、缺乏包管理工具;为了让 JS 可以在任何地方运行,以达到 Java、C#、PHP 这些后台语言具备开发大型应用的能力,CommonJS 应运而生。

该规范的核心思想是允许模块通过require方法来 同步加载 所依赖的其他模块,然后通过exportsmodule.exports来导出需要的接口

由此,除了浏览器程序,你还可以使用 JavaScript 开发: 1)服务器端程序 2)命令行工具 3)图形化界面程序

同时 CommonJS 为 node.js 的诞生奠定了继承,node.js 是一款将 javascript 运用于服务器端编程的语言,这标志着 js 模块化编程正式诞生。

CommonJS 实现

require("module");
module.exports = function(x) {
  console.log(x);
};
exports.cout = function(x) {
  console.log(x);
};

特点:

commonjs 是需要时读取并加载 ,并且是同步的加载,只有加载完,才会执行下面的操作

优点:

服务器端模块便于重用 简单并容易使用

缺点:

同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的 不能非阻塞的并行加载多个模块

node.js 就是 CommonJS 规范的实现。NPM 是 Node 的包管理工具,解决了 Node 的依赖包管理问题,其中的模块当然也要遵循 Commonjs 的规范。

浏览器如何兼容 CommonJS

浏览器不兼容 CommonJS 的根本原因,在于缺少四个 Node.js 环境的变量。

  • module
  • exports
  • require
  • global

只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。

使用Browserify-浏览器端的 CommonJS 实现,对 NPM 模块进行编译打包,再利用 <script></script>引用

AMD

nodeJS 出现后,服务器的模块概念已经形成,自然客户端模块化的思想也就需要了。而且最好的状态是两者能够兼容。 同时我们要注意因为遵循 CommonJS 规范,require 是同步的,对于服务器是不存在影响的,但是对于浏览器就不能采用“同步加载了”。 因此AMD(异步模块定义)诞生了。 它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

AMD 实现:

define("module", ["dep1", "dep2"], function(d1, d2) {
  return someExportedValue;
});
require(["module", "../file"], function(module, file) {
  /* ... */
});
  • AMD 是依赖前置
  • AMD 对于模块的加载是提前读取并加载

特点:

AMD 是依赖前置 AMD 对于模块的加载是提前读取并加载

优点:

适合在浏览器环境中异步加载模块 可以并行加载多个模块

缺点:

提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅 不符合通用的模块化思维方式,是一种妥协的实现

目前实现了 AMD 规范的 JS 库有:require.js

requirejs

require.js 的诞生解决了两个问题

实现 js 文件的异步加载,避免网页失去响应 管理模块之间的依赖,便于代码编写和维护

require 可以加载满足 AMD 规范的模块,也可以加载不满足 AMD 规范的模块

CMD(Common Module Definition)

CMD(通用模块),是同步

  • CMD 是就近依赖
  • CMD 是提前读取,需要时再加载

AMD 和 CMD 的区别

1、AMD 推崇 依赖前置 ,在定义模块的时候就要声明其依赖的模块 2、CMD 推崇 就近依赖,只有在用到某个模块的时候再去 require AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同。

同样都是异步加载模块,AMD 在加载模块完成后就会执行改模块,所有模块都加载执行完后会进入 require 的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行(加载完再进入主逻辑)

CMD 加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的(在主逻辑中加载)

UMD

Universal Module Definition 规范类似于兼容 CommonJS 和 AMD 的语法糖,是模块定义的跨平台解决方案。

ES6 Module

ES6 正式提出了内置的模块化语法,ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。 在浏览器端无需额外引入 requirejs 就可以进行模块化

  • 通过关键字export 直接导出

可以通过default关键字,可以使用默认导出单个变量,函数或者类

export default function(num1, num2) {
  return num1 + num2;
}
  • 通过关键字import 导入
import { identifier1, identifier2 } from "./example.js";
import * as example from "./example.js";
import sum from "./example.js"; //default 默认值时 ,可以不使用{}

优点:

容易进行静态分析 面向未来的 EcmaScript 标准 缺点: 原生浏览器端还没有实现该标准 全新的命令字,新版的 Node.js 才支持 实现:

Babel 把 ES2015+ 编译为传统的 js,配置文件是.babelrc

若浏览器支持

<script type="module">
  import { foo, num } from "./test.js";
  console.log(num);
  console.log(foo());
</script>

test.js

export const num = "123";
export function foo() {
  return "456";
}

Webpack

bundle your assets/scripts/images/styles/

webpack is a static module bundler for modern JavaScript applications. 是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以按需将加载的模块进行代码隔离,异步加载。

可以对你使用到的 js/css/images 进行捆绑打包处理,对于 WebPack 而言所有的一切都是模块。 同时我们要知道 webpack 也是根据 CommonJS 的形式来书写的。

核心概念

  • Entry 入口文件 // 所有模块的入口,Webpack 从入口开始递归解析出所有依赖的模块
  • Output 输出 // 将入口依赖的模块打包成文件进行输出
  • Loaders 加载器 针对不用的文件 使用不同工具进行捆绑
  • Plugins

配置文件

pageage.json 是 npm 的包管理配置文件

总结

module、exports、require和global是CommonJS的规范,是针对Node.js Npm的。

由于 npm 上 CommonJS 的类库众多,以及 CommonJS 和 ES6 之间的差异,Node.js 无法直接兼容 ES6。所以现阶段 require/exports 任然是必要且实必须的。 如果浏览器支持,使用Browserify对 NPM 模块进行编译打包。

import是es6的标准(官方),部分浏览器支持,可以直接使用<script type="module"></script。 若浏览器不支持,可使用Babel进行转换。

注意即使我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5的CommongJS再执行,import语法会被转码为require。

这也就是为什么说 require/exports 是必要且必须的。因为事实是,目前你编写的 import/export 最终都是编译为 require/exports 来执行的。