在沒有框架的約束下,我們開發(fā)項(xiàng)目可能都是基于過程的,想到哪里就添加一個(gè)函數(shù)。這在項(xiàng)目開發(fā)的初期可能是很快的,特別是對于前端項(xiàng)目。但在后期修改需求的時(shí)候就發(fā)現(xiàn) 項(xiàng)目文件存在 功能不明確,職責(zé)混亂的情況,假如有Vue.js 或者Angular.js 等框架約束,這種情況會(huì)相對好些。本文記錄下基于ES6 實(shí)踐模塊化開發(fā)的過程,本文所用到的代碼在github項(xiàng)目上,歡迎各位大神指點(diǎn)。

這些MVC框架基本都著眼于以數(shù)據(jù)模型為中心,打造數(shù)據(jù)驅(qū)動(dòng)的模塊化前端應(yīng)用??蚣芸赡軐映霾桓F,學(xué)也學(xué)不完,但基本的思想是不變的。以 Angular.js 的架構(gòu) 為例,Component(組件) 和 Template(HTML模板) 分別代表了Web App的數(shù)據(jù) 和 視圖兩大部分,數(shù)據(jù)的存儲、更新過程都是在我們定義的組件中,組件中包含的數(shù)據(jù)模型更新都會(huì)通過數(shù)據(jù)綁定引起視圖的更新。
而用戶對用戶界面的操作,可以通過事先定義的各種Directive(指令),反饋到數(shù)據(jù)模型中。比如 ngModel 這樣的指令就可用于綁定視圖對數(shù)據(jù)模型的更新?;贏ngular這樣的框架開發(fā)過程中,基本就是不斷寫組件,寫模板,寫指令的過程。那么扯遠(yuǎn)了,Angular 的模塊化系統(tǒng)和ES6 的還是有很大差異的。說了半天,框架畢竟是別人團(tuán)隊(duì)開發(fā)的,你大可去用,從ES6 這樣的本源出發(fā)去學(xué)習(xí)實(shí)踐更加以不變應(yīng)萬變。
ES6 簡單入門
簡單地說,ES6 新的特性可分為以下幾點(diǎn):
- Classes and Modules (這回主要談一談模塊)
- New methods for strings and Arrays, Promises, Maps, Sets
- Completely new features: Generators, Proxies
定義Class
定義一個(gè)類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ',' + this.y + ')';
}
}
// es6 的class 等同于 function,就是構(gòu)造函數(shù)
Point.prototype.constructor === Point // true
var point = new Point(2, 3);
point.toString()
point.hasOwnProperty('x') //true
point.hasOwnProperty('toString') // false
// toString 方法是原型對象Point 的屬性, 而不是屬于point 實(shí)例的屬性,是通過查找原型鏈得來的。
es6 私有屬性和方法定義
私有方法可以通過將 function 定義在class 作用域之外
// 例如 想要給Point 類一個(gè)私有方法
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 靜態(tài)方法
上面提及的類 都需要實(shí)例化后才能使用,那靜態(tài)方法可以使我們無需實(shí)例化就 通過類直接調(diào)用
class Format {
static transform(jsonStr) {
return JSON.parse(jsonStr);
}
}
// 靜態(tài)屬性 extension
Format.extension = {
geojson: ".json"
}
class GeoJSON extends Format {
}
GeoJSON.transform("{'name': 'hello'}" // 直接調(diào)用 靜態(tài)方法
ES6轉(zhuǎn)碼打包
由于大部分瀏覽器還沒有支持ES6 模塊,所以可采用Babel 轉(zhuǎn)
碼 來把我們的代碼轉(zhuǎn)化為es5.
然后用 Webpack 打包所有js文件 為一個(gè)bundle ,也可以采用SystemJS的依賴管理方案,實(shí)現(xiàn)瀏覽器端的模塊加載。
由于之前在Angular的 實(shí)踐過程中采用的是 SystemJS,所以這次把兩種方法都討論演示下。需要說明的是,這兩種瀏覽器端加載es6模塊的方法都需要Babel的支持,根據(jù)具體情況可選用 Webpack 或SystemJS。
模塊編寫過程
比如我們現(xiàn)在有 drone 和 bullet 兩個(gè)類,drone 可以通過fire() 方法創(chuàng)建bullet 實(shí)例,并且通過一個(gè)全局的 RenderBullet 方法計(jì)算bullet 軌跡。
就這么簡單的需求,因?yàn)閐rone 和bullet 在我們的游戲應(yīng)用中是 基礎(chǔ)類,所以單獨(dú)寫成模塊。常數(shù)變量至于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 **的結(jié)構(gòu):
/**
* 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];
}
}
常量模塊,包含靜態(tài)屬性,無需實(shí)例化直接調(diào)用:
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
};
至此,這就完成了幾個(gè)基礎(chǔ)模塊的編寫,注意: 現(xiàn)在drone.js, bullet.js const.js 這幾個(gè)模塊都在項(xiàng)目的src文件夾下,基于Babel 和 Webpack 轉(zhuǎn)碼打包需要如下過程:
Babel 和Webpack 安裝配置
- 首先npm 安裝Babel 和 Webpack 庫:
npm install babel-cli babel-core babel-loader webpack babel-preset-latest --save-dev
- 第二,配置 .babelrc 。在項(xiàng)目根目錄下創(chuàng)建 .babelrc,前面有一個(gè)點(diǎn)啊,別說沒玩過linux。。配置文件都這熊樣,內(nèi)容跟官網(wǎng)一樣。
{ "presets": ["latest"] }
- 第三,配置 webpack.config.js如下.
module.exports = {
entry: {
index: [
"./src/app.js"
]
},
output: {
path: "./dist/",
filename: "bundle.js",
// app.js 中導(dǎo)出的模塊都在Alex 這個(gè)Root 命名空間下
library: 'Alex',
libraryTarget: 'umd',
},
module: {
loaders: [
{
// 用babel 作為 js loader,打包前轉(zhuǎn)碼為es5,沒有中間文件
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel'
}]
}
};
說明一下,entry.index 指向的 ./src/app.js 是應(yīng)用的入口文件,也就是說,drone, bullet 等等模塊是寫好了,但是還需要一個(gè)Root 模塊來導(dǎo)出所有模塊(API模式)或者啟動(dòng)應(yīng)用(APP模式)。 當(dāng)然上述兩個(gè)模式是我胡謅的,但是經(jīng)過實(shí)踐確實(shí)證明這兩種模式對應(yīng)模塊化的不同需求。
- 假如你的 業(yè)務(wù)邏輯代碼 都需要 采用es6 來模塊化編寫(往往是大型應(yīng)用),那么你的app.js 應(yīng)該包含業(yè)務(wù)代碼(APP模式)
- 假如你的 模塊只是作為 API 供外部代碼調(diào)用,比如 f3earth 這樣的采用es6 編寫的 API,那么你的app.js 應(yīng)該只包含模塊導(dǎo)出的過程(API模式)
比如我的app.js 長這樣:
import Drone from './drone';
// 引入自行封裝的Canvas,渲染游戲場景
import Canvas from './chart/canvas';
export {
Drone,
Canvas
}
這里將所有子模塊再次導(dǎo)出為一個(gè)根模塊,對應(yīng)webpack.config.js 中配置的名為 Alex 的根模塊。在業(yè)務(wù)代碼中通過 Alex.Drone, Alex.Canvas 來調(diào)用不同的類。
至此,就完成了打包前的工作,在根目錄下 cmd中 通過webpack命令開始打包。完成之后,在 dist 目錄下產(chǎn)生 bundle.js,那么這個(gè)文件包含了我們剛才所編寫的所有模塊,可供業(yè)務(wù)代碼調(diào)用。
如果想詳細(xì)了解 Babel,可以直接參考其官網(wǎng)栗子,各種babel 的用法(npm script,或者在webpack中作為loader)
如果想了解更多關(guān)于webpack,可以參考我看過比較簡明易懂的 webpack 入門 這篇文章
寫在最后
根據(jù)上面的過程,我基本編寫了一個(gè)架子,有了幾個(gè)基礎(chǔ)類,但是功能還很弱,而且基于Canvas 的渲染類還在開發(fā)。你看看,這都是些造輪子的工作,但是難免有些人揍喜歡造輪子。。蘇美爾人造出輪子后還是有人在不斷通過造輪子學(xué)習(xí)。
最后我把項(xiàng)目代碼放到了github上,歡迎想了解 ES6 模塊化以及 Webpack 打包以及SystemJS 的同學(xué)去圍觀,clone 下來改裝下可以打造自己的飛機(jī)大戰(zhàn)啊哈哈! 另外也掛出我放在云服務(wù)器上的基于Angular-cli的WorkTile Demo,比較簡陋,歡迎圍觀。