基于ES6的模块化开发(附件:基于Angular-cli的WorkTile效果)


在没有框架的约束下,我们开发项目可能都是基于过程的,想到哪里就添加一个函数。这在项目开发的初期可能是很快的,特别是对于前端项目。但在后期修改需求的时候就发现 项目文件存在 功能不明确,职责混乱的情况,假如有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-cli的WorkTile Demo,比较简陋,欢迎围观。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容