Skip to content

JavaScript 模块化七日谈 ​

原文资源:

TIP

因为原网站的slide每次浏览都太慢了,大屏的过渡动画还花眼

原文作者黄玄,微信电影票前端工程师,前阿里巴巴前端工程师

Blog / @Huxpro / GitHub / 知乎

image_1715415354592.png

Why Modular? WEB IS EVOLVINGimage_1715415434532.png

  • Web sites are turning into Web Apps
  • Code complexity grows as the site gets bigger
  • Highly decoupled JS files/modules is wanted
  • Deployment wants optimized code in few HTTP calls

第一日 上古时期 Module? 从设计模式说起 ​

最早,我们这么写代码 ​

javascript
function foo(){
    //...
}
function bar(){
    //...
}

Global 被污染,很容易命名冲突

简单封装:Namespace 模式 ​

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

MYAPP.foo();
  • 减少 Global 上的变量数目
  • 本质是对象,一点都不安全

匿名闭包 :IIFE 模式 ​

javascript
var Module = (function(){
    var _private = "safe now";
    var foo = function(){
        console.log(_private)
    }

    return {
        foo: foo
    }
})()

Module.foo();
Module._private; // undefined

函数是 JavaScript 唯一的 Local Scope

再增强一点 :引入依赖 ​

javascript
var Module = (function($){
    var _$body = $("body");     // we can use jQuery now!
    var foo = function(){
        console.log(_$body);    // 特权方法
    }

    // Revelation Pattern
    return {
        foo: foo
    }
})(jQuery)

Module.foo();

这就是模块模式也是现代模块实现的基石.

第二日 石器时代 Script Loader 只有封装性可不够,我们还需要加载 ​

Let's Back To Script Tags ​

javascript
body
    script(src="jquery.js")
    script(src="app.js")    // do some $ things...
  • Order is essential
  • Load in parallel
  • DOM 顺序即执行顺序

但现实是这样的… ​

javascript
body
    script(src="zepto.js")
    script(src="jhash.js")
    script(src="fastClick.js")
    script(src="iScroll.js")
    script(src="underscore.js")
    script(src="handlebar.js")
    script(src="datacenter.js")
    script(src="deferred.js")
    script(src="util/wxbridge.js")
    script(src="util/login.js")
    script(src="util/base.js")
    script(src="util/city.js")
    script(src="util/date.js")
    script(src="util/cookie.js")
    script(src="app.js")
  • 难以维护 Very difficult to maintain!
  • 依赖模糊 Unclear Dependencies
  • 请求过多 Too much HTTP calls

LABjs - Script Loader(2009): Loading And Blocking JavaScript

Using LABjs will replace all that ugly "script tag soup"

How Does It Works? ​

javascript
script(src="LAB.js" async)
$LAB.script("framework.js").wait()
    .script("plugin.framework.js")
    .script("myplugin.framework.js").wait()
    .script("init.js");

Executed in parallel?

First-come, First-served (when execution order is not important)

Sugar ​

javascript
$LAB
.script( [ "script1.js", "script2.js", "script3.js"] )
.wait(function(){ // wait for all scripts to execute first
    script1Func();
    script2Func();
    script3Func();
});

Dependency Management: 基于文件的依赖管理

第三日 蒸汽朋克 Module Loader 模块化架构的工业革命 ​

YUI3 Loader - Module Loader (2009)

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

回顾昔日王者的风采: ​

javascript
// YUI - 编写模块
YUI.add('dom', function(Y) {
  Y.DOM = { ... }
})

// YUI - 使用模块
YUI().use('dom', function(Y) {
  Y.DOM.doSomeThing();
  // use some methods DOM attach to Y
})

Creating Custom Modules ​

javascript
// hello.js
YUI.add('hello', function(Y){
    Y.sayHello = function(msg){
        Y.DOM.set(el, 'innerHTML', 'Hello!');
    }
},'3.0.0',{
    requires:['dom']
})

// main.js
YUI().use('hello', function(Y){
    Y.sayHello("hey yui loader");
})

基于模块的依赖管理

Let's Go A Little Deeper ​

javascript
// Sandbox Implementation
function Sandbox() {
    // ...
    // initialize the required modules
    for (i = 0; i < modules.length; i += 1) {
        Sandbox.modules[modules[i]](this);
    }
    // ...
}

Y 其实是一个强沙箱

所有依赖模块通过 attach 的方式被注入沙盒

attach:在当前 YUI 实例上执行模块的初始化代码,使得模块在当前实例上可用

Still "Script Tag Soup"? ​

javascript
script(src="/path/to/yui-min.js")       // YUI seed
script(src="/path/to/my/module1.js")    // add('module1')
script(src="/path/to/my/module2.js")    // add('module2')
script(src="/path/to/my/module3.js")    // add('module3')

