程序员

基于ES6的模块化开发


在没有框架的约束下,我们开发项目可能都是基于过程的,想到哪里就添加一个函数。这在项目开发的初期可能是很快的,特别是对于前端项目。但在后期修改需求的时候就发现 项目文件存在 功能不明确,职责混乱的情况,假如有Vue.js 或者Angular.js 等框架约束,这种情况会相对好些。本文记录下基于ES6 实践模块化开发的过程,稍后会把文本所用到的代码放到github项目上,欢迎各位大神指点。

Angular.js 架构图

这些MVC框架基本都着眼于以数据模型为中心,打造数据驱动的模块化前端应用。框架可能层出不穷,学也学不完,但基本的思想是不变的。以 Angular.js 的架构 为例,Component(组件) 和 Template(HTML模板) 分别代表了Web App的数据 和 视图两大部分,数据的存储、更新过程都是在我们定义的组件中,组件中包含的数据模型更新都会通过数据绑定引起视图的更新。

而用户对用户界面的操作,可以通过事先定义的各种Directive(指令),反馈到数据模型中。比如 ngModel 这样的指令就可用于绑定视图对数据模型的更新。基于Angular这样的框架开发过程中,基本就是不断写组件,写模板,写指令的过程。那么扯远了,Angular 的模块化系统和ES6 的还是有很大差异的。说了半天,框架毕竟是别人团队开发的,你大可去用,从ES6 这样的本源出发去学习实践更加以不变应万变。

ES6 简单入门

简单地说,ES6 新的特性可分为以下几点:

  • Classes and Modules (这回主要谈一谈模块)
  • New methods for strings and Arrays, Promises, Maps, Sets
  • Completely new features: Generators, Proxies

定义Class

定义一个类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ',' + this.y + ')';
  }
}

// es6 的class 等同于 function,就是构造函数
Point.prototype.constructor === Point // true

var point = new Point(2, 3);
point.toString()

point.hasOwnProperty('x') //true
point.hasOwnProperty('toString') // false
// toString 方法是原型对象Point 的属性, 而不是属于point 实例的属性,是通过查找原型链得来的。

es6 私有属性和方法定义

私有方法可以通过将 function 定义在class 作用域之外

// 例如 想要给Point 类一个私有方法
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.type = type;
  }

  print () {
     toString.call(this);
  }
}

function toString( point ) {
   return point.x + "," + point.y;
}
var type = 'Point';

class 静态方法

