├── .gitignore ├── README.md ├── bower.json ├── doc └── api.zh.md ├── src └── underscore-template.js └── test ├── test.html └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | Thumbs.db 2 | ehthumbs.db 3 | [Dd]esktop.ini 4 | $RECYCLE.BIN/ 5 | .DS_Store 6 | .klive 7 | .dropbox.cache 8 | 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *.lnk 13 | 14 | .svn 15 | .idea 16 | 17 | node_modules/ 18 | bower_components/ 19 | npm-debug.log 20 | 21 | *.zip 22 | *.gz 23 | 24 | demo/ 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Underscore-template 2 | 3 | > More APIs for Underscore's template engine - template fetching, rendering and caching. 4 | 5 | ## 简介 6 | 7 | 著名的 JavaScript 工具库 Underscore 内置了一个小巧而完备的前端模板引擎,而 Underscore-template 对它进行了封装和增强,将它更好地融入了开发流程: 8 | 9 | * 提供简洁易用的 API,使用者不需要操心底层细节 10 | * 为各个模板命名,纳入统一的模板库,以便管理和调用 11 | * 可在 JS 中编写模板,也可以在 HTML 中编写模板;且两种方案可平滑切换 12 | * 通过两级缓存来优化性能,避免模板的重复获取和重复编译 13 | 14 | ## 兼容性 15 | 16 | 依赖以下类库: 17 | 18 | * Underscore 19 | 20 | 支持以下浏览器: 21 | 22 | * Chrome / Firefox / Safari 等现代浏览器 23 | * IE 6+ 24 | 25 | ## 安装 26 | 27 | 0. 通过 Bower 安装: 28 | 29 | ```sh 30 | $ bower install underscore-template 31 | ``` 32 | 33 | 0. 在页面中加载 Underscore-template 的脚本文件及必要的依赖: 34 | 35 | ```html 36 | 37 | 38 | ``` 39 | 40 | ## API 文档 41 | 42 | * Underscore-template 提供了简洁易用的 API,[详见此文档](https://github.com/cssmagic/underscore-template/issues/5)。 43 | * 此外,建议阅读 [Wiki](https://github.com/cssmagic/underscore-template/wiki) 来获取更多信息。 44 | 45 | ## 单元测试 46 | 47 | 0. 把本项目的代码 fork 并 clone 到本地。 48 | 0. 在本项目的根目录运行 `bower install`,安装必要的依赖。 49 | 0. 在浏览器中打开 `test/test.html` 即可运行单元测试。 50 | 51 | ## 谁在用? 52 | 53 | 移动 UI 框架 [CMUI](https://github.com/CMUI/CMUI) 采用 Underscore-template 作为轻量的前端模板解决方案,因此所有 CMUI 用户都在使用它: 54 | 55 | * [百姓网 - 手机版](http://m.baixing.com/) 56 | * [薇姿官方电子商城 - 手机版](http://m.vichy.com.cn/) 57 | * [优e网 - 手机版](http://m.uemall.com/) 58 | 59 | *** 60 | 61 | ## License 62 | 63 | [MIT License](http://www.opensource.org/licenses/mit-license.php) 64 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "underscore-template", 3 | "homepage": "https://github.com/cssmagic/underscore-template", 4 | "authors": [ 5 | "cssmagic " 6 | ], 7 | "description": "More APIs for Underscore's template engine.", 8 | "main": "src/underscore-template.js", 9 | "moduleType": [ 10 | "globals" 11 | ], 12 | "keywords": [ 13 | "underscore", 14 | "template" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "**.sh", 20 | "**.min.js", 21 | "package.json", 22 | "node_modules", 23 | "bower_components", 24 | "test", 25 | "doc", 26 | "demo" 27 | ], 28 | "dependencies": { 29 | "underscore": "1.*" 30 | }, 31 | "devDependencies": { 32 | "jquery": "*", 33 | "mocha.css": "0.1.0", 34 | "mocha": "1.*", 35 | "chai": "1.*" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /doc/api.zh.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "API 文档" 3 | --- 4 | 5 | ## 导言   6 | 7 | #### 模板引擎   8 | 9 | 模板引擎的作用是将数据渲染成为 HTML 代码片断,它将生成 HTML 代码的工作分解为“准备数据”和“编写模板”两个步骤,将开发者从繁重的字符串拼接任务中解脱出来。 10 | 11 | #### Underscore 的模板引擎   12 | 13 | Underscore 内置了一个小巧而完备的前端模板引擎(`_.template()`),而 Underscore-template 这个库则将它更好地融入了开发流程。 14 | 15 | 因此,在使用这个库之前,你需要了解 Underscore 的模板引擎。推荐资源如下: 16 | 17 | * [Underscore 模板引擎 API 官方文档](http://underscorejs.org/#template) 18 | * [中文注解](https://github.com/cssmagic/blog/issues/4) 19 | 20 | #### 功能简介   21 | 22 | 这个库的主要作用是建立一个模板库,把页面中需要前端模板管理起来,以便在需要的时候调用。 23 | 24 | 同时,它在内部做了一些性能优化,通过缓存来避免模板的重复编译。但这些优化对使用者来说是透明的,不需要特别操心。 25 | 26 | ## JavaScript 接口   27 | 28 | ### `template.add(id, template)` 29 | 30 | 定义一个模板,即将该模板纳入模板库,以便稍后使用。每个模板都需要有一个独一无二的 ID。 31 | 32 | #### 参数 33 | 34 | * `id` -- 字符串。模板名。 35 | * `template` -- 字符串。模板代码。 36 | 37 | #### 返回值 38 | 39 | 布尔值。定义模板成功时为 `true`,失败时为 `false`。 40 | 41 | #### 示例 42 | 43 | 以下代码定义了一个名为 `my-list` 的模板,我们可以在后面使用它来渲染数据。 44 | 45 | ```js 46 | template.add('my-list', [ 47 | '' 52 | ].join('\n')) 53 | ``` 54 | 55 | *** 56 | 57 | ### `template.render(id, data)` 58 | 59 | 使用指定模板来渲染数据,得到 HTML 代码片断。 60 | 61 | #### 参数 62 | 63 | * `id` -- 字符串。模板名。 64 | * `data` -- 对象(或数组等)。待渲染的数据。 65 | 66 | #### 返回值 67 | 68 | 字符串。渲染生成的 HTML 代码片断。如果出现参数缺失或模板不存在等错误,则返回空字符串。 69 | 70 | #### 示例 71 | 72 | 假设我们有以下数据: 73 | 74 | ```js 75 | var todoList = { 76 | list: [ 77 | '买一台新手机', 78 | '吃一顿大餐', 79 | '来一次说走就走的旅行' 80 | ] 81 | } 82 | ``` 83 | 84 | 以下代码即调用名为 `my-list` 的模板(上面已定义)来渲染上述数据: 85 | 86 | ```js 87 | var html = template.render('my-list', todoList) 88 | ``` 89 | 90 | 我们会得到如下 HTML 代码片断: 91 | 92 | ```html 93 | 98 | ``` 99 | 100 | ## 开发流程   101 | 102 | #### 开发阶段   103 | 104 | 由于 JS 长期缺乏原生的多行字符串功能,在 JS 文件中编辑模板很不方便。因此,在实践中,开发者们普遍将模板代码写在 HTML 中。(如下所示,我们使用一个 ` 114 | ``` 115 | 116 | 值得高兴的是,上面提到的 `.render()` 方法也接受以这种方式编写的模板。为了让它能认出这个模板,你需要为这个 ` 122 | ``` 123 | 124 | 然后,我们就可以在 JS 中直接使用这个模板了: 125 | 126 | ```js 127 | var html = template.render('my-list', todoList) 128 | ``` 129 | 130 | #### 部署到生产环境   131 | 132 | 把模板代码写在 HTML 中,意味着这部分代码会随着 HTML 页面的多次加载而重复加载,且各页面不能共享相同的模板,导致不必要的流量消耗;而把模板代码存放在 JS 文件中,则可以充分利用客户端缓存。 133 | 134 | 因此,在开发阶段,我们在 HTML 中编写并修改模板代码,直至完成功能开发;在准备部署到生产环境时,我们可以按照如下步骤把模板代码从 HTML 中转移到 JS 文件中: 135 | 136 | 0. 将 HTML 中的模板代码复制出来,转换为 JS 可用的多行字符串。 137 | 0. 用 `.add()` 方法来定义模板。 138 | 0. 在 HTML 中删除那些用于存放模板代码的 ` 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | void function () { 2 | // config 3 | _.templateSettings.variable = 'data' 4 | 5 | // const 6 | var TEST_STR = 'This is test string' 7 | var LONGER_STR = 'This substring is longer than another' 8 | var TEMPLATE_ID_1 = 'paragraph' 9 | var TEMPLATE_ID_2 = 'person' 10 | var HELLO = 'Hello world!' 11 | var PREFIX = 'template-' 12 | var SCRIPT_TYPE = 'text/template' 13 | 14 | // template code 15 | var templateCode1 = '