YUI().use('module1', 'module2', 'module3', function(Y) {
    // you can use all this module now
});
  • you don't have to include script tags in a set order
  • separation of loading from execution

漏了一个问题? Too much HTTP calls YUI Combo

How Combo Works ​

javascript
script(src="http://yui.yahooapis.com/3.0.0/build/yui/yui-min.js")
script(src="http://yui.yahooapis.com/3.0.0/build/dom/dom-min.js")

↓ magic combo

javascript
script(src="http://yui.yahooapis.com/combo?
    3.0.0/build/yui/yui-min.js&
    3.0.0/build/dom/dom-min.js")
  • Serves multiple files in a single request
  • GET 请求,需要服务器支持
  • alibaba/nginx-http-concat

合并 Concat

压缩 Minify

混淆 Uglify

第四日 号角吹响 CommonJS 征服世界的第一步是跳出浏览器 ​

CommonJS - API Standard (2009.08)

javascript:
not just for browsers any more!

MODULES/1.0

模块的定义与引用 ​

javascript
// math.js
exports.add = function(a, b){
    return a + b;
}

// main.js
var math = require('math')      // ./math in node
console.log(math.add(1, 2));    // 3

Magic Free Variable

NodeJS : Simple HTTP Server ​

javascript
// server.js
var http = require("http"),
    PORT = 8000;

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

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

Synchronously ​

javascript
// 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!');

同步/阻塞式加载

同步加载对服务器/本地环境并不是问题 ​

硬盘 I/O网速 I/O
HDD:100 MB/sADSL: 4 Mb/s
SSD:600 MB/s4G:100 Mb/s
SATA-III:6000 Mb/sFiber:100 Mb/s

TIP

浏览器环境才是问题!

第五日 双塔奇兵 AMD/CMD 浏览器环境模块化方案 ​

分歧和冲突 ​

  • Modules/Async
  • Modules/Wrappings
  • Modules/Transport
  • Modules/2.0
  • 《前端模块化开发的那点历史 - 玉伯》

TIP

AMD(Async Module Definition) :RequireJS 对模块定义的规范化产出

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

RequireJS - AMD Implementation (2011)

JavaScript file and module loader.
It is optimized for in-browser use

If require() is async? ​

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

function Programmer (){
    //do something
}

Programmer.prototype = new Employee();

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

this code will not work

Function Wrapping ​

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

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

looks familiar?

Sugar - simplified CommonJS wrapping ​

javascript
define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

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

    return function () {};
});

AMD vs CommonJS ​

书写风格 ​

javascript
// Module/1.0
var a = require("./a");  // 依赖就近
a.doSomething();

var b = require("./b")
b.doSomething();

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

Both OK

执行时机 ​

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

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

Early Download, Early Executing

TIP

SeaJS - CMD Implementation (2011)

Extremely simple experience of modular development

More like CommonJS Style ​

javascript
define(function(require, exports) {
    var a = require('./a');
    a.doSomething();

    exports.foo = 'bar';
    exports.doSomething = function() {};
});
javascript
// RequireJS 兼容风格
define('hello', ['jquery'], function(require, exports, module) {
    return {
        foo: 'bar',
        doSomething: function() {}
    };
});

AMD vs CMD - the truly different ​

Still Execution Time

javascript
// AMD recommended
define(['a', 'b'], function(a, b){
    a.doSomething();    // 依赖前置,提前执行
    b.doSomething();
})

// CMD recommanded
define(function(require, exports, module){
    var a = require("a");
    a.doSomething();
    var b = require("b");
    b.doSomething();    // 依赖就近,延迟执行
})

Early Download, Lazy Executing

RequireJS 最佳实践 ​

Use Case ​

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

config ​

javascript
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"
        }
    ]
})

Optimized Build ​

node r.js -o build.js

javascript
// 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 大势所趋,去掉这层包裹! ​

NPM: Node Package Manger

Browsers don't have the require method defined,

but Node.js does.

Browserify - CommonJS In Browser (2011 / 2014 stable)

require('modules') in the browser by bundling up all of your dependencies

Install ​

shell
$ npm install -g browserify

Actually You Need Do Nothing But Write CommonJS Code!

shell
# magic just happened!
$ browserify main.js -o bundle.js

A Little Low-Level ​

Browserify parses the AST for require() calls to traverse the entire dependency graph of your project.

AST: Abstract Syntax Tree

image_1715418167992.png

每一次改动都需要手动 recompile ?

No, auto-recompile.

Watch - Watchify

shell
$ npm install -g watchify

# WATCH!
$ watchify app.js -o bundle.js -v

Build 后要如何 Debug

Source Map

shell
# debug mode
$ browserify main.js -o bundle.js --debug

逆向所有合并、压缩、混淆!

Npm Run

$ npm run [command] [-- <args>]
json
// package.json
{
    //....
    "scripts": {
        "build": "browserify app.js -o bundle.js",
        "watch": "watchify app.js -o bundle.js -v"
    }
}

Webpack - Module Bundler (2014)

transforming, bundling, or packaging just about any resource or asset Webpack For Browserify Users

Webpack For Browserify Users

shell
# These are equivalent:
$ browserify main.js > bundle.js
$ webpack main.js bundle.js

but

javascript
// better with a webpack.config.js
module.exports = {
    entry: "./main.js",
    output: {
        filename: "bundle.js"
    }
}

Simple CLI ​

shell
# make sure your directory contains webpack.config.js

# Development: debug + devtool + source-map + pathinfo
webpack main.js bundle.js -d

# Production: minimize + occurence-order
webpack main.js bundle.js -p

# Watch Mode
webpack main.js bundle.js --watch

Everything is already there!

Browserify vs Webpack ​

小而美 VS 大而全

  • Browserify VS Webpack - JS Drama
  • Comparison - Webpack
  • Webpack for browserify users
  • Browserify for webpack users

Is webpack just the other Browserify? ​

WEBPACK - LIKE A PRO webpack 特别增强篇

Motivation ​

  • Compatibility (CommonJS, AMD, ES6...)
  • Code Splitting
  • Loaders & Plugins
  • Development Tools & Workflow

Using Loaders ​

javascript
// webpack.config.js
module.exports = {
    entry: './main.js',
    output: {
        filename: 'bundle.js'
    }{,
    module: {
        loaders: [{
            test: /\.js$/,
            loader: 'babel-loader'
        }]
    }}
}

JSX Demo

Why Only JavaScript?

There are many other static resources that need to be handled

Require() Static Resources!

javascript
// Ensure the stylesheet is loaded
require('./bootstrap.css');

// get a URL or DataURI
var myImage = document.createElement('img');
myImage.src = require('./myImage.jpg');

They are part of dependency graph,包含静态资源的依赖管理

Require() Anything! ​

javascript
// CSS Preprocesser
require('./style.less');
require('./anotherStyle.scss');

// Compile-to-JS Language
var myModule = require('./myModule.coffee');
var myTypedModule = require('./myTypedModule.ts');

Universal Module System,对所有模块一视同仁的的依赖管理

Sass & Images ​

javascript
var webpackConfig = {
    module: {
        loaders: [{
            test: /\.scss$/,
            loaders: 'style!css!sass'
        }, {
            test: /\.(png|jpg|svg)$/,
            loader: 'url?limit=20480' //20k
        }]
    }}
}
  • Deal with CSS problems
  • Inlining your images to DataURI

Using Plugins ​

React Hot Loader!

javascript
var config = {
    entry: ['webpack/hot/dev-server', './app/main.js'],
    module: {
        loaders: [{
            test: /\.(js|jsx)$/,
            loaders: ['react-hot', 'babel']
        }]
    },
    plugins: [
        //Enables Hot Modules Replacement
        new webpack.HotModuleReplacementPlugin(),
    ],
};

Code Splitting ​

split your codebase into “chunks” which are loaded on demand.

More Resources ​

第七日 王者归来 ES6 Module 最后的战役 ​

TIP

There Is No Module In JavaScript! (Until ECMAScript 6)

Well...But There Is No Runtime...

Babel - JavaScript Compiler (2015) ​

Use next generation JavaScript, today.

Let's get into it!! ​

Single Default Module

javascript
// math.js
export default math = {
    PI: 3.14,
    foo: function(){}
}
// app.js
import math from "./math";
math.PI
# babel magic!
$ babel-node app.js

Named Exports ​

javascript
// export Declaration
export function foo(){
    console.log('I am not bar.');
}
// export VariableStatement;
export var PI = 3.14;
export var bar = foo;   // function expression
// export { ExportsList }
var PI = 3.14;
var foo = function(){};

export { PI, foo };

Importing Named Exports ​

javascript
// 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()

"Lists" ​

javascript
// components.js
import Button from './Button';
import Header from './Header';

// export { ExportsList }
// Not ES6 Destructing. Not object property shorthand
export {
    Button,
    Header
}
// app.js
// import { ImportList }
import { Button, Header } from "./components";

For More Detail... ​

“ 习惯了就会觉得很好用的 (´Д` ) ”

Babel + browserify = Babelify

Babel + webpack = Babel-Loader

QUESTION Hey, don't be shy.