上面提及的类 都需要实例化后才能使用,那静态方法可以使我们无需实例化就 通过类直接调用

 class Format {
    static transform(jsonStr) {
      return JSON.parse(jsonStr);
    }
 }

  // 静态属性 extension
 Format.extension = {
   geojson: ".json"
 }

 class GeoJSON extends Format {
 }

 GeoJSON.transform("{'name': 'hello'}"   // 直接调用 静态方法

ES6转码打包


由于大部分浏览器还没有支持ES6 模块,所以可采用Babel 转
来把我们的代码转化为es5.

然后用 Webpack 打包所有js文件 为一个bundle ,也可以采用SystemJS的依赖管理方案,实现浏览器端的模块加载

由于之前在Angular的 实践过程中采用的是 SystemJS,所以这次把两种方法都讨论演示下。需要说明的是,这两种浏览器端加载es6模块的方法都需要Babel的支持,根据具体情况可选用 Webpack 或SystemJS。

模块编写过程

比如我们现在有 drone 和 bullet 两个类,drone 可以通过fire() 方法创建bullet 实例,并且通过一个全局的 RenderBullet 方法计算bullet 轨迹。

就这么简单的需求,因为drone 和bullet 在我们的游戏应用中是 基础类,所以单独写成模块。常数变量至于const.js 中。

// drone.js
import Const from './const';
import Bullet from './bullet';

/**
 * Drone class with control method.
 */
export default class Drone {
    constructor(opts) {
        this.id;
        this.speed = opts.speed ? opts.speed: 0.01;
        this.direction = opts.direction ? opts.direction: 0;
        this.name = opts.name ? opts.name: this.randomName();
        this.life = Const.DroneParam.LIFE;
        this.bullets = [];
        this.firing = false;
        this.point = {
            type: 'Point',
            coordinates: [121, 31]
        }
        this.bulletNum = 2;
    }
    // .... 省略飞控代码。。  

    fire () {
        // if not firing, start firing for specific duration.
        if (!this.firing) {
            for (let i = 0; i < this.bulletNum; i ++) {
                this.bullets.push(new Bullet(this));
            }
            this.firing = true;
            setTimeout(() => this.firing = false, Cost.DroneParam.FIRINGTIME);
        }
    }
}

下面简单看下bullet.js 的结构:

/**
 * Bullet based on Drone instance
 */
export default class Bullet {
    // opts should contain the Drone's direction and geometry
    constructor(opts) {
        this.id;
        this.direciton = opts.direction ? opts.direction: 0;
        this.spoint = {
            type: 'Point',
            coordinates: [0, 0]
        };
        // DeepCopy the drone coords to bullet.
        this.spoint.coordinates[0] = opts.point.coordinates[0];
        this.spoint.coordinates[1] = opts.point.coordinates[1];
    }
}

常量模块,包含静态属性,无需实例化直接调用:

export default class Const {
}

// Static Props outside of class definition
Const.DroneParam = {
    MAXSPEED: 3.999,
    FIRINGTIME: 800,
    LIFE: 10,
    // Firing range.. 0.2 rad in LngLat
    RANGE: 0.2 
};

至此,这就完成了几个基础模块的编写,注意: 现在drone.js, bullet.js const.js 这几个模块都在项目的src文件夹下,基于Babel 和 Webpack 转码打包需要如下过程:

Babel 和Webpack 安装配置

  • 首先npm 安装Babel 和 Webpack 库:

    npm install babel-cli babel-core babel-loader webpack babel-preset-latest –save-dev

  • 第二,配置 .babelrc 。在项目根目录下创建 .babelrc,前面有一个点啊,别说没玩过linux。。配置文件都这熊样,内容跟官网一样。

    { "presets": ["latest"] }
  • 第三,配置 webpack.config.js如下.
module.exports = {
    entry: {
        index: [
            "./src/app.js"
            ]
    },
    output: {
        path: "./dist/",
        filename: "bundle.js",
        // app.js 中导出的模块都在Alex 这个Root 命名空间下
        library: 'Alex',
        libraryTarget: 'umd',
    },
    module: {
        loaders: [
        {
            // 用babel 作为 js loader,打包前转码为es5,没有中间文件
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel'
        }]
    }
};

说明一下,entry.index 指向的 ./src/app.js 是应用的入口文件,也就是说,drone, bullet 等等模块是写好了,但是还需要一个Root 模块来导出所有模块(API模式)或者启动应用(APP模式)。 当然上述两个模式是我胡诌的,但是经过实践确实证明这两种模式对应模块化的不同需求。

  • 假如你的 业务逻辑代码 都需要 采用es6 来模块化编写(往往是大型应用),那么你的app.js 应该包含业务代码(APP模式)
  • 假如你的 模块只是作为 API 供外部代码调用,比如 f3earth 这样的采用es6 编写的 API,那么你的app.js 应该只包含模块导出的过程(API模式)

比如我的app.js 长这样:

import Drone from './drone';
// 引入自行封装的Canvas,渲染游戏场景
import Canvas from './chart/canvas';

export {
        Drone,
        Canvas
}

这里将所有子模块再次导出为一个根模块,对应webpack.config.js 中配置的名为 Alex 的根模块。在业务代码中通过 Alex.Drone, Alex.Canvas 来调用不同的类。
至此,就完成了打包前的工作,在根目录下 cmd中 通过webpack命令开始打包。完成之后,在 dist 目录下产生 bundle.js,那么这个文件包含了我们刚才所编写的所有模块,可供业务代码调用。

如果想详细了解 Babel,可以直接参考其官网栗子,各种babel 的用法(npm script,或者在webpack中作为loader)
如果想了解更多关于webpack,可以参考我看过比较简明易懂的 webpack 入门 这篇文章

写在最后

根据上面的过程,我基本编写了一个架子,有了几个基础类,但是功能还很弱,而且基于Canvas 的渲染类还在开发。你看看,这都是些造轮子的工作,但是难免有些人揍喜欢造轮子。。苏美尔人造出轮子后还是有人在不断通过造轮子学习。

最后我把项目代码放到了github上,欢迎想了解 ES6 模块化以及 Webpack 打包以及SystemJS 的同学去围观,clone 下来改装下可以打造自己的飞机大战啊哈哈! 另外也挂出我放在云服务器上Angular Demo,比较简陋,欢迎围观。