Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端模块化:CommonJS,AMD,CMD,ES6发展 #29

Open
zgfang1993 opened this issue Nov 25, 2018 · 0 comments
Open

前端模块化:CommonJS,AMD,CMD,ES6发展 #29

zgfang1993 opened this issue Nov 25, 2018 · 0 comments

Comments

@zgfang1993
Copy link
Owner

前端模块化

模块化开发可以提高代码复用率,方便代码管理。一般来说一个文件就是一个模块,得外暴露变量或者函数。目前比较流行的模块化的规范有:AMD、CMD和CommonJs以及ES6 的 module。

AMD(Asynchronous Module Definition) CMD(Common Module Definition) CommonJs ES6 Module
require.js 在推广过程中对模块化定义的规范产出的。 sea.js在推广过程中对模块化定义的规范产出的。 Node推广使用。  
提前执行(异步加载:依赖先执行)+延迟执行。 延迟执行(运行到需要时加载,根据顺序执行)。    
浏览器 浏览器 服务器端 浏览器和服务器
依赖前置,提前执行(在定义模块的时候就声明依赖的模块) 依赖就近,延迟执行(只有在需要用的时候才去require 执行)    
CommonJs ES6 Module
运行时加载;CommonJs模块就是对象(module.exports属性)),即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法。 编译时加载;ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块。
输出的是值的拷贝(一旦输出一个值,模块内部的变化就影响不到这个值。) 输出的是值的引用(JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。即原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。)

CommonJS

commonjs主要由node推广使用。

关键命令:module.exports =require()

例子:

// 定义模块math.js

function add(a, b) {
  return a + b;
}
module.exports = { //对外暴露的函数、变量
  add: add
}
// main.js 

// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);
// sever.js

// 引用核心模块时,不需要带路径
var http = require("http"),
    PORT = 8000;

http.createServer(function(req, res){
    res.end("Hello World");
}).listen(PORT);

console.log("listenning to " + PORT);

下面的例子可以体现[同步/阻塞式加载]的特性

// timeout.js
var EXE_TIME = 2;

(function(second){
    var start = +new Date();
    while(start + second*1000 > new Date()){}
})(EXE_TIME)

console.log("2000ms executed")
// main.js
require('./timeout');   // sync load
console.log('done!');

如果commonjs require方法是异步的话,下面这么写就会报错

//CommonJS Syntax
var Employee = require("types/Employee");

function Programmer (){
    //do something
}

Programmer.prototype = new Employee();

//如果 require call 是异步的,那么肯定 error
//因为在执行这句前 Employee 模块根本来不及加载进来

从下图可以看出,同步加载对服务器/本地环境并不是问题。-> 浏览器环境会有问题!!

image-20181125203742127

CommonJS是同步加载的,因此更适合服务器端。只有加载完成之后才能进行下面的操作。因为在服务器端模块文件一般存放在本地,再加上有缓存,加载速度十分快
因此这种就不适合用在浏览器端,浏览器端的各个 script 标签中的文件来自各个服务器,如果上个模块加载的时间很长,就会导致浏览器“假死”,因此浏览器端我们采用另外一种异步的加载方式AMD。

AMD(require.js)

关键命令:define()require()

AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,才会运行这个回调函数。这里介绍用require.js实现AMD规范的模块化:用require.config()指定引用路径等,用define()定义模块,用require()加载模块。

//AMD Wrapper
define(
    ["types/Employee"],  //依赖
    function(Employee){  //这个回调会在所有依赖都被加载后才执行
        function Programmer(){
            //do something
        };

        Programmer.prototype = new Employee();
        return Programmer;  //return Constructor
    }
)

Require.js使用例子:

/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>


/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});

// 定义方式一:定义math.js模块
define(function () {
    var basicNum = 0;
    var add = function (x, y) {
        return x + y;
    };
    return {
        add: add,
        basicNum :basicNum
    };
});

// 定义方式二:定义一个依赖underscore.js的模块
define(['underscore'],function(_){
  var classify = function(list){
    _.countBy(list,function(num){
      return num > 30 ? 'old' : 'young';
    })
  };
  return {
    classify :classify
  };
})

// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
  var sum = math.add(10,20);
  $("#sum").html(sum);
});

引用模块的时候,我们将模块名放在[]中作为reqiure()的第一参数;
如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]中作为define()的第一参数。

requirejs最佳实践

require([
    'React',    // 尽量使用 ModuleID
    'IScroll',
    'FastClick'
    'navBar',   // 和同目录下的 js 文件
    'tabBar',
], function(
    React,      // Export
    IScroll
    FastClick
    NavBar,
    TabBar,
){});

config

require.config({
    // 查找根路径,当加载包含协议或以/开头、.js结尾的文件时不启用
    baseUrl: "./js",
    // 配置 ModuleID 与 路径 的映射
    paths: {
        React: "lib/react-with-addons",
        FastClick: "http://cdn.bootcss.com/fastclick/1.0.3/fastclick.min",
        IScroll: "lib/iscroll",
    },
    // 为那些“全局变量注入”型脚本做依赖和导出配置
    shim: {
        'IScroll': {
            exports: "IScroll"
        },
    },
    // 从 CommonJS 包中加载模块
    packages: [
        {
            name: "ReactChart",
            location: "lib/react-chart",
            main: "index"
        }
    ]
})

优化打包

node r.js -o build.js

// build.js
// 简单的说,要把所有配置 repeat 一遍
({
    appDir: './src',
    baseUrl: './js',
    dir: './dist',
    modules: [
        {
            name: 'app'
        }
    ],
    fileExclusionRegExp: /^(r|build)\.js$/,
    optimizeCss: 'standard',
    removeCombined: true,
    paths: {
        React : "lib/react-with-addons",
        FastClick: "http://cdn.bootcss.com/fastclick/1.0.3/fastclick.min",
        IScroll: "lib/iscroll"
    },
    shim: {
        'IScroll': {
            exports: "IScroll"
        },
    },
    packages: [
        {
            name: "ReactChart",
            location: "lib/react-chart",
            main: "index"
        }
    ]
})

CMD(sea.js)

CMD与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。

/** AMD写法 **/
define(["a", "b", "c"], function(a, b, c) { 
     // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

sea.js的小例子🌰:

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});

// 加载模块
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

es6 module

关键命令:exportexport detaultimport

ES6 在语言标准的层面上,实现了模块功能,浏览器和服务器通用的模块解决方案。

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };

/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。

备注: 以上只是一个总结,详细的可看下面参考资料

参考

前端模块化:CommonJS,AMD,CMD,ES6

JavaScript 模块化七日谈 推荐👍

前端模块化开发那点历史

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant