前端模块化开发

1. 前言

现在的前端开发, 通常是一个单页面应用,每一个视图通过异步的方式加载,这导致页面初始化和使用过程中会加载越来越多的 JS 代码,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统。

1.1 最简单的模块

其实我们曾把函数作为模块,但会污染全局变量,并且模块成员之间没什么关系。这个时候我们可以运用面向对象思想,使用立即执行函数实现闭包,可以避免变量污染,同时同一模块内的成员也有了关系,在模块外部无法修改我们没有暴露出来的变量、函数,这就是简单的模块。但是这样处理起来麻烦,并且远远不够。

1.2 期望的模块系统

模块的加载和传输,我们首先能想到两种极端的方式,一种是每个模块文件都单独请求,另一种是把所有模块打包成一个文件然后只请求一次。显而易见,每个模块都发起单独的请求造成了请求次数过多,导致应用启动速度慢;一次请求加载所有模块导致流量浪费、初始化过程慢。这两种方式都不是好的解决方案,它们过于简单粗暴。

分块传输,按需进行懒加载,在实际用到某些模块的时候再增量更新,才是较为合理的模块加载方案。要实现模块的按需加载,就需要一个对整个代码库中的模块进行静态分析、编译打包的过程。

在上面的分析过程中,我们提到的模块仅仅是指 JS 模块文件。然而,在前端开发过程中还涉及到样式、图片、字体、HTML 模板等等众多的资源。如果他们都可以视作模块,并且都可以通过require的方式来加载,将带来优雅的开发体验,那么如何做到让 require 能加载各种资源呢?在编译的时候,要对整个代码进行静态分析,分析出各个模块的类型和它们依赖关系,然后将不同类型的模块提交给适配的加载器来处理。Webpack 就是在这样的需求中应运而生。

2. 模块系统

2.1 script

  • 全局作用域下容易造成变量冲突
  • 文件只能按照 <script> 的书写顺序进行加载
  • 开发人员必须主观解决模块和代码库的依赖关系
  • 在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪

2.2 CommonJS

服务器端的 Node.js 遵循 CommonJS 规范,该规范的核心思想是允许模块通过 require 方法来同步加载所要依赖的其他模块,然后通过 exportsmodule.exports 来导出需要暴露的接口。

require('module');
require('../file.js');
exports.doStuff = function() {};
module.exports = someValue;

// moduleA.js
module.exports = function(value) {
  return value * 2;
};

// moduleB.js
var multiplyBy2 = require('./moduleA');
var result = multiplyBy2(4);

优点:

  • 服务器端模块便于重用
  • NPM 中已经有将近 20 万个可以使用模块包
  • 简单并容易使用

缺点:

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

2.3 AMD

define(id?, dependencies?, factory),它要在声明模块的时候指定所有的依赖 dependencies,并且还要当做形参传到 factory 中,对于依赖的模块提前执行,依赖前置。

define('module', ['dep1', 'dep2'], function(d1, d2) {
  return someExportedValue;
});
require(['module', '../file'], function(module, file) {
  /* ... */
});

一些用例:
定义一个名为 myModule 的模块,它依赖 jQuery 模块:

define('myModule', ['jquery'], function($) {
  // $ 是 jquery 模块的输出
  $('body').text('hello world');
});
// 使用
define(['myModule'], function(myModule) {});

注意:在 webpack 中,模块名只有局部作用域,在 Require.js 中模块名是全局作用域,可以在全局引用。
定义一个没有 id 值的匿名模块,通常作为应用的启动函数:

define(['jquery'], function($) {
  $('body').text('hello world');
});

依赖多个模块的定义:

define(['jquery', './math.js'], function($, math) {
  // $ 和 math 一次传入 factory
  $('body').text('hello world');
});

模块输出:

define(['jquery'], function($) {
  var HelloWorldize = function(selector) {
    $(selector).text('hello world');
  };

  // HelloWorldize 是该模块输出的对外接口
  return HelloWorldize;
});

在模块定义内部引用依赖:

define(function(require) {
  var $ = require('jquery');
  $('body').text('hello world');
});

优点:

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

缺点:

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

2.4 CMD

define(function(require, exports, module) {
  var $ = require('jquery');
  var Spinning = require('./spinning');
  exports.doSomething = ...
  module.exports = ...
})

优点:

  • 依赖就近,延迟执行
  • 可以很容易在 Node.js 中运行

缺点:

  • 依赖 SPM 打包,模块的加载逻辑偏重

2.5 UMD (暂未接触)

2.6 ES6 模块

ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

import "jquery";
export function doStuff() {}
module "localModule" {}

优点:

  • 容易进行静态分析
  • 面向未来的 EcmaScript 标准

