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