代码改变世界

【读书笔记】-- JavaScript模块

2017-02-14 23:36  stoneniqiu  阅读(...)  评论(...编辑  收藏

在JavaScript编程中我们用的很多的一个场景就是写模块。可以看成一个简单的封装或者是一个类库的开始,有哪些形式呢,先来一个简单的模块。

简单模块

       var foo = (function() {
                var name = "foo";
                function hello() {
                    console.log("hello "+name);
                }
                function doWork() {
                    console.log("do work");
                }
                return {
                    hello: hello,
                    doWork: doWork
                };
            })();
            foo.hello(); // hello foo
            foo.doWork(); // do work

用IIFE创建一个闭包,隔离作用域,避免变量相互干扰。得到foo对象可以直接用了。这种适合小的模块,比如在ag中的写Service。

(function () {
    angular
        .module('readApp')
        .service('authentication', authentication);
    
    authentication.$inject = ['$window','$http'];
    function authentication($window, $http) {
        var saveToken = function (token) {
            $window.localStorage['read-token'] = token;
        };
        var getToken = function () {
            return $window.localStorage['read-token'];
        };
        var register = function(user) {
            return $http.post('/api/register', user).success(function(data) {
                saveToken(data.token);
            });
        };
        var login = function(user) {
            return $http.post('/api/login', user).success(function(data) {
                saveToken(data.token);
            });
        };
        var logout = function() {
            $window.localStorage.removeItem('read-token');
        };

        var isLoggedIn = function() {
            var token = getToken();
            if (token) {
                var payload = JSON.parse($window.atob(token.split('.')[1]));
                return payload.exp > Date.now() / 1000;
            } else {
                return false;
            }
        };
        var currentUser = function() {
            if (isLoggedIn()) {
                var token = getToken();
                var payload = JSON.parse($window.atob(token.split('.')[1]));
                return {
                    email: payload.email,
                    name: payload.name,
                };
            }
        };
        

        return {
            saveToken: saveToken,
            getToken: getToken,
            register: register,
            login: login,
            logout: logout,
            isLoggedIn: isLoggedIn,
            currentUser: currentUser,
        };
    }
})();
View Code

也可以直接一个大括号的:

   var foo = {
                other: function () {
                    console.log("do other");
                },
                hello:function() {
                    console.log("working");
                },
                doWork:function() {
                    console.log("working");
                    foo.other();
                }
            }

这个形式我们在jquery内部或者一些工具类js中见过。简单直接。如果有内部相互调用,建议直接用对象名。这样不必在每一个方法里面写一个 var self=this 。缺点就是太长了不好维护,多增加一个变量都要加个key:value的形式。只适合简单场景。但如果模块内部方法比较多,还是建议在内部创建对象。

扩展模块

  var foo = (function () {
                var self = {},name = "foo";
                function _wroking() {
                    console.log("working");
                }
                self.hello = function () {
                    console.log("hello " + name);
                }
                self.doWork = function () {
                    _wroking();
                }
                return self;
            })();

这样内部方法和外部方法就有明确的区分,可以看下Zepto的大体结构:

var Zepto = (function() {
        var undefined, key, $, classList, emptyArray = [], slice = emptyArray.slice, filter = emptyArray.filter,
                zepto = {}, camelize, uniq;
                //...........
                function isObject(obj) { return type(obj) == "object" }
                //..........
                zepto.matches = function (element, selector) {
                //...
                }
                zepto.init = function(selector, context) {
                //......
                }
                $ = function (selector, context) {
                    return zepto.init(selector, context)
                }
                function extend(target, source, deep) {
                 //...
                }
                $.extend = function (target) {
                }
            })()

            window.Zepto = Zepto
            window.$ === undefined && (window.$ = Zepto)

内部定义了zepot对象,并注意到有一个extend方法,便于后面扩展zepot的这个模块,当然也可以扩展其他的模块。target是指定的。

  function extend(target, source, deep) {
    for (key in source)
      if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
        if (isPlainObject(source[key]) && !isPlainObject(target[key]))
          target[key] = {}
        if (isArray(source[key]) && !isArray(target[key]))
          target[key] = []
        extend(target[key], source[key], deep)
      }
      else if (source[key] !== undefined) target[key] = source[key]
  }

  // Copy all but undefined properties from one or more
  // objects to the `target` object.
  $.extend = function(target){
    var deep, args = slice.call(arguments, 1)
    if (typeof target == 'boolean') {
      deep = target
      target = args.shift()
    }
    args.forEach(function(arg){ extend(target, arg, deep) })
    return target
  }
View Code

再扩展子模块的时候就方便了:

            ;(function ($) {
                //...
                $.event = { add: add, remove: remove }
                $.proxy = function(fn, context) {
                  //...
                }
            })(Zepto);

现代的模块机制

AMDCMD都是将模块的定义封装进了一个友好的API。就是require.js和sea.js中的define方法。先看一个简单的现代模块实现:

var MyModules = (function () {
                var modules = {};

                function define(name, deps, impl) {
                    for (var i = 0; i < deps.length; i++) {
                        deps[i] = modules[deps[i]];
                    }
                    modules[name] = impl.apply(impl, deps);
                }

                function get(name) {
                    return modules[name];
                }

                return {
                    define: define,
                    get: get
                }
            })();
            MyModules.define("bar", [], function () {
                function hello(who) {
                    return "Let me introduce: " + who;
                }
                return {
                    hello: hello
                };
            });
            MyModules.define("foo", ["bar"], function (bar) {
                var hungry = "jazz";

                function awsome() {
                    console.log(bar.hello(hungry));
                }

                return {
                    awsome: awsome
                }
            });
            var bar = MyModules.get("bar");
            var foo = MyModules.get("foo");

            console.log(bar.hello("hippo"));//Let me introduce: hippo
            foo.awsome();//Let me introduce: jazz

MyModules的define方法包含name,deps,impl三个参数,name表示是模块名称,deps表示是依赖项,impl表示实现。关键是modules[name] = impl.apply( impl, deps );这一句,上面的for循环将一个模块名称数组先转成一个包含具体模块的数组,然后apply给具体的实现方法。相当于是注入了依赖项。注意到定义foo模块的时候,依赖了bar模块,只需要在deps这个参数加入[“bar”]即可。

在sea.js中,模块的状态做了区分:

var STATUS = Module.STATUS = {
  // 1 - The `module.uri` is being fetched 相当于初始化
  FETCHING: 1,
  // 2 - The meta data has been saved to cachedMods 缓存在cacheMods
  SAVED: 2,
  // 3 - The `module.dependencies` are being loaded
  LOADING: 3,
  // 4 - The module are ready to execute
  LOADED: 4,
  // 5 - The module is being executed
  EXECUTING: 5,
  // 6 - The `module.exports` is available
  EXECUTED: 6
}

看下define方法:

Module.define = function (id, deps, factory) {
  var argsLen = arguments.length

  // define(factory)
  if (argsLen === 1) {
    factory = id
    id = undefined
  }
  else if (argsLen === 2) {
    factory = deps

    // define(deps, factory)
    if (isArray(id)) {
      deps = id
      id = undefined
    }
    // define(id, factory)
    else {
      deps = undefined
    }
  }

  // Parse dependencies according to the module factory code
  if (!isArray(deps) && isFunction(factory)) {
    deps = parseDependencies(factory.toString())
  }

  var meta = {
    id: id,
    uri: resolve(id),
    deps: deps,
    factory: factory
  }

  // Try to derive uri in IE6-9 for anonymous modules
  if (!meta.uri && doc.attachEvent) {
    var script = getCurrentScript()

    if (script) {
      meta.uri = script.src
    }

    // NOTE: If the id-deriving methods above is failed, then falls back
    // to use onload event to get the uri
  }

  // Emit `define` event, used in nocache plugin, seajs node version etc
  emit("define", meta)

  meta.uri ? save(meta.uri, meta) :
      // Save information for "saving" work in the script onload event
      anonymousMeta = meta
}
View Code

save的内部是通过Module.get 缓存起uri和deps(依赖项)构建的Module对象。

Module.get = function(uri, deps) {
  return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
}

require也是通Module.get来获取模块:

seajs.require = function(id) {
  return (cachedMods[resolve(id)] || {}).exports
}

不像例子中是一次性加载模块,sea.js可以在需要的地方再加载对应的模块。因为我们在很多js框架中看到下面这一块:

(function (root, factory) { 
    if (typeof define === 'function' && define.amd) {
        define(factory);
    } else if (typeof exports === 'object') {
        module.exports = factory();
    } else {
        root.xxx= factory();
    }
})(this, function () {
//。。。
}

 支持amd规范和node模块。

ES6中的模块

以上的模块都是基于函数的,API在运行时都可以被修改。ES6中为模块增加了一级语法支持,通过模块系统进行加载时,ES6会将文件独立的模块来处理。使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。

下面是一个circle.js文件,它输出两个方法areacircumference

// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}

现在,加载这个模块。

// main.js

import { area, circumference } from './circle';

console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

上面写法是逐一指定要加载的方法,整体加载的写法如下。

import * as circle from './circle';

这语法让人有点激动。而且是不允许修改的,这样就很大的确保了接口的稳定性。

import * as circle from './circle';

// 下面两行都是不允许的 
circle.foo = 'hello';
circle.area = function () {};

小结:模块在JavaScript运用很广泛,好的模块构建能确定一个清晰的结构,有助于分工和维护。

参考文档:

ES6 Module: http://es6.ruanyifeng.com/#docs/module

CMD规范:https://github.com/seajs/seajs/issues/242

阅读书籍:

资源在读书群中。