缺点:

  • 原生浏览器端还没有实现该标准
  • 全新的命令字,新版的 Node.js 才支持

实现:

3. 模块系统/规范对比

3.1 AMD 与 CMD

从前有两个规范,一个是 AMD,一个是 CMD。RequireJS 是 AMD 规范的实现,SeaJS 是 CMD 规范的实现。一个主张提前加载依赖,一个主张延迟加载依赖。后来出现了 CommomJS 规范,CommomJS 是服务端规范,node 就是采用这个规范,他是同步加载,毕竟服务端不用考虑异步。

3.2 AMD 与 CommonJs

AMD 的应用场景则是浏览器,异步加载的模块机制。require.js 的写法大致如下:

define(['firstModule'], function(module) {
  //your code...
  return anotherModule;
});

CommonJs 是应用在 NodeJs,是一种同步的模块机制。它的写法大致如下:

var firstModule = require('firstModule');
//your code...
module.export = anotherModule;

其实我们单比较写法,就知道 CommonJs 是更为优秀的。它是一种同步的写法,友好而且代码也不会繁琐臃肿。但更重要的原因是,随着 npm 成为主流的 JS 组件发布平台,越来越多的前端项目也依赖于 npm 上的项目,或者自身就会发布到 npm 平台。所以我们对如何可以使用 npm 包中的模块是我们的一大需求。

3.3 browserify 与 webpack

browserify 工具支持我们直接使用 require()的同步语法去加载 npm 模块,使用不多这里就不做介绍。

  1. Webpack 其实就是一个打包工具,他的思想就是一切皆模块,css 是模块,js 是模块,图片是模块。并且提供了一些列模块加载(各种-loader)来编译模块。官方推荐使用 commonJS 规范,但是也支持 CMD 和 AMD。无论是node应用模块,还是webpack配置 ,均是采用CommonJS模块化规范。

  2. webpack 支持哪些功能特性:

  • 支持 CommonJs 和 AMD 模块,意思也就是我们基本可以无痛迁移旧项目。
  • 支持模块加载器和插件机制,可对模块灵活定制。特别是我最爱的 babel-loader,有效支持 ES6。
  • 可以通过配置,打包成多个文件。有效利用浏览器的缓存功能提升性能。
  • 将样式文件和图片等静态资源也可视为模块进行打包。配合 loader 加载器,可以支持 sass,less 等 CSS 预处理器。
  • 内置有 source map,即使打包在一起依旧方便调试。
  • 看完上面这些,可以想象它就是一个前端工具,可以让我们进行各种模块加载,预处理后,再打包。之前我们对这些的处理是放在 grunt 或 gulp 等前端自动化工具中。有了 webpack,我们无需借助自动化工具对模块进行各种处理,让我们工具的任务分的更加清晰。

4. 相关知识

4.1 ES6 模块

4.1.1 对象的导出

1. export default{
        add(){}
 }
2. export fucntion add(){} 相当于 将add方法当做一个属性挂在到exports对象

4.1.2 对象的导入

如果导出的是:export default{ add(){}}
那么可以通过  import obj from './calc.js'
如果导出的是:
export fucntion add(){} 
export fucntion substrict(){} 
export const PI=3.14
那么可以通过按需加载 import {add,substrict,PI} from './calc.js'

4.2 Node 模块

4.2.1 传统非模块化开发有如下的缺点

  1. 命名冲突
  2. 文件依赖

4.2.2 前端标准的模块化规范

  1. AMD - requirejs
  2. CMD - seajs

4.2.3 服务器端的模块化规范

CommonJS - Node.js

4.2.4 Node 模块化相关的规则

  1. 如何定义模块:一个 js 文件就是一个模块,模块内部的成员都是相互独立
  2. 模块成员的导出和引入:
    exports 与 module 的关系:module.exports = exports = {};
    模块成员的导出最终以 module.exports 为准
    如果要导出单个的成员或者比较少的成员,一般我们使用 exports 导出;如果要导出的成员比较多,一般我们使用 module.exports 的方式;这两种方式不能同时使用
var sum = function(a, b) {
  return parseInt(a) + parseInt(b);
};
// 方法1
// 导出模块成员
exports.sum = sum;
//引入模块
var module = require('./xx.js');
var ret = module.sum(12, 13);

// 方法2
// 导出模块成员
module.exports = sum;
//引入模块
var module = require('./xx.js');
module();

// // 方法1
// exports.sum = sum;
// exports.subtract = subtract;
//
// var m = require('./05.js');
// var ret = m.sum(1,2);
// var ret1 = m.subtract(1,2);
// console.log(ret,ret1);
//
// // 方法2
// module.exports = {
//     sum : sum,
//     subtract : subtract,
//     multiply : multiply,
//     divide : divide
// }
//
// var m = require('./05.js');
// console.log(m);

