代码 + README,似乎不能很好地表达自己,之前弄了 Brix Core 与 其演示项目, 并在两个项目的说明档里表达了自己做这两件事情的想法,但是从反馈上来看,没能顺利达到目标。
所以写了这篇博文,说明一下为什么我要简化 Brix 即我所设想的 Brix 又该是如何。
当我们讨论 Brix 时,我们在讨论什么
李牧老师为 Brix 组件框架画了个 蓝图, 同时要求组件可以拥有一些高级特性,例如局部刷新、嵌套组件等。
要完成以上这些目标,Brix 必须有如下身份:
- Style: 基础样式
- Loader: 组件加载器
- Manager: 组件管理工具
- Base: 组件基类
- Gallery:基础组件
Loader 与 Base 都应该能够加载其当前持有节点下的所有组件,即所有拥有 @bx-name 属性的子节点。 Loader 自身需要提供组件的版本、时间戳配置,提供指定区块加载。Base 则需要提供某种方式以获取子模板, 并可以在其关联数据发生变更时,重新渲染这部分子模板,并加载渲染结果中可能有的子组件。
Manager ,依赖于 Loader 形成的配置规范。Style 则提供组件的基础样式。Gallery 则提供基本的组件, 例如面包屑导航、下拉选择等。
而目前我们是将这五者放在一块谈,Style 里有部分是以 Gallery 形式出现的,Loader 需要确立的东西显得混乱,让 Manager 有点无所适从,Base 提供的功能,缺乏明确的文档、用例来说明。
当我们谈论 Brix 时,它可能是其中任何一个角色,太容易因为蓝图太过宏伟而吓跑用户了。 我所设想的 Brix 是,包含以上所有功能,但各个模块各司其职,明确其接口,简化各自的使用方式:
想要像 Bootstrap 那样使用 Brix?你只要引入 brix.css;
那 Bootstrap 里的弹窗、提示浮层又在哪里?你只要加上 KISSY.use('brix/app')
,并在你的 HTML
里像 Bootstrap 要求的那样标记出你的组件;
<div bx-app>
<div bx-name="brix/dialog">
<h2>Congrats!</h2>
<p>There's a Nigeria pricess wants to marry you!</p>
<span class="btn btn-blue">Yay!</span>
</div>
</div>
想要利用 Brix 提供的组件封装自己的?你只需要:
KISSY.add('myapp/dropdown/index', function(S, Dropdown) {
function MyDropdown(opts) {
MyDropdown.superclass.constructor.call(this, opts)
}
S.extend(MyDropdown, Dropdown)
return MyDropdown
}, {
requires: ['brix/dropdown']
})
然后在你的页面中与默认组件一样使用自己的组件:
<div bx-name="myapp/dropdown">
<ul>
<li>Please select...</li>
<li>One</li>
<li>Two</li>
<li>睡!</li>
</ul>
</div>
想要分享自己的组件,或者安装别人的组件? npm install bpm -g
,让 BPM 来帮你做剩下的事情吧亲。
所以,我想做的事情是:
拆掉 Brix
拆成四个项目:
- Brix Core:提供加载器与 Base
- Brix Style:提供基础样式
- Brix Gallery 提供基本组件
- Brix Manager 组件依赖管理工具
Brix Style 相对独立,我已经提出来,放到了 brixjs/brix-style。
Brix Core 是后两者(Gallery 与 Manager)的依赖,需要明确组件目录结构,开发、线上组件加载方式等, 是重中之重,根据之前的一些想法,我写了个演示项目,放在 dotnil/brix-core,并在其说明档中解释了一些。 但是如篇首所说,效果不彰,因此有了本文。
Brix Gallery 与 Brix Manager 容后再谈。
设想的使用方式
组件的文件组织
目录下会有一到五个文件:
- index.{less,css}
- index.js
- template.html
- template.js
- data.json
除了 template.js,组件开发者需要提供剩余四个文件,index.less 或者 index.css 均可, 推荐使用前者。
data.json 与 template.html 结合,可以渲染出一份示例 HTML 片段,用于演示此组件需要的数据输入, 和 index.js 所需要的 DOM 结构。
template.js 是 template.html 的编译结果,在线上使用时,会采用编译后的内容,即前者。
项目的文件组织
项目的初始目录中,需要含有两个目录,用来放置相关组件:
- imports
- components
imports 即此项目引入的组件,结构如下:
imports
├── ux.shopping-ads
│ └── ceiling
│ ├── 0.1.0
│ │ └── index.js
│ └── 0.1.1
│ └── index.js
└── ux.tanx
├── dropdown
│ └── 0.1.5
│ ├── index.js
│ ├── template.html
│ └── template.js
├── grid
│ └── 0.2.1
│ └── index.js
└── message
└── 0.1.2
└── index.js
引入的组件基本路径为 imports/ux.shopping-ads/ceiling/0.1.0/
:
+--------------------+-------------------+
| imports | |
| └──ux.shopping-ads | namespace |
| └──ceiling | component name |
| └──0.1.0 | component version |
+--------------------+-------------------+
components 即此项目自身的组件,结构如下:
components
└── ux.brix-test
├── ceiling
│ └── index.js
└── footer
├── index.js
└── template.html
项目自身的组件基本路径为 components/ux.brix-test/ceiling/
。与 imports 目录的区别是,
components 目录中,不需要在组件名目录下创建组件的相关版本目录,直接写组件的相关文件就可以了。
以上内容,与当前版本的 Brix 出入不大,只是在 components 目录设计中,加入了一层当前项目的命名空间, 即示例中的 ux.brix-test。
组件的使用方式
在现有的 Brix 组件名划分中,有三种组件名:
- 业务组件:components/ceiling
- 外部组件:imports/ux.tanx/ceiling/0.1.0
- 核心组件:brix/gallery/ceiling/0.1.2
我建议将这三种组件名统一掉,一律为
- 业务组件:ux.brix-test/ceiling
- 外部组件:ux.tanx/ceiling
- 核心组件:brix/ceiling
外部组件,本来就是从各个业务组件中提炼出来的,而核心组件,除了命名空间为 brix 之外,也不应有何不同。 并且,这里我去掉了版本,将其提取出来,放到统一的地方配置。
名字统一掉之后,在页面中,我们只需要统一用 @bx-name 来引用:
<div bx-name="ux.tanx/ceiling">
<p>你好,逸才</p>
<span>已买到的宝贝</span>
</div>
不再像现有 Brix 实现那样,还需要额外指定一下 @bx-path,说明一下这个组件是从哪儿来的。
那我该怎么锁定组件的版本呢?得先从 Brix Core 的入口模块说起:
Brix Core
brix/app
Brix 将暴露出一个叫做 brix/app 的入口模块,项目开发者只需要像平常 KISSY 模块一般使用它:
KISSY.use('brix/app', function(S, app) {
// config and boot your app
})
以下代码示例,为求简单,只讲包装其中的部分,省略外层的
KISSY.use('brix/app', function(S, app) { ... })
app 提供暴露出一些常用方法,例如:
- config
- set
- boot
- on
- bootStyle
config 用来配置你的 app,不管是单页应用还是传统页面,我们更能接受的都是一个页面一次配置, 即页面初始化,加载 brix/app,配置 app,这些事情,只需要做一次。通常项目需要配置的内容如下:
app.config({
namespace: 'ux.brix-test', // 项目自己的 namespace
imports: { // 引入的组件
'ux.tanx': {
ceiling: '0.1.2'
}
},
components: ['footer'], // 项目自身组件,app.bootStyle 需要用到
timestamp: 130416 // 时间戳,项目发布时写入
})
在这其中,必配项为:
- namespace
- imports
- components (如果你需要 brix/app 帮你加载样式,不推荐这样做,后面会介绍更好的办法)
timestamp 在开发时不需配置,在项目资源文件发布之后,再另行配置即可。实际页面里的初始化, 可能类似这样:
<script src="http://a.tbcdn.cn/s/kissy/1.3.0/seed.js"></script>
<script>
KISSY.config('packages', {
brix: {
base: 'http://a.tbcdn.cn/s/brix/'
}
})
KISSY.use('brix/app', function(S, app) {
app.config({
namespace: 'ux.brix-test',
imports: {
// import 进来的组件
},
components: [
// ux.brix-test 项目自身的组件
],
timestamp: 130416
})
})
</script>
这样,即可在开发时忘掉时间戳这茬,上线时又可以先发布资源文件,再更新 vm 或者 TMS 中的 timestamp 来完成发布流程。
配置的部分讲完了,接下来就是启动页面,方式如下:
// 如果只需要初始化页面中所有组件,其他啥也不用做:
app.boot() // 等同于 app.boot('[bx-app]')
// 即如果选择器参数忽略,会自动找有自定义属性 bx-app 标记的节点
// 返回 brix/page 模块的实例
// 如果需要传递数据,以便使用模板的组件正确渲染:
app.boot({
// 数据
// ...
}) // 等同于 app.boot('[bx-app]', { ... })
// 如果只需要初始化某个节点(单页应用中的某个 View)
app.boot('#J_someView', { ... })
// 如果需要初始化完毕之后进行额外的操作:
app.boot('#J_myView', { ... }).on('bx:ready', function() {
// app.boot() 返回的是 brix/page 实例,可以通过监听该实例的 bx:ready 事件,
// 以进一步操作被初始化页面中组件。
// 按照组件名查找组件,可能返回多个
this.bxFind('ux.brix-test/ceiling')
// 按照组件所在节点 ID 查找组件,返回结果唯一
this.bxFind('#J_ceiling')
})
那个 page 参数是神马? page 即 Brix Core 暴露出来的另一个模块,brix/page,它只在 app.boot 触发的 bx:ready 事件回调中返回,用处是持有当前被初始化的节点,以及该节点下直属的组件。
通过 page.find,用户即可查找该 HTML 区域中的组件,对其进行进一步操作。
至此,brix/app 模块就基本讲完了,它做的事情非常简单,提供用户配置组件版本、自定义组件的时间戳, 让用户通过 app.boot 方法初始化整个页面、或者某个局部节点。
为什么不推荐使用 app.bootStyle
app.bootStyle 做的事情是,获取 app.config 中配置掉的所有 imports 与 components, 将它们的 index.css 一并 KISSY.use 一下,类似:
KISSY.use('ux.brix-test/ceiling/index.css, ux.tanx/table/index.css')
这里的问题在于,CSS 文件的优化空间很小,优化不到两个不同组件引入了相同 CSS 模块的情况, 例如可能有类似以下情况:
// ux.brix-test/ceiling/index.scss
@import "brix/mixins.scss";
.yangqi {
@include border-radius(2px);
}
// ux.tanx/table/index.scss
@import "brix/mixins.scss";
.shangdangci {
@include border-radius(5px);
}
在这俩组件生成的 index.css 里,可能都会有份 brix/mixins 的编译结果,但在项目级别上看, 很容易就发现这 mixins.scss ,不应该存在多份。
如果能够做到在项目开发时,样式与脚本分开处理,使用 less 或者 scss 来自动打包项目的样式文件, 效果应该会好很多。
imports 对象该如何生成
app.config('imports', { ... })
参数里,所需要传入的对象是这种结构:
{
'ux.tanx': {
breadcrumb: '0.1.0',
dropdown: '0.1.2'
},
'ux.diamond': {
table: '0.1.4',
list: '0.2.0'
},
brix: {
kwicks: '0.3.1'
}
}
假如项目使用到的外部与标准组件十分多,这个对象会变得非常大,难以维护。所以在设计上,这个文件理应提出来, 作为一个单独的 JS 或者 JSON 文件,放在项目根部,在实际页面中只需要引用这份 JS 即可。我想,应该有个 Brixlock.js 文件,它的内容类似:
var Brixlock = { ... } // imports 对象的实际内容
注意,imports 对象需要描述当前项目 所有 引入的组件的版本,这样才能保证 brix/app 在根据 bx-name 初始化页面时可以顺利进行。但在实际应用中这么做肯定是不现实的,所以完整的解决方案, 应该有两个文件。
项目根目录下,会有个 brix.json 文件,这个文件描述了这个项目中直接使用到的组件版本,描述格式类似 Rails 项目的 Gemfile,或者 NPM 的 package.json :
{
"imports": {
"ux.tanx": {
"ceiling": "~ 0.1.0"
}
},
"components": [ // 如果不写,则解析 components 下所有组件
"nav",
"sidenav"
]
}
然后,在 components/ux.brix-test/nav 和 components/ux.brix-test/sidenav 目录中, 则有 brick.json 文件,结构同样类似 package.json ,用于描述当前组件所依赖的父组件, 以 components/ux.brix-test/nav 为例:
{
"name": "ux.brix-test/nav",
"version": "0.1.0", // 因为是项目组件,此处版本写不写无所谓,只在发布时需要
"dependencies": {
"brix/dropdown": "~ 0.1.0",
"brix/checkbox": "~ 0.1.0"
}
}
imports/ux.tanx/ceiling/0.1.0 目录下,自然也会有这么一份 brick.json ,内容类似:
{
"name": "ux.tanx/ceiling",
"version": "0.1.0",
"dependencies": {
"brix/dropdown": "~ 0.1.0"
}
}
但这份文件不需要使用者关心,我们可以拜托 Brix Manager 来帮忙做这件事情。
所以,理想的情况,应当是用户在开发一个页面时,通过 brix.json 文件来描述当前页面会直接使用到的组件, 并且以比较宽松的方式指定外部组件的版本,如果需要限定自定义组件的依赖查找,则在额外写个 components 数组,最终形成类似前文所述那种描述。
剩下的事情,就拜托 Brix Manager 来做,让它去下载外部组件的 brick.json ,去找自定义组件的 brick.json ,解析它们,如果它们所依赖的组件还有依赖,则继续往上找,直到所有依赖都获取完毕, 形成了多颗倒着长的树。这个时候,再去把这棵树铺平,找到满足依赖关系的所有组件,把它们都下载下来, 放到 imports 目录,与最终所下载的确切版本一起,把这个铺平的映射,反映到 Brixlock.js 文件中去。
这里有两件比较有难度的事情:
- 需要写明组件的依赖,同时在 brick.json 中,又不能写得太死;
- 组件的版本需要满足一定规则,例如 http://semver.org,使得这一切依赖关系是可以推断的。
说到底,这对 Brix Manager 是个大挑战。
Brix Manager 该当如何
简单说,Brix Manager 需要做两件事情:
- publish 组件到仓库,并将相应版本发布到 CDN
- install 组件到本地
但同时,Brix Manager 需要支持一些高级特性,包括:
- 从一段 HTML 片段中解析需要引入的组件,生成或者更新 brix.json,并形成 Brixlock.js
- 从 brix.json 与 brick.json 解析出整个项目的依赖组件图,最终形成 Brixlock.js
组件依赖如何处理
我设想的 Brix 组件,是应该有其依赖的。具体做法,思考 ing,容后谈。