<%= data.text %>

' 16 | var templateCode2 = [ 17 | '

' 22 | ].join('\n') 23 | 24 | // template data 25 | var templateData1 = {text: HELLO} 26 | var templateData2 = [ 27 | {name: 'Peter', age: '31'}, 28 | {name: 'Judy', age: '24'} 29 | ] 30 | 31 | // result 32 | var result1 = '

' + HELLO + '

' 33 | var result2 = [ 34 | '

' 38 | ].join('\n') 39 | 40 | describe('Util', function () { 41 | var _trim = template.__trim 42 | var _include = template.__include 43 | var _startsWith = template.__startsWith 44 | var _endsWith = template.__endsWith 45 | var _toTemplateId = template.__toTemplateId 46 | var _toElementId = template.__toElementId 47 | var _isTemplateCode = template.__isTemplateCode 48 | 49 | describe('_trim()', function () { 50 | it('removes all spaces from the beginning and end of the supplied string', function () { 51 | var arg 52 | arg = ' foo bar ' 53 | expect(_trim(arg)).to.equal('foo bar') 54 | arg = ' foo bar' 55 | expect(_trim(arg)).to.equal('foo bar') 56 | arg = 'foo bar ' 57 | expect(_trim(arg)).to.equal('foo bar') 58 | arg = 'foo bar' 59 | expect(_trim(arg)).to.equal('foo bar') 60 | arg = '' 61 | expect(_trim(arg)).to.equal('') 62 | }) 63 | }) 64 | describe('_include()', function () { 65 | it('returns false when the length of substring is shorter than supplied string', function () { 66 | expect(_include(TEST_STR, LONGER_STR)).to.be.false 67 | }) 68 | it('returns whether substring is found within the supplied string', function () { 69 | var arg 70 | arg = 'test' 71 | expect(_include(TEST_STR, arg)).to.be.true 72 | arg = 'st st' 73 | expect(_include(TEST_STR, arg)).to.be.true 74 | arg = 'test String' 75 | expect(_include(TEST_STR, arg)).to.be.false 76 | arg = 'foobar' 77 | expect(_include(TEST_STR, arg)).to.be.false 78 | }) 79 | }) 80 | describe('_startsWith()', function () { 81 | it('returns false when the length of substring is shorter than supplied string', function () { 82 | expect(_startsWith(TEST_STR, LONGER_STR)).to.be.false 83 | }) 84 | it('returns whether a string begins with the characters of another string', function () { 85 | var arg 86 | arg = 'This' 87 | expect(_startsWith(TEST_STR, arg)).to.be.true 88 | arg = 'this' 89 | expect(_startsWith(TEST_STR, arg)).to.be.false 90 | arg = 'This i' 91 | expect(_startsWith(TEST_STR, arg)).to.be.true 92 | arg = 'foobar' 93 | expect(_startsWith(TEST_STR, arg)).to.be.false 94 | }) 95 | }) 96 | describe('_endsWith()', function () { 97 | it('returns false when the length of substring is shorter than supplied string', function () { 98 | expect(_endsWith(TEST_STR, LONGER_STR)).to.be.false 99 | }) 100 | it('returns whether a string begins with the characters of another string', function () { 101 | var arg 102 | arg = 'string' 103 | expect(_endsWith(TEST_STR, arg)).to.be.true 104 | arg = 't string' 105 | expect(_endsWith(TEST_STR, arg)).to.be.true 106 | arg = 'est String' 107 | expect(_endsWith(TEST_STR, arg)).to.false 108 | arg = 'foobar' 109 | expect(_endsWith(TEST_STR, arg)).to.be.false 110 | }) 111 | }) 112 | describe('_toTemplateId()', function () { 113 | it('returns directly if initial character is not space, `#` or `!`', function () { 114 | var arg 115 | arg = 'foobar' 116 | expect(_toTemplateId(arg)).to.equal(arg) 117 | }) 118 | it('removes `' + PREFIX + '` prefix', function () { 119 | var arg 120 | arg = PREFIX + 'foo' 121 | expect(_toTemplateId(arg)).to.equal('foo') 122 | }) 123 | it('removes all initial `#` and `!` characters', function () { 124 | var arg 125 | arg = '###foo#bar' 126 | expect(_toTemplateId(arg)).to.equal('foo#bar') 127 | arg = '!!!foo#bar' 128 | expect(_toTemplateId(arg)).to.equal('foo#bar') 129 | arg = '#!!foo!bar' 130 | expect(_toTemplateId(arg)).to.equal('foo!bar') 131 | arg = '#!!' + PREFIX + 'foo!bar' 132 | expect(_toTemplateId(arg)).to.equal('foo!bar') 133 | }) 134 | it('ignores initial and ending spaces', function () { 135 | var arg 136 | arg = ' ' 137 | expect(_toTemplateId(arg)).to.equal('') 138 | arg = ' foo ' 139 | expect(_toTemplateId(arg)).to.equal('foo') 140 | arg = ' ###bar ' 141 | expect(_toTemplateId(arg)).to.equal('bar') 142 | arg = ' #!foobar ' 143 | expect(_toTemplateId(arg)).to.equal('foobar') 144 | arg = ' #!' + PREFIX + 'foo ' 145 | expect(_toTemplateId(arg)).to.equal('foo') 146 | arg = ' #! ' + PREFIX + 'bar ' 147 | expect(_toTemplateId(arg)).to.equal('bar') 148 | }) 149 | it('converts falsy value to empty string', function () { 150 | var arg 151 | arg = undefined 152 | expect(_toTemplateId(arg)).to.equal('') 153 | arg = null 154 | expect(_toTemplateId(arg)).to.equal('') 155 | arg = false 156 | expect(_toTemplateId(arg)).to.equal('') 157 | arg = NaN 158 | expect(_toTemplateId(arg)).to.equal('') 159 | }) 160 | it('converts invalid type to empty string', function () { 161 | var arg 162 | arg = 1999 163 | expect(_toTemplateId(arg)).to.equal('') 164 | arg = {name: 'Peter', age: '20'} 165 | expect(_toTemplateId(arg)).to.equal('') 166 | arg = [1,2,3] 167 | expect(_toTemplateId(arg)).to.equal('') 168 | }) 169 | }) 170 | describe('_toElementId()', function () { 171 | it('adds `' + PREFIX + '` prefix', function () { 172 | var arg 173 | arg = 'foo' 174 | expect(_toElementId(arg)).to.equal(PREFIX + 'foo') 175 | }) 176 | it('ignores initial and ending spaces', function () { 177 | var arg 178 | arg = ' ' 179 | expect(_toElementId(arg)).to.equal(PREFIX) 180 | arg = ' foo ' 181 | expect(_toElementId(arg)).to.equal(PREFIX + 'foo') 182 | }) 183 | it('converts falsy value to `' + PREFIX + '`', function () { 184 | var arg 185 | arg = undefined 186 | expect(_toElementId(arg)).to.equal(PREFIX) 187 | arg = null 188 | expect(_toElementId(arg)).to.equal(PREFIX) 189 | arg = false 190 | expect(_toElementId(arg)).to.equal(PREFIX) 191 | arg = NaN 192 | expect(_toElementId(arg)).to.equal(PREFIX) 193 | }) 194 | it('converts invalid type to `' + PREFIX + '`', function () { 195 | var arg 196 | arg = 1999 197 | expect(_toElementId(arg)).to.equal(PREFIX) 198 | arg = {name: 'Peter', age: '20'} 199 | expect(_toElementId(arg)).to.equal(PREFIX) 200 | arg = [1,2,3] 201 | expect(_toElementId(arg)).to.equal(PREFIX) 202 | }) 203 | }) 204 | describe('_isTemplateCode()', function () { 205 | it('does basic functionality', function () { 206 | expect(_isTemplateCode(templateCode1)).to.be.true 207 | expect(_isTemplateCode(templateCode2)).to.be.true 208 | 209 | var code 210 | code = '<%= data.foo %>' 211 | expect(_isTemplateCode(code)).to.be.true 212 | code = '<%- data.bar %>' 213 | expect(_isTemplateCode(code)).to.be.true 214 | code = '<% if (data.isValid) { %>

