JS模块的发展

JavaScript module

Posted by Ericteen on January 5, 2018

JavaScript 模块的发展

什么是模块?

模块化编程中,开发者将程序拆分成很多离散的功能块,这些块块被称作模块(module)。

通常,模块有这样几个好处:

  • 可维护性。模块是独立的,一个设计良好的模块会让外界对自身有很少的依赖。这样,自身就可以独立进行更新和改进。
  • 命名空间。JS 中,如果一个变量在顶层函数之外进行声明,它就变成全局可用。由此带来的命名冲突,对实际开发会有很大的影响。
  • 代码重用。我们有时候会喜欢从之前写过的项目中拷贝代码到新的项目,这没有问题,但是更好的方法是,通过模块引用的方式,来避免重复的代码库。我们可以在更新了模块之后,让引用了该模块的所有项目都同步更新,还能指定版本号,避免 API 变更带来的麻烦。

模块化在日常开发中的重要性不言而喻。尤其是当页面复杂度不断增高,单一的页面逐渐变成网页应用,充分解耦的 JS 文件/模块对代码拆分、tree shaking、部署优化都有十分重要的作用。

JS 早期并未提供对模块的支持,直到 ES2015,才有课原生的 importexport。在此之前还涌现出了很多模块规范,如 CommonJS、AMD等。本文将归纳一下在发展过程中,出现的一些模块化解决方案。

从设计模式说起

早期,我们这样写代码:

function foo() {
  //...
}

function bar() {
  //...
}

但是,这样写的话全局(Global)容易被污染,容易造成命名冲突。

简单封装,Namespace 模式。这样做可以减少全局的变量数,但同时,MYAPP 本质上是对象,安全性堪忧。

var MYAPP = {
  foo: function (){},
  bar: function (){}
}

MYAPP.foo()

匿名闭包,IIFE 模式。函数式 JavaScript 唯一的 Local Scope。

var Module = (function () {
  var _private = 'safe private'
  var foo = function () {
    console.log(_private)
  }

  return {
    foo: foo
  }
})()

Module.foo() // 'safe private'
Module._private //  undefined
// 引入依赖
var Module = (function ($) {
  var _$body = $('body')
  var foo = function () {
    console.log(_$body) // 特权方法
  }
  // Revelation Pattern
  return {
    foo: foo
  }
})(jQuery)

Module.foo()

这就是模块模式(Module Pattern),也是现代模块实现的基石。

Script Loader

只有封装性可不够,同时还需要解决加载的问题。

body
  script(src='jquery.js')
  script(src='app.js') // do some $ things...

DOM 的顺序就是执行的顺序,并行加载。由此带来很多问题:

  • 难以维护
  • 依赖模糊
  • 请求过多

LABjs Script Loader(2009)

script(src='LAB.js' async)
$LAB.script('framework.js').wait()
    .script('plugin.framework.js')
    .script('myplugin.framework.js').wait()
    .script('init.js')

First Come, First Serve

$LAB
.script(['script1.js', 'script2.js', 'script3.js'])
.wait(function () {
  script1Func()
  script2Func()
  script3Func()
})

基于文件的依赖管理。

Module Loader

YUI3 Loader, Module Loader(2009)

YUI is lightweight core and modular architecture make it scalable, fast and robust.

CommonJS - API 标准

模块的定义和引用

// math.js
exports.add = function(a, b) {
  return a + b
}
// main.js
var math = require('math')
console.log(math.add(1, 2)) // 3

node.js 简单的 http 服务器

// server.js
var http = require('http')
var PORT = 8000

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

console.log("listenning to " + PORT)
node server.js

AMD/CMD 浏览器环境模块化方案

AMD(Async Module Definition): RequireJS 对模块定义的规范化产出。JavaScript 文件和模块的加载器,针对浏览器端进行优化。

CMD(Common Module Definition): SeaJS 对模块定义的规范化产出。

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

    Programmer.prototype = new Employee()
    return Programmer // return constructor
  }
)
// CommonJS 的简化写法
define(function(require) {
  var dependency1 = require('dependency1')
  var dependency2 = require('dependency2')

  return function() {}
})

// parse out require...
define(
    ['require', 'dependency1', 'dependency2'],
function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2')

    return function () {}
})

AMD 和 CommonJS 的书写风格对比

// CommonJS
var a = require("./a");  // 依赖就近
a.doSomething()

// AMD recommended style
define(["a", "b"], function(a, b){ // 依赖前置
    a.doSomething()
    b.doSomething()
})

AMD 和 CommonJS 的执行时机对比

var a = require("./a");  // 执行到此时,a.js 同步下载并执行

// AMD with CommonJS sugar
define(["require"], function(require){
    // 在这里, a.js 已经下载并且执行好了
    var a = require("./a")
})

// 早下载早执行

RequireJS 最佳实践

// use case
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"
        }
    ]
})

Browserify & Webpack

webpack(Universal Module System): 对所有模块一视同仁的依赖管理。

对比 node.js 的模块,webpack 的模块可以用多种方式表示它的依赖。例如:

  • ES2015 的 import
  • CommonJS 的 require()
  • AMD 的 definerequire
  • 在 css/sass/less 文件中的 @import
  • 在样式表中的图片 url(...) 或 html 文件中 <img src="..." />>

ES6 Module

There is mo module in JavaScript until ES6.

但运行时并不完善,缺少运行时(runtime),Babel的出现,使我们可以在当下使用下一代的 JavaScript。

Single defaule module

// math.js
export default math = {
  PI: 3.14,
  foo: function (){}
}

// app.js
import math from './math.js'
math.PI // 3.14

# babel magic
$ bable-node app.js

named exports

// export declaration
export function foo() {
  console.log('I am not bar.')
}

// export variable statement
const PI = 3.14
foo = function (){}

export { PI, foo }

Importing named exports

// import { ImportsList } from "module-name"
import { PI } from "./math"
import { PI, foo } from "module-name"

// import IdentifierName as ImportedBinding
import { foo as bar } from "./math"
bar() // use alias bar

// import NameSpaceImport
import * as math from "./math"
math.PI
math.foo()

参考

JavaScript 模块化七日谈