4.3 webpack

4.3.1 模块打包器

根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。如何在一个大规模的代码库中,维护各种模块资源的分割和存放,维护它们之间的依赖关系,并且无缝的将它们整合到一起生成适合浏览器端请求加载的静态资源。市面上已经存在的模块管理和打包工具并不适合大型的项目,尤其单页面 Web 应用程序。最紧迫的原因是如何在一个大规模的代码库中,维护各种模块资源的分割和存放,维护它们之间的依赖关系,并且无缝的将它们整合到一起生成适合浏览器端请求加载的静态资源。

这些已有的模块化工具并不能很好的完成如下的目标:

  • 将依赖树拆分成按需加载的块
  • 初始化加载的耗时尽量少
  • 各种静态资源都可以视作模块
  • 将第三方库整合成模块的能力
  • 可以自定义打包逻辑的能力
  • 适合大项目,无论是单页还是多页的 Web 应用

4.3.2 Webpack 的特点

Webapck 和其他模块化工具有什么区别呢?

  1. 代码拆分
    Webpack 有两种组织模块依赖的方式,同步和异步。异步依赖作为分割点,形成一个新的块。在优化了依赖树后,每一个异步区块都作为一个文件被打包。
  2. Loader
    Webpack 本身只能处理原生的 JavaScript 模块,但是 loader 转换器可以将各种类型的资源转换成 JavaScript 模块。这样,任何资源都可以成为 Webpack 可以处理的模块。
  3. 智能解析
    Webpack 有一个智能解析器,几乎可以处理任何第三方库,无论它们的模块形式是 CommonJS、 AMD 还是普通的 JS 文件。甚至在加载依赖的时候,允许使用动态表达式 require("./templates/" + name + ".jade")
  4. 插件系统
    Webpack 还有一个功能丰富的插件系统。大多数内容功能都是基于这个插件系统运行的,还可以开发和使用开源的 Webpack 插件,来满足各式各样的需求。
  5. 快速运行
    Webpack 使用异步 I/O 和多级缓存提高运行效率,这使得 Webpack 能够以令人难以置信的速度快速增量编译。

4.3.3 webpack 是什么?

CommonJS 和 AMD 是用于 JavaScript 模块管理的两大规范,前者定义的是模块的同步加载,主要用于 NodeJS;而后者则是异步加载,通过 requirejs 等工具适用于前端。随着 npm 成为主流的 JavaScript 组件发布平台,越来越多的前端项目也依赖于 npm 上的项目,或者 自身就会发布到 npm 平台。因此,让前端项目更方便的使用 npm 上的资源成为一大需求。
web 开发中常用到的静态资源主要有 JavaScript、CSS、图片、Jade 等文件,webpack 中将静态资源文件称之为模块。 webpack 是一个 module bundler(模块打包工具),其可以兼容多种 js 书写规范,且可以处理模块间的依赖关系,具有更强大的 js 模块化的功能。Webpack 对它们进行统 一的管理以及打包发布

4.3.4 为什么使用 webpack?

1. 对 CommonJS 、 AMD 、ES6 的语法做了兼容
2. 对 js、css、图片等资源文件都支持打包
3. 串联式模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如提供对 CoffeeScript、ES6 的支持
4. 有独立的配置文件 webpack.config.js
5. 可以将代码切割成不同的 chunk,实现按需加载,降低了初始化时间
6. 支持 SourceUrls 和 SourceMaps,易于调试
7. 具有强大的 Plugin 接口,大多是内部插件,使用起来比较灵活
8.webpack 使用异步 IO 并具有多级缓存。这使得 webpack 很快且在增量编译上更加快

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

推荐阅读更多精彩内容

  • 前端模块化开发简介 历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖...
    荣儿飞阅读 4,237评论 0 6
  • 在JavaScript发展的初期是为了实现简单的页面交互逻辑, 就这么一句话. 如今, 浏览器性能得到极大的提高,...
    HelloJames阅读 621评论 0 2
  • CommonJS 服务器端的 Node.js 遵循 CommonJS规范,该规范的核心思想是允许模块通过 requ...
    LiLi原上草阅读 172评论 0 0
  • 前端模块化开发 常见的三大模块化框架。 CommonJS: 1.根据CommonJS规范,一个单独的文件就是一个模...
    一长亭阅读 331评论 0 2
  • 概念 模块化开发,一个模块就是一个实现特定功能的文件,有了模块我们就可以更方便的使用别人的代码,要用什么功能就加载...
    biu丶biubiu阅读 241评论 0 0