...

<% } %>' 215 | expect(_isTemplateCode(code)).to.be.true 216 | 217 | // if we have specified variable name 218 | code = '<%= foo %>' 219 | expect(_isTemplateCode(code)).to.be.false 220 | code = '<%- bar %>' 221 | expect(_isTemplateCode(code)).to.be.false 222 | code = '<% if (true) { %>

...

<% } %>' 223 | expect(_isTemplateCode(code)).to.be.false 224 | 225 | // if we have not specified variable name 226 | var configVar = _.templateSettings.variable 227 | _.templateSettings.variable = '' 228 | code = '<%= foo %>' 229 | expect(_isTemplateCode(code)).to.be.true 230 | code = '<%- bar %>' 231 | expect(_isTemplateCode(code)).to.be.true 232 | code = '<% if (true) { %>

...

<% } %>' 233 | expect(_isTemplateCode(code)).to.be.true 234 | // restore config 235 | _.templateSettings.variable = configVar 236 | 237 | code = undefined 238 | expect(_isTemplateCode(code)).to.be.false 239 | code = '' 240 | expect(_isTemplateCode(code)).to.be.false 241 | code = null 242 | expect(_isTemplateCode(code)).to.be.false 243 | code = 'foobar' 244 | expect(_isTemplateCode(code)).to.be.false 245 | code = '

foobar

' 246 | expect(_isTemplateCode(code)).to.be.false 247 | }) 248 | }) 249 | }) 250 | 251 | describe('APIs', function () { 252 | // elem 253 | var $body = $(document.body) 254 | 255 | // test data 256 | var data 257 | var html1 258 | var html2 259 | var _cacheTemplate = template.__cacheTemplate 260 | var _cacheCompiledTemplate = template.__cacheCompiledTemplate 261 | 262 | // string 263 | function clean(str) { 264 | return str.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ') 265 | } 266 | 267 | // util 268 | function clearCodeCache() { 269 | delete _cacheTemplate[TEMPLATE_ID_1] 270 | delete _cacheTemplate[TEMPLATE_ID_2] 271 | } 272 | function clearCompiledCache() { 273 | delete _cacheCompiledTemplate[TEMPLATE_ID_1] 274 | delete _cacheCompiledTemplate[TEMPLATE_ID_2] 275 | } 276 | 277 | // dummy script elements 278 | var $elem1 279 | var $elem2 280 | function prepareDummyScript() { 281 | $elem1 = $('