├── .gitignore ├── .npmignore ├── README.backup.md ├── README.md ├── app └── index.js ├── changelog.md ├── controller └── index.js ├── decorator └── index.js ├── directive └── index.js ├── factory └── index.js ├── filter └── index.js ├── generator-util.js ├── karma-init └── index.js ├── package.json ├── route └── index.js ├── subgenerator-base.js ├── templates ├── common │ ├── _Gruntfile.js │ ├── _README.md │ ├── _bower.json │ ├── _package.json │ ├── app │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── yeoman.png │ │ └── index.html │ ├── grunt-task-params.js │ └── sprite.css.handlebars ├── javascripts │ ├── _preinstall │ │ ├── base │ │ │ ├── base.js │ │ │ ├── directives │ │ │ │ ├── dialog-confirm.js │ │ │ │ ├── dialog-subpage.js │ │ │ │ └── msg.js │ │ │ └── services │ │ │ │ ├── dialog.js │ │ │ │ ├── loading.js │ │ │ │ ├── log.js │ │ │ │ ├── msg.js │ │ │ │ ├── page-title.js │ │ │ │ ├── resource.js │ │ │ │ ├── root-data.js │ │ │ │ ├── template.js │ │ │ │ ├── util.js │ │ │ │ └── xhr.js │ │ └── services │ │ │ └── resource-pool.js │ ├── app.js │ ├── controller.js │ ├── decorator.js │ ├── directive.js │ ├── e2e │ │ ├── protractor.conf.js │ │ └── spec.js │ ├── factory.js │ ├── filter.js │ ├── unit-mock │ │ └── mock.js │ └── unit │ │ ├── controller.js │ │ ├── directive.js │ │ ├── filter.js │ │ └── service.js ├── mock │ └── mock.json ├── styles │ ├── page.less │ └── simple │ │ ├── _variable.less │ │ ├── all.less │ │ ├── common.less │ │ └── sprites.css └── views │ ├── _preinstall │ └── _widgets │ │ ├── dialog-confirm │ │ └── dialog-confirm.html │ │ └── msg │ │ └── msg.html │ └── view.html ├── test └── test-app.js ├── view └── index.js └── webdriver ├── chromedriver_mac32.zip └── chromedriver_win32.zip /.gitignore: -------------------------------------------------------------------------------- 1 | #directories 2 | node_modules/ 3 | .idea/ 4 | 5 | #files 6 | .DS_Store 7 | 8 | 9 | npm-debug.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | #directories 2 | node_modules/ 3 | .idea/ 4 | 5 | #files 6 | .DS_Store 7 | 8 | 9 | npm-debug.log -------------------------------------------------------------------------------- /README.backup.md: -------------------------------------------------------------------------------- 1 | # generator-ngstone 2 | used to build a **AngularJS** project scaffolding. 3 | 4 | > [Yeoman](http://yeoman.io) generator 5 | 6 | ##**此分支用于向移动端迁移,如果你还能看到这句话,说明文档还未更新!!!** 7 | 8 | ##这是个什么东东? 9 | 10 | 这是一个用于生成Angular项目工程脚手架的**工具**,在你项目的整个生命周期都可以使用它,包括初始化,生成指定模块,就是用命令行完成我们以前需要人肉完成的重复劳动。目的就是大大提高我们的开发效率,且有利于统一代码风格。它将带你进入 *前端工业化时代* ! 11 | 12 | 它本质上也是一个nodejs的程序包,由yo调用,那yo又是什么呢? 13 | yo是用于各种generator的基础工具,不仅仅可以用于Angular,甚至其他语言的项目脚手架也可以生成。在你用过之后,很可能也会有激情自己也去写一个generator. 14 | 15 | ##它是怎么来的 16 | 17 | 拜读过大漠前辈的《用AngularJS开发下一代web应用》,里面提到了yeoman(yo)这个工具。上网查了一下,是一个很牛X的东西。官网提供了angular的generator,我安装后,在实际项目中使用起来非常爽,不过还是有很多地方想要有自己的定制,并且天朝的网络状况十分糟糕(你懂的),官方版不太适合在天朝使用,于是就参照官方的generator-angular花了一个多星期研究,终于诞生了第一版,然后就开始写文档了。欢迎fork,欢迎提出宝贵的意见~ 18 | 19 | ##工具说明 20 | 21 | 【名词解释】: 22 | *目标工程*:指通过本工具生成的Angular项目工程 23 | 24 | 本工具参考[generator-angular](https://github.com/yeoman/generator-angular)完成开发。在很多细节考虑不如官方的周全,而且很多地方作了简化处理,以后会逐步完善。 25 | 26 | * 建议对Angular已经有一定了解之后,再来使用本工具 27 | * 浏览器兼容性:不考虑低版本IE,建议限定目标用户使用webkit系浏览器。 28 | * 样式方案采用less,因为目前很多IDE都有内置自动编译插件(比如本人使用的webstorm),所以gruntfile里就暂不添加less任务了 29 | 30 | 目标工程安装将以下模块(暂不可更改): 31 | 32 | 1. bootstrap#3.3.0 33 | 2. fontawesome~4.2.0 34 | 3. jquery~2.1.0 35 | 4. angular~1.2.0 36 | 5. angular-sanitize~1.2.0 37 | 6. angular-animate~1.2.0 38 | 7. angular-route~1.2.0 39 | 8. angular-bootstrap~0.12.0 40 | 41 | --- 42 | 43 | * 目标工程不使用官方的resource模块,因为在ajax请求上,本人作了一些定制,所以参照官方的resource自己写了一个(基础服务模式自动安装,下面会讲到)~ 44 | * 目标工程ajax请求遵循RESTful风格,resource使然,当然如果你有特殊需求,发送非REST请求也是可以的 45 | * 目标工程对单元测试方面还需完善,工具本身的自动化测试也需要完善。 46 | * 本工具只是帮助你完成一些基础性的代码,很多细节还需要你自己来把控,千万不要**过度依赖**哦~ 47 | 48 | 49 | ###功能 50 | 51 | * 初始化目标工程目录结构,下载开源组件(通过bower),下载npm模块,生成Gruntfile.js 52 | * 通过命令生成controller,directive,factory,filter,decorator以及相应的测试文件 53 | * 可根据需要,选择是否生成单元测试或e2e测试的文件 54 | * 可根据需要,选择使用angular-route或angular-ui-router 55 | * 工具内置 *标准* 与 *安装基础服务与布局* 两种初始化模式,其中 *标准* 模式的功能与官方版大致相同,可根据需要进行选择 56 | 57 | 58 | 以下功能与本工具无直接关系,只不过是事先在gruntfile里做好了相应的配置,命令只是把gruntfile拷贝到你的项目里。如果你觉得好用,拷贝到别的项目里也是可用的~ 59 | 60 | * Gruntfile内容已经写入了一个完整可用的开发,构建,测试流程。只需执行相应grunt的命令即可。 61 | * 本项目设置了一个简单的数据模拟机制,开发时无需依赖后端,后面会继续讲到 62 | * 开发过程中,grunt任务会监听所有app下的所有js,css,html和所有数据模拟json文件的内容变化,一旦发生变化立即刷新页面,省去了人工刷新~ 63 | * 官方版并没有将模板文件进行打包,只是简单的作了htmlmin,构建后,还是需要通过ajax获取模板,本工具调用了grunt-angular-templates任务将视图文件打包放入angular的$templateCache,构建后即可通过缓存读取模板,提高应用性能 64 | * 关于angular的依赖注入写法,对angular有一定了解的同学应该都知道,有三种方式 *推测注入* 、 *显式注入* 、 *内联注入*(也有其他的叫法,暂时这么叫吧~),其中 *推测注入* 方式在ugilfy后不可用。但是在目标工程里,我们只需要使用 *推测注入* 即可,也就是通过形参让Angular自己去查找依赖,构建过程中,在ugify之前,会通过grunt-ng-annotate任务把 *推测注入* 改为 *内联注入* ,是不是很方便~不过这是官方版提供的,我把它挪过来了~ 65 | 66 | ##如何使用 67 | 68 | 因为这是一个npm程序包,所以它要运行在nodejs环境,安装nodejs的步骤,这里略过。 69 | 70 | ###1.安装yo bower grunt-cli generator-ngstone 71 | 如果安装过yo bower grunt-cli请忽略,仅安装本工具即可 72 | ``` 73 | npm install -g yo bower generator-ngstone 74 | ``` 75 | 0.2.8版本后将向移动端迁移 76 | ###2.初始化你的项目 77 | 78 | 找一个合适的地方创建你的工程根目录并进入此新目录(比如bookstore,本工具所有命令都是在此根目录下执行): 79 | 80 | ``` 81 | mkdir bookstore && cd $_ 82 | ``` 83 | (win平台的同学可忽略上面的命令,采用其他方式新建目录) 84 | 初始化: 85 | ``` 86 | yo ngstone 87 | ``` 88 | 这里会询问三个问题: 89 | 90 | 1. *希望使用哪一个路由组件?* 91 | 提供官方的angular-route和第三方的angular-ui-router连个选项,使用上下箭头选择,然后回车,后面工具会根据选择结果作相应初始化 92 | 我们这里先选择官方的angular-route,angular-ui-router后续再讲 93 | 2. *是否需要单元测试文件?(运行单元测试需要额外初始化单元测试环境)(Y/n)* 94 | 若选择y,会在初始化时以及创建controller、directive等模块时添加单元测试所需的测试文件 95 | 选择n则不创建 96 | 默认y 97 | 这里直接回车,即选择y 98 | 3. *是否需要e2e测试文件?(运行e2e测试需要自行安装全局protractor)(Y/n)* 99 | 若选择y,会在创建route模块时添加e2e测试所需的测试文件 100 | 选择n则不创建 101 | 默认y 102 | 这里直接回车,即选择y 103 | 4. *是否初始化基础服务与布局?(Y/n)* 104 | 若选择y,将会**深度**创建一系列的基础service和指令供你使用,还会初始化样式,这套基础服务与布局,适合比较开发桌面管理系统,如果是移动端项目,还需要作一些修改。 105 | 我们这里先选择n,初始化简单版的基础工程。 106 | 由于工具会调用`bower install` 和 `npm install`下载组件和模块,可能过程会比较慢,由你当前的网速而定~ 107 | 初始化完成后,你会发现bookstore下,生成了很多目录与文件。 108 | 简单介绍一下: 109 | 110 | ``` 111 | app/ //项目的html,js,less,css都在这里 112 | mock/ //无后端开发时,需要模拟数据,所有的数据模拟文件都在这里 113 | test/ //所有的测试文件与配置都在这里 114 | bower_components/ //bower安装的组件都在这里 115 | node_modules/ //所有node模块都在这里 116 | .yo-rc.json //记录与本工程相关的generator信息,一般无需手动修改 117 | bower.json //bower配置文件 118 | Gruntfile.js //不解释了~ 119 | package.json //不解释了~ 120 | README.md //不解释了~ 121 | ``` 122 | 123 | 本来想把.gitignore也添加进来的,想想有些同学不是用git的,那么这个文件也就多余了,有需要的同学就自行添加吧。 124 | 125 | 工具将以你的根目录名称+App作为angular的app应用模块名称,所有后续的指令,服务等,都是基于此名称的模块建立的。那我们这里的app名称就是 `bookstoreApp` 。你可以打开app目录下的任意一个js查看。 126 | 127 | 128 | ###3.启动服务器 129 | 130 | 131 | 还是在bookstore的目录下: 132 | 133 | ``` 134 | grunt serve 135 | ``` 136 | 137 | 这会调用grunt任务,很快就会调用你的默认浏览器,并打开 http://localhost:9000/ 这个网址。 138 | 如果你看到页面上有一个可爱的小胡子,以及下面的这样一句话: 139 | 140 | ``` 141 | this is the main view. to be continued... 142 | ``` 143 | 144 | 那么恭喜你成功初始化了一个Angular项目! 145 | 146 | ###4.开发一个新页面 147 | 比如我们要开发一个图书list页面,执行: 148 | ``` 149 | yo ngstone:route list 150 | ``` 151 | 152 | 命令行最后几行提示: 153 | 154 | ``` 155 | script added into index.html: scripts/controllers/list.js 156 | create test/e2e/list/list.js 157 | create app/scripts/controllers/list.js 158 | create test/spec/controllers/list.js 159 | create app/views/list/list.html 160 | ``` 161 | 意思是,添加了引用controller的script标签到index.html,并创建了controller和测试文件,以及视图文件。 162 | 163 | 然后,回到浏览器,把地址栏里的 http://localhost:9000/#/main 改为 http://localhost:9000/#/list 回车, 164 | 你会发现,最下面那句话,之前的main变成了list,也就是 165 | 166 | ``` 167 | this is the list view. to be continued... 168 | ``` 169 | 170 | list这个位置的单词是加大字号并加了蓝色的,加以明显的提示和区分。 171 | 172 | 怎么样,创建一个新页面是不是很快? 173 | 接下来我们要制作一个图书列表,首先将下面的代码,替换到 app/views/list/list.html 中: 174 | 175 | ```html 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 |
书名作者价格
{{book.name}}{{book.author}}{{book.price}}
192 | ``` 193 | 194 | 195 | 此时,如果你回到浏览器,它会自动刷新,你会看到一个表头。 196 | 然后,打开app/scripts/controllers/list.js编辑controller,添加一个数组: 197 | 198 | 199 | ``` 200 | angular.module('bookstoreApp') 201 | .controller('ListCtrl', function ($scope) { 202 | $scope.books = [ 203 | { 204 | id:1, 205 | name:'用AngularJS开发下一代web应用', 206 | author:'大漠穷秋', 207 | price:1.20 208 | }, 209 | { 210 | id:2, 211 | name:'浪潮之巅', 212 | author:'吴军', 213 | price:1.21 214 | }, 215 | { 216 | id:3, 217 | name:'macTalk', 218 | author:'池建强', 219 | price:1.22 220 | } 221 | ] 222 | }); 223 | ``` 224 | 回到浏览器,还是会自动刷新,表格里面显示了刚刚添加的数组的内容。 225 | 刚刚我们是通过手动修改地址栏访问这个list页面的,现在我们修改index.html的导航,通过点击导航的方式访问list页面: 226 | 227 | ```html 228 | 232 | ``` 233 | 再回到浏览器,分别点击右上角的Home和List,就可以在主页和list页之前切换了!(这个是Angular的路由功能,已经不是本工具应该讨论的范畴了,就不深入说明了- -) 234 | 这样一个简单的页面就开发完了,是不是很轻松? 235 | 236 | ###5.添加directive 237 | 238 | 指令的生成有2个选项,所以单独拿出来说。比如我们要添加一个叫做hello的指令,执行: 239 | 240 | ``` 241 | yo ngstone:directive hello 242 | ``` 243 | 244 | 控制台会输出: 245 | 246 | ``` 247 | script added into index.html: scripts/directives/hello.js 248 | create app/scripts/directives/hello.js 249 | create test/spec/directives/hello.js 250 | ``` 251 | 252 | 在index.html生成script标签,添加指令模块文件和单元测试文件,其中指令模块的内容是不带template选项的: 253 | 254 | ```javascript 255 | angular.module('bookstoreApp') 256 | .directive('hello', function () { 257 | return { 258 | restrict:'E', 259 | replace:true, 260 | scope:{ 261 | 262 | }, 263 | link: function ($scope,$element,attrs) { 264 | 265 | } 266 | } 267 | }); 268 | ``` 269 | 270 | 如果命令添加一个选项: 271 | 272 | ``` 273 | yo ngstone:directive hello --template 274 | ``` 275 | 276 | 则会添加一个template属性: 277 | 278 | ```javascript 279 | angular.module('bookstoreApp') 280 | .directive('hello', function () { 281 | return { 282 | restrict:'E', 283 | replace:true, 284 | template:'
', //添加的template属性 285 | scope:{ 286 | 287 | }, 288 | link: function ($scope,$element,attrs) { 289 | 290 | } 291 | } 292 | }); 293 | ``` 294 | 295 | 如果选项是 `templateUrl` : 296 | 297 | ``` 298 | yo ngstone:directive hello --templateUrl 299 | ``` 300 | 301 | 控制台输出: 302 | 303 | ``` 304 | script added into index.html: scripts/directives/hello.js 305 | create app/views/_widgets/hello/hello.html 306 | create app/scripts/directives/hello.js 307 | create test/spec/directives/hello.js 308 | ``` 309 | 310 | 你会发现多了一个视图文件,而且是创建到app/views/_widgets这个目录下的,指令文件内容也相应的变成了: 311 | 312 | ```javascript 313 | angular.module('bookstoreApp') 314 | .directive('hello', function () { 315 | return { 316 | restrict:'E', 317 | replace:true, 318 | templateUrl:'./views/_widgets/hello/hello.html', 319 | scope:{ 320 | 321 | }, 322 | link: function ($scope,$element,attrs) { 323 | 324 | } 325 | } 326 | }); 327 | ``` 328 | 329 | 省去了人工配置的麻烦~ 330 | 讲完了ngstone:directive的两个选项,接下来让我们完成这个hello指令。假定我使用了外部模板,即,使用`--templateUrl` 这个选项创建指令,我们在link函数里写上: 331 | 332 | ``` 333 | $element.after('

Hello Directive!

') 334 | ``` 335 | 336 | 再在app/views/list/list.html的末尾(table标签后)加上: 337 | 338 | ```html 339 | 340 | ``` 341 | 342 | 回到浏览器查看list页面,你会发现,最下面出现了: 343 | 344 | 345 | ``` 346 | this is the hello view. to be continued... 347 | 348 | Hello Directive! 349 | ``` 350 | 351 | 表明我们的directive添加成功了! 352 | 353 | ###6.添加factory,filter,decorator 354 | 355 | 添加它们的命令和directive类似,只不过没有那两个template选项,就不再演示了。。 356 | 357 | 另外,provider,service,value,constant我就不额外添加命令了,因为和factory太相似了,而且我个人认为还是factory用的频度最高,而且也够用了。实在不行,生成factory后再稍微改一改,再者它们本质都属于service,问题不大~ 358 | 359 | 还有,decorator特殊一点,它不生成测试文件。 360 | 361 | 大家可以自行尝试添加。 362 | 363 | 文档最后我会把所有可用的命令罗列出来。 364 | 365 | 366 | ###7.单元测试 367 | 单元测试需要用到*PhantomJS*,它是一个开源的虚拟浏览器,没有UI界面,其他行为和真正的浏览器一样,用于单元测试再适合不过。 368 | 由于PhantomJS在某些情况下(天朝干的,你懂的)安装较慢,0.1.1版本开始,将单元测试环境初始化从工程初始化中独立出来。 369 | 你可以先尝试执行 370 | 371 | ```bash 372 | yo ngstone:karma-init 373 | ``` 374 | 375 | 这条命令会安装phantomjs、karma-jasmine、grunt-karma、karma-phantomjs-launcher等模块,如果你的机器能够顺利完成的话,可以跳过此段落。 376 | 如果安装不顺利,八成是卡在phantomjs的安装.可以尝试执行 `npm install -g phantomjs` 或者参考官网 http://phantomjs.org/download.html , 377 | 另外,通过homebrew也可以安装 `brew install phantomjs`。安装好phantomjs后,再执行 `yo ngstone:karma-init` ,应该问题都不大了。 378 | 再啰嗦一句,phantomjs装好后,是全局的,以后再次使用它就不用再安装了。 379 | 380 | 381 | 382 | 单元测试环境安装完毕后,打开test/spec/controllers/list.js,把最下面的it(...),改成: 383 | 384 | ```javascript 385 | it('should attach a list of books to the scope', function () { 386 | expect(scope.books.length).toBe(4); 387 | }); 388 | ``` 389 | 390 | 命令行执行: 391 | 392 | ``` 393 | grunt test-unit 394 | ``` 395 | 396 | 这里会启动*PhantomJS*,执行我们的代码。 397 | 执行完毕后,我们会看到类似如下的提示: 398 | 399 | ``` 400 | PhantomJS 1.9.8 (Mac OS X) Controller: ListCtrl should attach a list of books to the scope FAILED 401 | Expected 3 to be 4. 402 | at /....../bookstore/test/spec/controllers/list.js:20 403 | PhantomJS 1.9.8 (Mac OS X): Executed 2 of 2 (1 FAILED) (0.004 secs / 0.026 secs) 404 | ``` 405 | 表明我的controller单元测试没有通过. scope.books.length应该为3,而不是4 406 | 407 | 我们回过去把 `expect(scope.books.length).toBe(4);`最后的4改成3,再运行单元测试,会出现如下结果: 408 | 409 | ``` 410 | PhantomJS 1.9.8 (Mac OS X): Executed 2 of 2 SUCCESS (0.003 secs / 0.018 secs) 411 | 412 | Done, without errors. 413 | ``` 414 | 415 | 好了!这样单元测试就通过了。 416 | 417 | ###8.执行e2e测试 418 | e2e是 *end-to-end* 的简称,"端到端测试" 或 "场景测试" ,说白了就是让代码模拟人工测试,具有非常大的意义,规模大了可节省很多人力成本。 419 | 首先需要安装protractor,它是angular团队开发的专门用于e2e测试的node模块,并针对angularjs做了优化,它会调用webdriver执行我们的测试代码。 420 | 执行 421 | ```bash 422 | npm install -g protractor 423 | ``` 424 | protractor也是全局的,下次就不用装了~ 425 | 然后,需要下载webdriver。官方的下载地址是 http://chromedriver.storage.googleapis.com/index.html ,不过是被墙的,我这里 https://github.com/stoneChen/generator-ngstone/tree/master/webdriver 准备了一个,一个mac版,一个win版,你可以根据需要选择。 426 | 下过来后,解压缩出来是一个文件,把它放到 /usr/local/lib/node_modules/protractor/selenium下(win用户请自行对号入座,selenium目录可能不存在,自己建一个) 427 | 428 | 接下来我们造一个测试效果。 429 | 打开app/views/list/list.html, 在最下面添加: 430 | 431 | ```html 432 | 433 |

hello {{newer}}

434 | ``` 435 | 436 | 打开test/e2e/list/list.js,修改it(...),最后整个文件内容变成: 437 | 438 | ```javascript 439 | 'use strict'; 440 | describe('e2e test for page: list', function() { 441 | it('should do something', function() { 442 | browser.get('http://localhost:9000/#list'); 443 | var newer = element(by.model('newer')); 444 | var newerBinding = element(by.binding('newer')); 445 | newer.sendKeys('new comer'); 446 | expect(newerBinding.getText()).toEqual('hello new comer'); 447 | 448 | newer.clear(); 449 | newer.sendKeys('protractor'); 450 | expect(newerBinding.getText()).toEqual('hello protractor'); 451 | }); 452 | }); 453 | ``` 454 | 455 | 然后启动服务,执行: 456 | 457 | ```bash 458 | grunt serve 459 | ``` 460 | 再打开一个命令行窗口,还是这个目录,执行: 461 | 462 | ```bash 463 | grunt test-e2e 464 | ``` 465 | 466 | 接下来,就是见证奇迹的时刻了! 467 | 我们可以看到,另一个chrome程序被打开了,自动打开我们指定的url地址(list页面),在最下方的输入框中输入了new comer,清空,再输入protractor,最后自动关闭了窗口。 468 | 回到命令行界面,我们看到了类似如下的日志: 469 | 470 | ```bash 471 | Running "protractor_invoker:e2e" (protractor_invoker) task 472 | Using ChromeDriver directly... 473 | [launcher] Running 1 instances of WebDriver 474 | . 475 | 476 | Finished in 3.248 seconds 477 | 1 test, 2 assertions, 0 failures 478 | 479 | [launcher] 0 instance(s) of WebDriver still running 480 | [launcher] chrome #1 passed 481 | 482 | Done, without errors. 483 | ``` 484 | 485 | 表明我们执行了1个测试,2个断言,0个失败。 486 | 你可以尝试修改测试的输入值,再次运行,就会看到相应的错误信息~是不是很好玩?更多测试代码的语法见 http://angular.github.io/protractor/#/api 我也正在学习中~ 487 | 顺便带一句,上面的protractor_invoker插件,是我第一次写的grunt插件,只是很简单的调用全局protractor,有问题请速速通知我奥~ 488 | 489 | ###9.打包构建工程 490 | 491 | 功能开发好后,需要对静态文件作一系列的加工,这样上线后可以为我们带来加快访问速度,提高性能,减少网络流量等好处。接下来我们开始构建: 492 | 493 | ``` 494 | grunt build 495 | ``` 496 | 497 | 这过程做了很多事,视图打包进js(本人添加),依赖注入替换,js、css合并压缩,图片名添加后缀(刷新缓存)等。 498 | 需要说明一下,我把官网版的generator的gruntfile任务流,去掉了图片压缩和css autoprefixer。 499 | 图片压缩用到的grunt任务模块很大,而且windows下会报错,无法构建,因此去掉了它,如果需要图片压缩,请自行添加任务流。 500 | css autoprefixer为css3添加浏览器前缀,相信很多IDE都能搞定了吧,就不要了它了。 501 | 其他还有一些配置,在官方版的基础上删删改改,现在是可以完成构建任务的~ 502 | 至于构建任务流的具体过程,我就先不讲了,有兴趣的同学,可以仔细研究一下,或许我以后会补上来吧。 503 | 504 | 控制台唰唰唰的输出一坨日志后,构建完成。你会发现根目录下多出了一个dist目录,里面的内容比之前的简单多了: 505 | 506 | ``` 507 | fonts/ //所有的字体文件,包括bootstrap和fontawesome的 508 | images/ //所有的图片文件,文件名是添加过后缀的 509 | scripts/ //所有的js,是合并压缩过后的,里面只有两个文件,一个是所有的bower组件代码,一个是我们自己写的代码 510 | styles/ //所有的css,同上 511 | index.html //工程首页,是htmlmin过的 512 | ``` 513 | 如果你觉得构建后的js和css还是太多,你完全可以自己动手,修改构建配置,把它们继续合并~ 514 | 然后呢,执行: 515 | 516 | ``` 517 | grunt serve:dist 518 | ``` 519 | 520 | 就会以dist为webroot启动服务,同样调用你的默认浏览器打开地址:http://localhost:9000 效果是和不打包构建一样的,性能肯定是更高的!随着你的工程越来越大,性能提升会越来越明显。打开浏览器调试工具,查看请求列表,只要寥寥几条请求~够高大上么 521 | 522 | ps: 523 | index.html中带有build和endbuild字样的注释是不能删除的,它们是构建时,寻找被打包压缩文件列表的起止标记!!! 524 | 525 | ###如何模拟ajax数据 526 | 527 | 我们上面的列表数据是js里写死的,所以用不到ajax数据模拟。接下来开始: 528 | 529 | 我们回到第5步中创建的app/scripts/controllers/list.js,把那个数组改成ajax方式获取,这里需要引入$http服务: 530 | 531 | 532 | ```javascript 533 | angular.module('bookstoreApp') 534 | .controller('ListCtrl', function ($scope,$http) { 535 | $http.get('/book.json?_method=GET').success(function (data) { 536 | $scope.books = data; 537 | }) 538 | 539 | }); 540 | ``` 541 | 542 | 在mock目录下新建book目录,在book下新建book#GET.json,内容为: 543 | 544 | ```javascript 545 | [ 546 | { 547 | "id":1, 548 | "name":"用AngularJS开发下一代web应用", 549 | "author":"大漠穷秋", 550 | "price":1.20 551 | }, 552 | { 553 | "id":2, 554 | "name":"浪潮之巅", 555 | "author":"吴军", 556 | "price":1.21 557 | }, 558 | { 559 | "id":3, 560 | "name":"macTalk", 561 | "author":"池建强", 562 | "price":1.22 563 | } 564 | ] 565 | ``` 566 | 重新启动服务: 567 | 568 | ``` 569 | grunt serve 570 | ``` 571 | 572 | 访问list页面,发现跟之前的写死数据效果一样。 573 | 再看看控制台,打印出了刚才的json数据,如果这个时候,你修改json文件里的某条数据,保存后,浏览器也会立即刷新,是不是很贴心? 574 | 575 | 再讲一下这里的ajax数据模拟规则: 576 | 577 | 之前做到这个功能的时候,本来想再弄一个ajax地址与json文件目录的配置文件,想起玉伯曾经说的 *约定大于配置* 原则。还真是有道理,多一个配置文件,岂不是又增加了工作负担?于是我就搞了这么个约定: 578 | 579 | 现在RESTful请求大行其道,我们也不能落下,我列出一个映射表,相信效果会比啰嗦的文字效果好: 580 | 581 | 左边是ajax地址,右边是对应的模拟数据json路径 582 | 583 | /book.json?_method=GET ==> mock/book/book#GET.json //获取列表 584 | 585 | /book.json?_method=POST ==> mock/book/book#POST.json //新增数据 586 | 587 | /book/12.json?_method=GET ==> mock/book/book.N.json#POST.json //查看id为12的数据 588 | 589 | /book/25.json?_method=GET ==> mock/book/book.N.json#POST.json //查看id为25的数据,这2种情况返回相同的json,因为毕竟不是真正的查询数据库,所以搞了个N来代替数字 590 | 591 | /book/55.json?_method=PATCH ==> mock/book/book.N#PATCH.json //修改id为55的数据 592 | 593 | /book/77.json?_method=DELETE ==> mock/book/book.N#DELETE.json //删除id为77的数据 594 | 595 | 怎么样,是不是有点谱了?你可能还有个疑问,为什么多出了一个book目录? 596 | 这又是一个约定,当工程业务域多了以后,每个目录下最好还是以业务域分类的好,所以这里多了个book目录~ 597 | 598 | 在上个版本,对业务域名称上有些限制,不过每个项目的需求不同,而且也比较麻烦,决定还是取消掉。总的来讲,route名称,rest-ajax一级名称,mock目录名等均取消s后缀的限制。 599 | 600 | 服务器模拟ajax数据的逻辑代码在Gruntfile.js的mockMiddleware函数中,你也可以根据自己的需求做适当改进。 601 | 602 | ==== 603 | 604 | 以上是本工具的基本使用方法,下面讲一讲稍高级的用法 605 | 606 | ==== 607 | 608 | 609 | 610 | ##使用ui-router 611 | 当在初始化工程时,选择使用ui-router,ngstone:route命令会自动生成基础的state配置,指定controller和view的路径,你可以继续对其扩展/修改。 612 | 当选择使用ui-router时,我创建了一个抽象mainland父状态,用于两栏布局,代码详见app/scripts/app.js和app/views/_common/mainland.html。 613 | 此父状态名称不可更改,因为后续的route配置都是基于此名字,你可以适当修改app/views/_common/mainland.html 614 | 615 | 616 | 617 | 618 | ##安装基础服务与布局 619 | 620 | 这次让我们重新建一个工程,比如叫做school: 621 | 622 | ``` 623 | mkdir school && cd $_ 624 | ``` 625 | 626 | 然后初始化工程,当提示 *是否初始化基础服务与布局?* 选择Y,直接回车也可以: 627 | 628 | ``` 629 | yo ngstone 630 | ``` 631 | 从控制台输出上,你会看到明显比上次要多创建了很多文件。 632 | 633 | 请耐心等待几分钟,大部分时间花费在bower与npm安装模块上。额外说明一下,一旦这些模块安装成功过以后,下次再安装就会快很多,因为本地会有缓存。 634 | 635 | 初始化完毕后,我们浏览一下根目录下都创建了哪些文件: 636 | 637 | 从根目录的直接子目录上看,比之前多了一个biz目录,是干什么的呢?这是我想了好久的名字,biz代表business,即业务的意思,用business的话太长,拼写不方便,于是想了这么个词出来。所以呢,这个目录一定与具体的业务相关。初始化基础服务很重要或者说很强大的功能就是,可以通过配置文件快速生成一个业务单元的基础功能集(增删改查),那么这个目录就是用来存放这这些配置文件的。是不是很心动?别着急,后面会继续讲解如何操作,我们先继续浏览刚刚的 *初始化基础服务与布局* 到底初始化了什么东东。 638 | 639 | 根目录的直接子目录除了biz外的其他目录的作用都与之前的一样,我们往下看,打开app/scripts,你会发现controllers,directives,filters,services下都已经有了若干js模块,这些初始化出来的js模块,都是为快速生成业务单元服务的,所以我称他们为基础服务。包括下面的app/styles和app/views,以及index.html都增加了很多内容,后面我会挑一些比较重要的模块进行讲解。 640 | 641 | 细心的同学,应该发现工程里已经初始化了一个user业务单元,它是可以直接运行的,但是请注意,**这个user业务单元并不是我上面说的那个快速生成的结果**!请容许我插入一段angular心得: 642 | 643 | 最开始使用angular确实没有使用任何自定义指令神马的,做了好几个业务单元,很多代码都是相似的,都要 *人工* 一遍一遍的拷代码。虽然angular已经给我们带来非凡的编程体验,但我还是觉得需要解放一下生产力,花了有半个多月的时间才勉强写出了vdatagrid指令与vform指令,用于动态创建数据表格展示与表单提交。刚开始用着感觉还不错,可以少写很多类似的代码,尤其是form里的控件,减轻很多人工拷贝代码的痛苦。可是随着业务越来越复杂,指令代码也不断膨胀,调用指令的配置代码也变得越来越长,controller里的代码有一半以上都是这些配置项,而且越发觉得这指令越来越难以维护了,甚至指令的性能也有所降低。就在前不久,我打算自己写一个generator可以根据配置文件生成 "原始" 的angular代码,即不使用vdatagrid与vform指令。乍一看,是不是回到了解放前?no no no,我们是用命令写代码,而且效率要高的多哦~另外,如果业务上有复杂的需求也会好处理的多。 644 | 645 | 好了,心得阐述完毕,让我们先看一下使用vdatagrid与vform的效果如何。启动服务: 646 | 647 | ``` 648 | grunt serve 649 | ``` 650 | 自动打开浏览器访问http://localhost:9000/ ,你会看到蓝色顶部bar,下方,左侧菜单列表,右侧主体内容,这样的经典管理系统布局。右侧又见到了之前见过的可爱小胡子。我们点击左侧菜单的用户管理,右边就出现了一个数据列表,这样一个界面就包含了user业务的基础增删改查的功能。 651 | 652 | 之所以还保留vdatagrid和vform指令,是因为在某种程度上,它们还是挺好用的,留下来给大家参考,听君的喜好来选择! 653 | 654 | 接下来,我们开始创建一个新业务单元,因为工程名叫school嘛,那我们就来个student: 655 | 656 | ``` 657 | yo ngstone:biz-cfg student 658 | ``` 659 | 660 | 这个命令会创建在biz目录下创建student.json,里面是完整的配置内容,你需要做的只是 *填空* ~ 661 | 662 | 下面解释一下每个配置项的意义: 663 | 664 | ```javascript 665 | { 666 | "type":"origin", //生成代码类型,origin的意思就是我上面提到的*原始*angular代码,这一项暂时先不要动,也许以后我会补上别的选项 667 | "caption":"", //业务单元的名称,比如我们这里是"学生",在form标题中会用到 668 | "resource":{ //resource资源的配置,暂时只有urlPattern这么一项配置,默认给出的urlPattern能够满足基本需求,后面我会讲解resource的用法 669 | "urlPattern":"student/{id}" 670 | }, 671 | "mock":[ //根据这里给出的列表,生成ajax数据模拟json文件,也是省去人工创建文件的麻烦~ 672 | "student/student#GET.json", 673 | "student/student#POST.json", 674 | "student/student.N#GET.json", 675 | "student/student.N#PATCH.json", 676 | "student/student.N#DELETE.json" 677 | ], 678 | "dataGrid":{ //数据表格的配置 679 | "top":{ //新增按钮是内置固定的,如果不需要可以在生成后,删除相关代码 680 | "searchGroup":{ //搜索区域的配置 681 | "input": { //模糊搜索,如果不需要,可以去掉这项配置 682 | "ngModel": "keyword", 683 | "placeholder": "关键字" 684 | }, 685 | "select":{ //下拉框搜索,如果不需要,可以去掉这项配置 686 | "ngModel":"", 687 | "dataListName":"" 688 | }, 689 | "btn": { //搜索按钮,如果不需要,可以去掉这项配置 690 | "text": "搜索" 691 | } 692 | } 693 | }, 694 | "gridTable": { //表格配置 695 | "cols": [ //表头设置 696 | { 697 | "text": "", //显示的表头名称 698 | "property": "" //ng-repeat里要取值的属性名 699 | } 700 | ], 701 | "operation":[ //操作列配置 702 | { 703 | "type":"default", 704 | "text":"修改", 705 | "method":"modify" //修改和删除是比较典型的操作,如果配置了这两项,将生成更具体的代码,当然你也可以继续添加其他操作按钮,不过生成的代码就没那么具体,逻辑需要你自己扩充 706 | }, 707 | { 708 | "type":"danger", 709 | "text":"删除", 710 | "method":"del" 711 | } 712 | ] 713 | } 714 | }, 715 | "form":{//表单配置 716 | "fields":[ //表单控件配置 717 | { 718 | "key": "", //控件对应到对象的属性名,比如student.name,那么这里写name 719 | "type": "",//控件类型,可选项有text,number,url,email,password,radio,checkbox,select 720 | "label": "",//控件名称 721 | //dataListName:"",//如果type是radio|checkbox|select,这项配置可以帮你把ng-repeat的数组名生成出来 722 | "validators": {//校验设置 723 | "rules": { 724 | "": true //ng内置的和你后面扩充的都可以,比如"ng-required":true,"ng-minlength":6,可以配置若干项 725 | } 726 | //,messages:{}// 这项配置,可以指定校验信息,key与rules里的key一一对应,如果不指定,则使用默认校验信息,通常使用默认的就可以了,不必配置此项,需要注意,把前缀'ng-'去掉 727 | } 728 | } 729 | ] 730 | } 731 | } 732 | ``` 733 | 734 | 我这里配置了一份student.json: 735 | 736 | ``` 737 | { 738 | "type":"origin", 739 | "caption":"学生", 740 | "resource":{ 741 | "urlPattern":"student/{id}" 742 | }, 743 | "mock":[ 744 | "student/student#GET.json", 745 | "student/student#POST.json", 746 | "student/student.N#GET.json", 747 | "student/student.N#PATCH.json", 748 | "student/student.N#DELETE.json" 749 | ], 750 | "dataGrid":{ 751 | "top":{ 752 | "searchGroup":{ 753 | "input": { 754 | "ngModel": "keyword", 755 | "placeholder": "关键字" 756 | }, 757 | "select":{ 758 | "ngModel":"grade", 759 | "dataListName":"gradeList" 760 | }, 761 | "btn": { 762 | "text": "搜索" 763 | } 764 | } 765 | }, 766 | "gridTable": { 767 | "cols": [ 768 | { 769 | "text": "姓名", 770 | "property": "name" 771 | }, 772 | { 773 | "text": "年级", 774 | "property": "grade" 775 | }, 776 | { 777 | "text": "性别", 778 | "property": "gender" 779 | } 780 | ], 781 | "operation":[ 782 | { 783 | "type":"default", 784 | "text":"修改", 785 | "method":"modify" 786 | }, 787 | { 788 | "type":"danger", 789 | "text":"删除", 790 | "method":"del" 791 | }, 792 | { 793 | "type":"default", 794 | "text":"设为班长", 795 | "method":"monitor" 796 | } 797 | ] 798 | } 799 | }, 800 | "form":{ 801 | "fields":[ 802 | { 803 | "key": "name", 804 | "type": "text", 805 | "label": "姓名", 806 | "validators": { 807 | "rules": { 808 | "ng-required": true 809 | } 810 | } 811 | }, 812 | { 813 | "key": "grade", 814 | "type": "select", 815 | "label": "年级", 816 | "dataListName":"gradeList", 817 | "validators": { 818 | "rules": { 819 | "ng-required": true 820 | } 821 | } 822 | }, 823 | { 824 | "key": "gender", 825 | "type": "radio", 826 | "label": "性别", 827 | "dataListName":"genderList", 828 | "validators": { 829 | "rules": { 830 | "ng-required": true 831 | } 832 | } 833 | } 834 | ] 835 | } 836 | } 837 | ``` 838 | 839 | 把它拷贝到你的student.json中去,然后执行: 840 | 841 | ``` 842 | yo ngstone:biz student 843 | ``` 844 | 845 | 接下来就是见证奇迹的时刻! 846 | 847 | 看到控制台已经输出了好几行日志,创建了若干文件,可以去看一下生成的文件内容都是什么,是不是符合你的预期。 848 | 849 | 不过,我们还是需要手动添加student的菜单,打开app/scripts/services/global-data.js,找到"用户管理"这一行配置,在它下面添加: 850 | 851 | ```javascript 852 | {"children": [], "href": "#/student", "name": "学生管理"}, 853 | ``` 854 | 回到浏览器,发现菜单中已经有了学生管理,点击它,右侧立刻出现跟用户管理很相似的页面,不过表格只要一行空数据,那是因为我们还没补充ajax模拟数据,下拉框也没有东西,新增表单的性别项也没有数据。然后我们一个个补充完整。 855 | 856 | 打开app/scripts/controllers/student.js,在第11行后面,插入: 857 | 858 | ```javascript 859 | var genderList = [{value:1,label:'男'},{value:2,label:'女'}]; //手动添加 860 | var gradeList = [{value:1,label:'一年级'},{value:2,label:'二年级'},{value:3,label:'三年级'}]; //手动添加 861 | ``` 862 | 863 | 在第19行后面插入: 864 | 865 | ``` 866 | dialogScope.genderList = genderList; //手动添加 867 | dialogScope.gradeList = gradeList; //手动添加 868 | ``` 869 | 870 | 在第43行后面插入: 871 | 872 | ``` 873 | gradeList:gradeList, //手动添加 874 | ``` 875 | 876 | 现在完整的app/scripts/controllers/student.js 如下: 877 | 878 | ```javascript 879 | angular.module('schoolApp') 880 | .controller('StudentCtrl', function ($scope,resourcePool,msgService,dialogService) { 881 | var resourceClass = resourcePool.student; 882 | var genderList = [{value:1,label:'男'},{value:2,label:'女'}]; //手动添加 883 | var gradeList = [{value:1,label:'一年级'},{value:2,label:'二年级'},{value:3,label:'三年级'}]; //手动添加 884 | var openDialogForm = function (resourceInst,done) { 885 | dialogService.complexBox({ 886 | templateUrl: './views/student/dialog-form.html', 887 | onComplete: function (dialogScope, dialogInstance) { 888 | dialogScope.isEdit = !!resourceInst; 889 | dialogScope.student = resourceInst ? resourceInst.copy() : {}; 890 | dialogScope.genderList = genderList; //手动添加 891 | dialogScope.gradeList = gradeList; //手动添加 892 | dialogScope.ok = function () { 893 | if (dialogScope.dialogForm.$invalid) { 894 | return; 895 | } 896 | if (dialogScope.isEdit) { 897 | dialogScope.student.$update(function () { 898 | dialogInstance.close(); 899 | done && done(dialogScope.student); 900 | }) 901 | } else { 902 | resourceClass.new(dialogScope.student, function (resource) { 903 | dialogInstance.close(); 904 | done && done(resource); 905 | }) 906 | } 907 | }; 908 | } 909 | }) 910 | }; 911 | angular.extend($scope,{ 912 | currentPage: 1, 913 | searchParams:{}, 914 | gradeList:gradeList, 915 | doSearch:function () { 916 | $scope.searchParams.currentPage = $scope.currentPage; 917 | resourceClass.query($scope.searchParams, function (resources,data) { 918 | $scope.resources = resources; 919 | $scope.totalItems = data.totalCount; 920 | }) 921 | }, 922 | inputKeyup: function (ev) { 923 | if(ev.keyCode === 13){ 924 | $scope.doSearch(); 925 | } 926 | }, 927 | newRc: function () { 928 | openDialogForm(null, function (newReource) { 929 | $scope.resources.unshift(newReource); 930 | msgService.success('新增成功'); 931 | }) 932 | }, 933 | modify: function(rcItem,index) { 934 | openDialogForm(rcItem, function (newResource) { 935 | $scope.resources[index] = newResource; 936 | msgService.success('修改成功'); 937 | }) 938 | }, 939 | del: function(rcItem,index) { 940 | dialogService.confirm('确定删除吗?', function () { 941 | rcItem.$delete(function () { 942 | $scope.resources.splice(index, 1); 943 | msgService.success('删除成功'); 944 | }) 945 | }) 946 | }, 947 | monitor: function(rcItem,index) { 948 | } 949 | }); 950 | $scope.doSearch(); 951 | }); 952 | ``` 953 | 954 | 打开mock/student/student#GET.json,内容替换为: 955 | 956 | ``` 957 | { 958 | "stat":"OK", 959 | "data":{ 960 | "currentPage": 1, 961 | "totalPages": 3, 962 | "totalCount": 30, 963 | "collection": [ 964 | { 965 | "id":11, 966 | "name":"张三", 967 | "grade":1, 968 | "gender":1 969 | }, 970 | { 971 | "id":22, 972 | "name":"李四", 973 | "grade":2, 974 | "gender":1 975 | }, 976 | { 977 | "id":33, 978 | "name":"王五", 979 | "grade":2, 980 | "gender":1 981 | }, 982 | { 983 | "id":44, 984 | "name":"赵六", 985 | "grade":2, 986 | "gender":1 987 | }, 988 | { 989 | "id":55, 990 | "name":"李七", 991 | "grade":3, 992 | "gender":1 993 | } 994 | ] 995 | } 996 | } 997 | ``` 998 | 999 | 最后,打开mock/student/student#POST.json,内容替换为: 1000 | 1001 | ``` 1002 | { 1003 | "stat":"OK", 1004 | "data":{ 1005 | "model":{ 1006 | "id":999, 1007 | "name":"新学生", 1008 | "grade":2, 1009 | "gender":2 1010 | } 1011 | } 1012 | } 1013 | ``` 1014 | 1015 | 就大功告成啦! 1016 | 1017 | 回到浏览器,你可以尝试查询,分页,新增,删除,修改的操作,看不是都OK了。 1018 | 1019 | 如果要把性别变成中文的男或女,只要创建一个filter就好了,相信这难不倒你~ 1020 | 1021 | 提一句,由于不是真正的后端,数据查询功能暂时无法完整体现出来,因为无论查询条件如何,返回的都是json文件里的数据,没有变化。打包构建的命令,与之前的一样。 1022 | 1023 | 感觉如何?是不是很有feel?有些细节这里无法涵盖到,等着你发现哦~快去尝试开发更多功能吧。 1024 | 1025 | ps: 1026 | * ngstone:biz-cfg和ngstone:biz命令仅适用于创建典型的后台管理系统模块,可以快速实现基本的增删改查功能 1027 | * 由于ngstone:biz命令生成的代码会调用基础服务,在未安装基础服务的工程执行此命令没有意义。所以,0.2.1版本开始,没有初始化基础服务的工程,将阻止调用ngstone:biz-cfg和ngstone:biz命令 1028 | 1029 | 1030 | ##命令列表 1031 | 1032 | 1. yo ngstone 初始化工程 1033 | 2. yo ngstone:directive {name} 生成directive,插入script标签到index.html,生成测试文件 1034 | 3. yo ngstone:factory {name} 生成factory,插入script标签到index.html,生成测试文件 1035 | 4. yo ngstone:filter {name} 生成filter,插入script标签到index.html,生成测试文件 1036 | 5. yo ngstone:decorator {name} 生成decorator,插入script标签到index.html 1037 | 6. yo ngstone:controller {name} 生成controller,插入script标签到index.html,生成测试文件 1038 | 7. yo ngstone:view {name} 生成视图文件 1039 | 8. yo ngstone:route {name} 向app/scripts/app.js中添加路由配置,调用ngstone:controller和ngstone:view 1040 | 9. yo ngstone:biz-cfg {name} 生成business配置文件 1041 | 10. yo ngstone:biz {name} 根据business配置文件生成 1042 | 11. yo ngstone:karma-init 初始化单元测试环境 1043 | 1044 | ##基础服务中的关键模块说明 1045 | 1046 | 1. 登陆相关:登陆并没有作为一个路由页面存在,在index.html使用switch指令结合include指令实现登陆 "跳转",同时ajax服务中对每次请求,后端返回的数据进行状态属性检查,若为 `LOGIN_TIMEOUT` 则 "跳转到"登陆页 1047 | 2. ajaxService服务封装了ajax请求,如果用了resource服务,一般不需要直接调用此ajax服务,可修改后端返回状态属性的枚举值(即switch的代码段) 1048 | 3. resourceService服务用法大致与官方的resource一致,资源默认方法稍有不同,资源url中的参数格式使用 `{xxx}` 代替 `:xxx` ,资源实例添加了若干方法,去除action方法调用后的返回值(现在是undefined),个人认为这样会简单一些 1049 | 4. resourcPool,即资源池,用于存放所有的资源对象 1050 | 5. localStorageService服务是第三方的模块,用于实现本地数据持久化,初始化时用于存储菜单组折叠状态,你可以用于更多业务 1051 | 6. 更多说明将持续补充。。。 1052 | 1053 | ##与官方版generator-angular的对比 1054 | 1055 | 这里讲一下两者的关键区别,简要起见,我们约定: 1056 | *A*=generator-angular生成的工程 1057 | *B*=generator-ngstone生成的工程 1058 | 1. A的index.html的bower组件与我们自己模块的引用路径都是顶级路径,比如angular.js的引用路径: 1059 | 1060 | ```html 1061 | 1062 | ``` 1063 | 1064 | 而实际上,index.html与bower_components不在同一层目录! 1065 | 用grunt启动服务,的确没有问题,可以加载到bower组件,因为gruntfile中的connect任务有相应配置 1066 | 而我们有自己的后端,当我们部署工程的时候,bower组件就加载不到了!还要用nginx额外配置一下才能加载到,这样很不方便。 1067 | 于是在B中,我把路径前变成了相对路径,即: 1068 | 1069 | ``` 1070 | 1071 | ``` 1072 | 1073 | 这样,到了部署的时候也不会有问题了,省去nginx额外的配置。 1074 | 1075 | 2. A打包是不会将模板文件打包进js的,B会 1076 | 1077 | 3. A的代码都是2个空格缩进,B的是4个空格缩进,个人比较喜欢4空格缩进 1078 | 1079 | 4. A中启动服务(grunt serve)时,会根据bower.json里的配置,依次查找每个组件的bower配置main文件路径,往index.html中写入bower依赖的script或css引用标签,这本身是个很好的功能。但是,我遇到了两个问题。 1080 | 比如bootstrap组件,在angular工程中,我只需要它的css,而不需要引用它的js(它不是基于angular指令的组件,而是传统的jquery插件,我使用angular-bootstrap代替它)。而每次启动服务,bootstrap的js总是会被引用进来,这造成了浪费。 1081 | 再比如,ztree组件,我们通常需要的是jquery.ztree.all-3.5.js,而ztree中的bower配置的是jquery.ztree.core-3.5.js,这就造成引用不正确。 1082 | 当然,我可以手动修改上述两个组件bower配置的main项,但是我觉得把bower组件提交到版本控制不太好,就像不把本地npm模块提交到版本控制。 1083 | 所以呢,我就把这个 *本身很好却给我带来些许麻烦* 的功能给去掉了,感觉有点对不起yo团队囧。再所以呢,bower组件的依赖,需要手动写入到index.html。 1084 | 1085 | 5. A在初始化中安装单元测试环境,B在本版0.1.1把单元测试环境初始化从工程初始化独立出来,以加快初始化速度 1086 | 6. 未完待续。。。 1087 | 1088 | 1089 | 1090 | ##结语 1091 | 1092 | 本项目为本人 **个人** 第一个开源项目,欢迎大家支持,欢迎交流,共同进步~ 1093 | qq:597719186 1094 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # generator-ngstone 2 | used to build a **Mobile AngularJS** project scaffolding. 3 | 4 | > [Yeoman](http://yeoman.io) generator 5 | 6 | #正式迁移至移动端!!! 7 | 8 | ##说明 9 | 本工具[前身](https://github.com/stoneChen/generator-ngstone/tree/0.2.8-pc)为桌面版,现"华丽转身"为移动端专用,不过在初始化工程后,适当改动也可以适用于*高级桌面浏览器*。 10 | 此大版本舍弃了0.x版本的各种选择,比如angular-route与ui-router,是否初始化基础服务等。增加了autoprefixer,雪碧图自动化等实用功能。 11 | angular及其插件版本升级至~1.4.0. 12 | 增加移动端的一部分通用组件,如提示框、确认框,滑入式子页面等. 13 | 响应式设计(准确的讲,是针对手持设备的响应式),采用【动态计算定宽(640px)与屏幕宽度的比,动态设置页面缩放比】来实现。 14 | 特此将文档重新撰写,不涉及移动端特性的部分,文档内容基本保持不变。 15 | 下面进入getting started. 16 | 17 | ###1.安装 18 | 在此之前请确保你安装了yo bower grunt-cli这三个*全局*模块,然后再安装本模块: 19 | ````bash 20 | npm install -g generator-ngstone 21 | ```` 22 | 23 | ###2.初始化项目 24 | 新建一个目录,比如又叫bookstore(咦?我为什么要说"又"呢),cd 进去(本工具所有命令都是在此根目录下执行),执行: 25 | ````bash 26 | yo ngstone 27 | ```` 28 | 瞬间生成一坨文件,然后开始安装bower组件和node模块。 29 | 初始化完成后,你会发现bookstore下,生成了很多目录与文件。 30 | 简单介绍一下: 31 | 32 | ``` 33 | app/ //项目的html,js,less,css都在这里 34 | mock/ //无后端开发时,需要模拟数据,所有的数据模拟文件都在这里 35 | test/ //所有的测试文件与配置都在这里 36 | bower_components/ //bower安装的组件都在这里 37 | node_modules/ //所有node模块都在这里 38 | bower.json //bower配置文件 39 | Gruntfile.js //不解释了~ 40 | package.json //不解释了~ 41 | README.md //不解释了~ 42 | ``` 43 | 工具将以你的根目录名称+App作为angular的app应用模块名称,所有后续的指令,服务等,都是基于此名称的模块建立的。那我们这里的app名称就是 `bookstoreApp` 。你可以打开app目录下的任意一个js查看。 44 | 45 | ###3.执行雪碧图自动化任务 46 | 这一步是可选的。 47 | 把你项目的所有背景图片复制到app/images目录下,然后执行: 48 | ```bash 49 | grunt sprite 50 | ``` 51 | 将会生成app/styles/sprites.css与雪碧图,index.html已经默认引用了它,在后续开发中,调用相应的class即可引入背景。 52 | 若有不希望自动合并入雪碧图的图片,可以在 grunt-task-params.js中配置,下文会讲到。 53 | 若不执行此步骤,初始化文件中,已经有一个无代码的app/styles/sprites.css用于占位,不会404. 54 | 55 | ###4.启动服务器 56 | 还是在bookstore的目录下: 57 | 58 | ``` 59 | grunt serve 60 | ``` 61 | 62 | 这会调用grunt任务,很快就会调用你的默认浏览器,并打开 http://{{your IP}}:9000/ 这个网址。 63 | 如果你看到页面上有一个bookstoreApp标题,那么恭喜你成功初始化了一个Angular项目! 64 | 在以前的版本,在初始化工程时,就新建了一个叫做main的页面,为了跟之前的教程保持一致,这里执行一次: 65 | ````bash 66 | yo ngstone:route main 67 | ```` 68 | 来新建一个main页面,就当剧透啦。 69 | 回到浏览器,刷新,就可以看到yeoman的LOGO。 70 | 71 | ###5.开发一个新页面 72 | 比如我们要开发一个图书list页面,执行: 73 | ``` 74 | yo ngstone:route list 75 | ``` 76 | 77 | 命令行最后几行提示: 78 | 79 | ``` 80 | script added into index.html: scripts/controllers/list.js 81 | create test/e2e/list/list.js 82 | create app/scripts/controllers/list.js 83 | create test/spec/controllers/list.js 84 | create app/views/list/list.html 85 | ``` 86 | 意思是,添加了引用controller的script标签到index.html,并创建了controller和测试文件,以及视图文件。 87 | 88 | 然后,回到浏览器,把地址栏里的 http://{{your IP}}:9000/#/main 改为 http://{{your IP}}:9000/#/list 回车, 89 | 你会发现,最下面那句话,之前的main变成了list,也就是 90 | 91 | ``` 92 | this is the list view. to be continued... 93 | ``` 94 | 95 | list这个位置的单词是加大字号并加了蓝色的,加以明显的提示和区分。 96 | 97 | 怎么样,创建一个新页面是不是很快? 98 | 接下来我们要制作一个图书列表,首先将下面的代码,替换到 app/views/list/list.html 中: 99 | 100 | ````html 101 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
书名作者价格
{{book.name}}{{book.author}}{{book.price}}
118 |
119 | 120 | ```` 121 | 122 | 123 | 此时,如果你回到浏览器,它会自动刷新,你会看到一个表头。 124 | 然后,打开app/scripts/controllers/list.js编辑controller,添加一个数组: 125 | 126 | 127 | ``` 128 | angular.module('bookstoreApp') 129 | .controller('ListCtrl', function ($scope) { 130 | $scope.books = [ 131 | { 132 | id:1, 133 | name:'用AngularJS开发下一代web应用', 134 | author:'大漠穷秋', 135 | price:1.20 136 | }, 137 | { 138 | id:2, 139 | name:'浪潮之巅', 140 | author:'吴军', 141 | price:1.21 142 | }, 143 | { 144 | id:3, 145 | name:'macTalk', 146 | author:'池建强', 147 | price:1.22 148 | } 149 | ] 150 | }); 151 | ``` 152 | 回到浏览器,还是会自动刷新,表格里面显示了刚刚添加的数组的内容。 153 | 刚刚我们是通过手动修改地址栏访问这个list页面的,现在我们修改index.html的导航,通过点击导航的方式访问list页面: 154 | 155 | ```html 156 | 157 | 158 |
159 |

bookstoreApp

160 |
161 | 165 | ... 166 | 167 | ``` 168 | 再回到浏览器,分别点击标题下的的Home或List,就可以在主页和list页之前切换了!(这个是Angular的路由功能,已经不是本工具应该讨论的范畴了,就不深入说明了- -) 169 | 170 | ####使用hammerjs手势 171 | 本工具引入了ryanmullins-angular-hammer(hammerjs官网推荐),它封装了hammerjs的所有手势指令。 172 | 使用最多的手势非tap莫属了,我们尝试一下。 173 | 在main.html里,为div.page-main根元素添加指令: 174 | ````html 175 |
176 | ... 177 |
178 | ```` 179 | 在main.js里,添加: 180 | ````javascript 181 | $scope.tap = function(){ 182 | alert('hello angular!') 183 | } 184 | ```` 185 | 用手机访问应用,手指点击一下灰色区域,将会弹出hello angular! 186 | 更多手势的使用,请访问 [AngularHammer](http://ryanmullins.github.io/angular-hammer/) 187 | 188 | 这样,一个简单的页面就开发完了,awesome! 189 | 190 | ###6.添加directive 191 | 192 | 指令的生成有2个选项,所以单独拿出来说。比如我们要添加一个叫做hello的指令,执行: 193 | 194 | ``` 195 | yo ngstone:directive hello 196 | ``` 197 | 198 | 控制台会输出: 199 | 200 | ``` 201 | script added into index.html: scripts/directives/hello.js 202 | create app/scripts/directives/hello.js 203 | create test/spec/directives/hello.js 204 | ``` 205 | 206 | 在index.html生成script标签,添加指令模块文件和单元测试文件,其中指令模块的内容是不带template选项的: 207 | 208 | ```javascript 209 | angular.module('bookstoreApp') 210 | .directive('hello', function () { 211 | return { 212 | restrict:'E', 213 | replace:true, 214 | scope:{ 215 | 216 | }, 217 | link: function ($scope,$element,attrs) { 218 | 219 | } 220 | } 221 | }); 222 | ``` 223 | 224 | 如果命令添加一个选项: 225 | 226 | ``` 227 | yo ngstone:directive hello --template 228 | ``` 229 | 230 | 则会添加一个template属性: 231 | 232 | ```javascript 233 | angular.module('bookstoreApp') 234 | .directive('hello', function () { 235 | return { 236 | restrict:'E', 237 | replace:true, 238 | template:'
', //添加的template属性 239 | scope:{ 240 | 241 | }, 242 | link: function ($scope,$element,attrs) { 243 | 244 | } 245 | } 246 | }); 247 | ``` 248 | 249 | 如果选项是 `templateUrl` : 250 | 251 | ``` 252 | yo ngstone:directive hello --templateUrl 253 | ``` 254 | 255 | 控制台输出: 256 | 257 | ``` 258 | script added into index.html: scripts/directives/hello.js 259 | create app/views/_widgets/hello/hello.html 260 | create app/scripts/directives/hello.js 261 | create test/spec/directives/hello.js 262 | ``` 263 | 264 | 你会发现多了一个视图文件,而且是创建到app/views/_widgets这个目录下的,指令文件内容也相应的变成了: 265 | 266 | ```javascript 267 | angular.module('bookstoreApp') 268 | .directive('hello', function () { 269 | return { 270 | restrict:'E', 271 | replace:true, 272 | templateUrl:'./views/_widgets/hello/hello.html', 273 | scope:{ 274 | 275 | }, 276 | link: function ($scope,$element,attrs) { 277 | 278 | } 279 | } 280 | }); 281 | ``` 282 | 283 | 省去了人工配置的麻烦~ 284 | 讲完了ngstone:directive的两个选项,接下来让我们完成这个hello指令。假定我使用了外部模板,即,使用`--templateUrl` 这个选项创建指令,我们在link函数里写上: 285 | 286 | ``` 287 | $element.after('

Hello Directive!

') 288 | ``` 289 | 290 | 再在app/views/list/list.html的末尾(table标签后)加上: 291 | 292 | ```html 293 | 294 | ``` 295 | 296 | 回到浏览器查看list页面,你会发现,最下面出现了: 297 | 298 | 299 | ``` 300 | this is the hello view. to be continued... 301 | 302 | Hello Directive! 303 | ``` 304 | 305 | 表明我们的directive添加成功了! 306 | 307 | ###7.添加factory,filter,decorator 308 | 309 | 添加它们的命令和directive类似,只不过没有那两个template选项,就不再演示了。。 310 | 311 | 另外,provider,service,value,constant我就不额外添加命令了,因为和factory太相似了,而且我个人认为还是factory用的频度最高,而且也够用了。实在不行,生成factory后再稍微改一改,再者它们本质都属于service,问题不大~ 312 | 313 | 还有,decorator特殊一点,它不生成测试文件。 314 | 315 | 大家可以自行尝试添加。 316 | 317 | 文档最后我会把所有可用的命令罗列出来。 318 | 319 | 320 | ###8.单元测试 321 | 单元测试需要用到*PhantomJS*,它是一个开源的虚拟浏览器,没有UI界面,其他行为和真正的浏览器一样,用于单元测试再适合不过。 322 | 由于PhantomJS在某些情况下(天朝干的,你懂的)安装较慢,0.1.1版本开始,将单元测试环境初始化从工程初始化中独立出来。 323 | 你可以先尝试执行 324 | 325 | ```bash 326 | yo ngstone:karma-init 327 | ``` 328 | 329 | 这条命令会安装phantomjs、karma-jasmine、grunt-karma、karma-phantomjs-launcher等模块,如果你的机器能够顺利完成的话,可以跳过此段落。 330 | 如果安装不顺利,八成是卡在phantomjs的安装.可以尝试执行 `npm install -g phantomjs` 或者参考官网 http://phantomjs.org/download.html , 331 | 另外,通过homebrew也可以安装 `brew install phantomjs`。安装好phantomjs后,再执行 `yo ngstone:karma-init` ,应该问题都不大了。 332 | 再啰嗦一句,phantomjs装好后,是全局的,以后再次使用它就不用再安装了。 333 | 334 | 335 | 336 | 单元测试环境安装完毕后,打开test/spec/controllers/list.js,把最下面的it(...),改成: 337 | 338 | ```javascript 339 | it('should attach a list of books to the scope', function () { 340 | expect(scope.books.length).toBe(4); 341 | }); 342 | ``` 343 | 344 | 命令行执行: 345 | 346 | ``` 347 | grunt test-unit 348 | ``` 349 | 350 | 这里会启动*PhantomJS*,执行我们的代码。 351 | 执行完毕后,我们会看到类似如下的提示: 352 | 353 | ``` 354 | PhantomJS 1.9.8 (Mac OS X) Controller: ListCtrl should attach a list of books to the scope FAILED 355 | Expected 3 to be 4. 356 | at /....../bookstore/test/spec/controllers/list.js:20 357 | PhantomJS 1.9.8 (Mac OS X): Executed 2 of 2 (1 FAILED) (0.004 secs / 0.026 secs) 358 | ``` 359 | 表明我的controller单元测试没有通过. scope.books.length应该为3,而不是4 360 | 361 | 我们回过去把 `expect(scope.books.length).toBe(4);`最后的4改成3,再运行单元测试,会出现如下结果: 362 | 363 | ``` 364 | PhantomJS 1.9.8 (Mac OS X): Executed 2 of 2 SUCCESS (0.003 secs / 0.018 secs) 365 | 366 | Done, without errors. 367 | ``` 368 | 369 | 好了!这样单元测试就通过了。 370 | 371 | ###9.执行e2e测试 372 | e2e是 *end-to-end* 的简称,"端到端测试" 或 "场景测试" ,说白了就是让代码模拟人工测试,具有非常大的意义,规模大了可节省很多人力成本。 373 | 首先需要安装protractor,它是angular团队开发的专门用于e2e测试的node模块,并针对angularjs做了优化,它会调用webdriver执行我们的测试代码。 374 | 执行 375 | ```bash 376 | npm install -g protractor 377 | ``` 378 | protractor也是全局的,下次就不用装了~ 379 | 然后,需要下载webdriver。官方的下载地址是 http://chromedriver.storage.googleapis.com/index.html ,不过是被墙的,我这里 https://github.com/stoneChen/generator-ngstone/tree/master/webdriver 准备了一个,一个mac版,一个win版,你可以根据需要选择。 380 | 下过来后,解压缩出来是一个文件,把它放到 /usr/local/lib/node_modules/protractor/selenium下(win用户请自行对号入座,selenium目录可能不存在,自己建一个) 381 | 382 | 接下来我们造一个测试效果。 383 | 打开app/views/list/list.html, 在最下面添加: 384 | 385 | ```html 386 | 387 |

hello {{newer}}

388 | ``` 389 | 390 | 打开test/e2e/list/list.js,修改it(...),最后整个文件内容变成: 391 | 392 | ```javascript 393 | 'use strict'; 394 | describe('e2e test for page: list', function() { 395 | it('should do something', function() { 396 | browser.get('http://localhost:9000/#list'); 397 | var newer = element(by.model('newer')); 398 | var newerBinding = element(by.binding('newer')); 399 | newer.sendKeys('new comer'); 400 | expect(newerBinding.getText()).toEqual('hello new comer'); 401 | 402 | newer.clear(); 403 | newer.sendKeys('protractor'); 404 | expect(newerBinding.getText()).toEqual('hello protractor'); 405 | }); 406 | }); 407 | ``` 408 | 409 | 然后启动服务,执行: 410 | 411 | ```bash 412 | grunt serve 413 | ``` 414 | 再打开一个命令行窗口,还是这个目录,执行: 415 | 416 | ```bash 417 | grunt test-e2e 418 | ``` 419 | 420 | 接下来,就是见证奇迹的时刻了! 421 | 我们可以看到,另一个chrome程序被打开了,自动打开我们指定的url地址(list页面),在最下方的输入框中输入了new comer,清空,再输入protractor,最后自动关闭了窗口。 422 | 回到命令行界面,我们看到了类似如下的日志: 423 | 424 | ```bash 425 | Running "protractor_invoker:e2e" (protractor_invoker) task 426 | Using ChromeDriver directly... 427 | [launcher] Running 1 instances of WebDriver 428 | . 429 | 430 | Finished in 3.248 seconds 431 | 1 test, 2 assertions, 0 failures 432 | 433 | [launcher] 0 instance(s) of WebDriver still running 434 | [launcher] chrome #1 passed 435 | 436 | Done, without errors. 437 | ``` 438 | 439 | 表明我们执行了1个测试,2个断言,0个失败。 440 | 你可以尝试修改测试的输入值,再次运行,就会看到相应的错误信息~是不是很好玩?更多测试代码的语法见 http://angular.github.io/protractor/#/api 我也正在学习中~ 441 | 顺便带一句,上面的protractor_invoker插件,是我第一次写的grunt插件,只是很简单的调用全局protractor,有问题请速速通知我奥~ 442 | 443 | ###10.打包构建工程 444 | 445 | 功能开发好后,需要对静态文件作一系列的加工,这样上线后可以为我们带来加快访问速度,提高性能,减少网络流量等好处。接下来我们开始构建: 446 | 447 | ``` 448 | grunt build 449 | ``` 450 | 451 | 这过程做了很多事,视图打包进js(本人添加),依赖注入替换,js、css合并压缩,图片名添加后缀(刷新缓存)等。 452 | 需要说明一下,我把官网版的generator的gruntfile任务流,去掉了图片压缩。 453 | 图片压缩用到的grunt任务模块很大,而且windows下会报错,无法构建,因此去掉了它,如果需要图片压缩,请自行添加任务流。 454 | 其他还有一些配置,在官方版的基础上删删改改,现在是可以顺利完成构建任务的~ 455 | 至于构建任务流的具体过程,我就先不讲了,有兴趣的同学,可以仔细研究一下,或许我以后会补上来吧。 456 | 457 | 控制台唰唰唰的输出一坨日志后,构建完成。你会发现根目录下多出了一个dist目录,里面的内容比之前的简单多了: 458 | 459 | ``` 460 | fonts/ //所有的字体文件,包括bootstrap和fontawesome的 461 | images/ //所有的图片文件,文件名是添加过后缀的 462 | scripts/ //所有的js,是合并压缩过后的,里面只有两个文件,一个是所有的bower组件代码,一个是我们自己写的代码 463 | styles/ //所有的css,同上 464 | index.html //工程首页,是htmlmin过的 465 | ``` 466 | 如果你觉得构建后的js和css还是太多,你完全可以自己动手,修改构建配置,把它们继续合并~ 467 | 468 | *上面那句话是以前的版本,后来我才发现【把vendor.js和scripts.js分开,与,把vendor.css和all.css分开】的好处是,在项目不停迭代过程中,vendor.js和vendor.css几乎是不太会变化的,而scripts.js与all.css是几乎每次都要变化的,其实是合理利用了浏览器缓存。* 469 | 470 | 然后呢,执行: 471 | 472 | ``` 473 | grunt serve:dist 474 | ``` 475 | 476 | 就会以dist为webroot启动服务,同样调用你的默认浏览器打开地址:http://{{your IP}}:9002 效果是和不打包构建一样的,性能肯定是更高的!随着你的工程越来越大,性能提升会越来越明显。打开浏览器调试工具,查看请求列表,只要寥寥几条请求~够高大上么 477 | 478 | ps: 479 | index.html中带有build和endbuild字样的注释是不能删除的,它们是构建时,寻找被打包压缩文件列表的起止标记!!! 480 | 481 | ###如何模拟ajax数据 482 | 483 | 我们上面的列表数据是js里写死的,所以用不到ajax数据模拟。接下来开始: 484 | 485 | 我们回到第5步中创建的app/scripts/controllers/list.js,把那个数组改成ajax方式获取,这里需要引入$http服务: 486 | 487 | 488 | ```javascript 489 | angular.module('bookstoreApp') 490 | .controller('ListCtrl', function ($scope,$http) { 491 | $http.get('/book?_method=GET').success(function (data) { 492 | $scope.books = data; 493 | }) 494 | 495 | }); 496 | ``` 497 | 498 | 新建bookstore/mock/book/book#GET.json,内容为: 499 | 500 | ```javascript 501 | [ 502 | { 503 | "id":1, 504 | "name":"用AngularJS开发下一代web应用", 505 | "author":"大漠穷秋", 506 | "price":1.20 507 | }, 508 | { 509 | "id":2, 510 | "name":"浪潮之巅", 511 | "author":"吴军", 512 | "price":1.21 513 | }, 514 | { 515 | "id":3, 516 | "name":"macTalk", 517 | "author":"池建强", 518 | "price":1.22 519 | } 520 | ] 521 | ``` 522 | 重新启动服务: 523 | 524 | ``` 525 | grunt serve 526 | ``` 527 | 528 | 访问list页面,发现跟之前的写死数据效果一样。 529 | 再看看node控制台,打印出了刚才的json数据,如果这个时候,你修改json文件里的某条数据,保存后,浏览器也会立即刷新,是不是很贴心? 530 | 531 | 再讲一下这里的ajax数据模拟规则: 532 | 533 | 之前做到这个功能的时候,本来想再弄一个ajax地址与json文件目录的配置文件,想起玉伯曾经说的 *约定大于配置* 原则。还真是有道理,多一个配置文件,岂不是又增加了工作负担?于是我就搞了这么个约定: 534 | 535 | 现在RESTful请求大行其道,我们也不能落下,我列出一个映射表,相信效果会比啰嗦的文字效果好: 536 | 537 | 左边是ajax地址,右边是对应的模拟数据json路径 538 | 539 | /book.json?_method=GET ==> mock/book/book#GET.json //获取列表 540 | 541 | /book.json?_method=POST ==> mock/book/book#POST.json //新增数据 542 | 543 | /book/12.json?_method=GET ==> mock/book/book.N.json#POST.json //查看id为12的数据 544 | 545 | /book/25.json?_method=GET ==> mock/book/book.N.json#POST.json //查看id为25的数据,这2种情况返回相同的json,因为毕竟不是真正的查询数据库,所以搞了个N来代替数字 546 | 547 | /book/55.json?_method=PATCH ==> mock/book/book.N#PATCH.json //修改id为55的数据 548 | 549 | /book/77.json?_method=DELETE ==> mock/book/book.N#DELETE.json //删除id为77的数据 550 | 551 | 怎么样,是不是有点谱了?你可能还有个疑问,为什么多出了一个book目录? 552 | 这又是一个约定,当工程业务域多了以后,每个目录下最好还是以业务域分类的好,所以这里多了个book目录~ 553 | 554 | 在之前的版本,对业务域名称上有些限制,不过每个项目的需求不同,而且也比较麻烦,决定还是取消掉。总的来讲,route名称,rest-ajax一级名称,mock目录名等均取消s后缀的限制。 555 | 556 | 服务器模拟ajax数据的逻辑代码在Gruntfile.js的mockMiddleware函数中,你也可以根据自己的需求做适当改进。 557 | 558 | 559 | PPS: 560 | 以上例子直接使用了$http服务,但在实际开发中,通常使用resourceService这个服务,它以RESTful方式封装了ajax接口,使用起来更加便捷,还是上面这个例子,用resource的方式,这么写就可以了: 561 | 在app/scripts/services/resource-pool.js中,创建一个book资源对象: 562 | ```javascript 563 | angular.module('bookstoreApp') 564 | .factory('resourcePool', function (resourceService) { 565 | var create = resourceService.create; 566 | return { 567 | _url:{//非资源级别的直接写url 568 | 569 | } 570 | ,book:create('/book')//创建book资源对象 571 | ,session: create('/member/session') 572 | } 573 | }); 574 | ``` 575 | 576 | 在app/scripts/controllers/list.js中,获取这个资源对象,并调用query方法: 577 | 578 | ```javascript 579 | angular.module('bookstoreApp') 580 | .controller('ListCtrl', function ($scope,resourcePool) { 581 | var BookRC = resourcePool.book; 582 | BookRC.query({},function (resources) { 583 | $scope.books = resources; 584 | }) 585 | }); 586 | ``` 587 | 588 | 为了使业务处理更加灵活与统一,对后端返回的数据也有一定的要求,把刚刚的bookstore/mock/book/book#GET.json数据调整为: 589 | 590 | ```javascript 591 | { 592 | "stat":"OK", 593 | "data":{ 594 | "collection":[ 595 | { 596 | "id":1, 597 | "name":"用AngularJS开发下一代web应用", 598 | "author":"大漠穷秋", 599 | "price":1.20 600 | }, 601 | { 602 | "id":2, 603 | "name":"浪潮之巅", 604 | "author":"吴军", 605 | "price":1.21 606 | }, 607 | { 608 | "id":3, 609 | "name":"macTalk", 610 | "author":"池建强", 611 | "price":1.22 612 | } 613 | ] 614 | } 615 | } 616 | ``` 617 | 618 | 回到浏览器,发现一样可以渲染出刚刚的列表。优势不仅仅是这样,resource底层将列表中的每一条数据封装成了resource对象,获取其中的任一条数据,即可调用内置的删、改等方法,增、查需要通过资源类实现。 619 | 而resourcePool统一管理所有的资源类,这样在controller里不需要关注具体的ajax地址是什么,只需要关注数据! 620 | 总之,resourceService是为简化数据的CURD而生的! 621 | 详见app/scripts/base/services/resource.js源码 622 | 623 | ==== 624 | 625 | 以上是本工具的基本使用方法,下面讲一讲稍高级的用法 626 | 627 | ==== 628 | 629 | ##命令列表 630 | 631 | 1. yo ngstone 初始化工程 632 | 2. yo ngstone:directive {name} 生成directive,插入script标签到index.html,生成测试文件 633 | 3. yo ngstone:factory {name} 生成factory,插入script标签到index.html,生成测试文件 634 | 4. yo ngstone:filter {name} 生成filter,插入script标签到index.html,生成测试文件 635 | 5. yo ngstone:decorator {name} 生成decorator,插入script标签到index.html 636 | 6. yo ngstone:controller {name} 生成controller,插入script标签到index.html,生成测试文件 637 | 7. yo ngstone:view {name} 生成视图文件 638 | 8. yo ngstone:route {name} 向app/scripts/app.js中添加路由配置,调用ngstone:controller和ngstone:view 639 | 9. yo ngstone:karma-init 初始化单元测试环境 640 | 641 | ##base服务中的关键模块说明 642 | 643 | 1. xhrService服务封装了ajax请求,如果用了resourceService服务,一般不需要直接调用此ajax服务 644 | 2. resourceService服务用法大致与官方的resource一致,资源默认方法稍有不同,资源url中的参数格式使用 `{xxx}` 代替 `:xxx` ,资源实例添加了若干方法,去除action方法调用后的返回值(现在是undefined),个人认为这样会简单一些 645 | 3. resourcePool,即资源池,用于存放所有的资源对象 646 | 4. localStorageService服务是第三方的模块,用于实现本地数据持久化,初始化时用于存储菜单组折叠状态,你可以用于更多业务 647 | 5. dialogService封装了确认框、与子页面组件 648 | 6. rootDataService封装了$rootScope下自定义对象的存取,以防止管理混乱 649 | 650 | 每个base中模块,都有注释,更多说明将持续补充。。。 651 | 652 | 653 | ###grunt-task-params.js配置 654 | 这是将被Gruntfile.js require的参数模块,提供给某些grunt任务的额外参数配置 655 | 目前初始化工程后,直接支持: 656 | 1. 雪碧图的资源增量配置,主要用于排除某些不需要合并的图片,默认合并images目录下的所有图片(已排除images/_tmp/**/*.png,images/spritesheet.png,images/yeoman.png) 657 | 2. 配置需动态插入index.html的script,主要用于weinre调试脚本,静态假数据js等脚本的插入,而无需手动在index.html中删除,添加,删除,添加... 658 | 3. 配置node静态http服务是否绑定IP,各有利弊,详见grunt-task-params.js注释 659 | 660 | 总之,已经开了个口子给你,更多功能,请自行扩展。 661 | 662 | ##与官方版generator-angular的对比 663 | 664 | 这里讲一下两者的关键区别,简要起见,我们约定: 665 | *A*=generator-angular生成的工程 666 | *B*=generator-ngstone生成的工程 667 | 1. A的index.html的bower组件与我们自己模块的引用路径都是顶级路径,比如angular.js的引用路径: 668 | 669 | ```html 670 | 671 | ``` 672 | 673 | 而实际上,index.html与bower_components不在同一层目录! 674 | 用grunt启动服务,的确没有问题,可以加载到bower组件,因为gruntfile中的connect任务有相应配置 675 | 而我们有自己的后端,当我们部署工程的时候,bower组件就加载不到了!还要用nginx等前端反向代理额外配置一下才能加载到,这样很不方便。 676 | 于是在B中,我把路径前变成了相对路径,即: 677 | 678 | ``` 679 | 680 | ``` 681 | 682 | 这样,到了部署的时候也不会有问题了,省去nginx等的额外配置。 683 | 684 | 2. A打包是不会将模板文件打包进js的,B会 685 | 686 | 3. A的代码都是2个空格缩进,B的是4个空格缩进,个人比较喜欢4空格缩进 687 | 688 | 4. A中启动服务(grunt serve)时,会根据bower.json里的配置,依次查找每个组件的bower配置main文件路径,往index.html中写入bower依赖的script或css引用标签,这本身是个很好的功能。但是,我遇到了两个问题。 689 | 比如bootstrap组件,在angular工程中,我只需要它的css,而不需要引用它的js(它不是基于angular指令的组件,而是传统的jquery插件,我使用angular-bootstrap代替它)。而每次启动服务,bootstrap的js总是会被引用进来,这造成了浪费。 690 | 再比如,ztree组件,我们通常需要的是jquery.ztree.all-3.5.js,而ztree中的bower配置的是jquery.ztree.core-3.5.js,这就造成引用不正确。 691 | 当然,我可以手动修改上述两个组件bower配置的main项,但是我觉得把bower组件提交到版本控制不太好,就像不把本地npm模块提交到版本控制。 692 | 所以呢,我就把这个 *本身很好却给我带来些许麻烦* 的功能给去掉了,感觉有点对不起yo团队囧。再所以呢,bower组件的依赖,需要手动写入到index.html。 693 | 5. A在初始化中安装单元测试环境,B在本版0.1.1把单元测试环境初始化从工程初始化独立出来,以加快初始化速度 694 | 6. B增加了less自动化任务,包括watch与打包时都会自动编译成css 695 | 7. B增加了添加浏览器前缀的postcss处理 696 | 8. B在新建路由时,会新建与路由相对应的less由于是单页应用,所有的css都一次性加载,势必考虑到css命名空间的问题 697 | 9. B在common.less中增加了flex-box的基础class,直接调用相应class即可。 698 | 10. B增加了雪碧图的自动化处理 699 | 11. B增加了grunt额外参数配置,即上一节中的grunt-task-params.js 700 | 12. B增加了自动获取当前机器的IP,自动以此IP访问应用,好处是,结合chrome的插件二维码生成器,可以很快用手机扫二维码,访问应用 701 | 13. B增加了dialog,msg等常用组件。 702 | 14. B是针对移动端的,引入了hammerjs来处理手势事件 703 | 未完待续。。。 704 | 705 | ##结语 706 | 本项目为本人 **个人** 第一个开源项目,欢迎大家支持,欢迎交流,共同进步~ 707 | qq:597719186 708 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var yosay = require('yosay'); 6 | var generatorUtil = require('../generator-util'); 7 | 8 | module.exports = yeoman.generators.Base.extend({ 9 | constructor: function () { 10 | yeoman.generators.Base.apply(this, arguments); 11 | 12 | this.pkg = require('../package.json'); 13 | this.argument('appName', { 14 | type: String, 15 | required: false, 16 | defaults:path.basename(process.cwd())//hookFor执行以后,this.args会清空,不知为何。。。只能这样写 17 | }); 18 | this.appName = this._.camelize(this.appName); 19 | this.scriptAppName = generatorUtil.addAppNameSuffix(this.appName); 20 | this.classedName = this._.classify(this.name); 21 | this.cameledName = this._.camelize(this.name); 22 | this.appPath = 'app'; 23 | this.preinstallScripts = []; 24 | }, 25 | 26 | prompting: function () { 27 | var done = this.async(); 28 | // Have Yeoman greet the user. 29 | this.log(yosay( 30 | 'Welcome to the swell ' + chalk.red('Angular-stone') + ' generator!' 31 | )); 32 | var isCwdEmpty = generatorUtil.isDirEmpty(process.cwd()); 33 | var prompts = [ 34 | { 35 | type: 'confirm', 36 | name: 'needEmptyCwd', 37 | message: '当前目录非空,是否清空(bower_components与node_modules将跳过)?\n', 38 | default: true 39 | } 40 | ]; 41 | if(isCwdEmpty){ 42 | prompts.splice(0,1); 43 | } 44 | this.prompt(prompts, function (props) { 45 | if(props.needEmptyCwd){ 46 | //清空当前目录 47 | generatorUtil.clearDir(process.cwd(),true); 48 | } 49 | done(); 50 | }.bind(this)); 51 | }, 52 | writing: { 53 | common: function () { 54 | this.sourceRoot(path.join(__dirname, '../templates/common')); 55 | this.template( 56 | this.templatePath('_package.json'), 57 | this.destinationPath('package.json') 58 | ); 59 | this.template( 60 | this.templatePath('_README.md'), 61 | this.destinationPath('README.md') 62 | ); 63 | this.template( 64 | this.templatePath('_bower.json'), 65 | this.destinationPath('bower.json') 66 | ); 67 | this.template( 68 | this.templatePath('_Gruntfile.js'), 69 | this.destinationPath('Gruntfile.js') 70 | ); 71 | this.template( 72 | this.templatePath('grunt-task-params.js'), 73 | this.destinationPath('grunt-task-params.js') 74 | ); 75 | this.template( 76 | this.templatePath('sprite.css.handlebars'), 77 | this.destinationPath('sprite.css.handlebars') 78 | ); 79 | this.template( 80 | this.templatePath('app/index.html'), 81 | this.destinationPath('app/index.html') 82 | ); 83 | this.directory( 84 | 'app/images', 85 | this.destinationPath('app/images') 86 | ); 87 | 88 | }, 89 | javascripts: function () { 90 | this.sourceRoot(path.join(__dirname, '../templates/javascripts')); 91 | this.template( 92 | this.templatePath('app.js'), 93 | this.destinationPath('app/scripts/app.js') 94 | ); 95 | this.template( 96 | this.templatePath('unit-mock/mock.js'), 97 | this.destinationPath('test/unit-mock/mock.js') 98 | ); 99 | this.template( 100 | this.templatePath('e2e/protractor.conf.js'), 101 | this.destinationPath('test/protractor.conf.js') 102 | ); 103 | }, 104 | styles: function () { 105 | this.sourceRoot(path.join(__dirname, '../templates/styles')); 106 | var sourceDir = 'simple'; 107 | this.directory( 108 | sourceDir, 109 | this.destinationPath('app/styles') 110 | ); 111 | }, 112 | preinstall: function () { 113 | this.sourceRoot(path.join(__dirname, '../templates/javascripts/_preinstall')); 114 | //scripts 115 | //this._templateAndPreinstall('filters'); 116 | //this.preinstallScripts.push(path.join('scripts',subDir,f)); 117 | //this._templateAndPreinstall('base/directives'); 118 | //this._templateAndPreinstall('base/services'); 119 | //this._templateAndPreinstall('services'); 120 | this._templateAndPreinstall('base'); 121 | this.preinstallScripts.push(path.join('scripts','app.js'));//放在ngCustomBase后 122 | this._templateAndPreinstall('services'); 123 | //views 124 | this.sourceRoot(path.join(__dirname, '../templates/views')); 125 | this.directory( 126 | '_preinstall', 127 | this.destinationPath('app/views') 128 | ); 129 | } 130 | }, 131 | _templateAndPreinstall: function (subDir) { 132 | generatorUtil.readFiles(path.join(this.sourceRoot(),subDir), function (f,fullPath) { 133 | var paths = fullPath.split('_preinstall/');//无奈初次下策 134 | var relativePath = paths[1]; 135 | var destPath = path.join('app/scripts',relativePath); 136 | this.template(fullPath,this.destinationPath(destPath)); 137 | this.preinstallScripts.push(path.join('scripts',relativePath)); 138 | //generatorUtil.addScriptToIndex(this.appPath,path.join('scripts',subDir,f),this); 139 | },this); 140 | }, 141 | _addPreinstallScriptsToIndex: function () {//被迫的,如果靠前执行,会访问不到index.html囧 142 | this.preinstallScripts.forEach(function (script) { 143 | generatorUtil.addScriptToIndex(this.appPath,script,this); 144 | },this); 145 | }, 146 | install: function () { 147 | this._addPreinstallScriptsToIndex(); 148 | if(this.options['skip-install']){ 149 | return; 150 | } 151 | var self = this; 152 | this.log(chalk.yellow('start to install bower dependencies:')); 153 | this.bowerInstall([],{},function () { 154 | self.log(chalk.yellow('start to install npm dependencies:')); 155 | self.npmInstall(); 156 | }); 157 | }, 158 | end: function () { 159 | this.log(chalk.green('Initialization has been done. Have fun!')); 160 | } 161 | }) 162 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ##1.0.0 2 | 正式升级到1.0.0 3 | 1. 全面向移动端迁移,不考虑PC端,删除初始化工程时的多种选择(比如只使用ui-router,删除ui-router的支持),增强可维护性 4 | 2. 增加了新建route时新建子页面的less功能 5 | 3. 增加雪碧图自动化功能 6 | 4. 增加postcss任务为css属性添加浏览器前缀 7 | 5. 默认加载基础服务,并包装成base模块 8 | 6. 删除biz相关模块 9 | 10 | ##0.2.1 11 | 1. 集成ui-router,用户可在angular-route和angular-ui-router之间选择,工具会根据结果做不同的初始化,ngstone:route命令也会根据是否使用ui-router做相应更改 12 | 2. 去除route等命名限制(比如不能以s,x结尾),ajax模拟机制,有少许影响,详见readme 13 | 3. 其他优化 14 | 15 | ##0.1.1 16 | 1. 集成protractor的e2e测试(可选) 17 | 2. 单元测试改为可选,以及目录更改 18 | 3. 单元测试所需的phantomjs虚拟浏览器安装以及其他依赖安装,从初始化中独立出来,因为安装过程很慢,同时也为了初始化工程更快 19 | 20 | ##0.0.11 21 | 从0.0.1到0.0.11陆续完成完成第一版 22 | -------------------------------------------------------------------------------- /controller/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var generatorUtil = require('../generator-util'); 6 | var SubGeneratorBase = require('../subgenerator-base'); 7 | 8 | module.exports = SubGeneratorBase.extend({ 9 | writing: function () { 10 | this.generateSourceAndTest( 11 | 'controller', 12 | 'controllers' 13 | ) 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /decorator/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var SubGeneratorBase = require('../subgenerator-base'); 6 | 7 | module.exports = SubGeneratorBase.extend({ 8 | writing: function () { 9 | var skipTestFile = true;//decorator没有测试文件 10 | this.generateSourceAndTest( 11 | 'decorator', 12 | 'services', 13 | skipTestFile 14 | ) 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /directive/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var generatorUtil = require('../generator-util'); 6 | var SubGeneratorBase = require('../subgenerator-base'); 7 | 8 | module.exports = SubGeneratorBase.extend({ 9 | writing: function () { 10 | this.directiveTemplateProperty = ''; 11 | if(this.options['template']){ 12 | this.directiveTemplateProperty = "\n template:'
'," 13 | }else if(this.options['templateUrl']){ 14 | var viewName = this.dashedName; 15 | var subPath = path.join('views/_widgets',viewName,viewName + '.html'); 16 | var directiveTemplatePath = './' + subPath; 17 | this.directiveTemplateProperty = "\n templateUrl:'" + generatorUtil.transformSlash(directiveTemplatePath) + "',"; 18 | this.generateHtmlFile(viewName,path.join(this.appPath,subPath)); 19 | } 20 | this.generateSourceAndTest( 21 | 'directive', 22 | 'directives' 23 | ) 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /factory/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var SubGeneratorBase = require('../subgenerator-base'); 6 | 7 | module.exports = SubGeneratorBase.extend({ 8 | writing: function () { 9 | this.generateSourceAndTest( 10 | 'factory', 11 | 'services' 12 | ) 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /filter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var SubGeneratorBase = require('../subgenerator-base'); 6 | 7 | module.exports = SubGeneratorBase.extend({ 8 | writing: function () { 9 | this.generateSourceAndTest( 10 | 'filter', 11 | 'filters' 12 | ) 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /generator-util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | var chalk = require('chalk'); 5 | var child_process = require('child_process'); 6 | 7 | function transformSlash(path) {// windows 下生成的路径是右斜杠 8 | return path.replace(/\\/g, '/'); 9 | } 10 | 11 | function escapeRegExp(str) { 12 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 13 | } 14 | 15 | function rewrite(args) { 16 | /* jshint -W044 */ 17 | // check if splicable is already in the body text 18 | var re = new RegExp(args.splicable.map(function (line) { 19 | return '\\s*' + escapeRegExp(line); 20 | }).join('\n')); 21 | 22 | if (re.test(args.haystack)) { 23 | return args.haystack; 24 | } 25 | 26 | var lines = args.haystack.split('\n'); 27 | 28 | var otherwiseLineIndex = 0; 29 | lines.forEach(function (line, i) { 30 | if (line.indexOf(args.needle) !== -1) { 31 | otherwiseLineIndex = i; 32 | } 33 | }); 34 | 35 | var spaces = 0; 36 | while (lines[otherwiseLineIndex].charAt(spaces) === ' ') { 37 | spaces += 1; 38 | } 39 | 40 | var spaceStr = ''; 41 | while ((spaces -= 1) >= 0) { 42 | spaceStr += ' '; 43 | } 44 | 45 | lines.splice(otherwiseLineIndex, 0, args.splicable.map(function (line) { 46 | return spaceStr + line; 47 | }).join('\n')); 48 | 49 | return lines.join('\n'); 50 | } 51 | 52 | function rewriteFile(args) { 53 | args.path = args.path || process.cwd(); 54 | var fullPath = path.join(args.path, args.file); 55 | 56 | args.haystack = fs.readFileSync(fullPath, 'utf8'); 57 | var body = rewrite(args); 58 | 59 | fs.writeFileSync(fullPath, body); 60 | } 61 | function importLess(appPath,name,generatorInst) { 62 | var filePath = path.join(appPath, 'styles/all.less'); 63 | var content = fs.readFileSync(filePath, 'utf8'); 64 | content += "\n@import '" + name + "';" 65 | fs.writeFileSync(filePath, content); 66 | generatorInst.log(chalk.cyan('less imported into all.less: ') + name + '.less'); 67 | } 68 | function addAppNameSuffix(appname) { 69 | return appname + 'App'; 70 | } 71 | function addScriptSuffix(scriptName) { 72 | if(/^.+\.js$/.test(scriptName)){ 73 | return scriptName; 74 | }else{ 75 | return scriptName + '.js'; 76 | } 77 | } 78 | 79 | function removeDirRecursiveSync(itemPath) { 80 | if (fs.statSync(itemPath).isDirectory()) { 81 | var fileList = fs.readdirSync(itemPath); 82 | fileList.forEach(function(childItemName) { 83 | removeDirRecursiveSync(path.join(itemPath, childItemName)); 84 | }) 85 | fs.rmdirSync(itemPath); 86 | } else { 87 | fs.unlinkSync(itemPath); 88 | } 89 | } 90 | function clearDir(path,skipBowerAndNpmDir) { 91 | if( !fs.existsSync(path) ) { 92 | return; 93 | } 94 | var skip = ['bower_components','node_modules']; 95 | var files = fs.readdirSync(path); 96 | files.forEach(function (path) { 97 | if(skipBowerAndNpmDir && (skip.indexOf(path) !== -1)){ 98 | return; 99 | } 100 | removeDirRecursiveSync(path) 101 | }); 102 | } 103 | 104 | function readFiles(directory,handler,context) { 105 | var files = fs.readdirSync(directory); 106 | files.forEach(function (f) { 107 | if(f.indexOf('.DS_Store') !== -1){ 108 | return; 109 | } 110 | var fullPath = path.join(directory,f); 111 | if(fs.statSync(fullPath).isDirectory()){ 112 | readFiles(fullPath,handler,context); 113 | }else{ 114 | handler.call(context,f,fullPath); 115 | } 116 | }) 117 | } 118 | 119 | function addScriptToIndex(appPath,script,generatorInst) { 120 | var fullPath = path.join(appPath, 'index.html'); 121 | script = addScriptSuffix(script); 122 | rewriteFile({ 123 | file: fullPath, 124 | needle: '', 125 | splicable: [ 126 | '' 127 | ] 128 | }); 129 | generatorInst.log(chalk.green('script tag added into index.html: ') + script); 130 | } 131 | 132 | function isDirEmpty(dir) { 133 | var files = fs.readdirSync(dir); 134 | return !files.length; 135 | } 136 | function isBadBusinessName(name) { 137 | return !(/^[a-z]+?[^sx0-9]$/i).test(name);//每位非数字,且最后一位不能含有s|x 138 | } 139 | module.exports = { 140 | transformSlash:transformSlash, 141 | rewrite: rewrite, 142 | rewriteFile: rewriteFile, 143 | addAppNameSuffix:addAppNameSuffix, 144 | addScriptSuffix:addScriptSuffix, 145 | importLess:importLess, 146 | clearDir:clearDir, 147 | readFiles:readFiles, 148 | isDirEmpty:isDirEmpty, 149 | addScriptToIndex:addScriptToIndex, 150 | isBadBusinessName:isBadBusinessName 151 | }; 152 | -------------------------------------------------------------------------------- /karma-init/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | 6 | module.exports = yeoman.generators.Base.extend({ 7 | initializing: function () { 8 | var enabledComponents = [ 9 | 'jquery/dist/jquery.js', 10 | 'hammerjs/hammer.js', 11 | 'angular/angular.js', 12 | 'angular-route/angular-route.js', 13 | 'angular-animate/angular-animate.js', 14 | 'angular-sanitize/angular-sanitize.js', 15 | 'ryanmullins-angular-hammer/angular.hammer.js', 16 | 'angular-local-storage/dist/angular-local-storage.js', 17 | 'angular-mocks/angular-mocks.js' 18 | ]; 19 | this.invoke('karma:app', { 20 | options: { 21 | // 'skip-install': this.options['skip-install'], 22 | 'base-path': '../', 23 | 'bower-components': enabledComponents.join(','), 24 | 'app-files': 'app/scripts/**/*.js', 25 | 'test-files': [ 26 | 'test/unit-mock/**/*.js', 27 | 'test/unit/**/*.js' 28 | ].join(','), 29 | 'bower-components-path': 'bower_components' 30 | } 31 | }); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-ngstone", 3 | "version": "1.0.1", 4 | "description": "Yeoman generator", 5 | "license": "MIT", 6 | "main": "app/index.js", 7 | "repository": "stoneChen/generator-ngstone", 8 | "author": { 9 | "name": "cloudstone", 10 | "email": "baby31529@aliyun.com", 11 | "url": "https://github.com/stoneChen" 12 | }, 13 | "engines": { 14 | "node": ">=0.10.0" 15 | }, 16 | "scripts": { 17 | "test": "mocha" 18 | }, 19 | "files": [ 20 | "app", 21 | "biz", 22 | "biz-cfg", 23 | "controller", 24 | "decorator", 25 | "directive", 26 | "factory", 27 | "filter", 28 | "route", 29 | "templates", 30 | "test", 31 | "view", 32 | "karma-init", 33 | "generator-util.js", 34 | "subgenerator-base.js" 35 | ], 36 | "keywords": [ 37 | "yeoman-generator" 38 | ], 39 | "dependencies": { 40 | "yeoman-generator": "^0.18.0", 41 | "chalk": "^0.5.0", 42 | "yosay": "^0.3.0" 43 | }, 44 | "devDependencies": { 45 | "mocha": "*" 46 | }, 47 | "peerDependencies": { 48 | "yo": ">=1.0.0", 49 | "generator-karma": ">=0.8.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /route/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var SubGeneratorBase = require('../subgenerator-base'); 6 | var generatorUtil = require('../generator-util'); 7 | 8 | module.exports = SubGeneratorBase.extend({ 9 | writing: function () { 10 | this._rewriteAppJs(); 11 | this._generateE2ETest(); 12 | this._addLessFile(); 13 | this.invoke('ngstone:controller',{ 14 | args:[this.name] 15 | }); 16 | this.invoke('ngstone:view',{ 17 | args:[this.name,'--route'] 18 | }) 19 | }, 20 | _addLessFile: function () { 21 | this.sourceRoot(path.join(__dirname, '../templates/styles')); 22 | this.template( 23 | this.templatePath('page.less'), 24 | this.destinationPath(path.join(this.appPath,'styles',this.dashedName + '.less')) 25 | ); 26 | generatorUtil.importLess(this.appPath,this.dashedName,this); 27 | }, 28 | _rewriteAppJs:function () { 29 | this.uri = this.name; 30 | if (this.options.uri) { 31 | this.uri = this.options.uri; 32 | } 33 | 34 | var config = { 35 | file: path.join( 36 | this.appPath, 37 | 'scripts/app.js' 38 | ), 39 | needle: '.otherwise', 40 | splicable: [ 41 | ".when('/" + this.uri + "', {", 42 | " templateUrl: './views/" + (this.name + '/' + this.name) + ".html',",//path.join(this.name,this.name)在windows下斜杠是反的 43 | " controller: '" + this.classedName + "Ctrl'", 44 | "})" 45 | ] 46 | }; 47 | generatorUtil.rewriteFile(config); 48 | }, 49 | _generateE2ETest: function () { 50 | this.sourceRoot(path.join(__dirname, '../templates/javascripts')); 51 | this.template( 52 | this.templatePath('e2e/spec.js'), 53 | this.destinationPath(path.join('test/e2e',this.name,generatorUtil.addScriptSuffix(this.name))) 54 | ) 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /subgenerator-base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var generatorUtil = require('./generator-util'); 6 | 7 | module.exports = yeoman.generators.NamedBase.extend({ 8 | constructor: function () { 9 | yeoman.generators.NamedBase.apply(this, arguments); 10 | var generatorCfg = this.config.getAll(); 11 | 12 | //bower config 13 | var bowerConfig = require(path.join(process.cwd(), 'bower.json')); 14 | this.appName = bowerConfig.name; 15 | this.scriptAppName = generatorUtil.addAppNameSuffix(this.appName); 16 | this.classedName = this._.classify(this.name); 17 | this.cameledName = this._.camelize(this.name); 18 | this.dashedName = this._.dasherize(this.name); 19 | this.appPath = bowerConfig.appPath; 20 | }, 21 | generateSourceAndTest: function (templateName,dest,skipTestFile) { 22 | this.sourceRoot(path.join(__dirname, './templates/javascripts')); 23 | var sourceFileName = generatorUtil.addScriptSuffix(templateName); 24 | var targetFileName = generatorUtil.addScriptSuffix(this.dashedName); 25 | this.template( 26 | this.templatePath(sourceFileName), 27 | this.destinationPath(this.appPath,'scripts',dest,targetFileName) 28 | ); 29 | generatorUtil.addScriptToIndex(this.appPath,path.join('scripts',dest, this.dashedName),this); 30 | //add test file 31 | if(!skipTestFile){ 32 | if(templateName === 'factory'){ 33 | sourceFileName = generatorUtil.addScriptSuffix('service'); 34 | } 35 | this.template( 36 | this.templatePath('unit',sourceFileName), 37 | this.destinationPath('test/unit',dest,targetFileName) 38 | ); 39 | } 40 | }, 41 | generateHtmlFile: function (viewName,dest) { 42 | this.sourceRoot(path.join(__dirname, './templates/views')); 43 | dest = dest || path.join(this.appPath,'views',viewName,viewName + '.html'); 44 | this.template( 45 | this.templatePath('view.html'), 46 | this.destinationPath(dest)//创建一层同名的目录,可以在这里创建其他视图,如dialog 47 | ) 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /templates/common/_Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on <%= (new Date).toLocaleString() %> using <%= pkg.name %> <%= pkg.version %> 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | var os = require('os'); 10 | var fs = require('fs'); 11 | var path = require('path'); 12 | 13 | module.exports = function (grunt) { 14 | var gruntTaskParams = require('./grunt-task-params'); 15 | // Load grunt tasks automatically 16 | require('load-grunt-tasks')(grunt); 17 | 18 | // Time how long tasks take. Can help when optimizing build times 19 | require('time-grunt')(grunt); 20 | 21 | // Configurable paths for the application 22 | var appConfig = { 23 | projectName: require('./bower.json').name + 'App', 24 | app: require('./bower.json').appPath || 'app', 25 | dist: 'dist' 26 | }; 27 | /** 28 | * Get ip(v4) address 29 | * @return {String} the ipv4 address or 'localhost' 30 | */ 31 | var getIPAddress = function () { 32 | var ifaces = os.networkInterfaces(); 33 | var ip = ''; 34 | for (var dev in ifaces) { 35 | ifaces[dev].forEach(function (details) { 36 | if (ip === '' && details.family === 'IPv4' && !details.internal) { 37 | ip = details.address; 38 | return; 39 | } 40 | }); 41 | } 42 | return ip || "127.0.0.1"; 43 | }; 44 | function mockMiddleware(req, res, next) { 45 | //用于mock数据 46 | grunt.log.writeln('request from client:' + req.url); 47 | var urlReg = /^\/(.+)\?_method=(GET|POST|PATCH|DELETE|PUT).*$/;// /users/5.json?_method=GET 48 | var match = req.url.match(urlReg);// ['...','users/5','GET'] 49 | if(!match){ 50 | grunt.log.writeln('not matched,passed...'); 51 | return next(); 52 | } 53 | var fileNameSegments = []; 54 | var segments = match[1].split('/'); 55 | segments.forEach(function (pathName) { 56 | if(/\D+/.test(pathName)){//非数字 57 | fileNameSegments.push(pathName) 58 | }else if(/\d+/.test(pathName)){//数字 59 | fileNameSegments.push('N') 60 | } 61 | }); 62 | var fileName = fileNameSegments.join('.');// user.N 63 | fileName += '#' + match[2].toUpperCase() + '.json';// user.N#GET.json 64 | grunt.log.writeln('parsed fileName:' + fileName); 65 | var filePath = path.join('mock',segments[0],fileName); 66 | grunt.log.writeln('parsed filePath:' + filePath); 67 | var result = ''; 68 | if(grunt.file.exists(filePath)){ 69 | result = grunt.file.read(filePath); 70 | }else{ 71 | result = JSON.stringify({ 72 | stat:'ERROR', 73 | errors:'mock data file:[' + filePath + '] doesn\'t exist!' 74 | }) 75 | } 76 | grunt.log.writeln(result); 77 | res.end(result); 78 | } 79 | // Define the configuration for all the tasks 80 | grunt.initConfig({ 81 | 82 | // Project settings 83 | yeoman: appConfig, 84 | 85 | // Watches files for changes and runs tasks based on the changed files 86 | watch: { 87 | gruntfile: { 88 | files: ['Gruntfile.js'] 89 | }, 90 | livereload: { 91 | options: { 92 | livereload: '<%%= connect.options.livereload %>' 93 | }, 94 | files: [ 95 | '<%%= yeoman.app %>/index.html', 96 | '<%%= yeoman.app %>/views/**/*.html', 97 | '<%%= yeoman.app %>/scripts/**/*.js', 98 | '<%%= yeoman.app %>/styles/**/*.css', 99 | '<%%= yeoman.app %>/images/**/*.*', 100 | 'mock/**/*.json' 101 | ] 102 | }, 103 | less:{ 104 | files: "<%%= yeoman.app %>/styles/*.less", 105 | tasks: ["less","postcss"] 106 | } 107 | }, 108 | less: { 109 | all: { 110 | options: { 111 | paths: ["<%%= yeoman.app %>/styles"], 112 | yuicompress: true 113 | }, 114 | files: { 115 | "<%%= yeoman.app %>/styles/all.css": "<%%= yeoman.app %>/styles/all.less" 116 | } 117 | } 118 | }, 119 | //为css添加浏览器前缀 120 | postcss: { 121 | options: { 122 | //map: true, 123 | processors: [ 124 | require('autoprefixer-core')({browsers: '> 1%, last 2 versions, Firefox ESR, Opera 12.1'}) 125 | ] 126 | }, 127 | dist: { 128 | files: [{ 129 | expand: true, 130 | cwd: '<%%= yeoman.app %>/styles/', 131 | src: 'all.css', 132 | dest: '<%%= yeoman.app %>/styles/' //即使在开发环境下,希望也能够自动添加浏览器前缀,以便在开发环境下测试浏览器兼容性 133 | }] 134 | } 135 | }, 136 | // The actual grunt server settings 137 | connect: { 138 | options: { 139 | port: 9000, 140 | // Change this to '0.0.0.0' to access the server from outside. 141 | hostname: gruntTaskParams.serverBoundWithIP ? getIPAddress() : 'localhost',//connect似乎不能配置自动获取IP的方式打开地址,从anywhere偷了代码来 142 | open:true, 143 | livereload: 35729 144 | }, 145 | livereload: { 146 | options: { 147 | open:true, 148 | middleware: function (connect) { 149 | return [ 150 | function (req, res, next) {//首页注入脚本 151 | if(req.originalUrl !== '/'){ 152 | return next(); 153 | } 154 | var indexHTMLPath = appConfig.app + '/index.html'; 155 | var indexHTML = grunt.file.read(indexHTMLPath); 156 | var serveScripts = gruntTaskParams.serveScripts; 157 | var scriptTags = ['']; 158 | serveScripts.forEach(function (script) { 159 | scriptTags.push(''); 160 | }); 161 | var injectedHTML = indexHTML.replace(/<\/body>/, function(w) { 162 | return (scriptTags.concat([w])).join('\n'); 163 | }); 164 | res.end(injectedHTML); 165 | }, 166 | function (req, res, next) {//其他中间件逻辑 167 | return next(); 168 | }, 169 | connect().use( 170 | '/bower_components', 171 | connect.static('./bower_components') 172 | ), 173 | connect.static(appConfig.app), 174 | mockMiddleware 175 | ]; 176 | } 177 | } 178 | }, 179 | test: { 180 | options: { 181 | port: 9001, 182 | middleware: function (connect) { 183 | return [ 184 | connect.static('test'), 185 | connect().use( 186 | '/bower_components', 187 | connect.static('./bower_components') 188 | ), 189 | connect.static(appConfig.app) 190 | ]; 191 | } 192 | } 193 | }, 194 | dist: { 195 | options: { 196 | port: 9002, 197 | middleware: function (connect) { 198 | return [ 199 | connect.static('dist'), 200 | mockMiddleware 201 | ]; 202 | } 203 | } 204 | } 205 | }, 206 | // Empties folders to start fresh 207 | clean: { 208 | dist: { 209 | files: [ 210 | { 211 | dot: true, 212 | src: [ 213 | '.tmp', 214 | '<%%= yeoman.dist %>/{,*/}*', 215 | '!<%%= yeoman.dist %>/.git*' 216 | ] 217 | } 218 | ] 219 | }, 220 | server: '.tmp' 221 | }, 222 | 223 | // Renames files for browser caching purposes 224 | filerev: { 225 | dist: { 226 | src: [ 227 | '<%%= yeoman.dist %>/scripts/{,*/}*.js', 228 | '<%%= yeoman.dist %>/styles/{,*/}*.css', 229 | '<%%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 230 | '<%%= yeoman.dist %>/styles/fonts/*' 231 | ] 232 | } 233 | }, 234 | 235 | // Reads HTML for usemin blocks to enable smart builds that automatically 236 | // concat, minify and revision files. Creates configurations in memory so 237 | // additional tasks can operate on them 238 | useminPrepare: { 239 | html: '<%%= yeoman.app %>/index.html', 240 | options: { 241 | dest: '<%%= yeoman.dist %>', 242 | flow: { 243 | html: { 244 | steps: { 245 | js: ['concat', 'uglifyjs'], 246 | css: ['cssmin'] 247 | }, 248 | post: {} 249 | } 250 | } 251 | } 252 | }, 253 | 254 | // Performs rewrites based on filerev and the useminPrepare configuration 255 | usemin: { 256 | html: ['<%%= yeoman.dist %>/{,*/}*.html', '<%%= yeoman.dist %>/scripts/scripts*.js'],//后面这一项是给打包好的模板里的图片等加后缀的 257 | css: ['<%%= yeoman.dist %>/styles/{,*/}*.css'], 258 | options: { 259 | assetsDirs: ['<%%= yeoman.dist %>', '<%%= yeoman.dist %>/images'] 260 | } 261 | }, 262 | 263 | htmlmin: { 264 | dist: { 265 | options: { 266 | minifyJS:true, 267 | collapseWhitespace: true, 268 | conservativeCollapse: true, 269 | collapseBooleanAttributes: true, 270 | removeCommentsFromCDATA: true, 271 | removeOptionalTags: true 272 | }, 273 | files: [ 274 | { 275 | expand: true, 276 | cwd: '<%%= yeoman.dist %>', 277 | src: ['*.html'],//这里本来有会为拷贝过来的视图html文件的配置,因为使用了模板打包,所以就不需要了 278 | dest: '<%%= yeoman.dist %>' 279 | } 280 | ] 281 | } 282 | }, 283 | 284 | // ng-annotate tries to make the code safe for minification automatically 285 | // by using the Angular long form for dependency injection. 286 | ngAnnotate: { 287 | dist: { 288 | files: [ 289 | { 290 | expand: true, 291 | cwd: '.tmp/concat/scripts', 292 | src: ['*.js'], 293 | dest: '.tmp/concat/scripts' 294 | } 295 | ] 296 | } 297 | }, 298 | 299 | 300 | // Copies remaining files to places other tasks can use 301 | copy: { 302 | dist: { 303 | files: [ 304 | { 305 | expand: true, 306 | dot: true, 307 | cwd: '<%%= yeoman.app %>', 308 | dest: '<%%= yeoman.dist %>', 309 | src: [ 310 | '*.{ico,png,txt}', 311 | '*.html', 312 | 'images/**/*.*', 313 | '!images/_tmp/*.*',//_tmp临时目录不复制 314 | '!images/yeoman.png'//logo不复制 315 | ] 316 | }, 317 | { 318 | expand: true, 319 | cwd: 'bower_components/bootstrap/dist', 320 | src: 'fonts/*', 321 | dest: '<%%= yeoman.dist %>' 322 | }, 323 | { 324 | expand: true, 325 | cwd: 'bower_components/fontawesome', 326 | src: 'fonts/*', 327 | dest: '<%%= yeoman.dist %>' 328 | } 329 | //more copies 330 | ] 331 | } 332 | }, 333 | // Test settings 334 | karma: { 335 | unit: { 336 | configFile: 'test/karma.conf.js', 337 | singleRun: true 338 | } 339 | }, 340 | protractor_invoker:{ 341 | e2e:{ 342 | configFile:'test/protractor.conf.js' 343 | } 344 | }, 345 | concat: {//后期添加,将打包好了模板js,合并到之前的js里,由于useminPrepare通过读取index.html的注释生成concat的列表,无法将此模板文件打包进去,所以只能二次合并 346 | ngtemplates: { 347 | src: ['.tmp/concat/scripts/scripts.js', '.tmp/ngtemplates/ngtemplates.js'], 348 | dest: '.tmp/concat/scripts/scripts.js' 349 | } 350 | }, 351 | 352 | sprite:{//合并sprite 353 | all: { 354 | src: ['<%%= yeoman.app %>/images/**/*.png', 355 | '!<%%= yeoman.app %>/images/_tmp/**/*.png', 356 | '!<%%= yeoman.app %>/images/spritesheet.png',//不排除的话,这个图片会重复,越来越大,因为本身也是png 357 | '!<%%= yeoman.app %>/images/yeoman.png']//排除logo图片 358 | .concat(gruntTaskParams.spriteSrcEX), 359 | dest: '<%%= yeoman.app %>/images/spritesheet.png', 360 | destCss: '<%%= yeoman.app %>/styles/sprites.css', 361 | cssTemplate:'sprite.css.handlebars', 362 | padding:2 363 | } 364 | }, 365 | ngtemplates: {//后期添加,打包模板 366 | dist: { 367 | cwd: '<%%= yeoman.app %>', 368 | src: [ 369 | './views/**/*.html'//这里的./很重要,必须和指令里的templateUrl等一致,否则应用运行时,模板无法加载 370 | ], 371 | dest: '.tmp/ngtemplates/ngtemplates.js', 372 | options: { 373 | module: '<%%= yeoman.projectName %>', 374 | htmlmin: { 375 | collapseBooleanAttributes: true, 376 | collapseWhitespace: true, 377 | removeAttributeQuotes: false,//必须为false,否则usemin的正则无法匹配 378 | removeComments: true, // Only if you don't use comment directives! 379 | removeEmptyAttributes: true, 380 | removeRedundantAttributes: false, //removing this as it can removes properties that can be used when styling 381 | removeScriptTypeAttributes: true, 382 | removeStyleLinkTypeAttributes: true 383 | } 384 | } 385 | } 386 | } 387 | }); 388 | 389 | 390 | grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { 391 | if (target === 'build') { 392 | return grunt.task.run(['build', 'connect:dist:keepalive']); 393 | } 394 | if (target === 'dist') { 395 | return grunt.task.run(['connect:dist:keepalive']); 396 | } 397 | 398 | grunt.task.run([ 399 | 'less',//启动时,编译一次,防止【服务没启动时,less做过修改而不生效】 400 | 'postcss', 401 | 'connect:livereload', 402 | 'watch' 403 | ]); 404 | }); 405 | 406 | grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) { 407 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 408 | grunt.task.run(['serve:' + target]); 409 | }); 410 | 411 | grunt.registerTask('test-unit', [ 412 | 'clean:server', 413 | 'karma:unit' 414 | ]); 415 | grunt.registerTask('test-e2e', [ 416 | 'protractor_invoker' 417 | ]); 418 | grunt.registerTask('build', [ 419 | 'clean:dist', 420 | 'less',//打包时,编译一次,防止【服务没启动时,less做过修改而不生效】 421 | 'postcss', 422 | 'ngtemplates', 423 | 'useminPrepare', 424 | 'concat:generated', 425 | 'concat:ngtemplates', 426 | 'ngAnnotate', 427 | 'copy:dist', 428 | 'cssmin', 429 | 'uglify', 430 | 'filerev', 431 | 'usemin', 432 | 'htmlmin' 433 | ]); 434 | grunt.registerTask('default', ['test:unit','build']); 435 | }; 436 | -------------------------------------------------------------------------------- /templates/common/_README.md: -------------------------------------------------------------------------------- 1 | # <%= appName %> 2 | modern webapp generated by yeoman -------------------------------------------------------------------------------- /templates/common/_bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= appName %>", 3 | "version": "0.0.1", 4 | "appPath": "<%= appPath %>", 5 | "dependencies": { 6 | "jquery":"~2.1.0", 7 | "bootstrap": "3.3.4", 8 | "fontawesome": "~4.2.0", 9 | "angular": "~1.4.0", 10 | "angular-animate":"~1.4.0", 11 | "angular-sanitize": "~1.4.0", 12 | "angular-route": "~1.4.0", 13 | "angular-local-storage":"~0.1.0", 14 | "ryanmullins-angular-hammer":"~2.1.0" 15 | }, 16 | "devDependencies": { 17 | "angular-mocks": "~1.4.0" 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /templates/common/_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= appName %>", 3 | "version": "0.0.1", 4 | "description": "modern webapp generated by yeoman", 5 | "license": "MIT", 6 | "repository": "to be supplemented", 7 | "dependencies": {}, 8 | "devDependencies": { 9 | "autoprefixer-core": "^5.2.0", 10 | "grunt": "^0.4.1", 11 | "grunt-angular-templates": "^0.5.7", 12 | "grunt-contrib-clean": "^0.5.0", 13 | "grunt-contrib-concat": "^0.4.0", 14 | "grunt-contrib-connect": "^0.7.1", 15 | "grunt-contrib-copy": "^0.5.0", 16 | "grunt-contrib-cssmin": "^0.9.0", 17 | "grunt-contrib-htmlmin": "^0.3.0", 18 | "grunt-contrib-less": "^1.0.1", 19 | "grunt-contrib-uglify": "^0.4.0", 20 | "grunt-contrib-watch": "^0.6.1", 21 | "grunt-filerev": "^0.2.1", 22 | "grunt-ng-annotate": "^0.3.0", 23 | "grunt-postcss": "^0.5.1", 24 | "grunt-protractor-invoker":"*", 25 | "grunt-spritesmith": "^4.7.1", 26 | "grunt-usemin": "^2.1.1", 27 | "load-grunt-tasks": "^0.4.0", 28 | "time-grunt": "^0.3.1" 29 | }, 30 | "engines": { 31 | "node": ">=0.10.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/common/app/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoneChen/generator-ngstone/0bfa6500448cca59ceec225696bb7384712ae949/templates/common/app/images/favicon.ico -------------------------------------------------------------------------------- /templates/common/app/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoneChen/generator-ngstone/0bfa6500448cca59ceec225696bb7384712ae949/templates/common/app/images/yeoman.png -------------------------------------------------------------------------------- /templates/common/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 |

<%= scriptAppName %>

48 |
49 |
50 |
51 |
52 |
53 |
54 | 请稍后... 55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /templates/common/grunt-task-params.js: -------------------------------------------------------------------------------- 1 | //本文模块将被Gruntfile.js依赖(require),用于额外的参数配置 2 | module.exports = { 3 | //雪碧图附加资源配置列表,主要用于排除 4 | //demo:'!app/images/mine/bg.png' 5 | spriteSrcEX:[ 6 | 7 | ], 8 | //开发环境启动脚本,需动态添加的脚本 9 | serveScripts:[ 10 | //'http://192.168.183.253:8080/target/target-script-min.js#anonymous', //weinre调试脚本,请自行修改IP 11 | ], 12 | serverBoundWithIP:true //本地服务是否绑定IP地址,true则以IP启动(配合chrome插件,手机扫码访问更便捷,如果电脑IP变化了,则需要重启),false则以localhost启动(如果电脑IP变化了,不需重启) 13 | }; -------------------------------------------------------------------------------- /templates/common/sprite.css.handlebars: -------------------------------------------------------------------------------- 1 | /*本文件由grunt-sprite自动化生成,直接调用.iconsp.iconsp-即可调用相应背景*/ 2 | [class*="iconsp-"]{ 3 | position:relative;/*填上这个属性只是为了方便调用,如果有需要,请覆盖此属性*/ 4 | display:block; 5 | } 6 | [class*="iconsp-"]:before{ 7 | content:""; 8 | position:absolute; 9 | display: block; 10 | } 11 | {{#sprites}} 12 | .iconsp-{{name}}:before { 13 | width: {{px.width}}; 14 | height: {{px.height}}; 15 | background-image: url({{{escaped_image}}}); 16 | background-position: {{px.offset_x}} {{px.offset_y}}; 17 | } 18 | {{/sprites}} -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc overview 4 | * @name ngCustomBase 5 | * @description 定义base模块 6 | * # ngCustomBase 7 | * 8 | * base module declaration. 9 | */ 10 | angular.module('ngCustomBase',[]); -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/directives/dialog-confirm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc directive 4 | * @name ngCustomBase.directive:dialogConfirm 5 | * @description 确认框指令 6 | * # dialogConfirm 7 | */ 8 | angular.module('ngCustomBase') 9 | .directive('dialogConfirm', function () { 10 | return { 11 | restrict:'E', 12 | replace:true, 13 | templateUrl:'./views/_widgets/dialog-confirm/dialog-confirm.html', 14 | scope:{ 15 | content:'@', 16 | action:'&' 17 | }, 18 | link: function ($scope,$element,attrs) { 19 | var closeConfirm = function () { 20 | $element.remove(); 21 | $scope.$destroy(); 22 | }; 23 | var action = $scope.action(); 24 | $scope.ok = function () { 25 | var ret = action(); 26 | //if(angular.isFunction(ret)){ 27 | // ret(); 28 | //} 29 | closeConfirm(); 30 | }; 31 | $scope.cancel = closeConfirm; 32 | } 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/directives/dialog-subpage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc directive 4 | * @name ngCustomBase.directive:dialogSubpage 5 | * @description 滑入式子页面指令 6 | * # dialogSubpage 7 | */ 8 | angular.module('ngCustomBase') 9 | .directive('dialogSubpage', function ($window,utilService,$timeout) { 10 | var J_win = angular.element($window), 11 | J_body = angular.element('body'); 12 | var history = $window.history; 13 | var BODY_CLASSNAME = 'subpage-open'; 14 | var TRANSITION_END_IN = 'webkitTransitionEnd.in transitionend.in'; 15 | var TRANSITION_END_OUT = 'webkitTransitionEnd.out transitionend.out'; 16 | var subpageStack = []; 17 | 18 | var pushSubpage = function (pageData) { 19 | if(!subpageStack.length){//放入第一个子页面的时候,绑定popstate事件 20 | J_win.on('popstate.subpage', function () { 21 | if(!subpageStack.length){ 22 | return; 23 | } 24 | popSubpage(); 25 | }); 26 | } 27 | subpageStack.push(pageData); 28 | history.pushState({type:'subpage'},'subpage' + subpageStack.length); 29 | pageData.el.on(TRANSITION_END_IN, function (event) { 30 | //J_body.addClass(BODY_CLASSNAME);//1.防止两个滚动条 2.防止子页面获取焦点时,父页面在顶部漏出来 31 | pageData.el.off(TRANSITION_END_IN); 32 | }); 33 | 34 | }; 35 | var popSubpage = function () { 36 | var pageData = subpageStack.pop(); 37 | if(!subpageStack.length){//如果弹出后,没有子页面了,则恢复主页面,并移除popstate事件 38 | J_body.removeClass(BODY_CLASSNAME); 39 | J_win.off('popstate.subpage'); 40 | } 41 | utilService.safeApply(pageData.scope,function () { 42 | pageData.scope.slideIn = false;//使子页面滑出 43 | }); 44 | pageData.el.on(TRANSITION_END_OUT, function (event) { 45 | pageData.el.remove(); 46 | pageData.scope.$destroy(); 47 | }); 48 | }; 49 | 50 | return { 51 | restrict:'E', 52 | replace:true, 53 | transclude:true, 54 | template:'
', 55 | scope:{ 56 | onComplete:'&', 57 | direction:'@' 58 | }, 59 | link: function ($scope,$element,attrs,ctrl,$transclude) { 60 | if($scope.direction === 'up'){//自下向上滑入 61 | $element.addClass("bottom-top"); 62 | } 63 | $transclude($scope, function(clone) {//把transclude的内容添加到本scope 64 | $element.empty().append(clone); 65 | }); 66 | $scope.closePage = function () { 67 | history.go(-1); 68 | }; 69 | $scope.onComplete({subScope:$scope});//提供给调用者设置子页面的scope数据 70 | pushSubpage({ 71 | el:$element, 72 | scope:$scope 73 | }) 74 | //history.pushState({type:'subpage'},'subpage'); 75 | //J_win.on('popstate.subpage', closePage); 76 | //$timeout(function () { 77 | $scope.slideIn = true;//使子页面滑入 78 | //},10) 79 | //$element.on(TRANSITION_END_IN, function (event) { 80 | // J_body.addClass(BODY_CLASSNAME);//1.防止两个滚动条 2.防止子页面获取焦点时,父页面在顶部漏出来 81 | // $element.off(TRANSITION_END_IN); 82 | //}); 83 | // 84 | //function closePage (){ 85 | // J_body.removeClass(BODY_CLASSNAME);//恢复 86 | // utilService.safeApply($scope,function () { 87 | // $scope.slideIn = false;//使子页面滑出 88 | // }); 89 | // $element.on(TRANSITION_END_OUT, function (event) { 90 | // $element.remove(); 91 | // $scope.$destroy(); 92 | // }); 93 | // J_win.off('popstate.subpage'); 94 | //} 95 | } 96 | } 97 | }); 98 | -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/directives/msg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc directive 4 | * @name ngCustomBase.directive:msg 5 | * @description 提示信息指令 6 | * # msg 7 | */ 8 | angular.module('ngCustomBase') 9 | .directive('msg', function ($timeout) { 10 | return { 11 | restrict:'E', 12 | replace:true, 13 | templateUrl:'./views/_widgets/msg/msg.html', 14 | scope:{ 15 | msgData:'=' 16 | }, 17 | link: function ($scope,$element,attrs) { 18 | var timer = $timeout(destory,3000); 19 | $scope.close = function () { 20 | $timeout.cancel(timer); 21 | destory(); 22 | }; 23 | function destory(){ 24 | $scope.$destroy(); 25 | $element.fadeOut(200, function () { 26 | $element.remove(); 27 | }); 28 | } 29 | } 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/services/dialog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name ngCustomBase.dialog 5 | * @description 封装确认框组件与滑入式子页面组件 6 | * # dialog 7 | * Factory in the yoHeader 8 | */ 9 | angular.module('ngCustomBase') 10 | .factory('dialogService', function ($compile,$rootScope,templateService) { 11 | var J_body = angular.element('body'); 12 | //var J_subpageContainer = angular.element('.subpage-container'); 13 | return { 14 | confirm: function (options) { 15 | var confirmDOM = angular.element(''); 16 | confirmDOM.attr({ 17 | content:options.content, 18 | action:'action' 19 | }) 20 | var scope = (options.scope || $rootScope).$new(); 21 | angular.extend(scope,options); 22 | var compiledDOM = $compile(confirmDOM)(scope); 23 | J_body.append(compiledDOM); 24 | }, 25 | subpage: function (options) { 26 | var tplPromise = templateService.get(options.templateUrl); 27 | tplPromise.success(function (tpl) { 28 | var confirmDOM = angular.element(''); 29 | confirmDOM.attr({ 30 | 'on-complete':'onComplete(subScope)', 31 | 'direction':options.direction || 'left' 32 | }).html(tpl); 33 | var scope = (options.scope || $rootScope).$new(); 34 | angular.extend(scope,options); 35 | J_body.append(confirmDOM); 36 | $compile(confirmDOM)(scope); 37 | }); 38 | 39 | } 40 | }; 41 | }); -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/services/loading.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name ngCustomBase.loading 5 | * @description 管理页面loading状态的显示 6 | * # loading 7 | * Factory in the ngCustomBase. 8 | */ 9 | angular.module('ngCustomBase') 10 | .config(function (rootDataServiceProvider) { 11 | rootDataServiceProvider.register('ROOT_loadingStatData'); 12 | }) 13 | .factory('loadingService', function (rootDataService) { 14 | var ROOT_loadingStatData = rootDataService.data('ROOT_loadingStatData') 15 | return { 16 | show: function (flag,loadingText) { 17 | flag = angular.isUndefined(flag) ? true : flag; 18 | //loadingText = loadingText || '请稍后...'; 19 | ROOT_loadingStatData.set('show', flag); 20 | //ROOT_loadingStatData.set('loadingText', loadingText); 21 | } 22 | } 23 | }); -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/services/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name ngCustomBase.log 5 | * @description 为了配合weinre的log功能,重新写了一个log服务 6 | * # log 7 | * Factory in the ngCustomBase. 8 | */ 9 | angular.module('ngCustomBase') 10 | .factory('logService', function () { 11 | var methods = ['log','warn','error'];//这三个api一般够用 12 | var api = {}; 13 | methods.forEach(function (m) { 14 | api[m] = function () { 15 | var args = [].slice.call(arguments); 16 | //关键就在于不缓存console,weinre将会把全局console替换掉 17 | console[m].apply(console,args); 18 | } 19 | }); 20 | return api; 21 | }); -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/services/msg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name ngCustomBase.msg 5 | * @description 提示信息组件 6 | * # msg 7 | * Factory in the ngCustomBase. 8 | */ 9 | angular.module('ngCustomBase') 10 | .factory('msgService', function ($rootScope,$compile) { 11 | //var ROOT_msgData = rootDataService.data('ROOT_msgData'); 12 | //ROOT_msgData.set('hideMsg', function () { 13 | // ROOT_msgData.set('globalMsg', { 14 | // messages: '', 15 | // type: '', 16 | // show:false 17 | // }); 18 | //}); 19 | var J_body = angular.element('body'); 20 | return { 21 | alert: function (messages, type) { 22 | type = type || 'info'; 23 | (type === "error") && (type = "danger"); 24 | messages = angular.isArray(messages) ? messages : [ 25 | {msg: messages} 26 | ]; 27 | var msgEl = angular.element(''); 28 | var scope = $rootScope.$new(); 29 | scope.msgData = { 30 | messages: messages, 31 | type: type 32 | }; 33 | J_body.append($compile(msgEl)(scope)) 34 | }, 35 | success: function (msgs) { 36 | this.alert(msgs, 'success'); 37 | }, 38 | error: function (msgs) { 39 | this.alert(msgs, 'danger'); 40 | }, 41 | info: function (msgs) { 42 | this.alert(msgs, 'info'); 43 | }, 44 | warn: function (msgs) { 45 | this.alert(msgs, 'warning'); 46 | } 47 | } 48 | }); -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/services/page-title.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name ngCustomBase.pageTitle 5 | * @description 用于修改title,微信等webview仅仅修改document.title,将无法修改title,知乎上找到了此黑魔法[黑线] 6 | * # pageTitle 7 | * Factory in the ngCustomBase. 8 | */ 9 | angular.module('ngCustomBase') 10 | .factory('pageTitleService', function ($document) { 11 | var J_body = angular.element('body'); 12 | var J_iframe = $(''); 13 | var doc = $document[0]; 14 | return { 15 | update: function (newTitle) { 16 | if(!newTitle){ 17 | return; 18 | } 19 | doc.title = newTitle; 20 | J_iframe.on('load', function() { 21 | setTimeout(function() { 22 | J_iframe.off('load').detach(); 23 | }, 0) 24 | }).appendTo(J_body) 25 | } 26 | }; 27 | }); -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/services/resource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name ngCustomBase.resource 5 | * @description 基于官方angular-resource改造的resource 6 | * # resource 7 | * Factory in the ngCustomBase. 8 | */ 9 | angular.module('ngCustomBase') 10 | .factory('resourceService', function (xhrService) { 11 | var noop = angular.noop, 12 | forEach = angular.forEach, 13 | extend = angular.extend, 14 | copy = angular.copy, 15 | isArray = angular.isArray, 16 | isFunction = angular.isFunction; 17 | var $resourceMinErr = angular.$$minErr('rf-resource'); 18 | var DEFAULT_ACTIONS = { 19 | 'get': { 20 | method: 'get' 21 | }, 22 | 'query': { 23 | method: 'get', 24 | isArray: true, 25 | params: { 26 | currentPage: 1 27 | } 28 | }, 29 | 'new': { 30 | method: 'post' 31 | }, 32 | 'update': { 33 | method: 'patch' 34 | }, 35 | 'save': { 36 | method: 'put' 37 | }, 38 | 'delete': { 39 | method: 'del' 40 | } 41 | } 42 | var VARS_RE = /{([^{]+)}/g; 43 | var defaultResourceConfig = { 44 | // requestSufix: '.json',//在xhr里统一加 45 | primaryKey: 'id', 46 | createResource:true 47 | } 48 | 49 | 50 | function resourceFactory(url, paramDefaults, actions, resourceConfig) { 51 | var resourceConfig = extend({}, defaultResourceConfig, resourceConfig || {}); 52 | 53 | function Resource(resourceData) { 54 | extend(this, copy(resourceData)); 55 | } 56 | 57 | function getHttpConfig(actionParams, invokeParams, data) { 58 | data = data || {}; 59 | var finalParams = extend({}, paramDefaults, actionParams, invokeParams); 60 | var httpUrl = url; 61 | if (url.indexOf("{") > -1) { 62 | httpUrl = url.replace(VARS_RE, function (m, key) { 63 | var paramValue = finalParams[key] || data[key] || ''; 64 | paramValue = paramValue + ""; 65 | if (paramValue.charAt(0) === '@') { 66 | var tmpKey = paramValue.substr(1); 67 | paramValue = data[tmpKey] || ''; 68 | } 69 | delete finalParams[key];//去掉匹配url变量的属性,剩下的加到http的params中去 70 | return paramValue; 71 | }) 72 | } 73 | // '//','///','////'... -> '/' 74 | httpUrl = httpUrl 75 | .replace(/\/{2,}/g, '/') 76 | .replace(/\/$/, ''); 77 | // httpUrl += resourceConfig.requestSufix;//在xhr里统一加 78 | return { 79 | url: httpUrl, 80 | params: finalParams 81 | } 82 | } 83 | 84 | actions = extend({}, DEFAULT_ACTIONS, actions); 85 | forEach(actions, function (action, actionName) { 86 | var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); 87 | Resource[actionName] = function (a1, a2, a3, a4) { 88 | var params = {}, data, success, error; 89 | switch (arguments.length) { 90 | case 4: 91 | error = a4; 92 | success = a3; 93 | //fallthrough 94 | case 3: 95 | case 2: 96 | if (isFunction(a2)) { 97 | if (isFunction(a1)) { 98 | success = a1; 99 | error = a2; 100 | break; 101 | } 102 | 103 | success = a2; 104 | error = a3; 105 | //fallthrough 106 | } else { 107 | params = a1; 108 | data = a2; 109 | success = a3; 110 | break; 111 | } 112 | case 1: 113 | if (isFunction(a1)) success = a1; 114 | else if (hasBody) data = a1; 115 | else params = a1; 116 | break; 117 | case 0: 118 | break; 119 | default: 120 | throw $resourceMinErr('badargs', 121 | "Expected up to 4 arguments [params, data, success, error], got {0} arguments", 122 | arguments.length); 123 | } 124 | // var isInstanceCall = this instanceof Resource; 125 | var httpConfig = getHttpConfig(action.params, params, data); 126 | // var isNew = actionName === 'save' && (!data[resourceConfig.primaryKey]);//save默认更新 127 | httpConfig.method = action.method;//isNew ? 'post' : action.method; 128 | hasBody && (httpConfig.data = data); 129 | var promise = xhrService.send(httpConfig); 130 | promise.then(function (data) { 131 | if (action.isArray) { 132 | var retCollection = []; 133 | var collection = data['collection']; 134 | if (!isArray(collection)) { 135 | throw $resourceMinErr(httpConfig.url + '返回值解析错误', '方法{0}配置为array类型,服务器返回data.collection为: {1}', actionName, data.collection); 136 | } 137 | if(resourceConfig.createResource){ 138 | forEach(collection, function (item) { 139 | retCollection.push(new Resource(item)); 140 | }); 141 | }else{ 142 | retCollection = collection; 143 | } 144 | (success || noop)(retCollection, data); 145 | return retCollection; 146 | } else { 147 | var retModel = data['model'] || {}; 148 | var ret = resourceConfig.createResource ? new Resource(retModel) : retModel; 149 | (success || noop)(ret); 150 | return ret; 151 | } 152 | }, function (res) { 153 | (error || noop)(res); 154 | }) 155 | return promise; 156 | } 157 | 158 | Resource.prototype['$' + actionName] = function (params, success, error) { 159 | if (isFunction(params)) { 160 | error = success; 161 | success = params; 162 | params = {}; 163 | } 164 | Resource[actionName].call(this, params, this, success, error); 165 | }; 166 | }); 167 | extend(Resource.prototype,{ 168 | copy:function () { 169 | var self = this; 170 | var copyObj = {}; 171 | var keys = Object.getOwnPropertyNames(this); 172 | keys.forEach(function(k){ 173 | if(k.indexOf('$$') === 0){ 174 | return; 175 | } 176 | if(isArray(self[k])){ 177 | copyObj[k] = copy(self[k]); 178 | }else{ 179 | copyObj[k] = self[k]; 180 | } 181 | }) 182 | return new Resource(copyObj); 183 | }, 184 | isEqualById: function (resource) { 185 | return this.id === resource.id; 186 | }, 187 | isInArrayById: function (resources) { 188 | var id = this.id; 189 | var ret = false; 190 | resources.some(function (rc) { 191 | if(id === rc.id){ 192 | ret = true; 193 | return true; 194 | } 195 | }); 196 | return ret; 197 | }, 198 | removeFormArrayById: function (resources) { 199 | var id = this.id; 200 | var index = -1; 201 | resources.some(function (rc,idx) { 202 | if(id === rc.id){ 203 | index = idx; 204 | return true; 205 | } 206 | }); 207 | if(index > -1){ 208 | resources.splice(index,1); 209 | } 210 | } 211 | }); 212 | return Resource; 213 | } 214 | 215 | return { 216 | create: resourceFactory 217 | }; 218 | }) 219 | -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/services/root-data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name ngCustomBase.rootData 5 | * @description 封装$rootScope下对象的存取 6 | * # rootData 7 | * Factory in the ngCustomBase. 8 | */ 9 | angular.module('ngCustomBase') 10 | .provider('rootDataService', function () { 11 | var rootKeys = []; 12 | var RootData = function () {}; 13 | RootData.prototype = { 14 | get: function (name) { 15 | return this[name]; 16 | }, 17 | set: function (name, value) { 18 | this[name] = value; 19 | return this; 20 | } 21 | }; 22 | this.register = function (keys) {//可重复注册 23 | keys = angular.isArray(keys) ? keys : [keys]; 24 | rootKeys = rootKeys.concat(keys); 25 | }; 26 | this.$get = ['$rootScope',function ($rootScope) { 27 | rootKeys.forEach(function (key) { 28 | $rootScope[key] = new RootData(); 29 | }); 30 | return { 31 | data: function (key) { 32 | return $rootScope[key]; 33 | }, 34 | addWatcher: function (expression,watcher) { 35 | $rootScope.$watch(expression,watcher) 36 | } 37 | } 38 | }] 39 | 40 | }) 41 | ; -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/services/template.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name ngCustomBase.template 5 | * @description 为了使打包进js的模板,也能被获取,抽象出此服务 6 | * # template 7 | * Factory in the ngCustomBase. 8 | */ 9 | angular.module('ngCustomBase') 10 | .factory('templateService', function ($http, $templateCache) { 11 | return { 12 | get: function (templateUrl) { 13 | return $http.get(templateUrl, {cache: $templateCache}); 14 | } 15 | }; 16 | }); -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/services/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name ngCustomBase.util 5 | * @description 各种工具方法 6 | * # util 7 | * Factory in the ngCustomBase. 8 | */ 9 | angular.module('ngCustomBase') 10 | .factory('utilService', function ($rootScope) { 11 | 12 | return { 13 | /* 14 | list = [{value:1,label:'A'},{value:2,label:'b'}] 15 | key = 'value' 16 | ==>{'1':{value:1,label:'A'},'2':{value:2,label:'b'}} 17 | */ 18 | extractMapFromList: function (list,key) { 19 | key = key || 'value'; 20 | var o = {}; 21 | list.forEach(function (el) { 22 | var k = el[key] 23 | o[k] = el; 24 | }) 25 | return o; 26 | }, 27 | /* 28 | * 扩充第一个数组,把第二个元素,依次加入到第一个数组中 29 | * argument[0] = [1,2] 30 | * argument[1] = [4,5] 31 | * ==> [1,2,4,5] 32 | * */ 33 | concatArrays: function (oriArr,toExtractArr) { 34 | toExtractArr.forEach(function (el) { 35 | oriArr.push(el); 36 | }) 37 | }, 38 | safeApply: function (scope,fn) { 39 | if($rootScope.$$phase){ 40 | fn() 41 | }else{ 42 | scope.$apply(fn) 43 | } 44 | } 45 | }; 46 | }); -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/base/services/xhr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name ngCustomBase.xhr 5 | * @description 封装$http服务,统一处理业务外围逻辑 6 | * # xhr 7 | * Factory in the ngCustomBase. 8 | */ 9 | angular.module('ngCustomBase') 10 | .factory('xhrService', function ($q, $http, $log, $location,$window,localStorageService,msgService, loadingService, rootDataService) { 11 | var history = $window.history; 12 | var REST = { 13 | 'post': 'POST', 14 | 'patch': 'PATCH', 15 | 'get': 'GET', 16 | 'del': 'DELETE', 17 | 'put': 'PUT' 18 | }; 19 | //var ROOT_loginData = rootDataService.data('ROOT_loginData'); 20 | return { 21 | request: $http, 22 | setRequest: function (customRequest) { 23 | this.request = customRequest; 24 | }, 25 | send: function (options, hideLoading) { 26 | var self = this; 27 | loadingService.show(!hideLoading); 28 | // 调用的地方,只需要关心业务上的成功失败,http级别的error在这里统一处理,所以需要重新创建一个defer对象,而不能直接返回http的defer(这个defer只能触发http的失败) 29 | var deferred = $q.defer(); 30 | 31 | var method = REST[options.method] || 'GET'; 32 | options.url += '?_method=' + method; 33 | options.method = (method === 'GET' ? 'GET' : 'POST'); 34 | options.headers = options.headers || {}; 35 | options.headers['X-Requested-With'] = 'XMLHttpRequest'; 36 | if (options.method !== 'GET') { 37 | options.headers['contentType'] = 'application/json; charset=utf-8'; 38 | } 39 | self.request(options) 40 | .success(function (res, status, headers, config) { 41 | var stat = res.stat; 42 | switch (stat) { 43 | case 'OK': 44 | deferred.resolve(res.data); 45 | if(!res.silent && res.successMsg){ 46 | msgService.success(res.successMsg); 47 | } 48 | break; 49 | case 'LOGIN_TIMEOUT': 50 | localStorageService.set('pathBeforeLogin',$location.path()); 51 | $location.path('/login'); 52 | //history.pushState('') 53 | //ROOT_loginData.set('isLogin', false); 54 | //$modalStack.dismissAll('cancel');//关闭所有弹窗 55 | //msgService.warn('登陆超时,请重新登陆'); 56 | deferred.reject(res); 57 | break; 58 | default: 59 | if(!res.silent){ 60 | var msg = (res.errors && res.errors.length && res.errors) || '系统异常:未知原因'; 61 | msgService.error(msg); 62 | $log.error(msg); 63 | } 64 | deferred.reject(res); 65 | } 66 | }) 67 | .error(function (res, status, headers, config) { 68 | $log.error('请求失败:' + res,'请尝试检查响应结果格式。'); 69 | msgService.error('请求失败:' + res); 70 | deferred.reject(res, status, headers, config); 71 | }) 72 | .finally(function () { 73 | loadingService.show(false); 74 | }); 75 | return deferred.promise; 76 | } 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /templates/javascripts/_preinstall/services/resource-pool.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name <%= scriptAppName %>.resourcePool 5 | * @description 资源池,用于存放业务模块的资源类 6 | * # resourcePool 7 | * Factory in the <%= scriptAppName %>. 8 | */ 9 | angular.module('<%= scriptAppName %>') 10 | .factory('resourcePool', function (resourceService) { 11 | var create = resourceService.create; 12 | return { 13 | _url:{//非资源级别的直接写url 14 | 15 | } 16 | ,session: create('/member/session') 17 | } 18 | }); -------------------------------------------------------------------------------- /templates/javascripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc overview 4 | * @name <%= scriptAppName %> 5 | * @description 6 | * # <%= scriptAppName %> 7 | * 8 | * Main module of the application. 9 | */ 10 | angular 11 | .module('<%= scriptAppName %>', [ 12 | 'ngRoute', 13 | 'ngAnimate', 14 | 'ngSanitize', 15 | 'ngCustomBase', 16 | 'hmTouchEvents', 17 | 'LocalStorageModule' 18 | ]) 19 | .config(function ($routeProvider) { 20 | $routeProvider 21 | .otherwise({ 22 | redirectTo: '/main' 23 | }); 24 | }) 25 | .config(function($compileProvider){//链接白名单 26 | $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|javascript):/); 27 | }) 28 | .config(function (rootDataServiceProvider) {//注册$rootScope全局对象,传入key的数组 29 | //rootDataServiceProvider.register(['ROOT_xxxxData']) 30 | }) 31 | ; 32 | -------------------------------------------------------------------------------- /templates/javascripts/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc function 4 | * @name <%= scriptAppName %>.controller:<%= classedName %>Ctrl 5 | * @description 6 | * # <%= classedName %>Ctrl 7 | * Controller of the <%= scriptAppName %> 8 | */ 9 | angular.module('<%= scriptAppName %>') 10 | .controller('<%= classedName %>Ctrl', function ($scope,resourcePool) { 11 | $scope 12 | }); 13 | -------------------------------------------------------------------------------- /templates/javascripts/decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc function 4 | * @name <%= scriptAppName %>.decorator:<%= classedName %> 5 | * @description 6 | * # <%= classedName %> 7 | * Decorator of the <%= scriptAppName %> 8 | */ 9 | angular.module('<%= scriptAppName %>') 10 | .config(function ($provide) { 11 | $provide.decorator('<%= cameledName %>', function ($delegate) { 12 | // decorate the $delegate 13 | return $delegate; 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /templates/javascripts/directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc directive 4 | * @name <%= scriptAppName %>.directive:<%= cameledName %> 5 | * @description 6 | * # <%= cameledName %> 7 | */ 8 | angular.module('<%= scriptAppName %>') 9 | .directive('<%= cameledName %>', function () { 10 | return { 11 | restrict:'E', 12 | replace:true,<%= directiveTemplateProperty %> 13 | scope:{ 14 | 15 | }, 16 | link: function ($scope,$element,attrs) { 17 | 18 | } 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /templates/javascripts/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // conf.js 2 | exports.config = { 3 | directConnect:true, 4 | specs: ['e2e/**/*.js'] 5 | } -------------------------------------------------------------------------------- /templates/javascripts/e2e/spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | describe('e2e test for page: <%= name %>', function() { 3 | it('should do something', function() { 4 | browser.get('http://localhost:9000/#<%= name %>'); 5 | expect(1).toEqual(1); 6 | }); 7 | }); -------------------------------------------------------------------------------- /templates/javascripts/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc service 4 | * @name <%= scriptAppName %>.<%= cameledName %> 5 | * @description 6 | * # <%= cameledName %> 7 | * Factory in the <%= scriptAppName %>. 8 | */ 9 | angular.module('<%= scriptAppName %>') 10 | .factory('<%= cameledName %>Service', function () { 11 | 12 | return { 13 | 14 | }; 15 | }); -------------------------------------------------------------------------------- /templates/javascripts/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc filter 4 | * @name <%= scriptAppName %>.filter:<%= cameledName %> 5 | * @function 6 | * @description 7 | * # <%= cameledName %> 8 | * Filter in the <%= scriptAppName %>. 9 | */ 10 | angular.module('<%= scriptAppName %>') 11 | .filter('<%= cameledName %>', function () { 12 | 13 | return function (input) { 14 | return input; 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /templates/javascripts/unit-mock/mock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @ngdoc function 4 | * @name <%= scriptAppName %>.mockfile 5 | * @description 6 | * # 用于单元测试模拟数据,特别是全局的数据模拟,比如登陆数据,不需要每个controller都写登陆的模拟数据 7 | * mockfile of the <%= scriptAppName %> 8 | */ 9 | angular.module('<%= scriptAppName %>') 10 | .value('someData', { 11 | someProperty:123 12 | }); 13 | -------------------------------------------------------------------------------- /templates/javascripts/unit/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: <%= classedName %>Ctrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('<%= scriptAppName %>')); 7 | 8 | var <%= classedName %>Ctrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | <%= classedName %>Ctrl = $controller('<%= classedName %>Ctrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings).toBeUndefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /templates/javascripts/unit/directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: <%= cameledName %>', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('<%= scriptAppName %>')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | element = angular.element('<<%= _.dasherize(name) %>>>'); 17 | element = $compile(element)(scope); 18 | expect(element.text()).not.toBe('this is the <%= cameledName %> directive'); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /templates/javascripts/unit/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Filter: <%= cameledName %>', function () { 4 | 5 | // load the filter's module 6 | beforeEach(module('<%= scriptAppName %>')); 7 | 8 | // initialize a new instance of the filter before each test 9 | var <%= cameledName %>; 10 | beforeEach(inject(function ($filter) { 11 | <%= cameledName %> = $filter('<%= cameledName %>'); 12 | })); 13 | 14 | it('should return the input prefixed with "<%= cameledName %> filter:"', function () { 15 | var text = 'angularjs'; 16 | expect(<%= cameledName %>(text)).not.toBe('<%= cameledName %> filter: ' + text); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /templates/javascripts/unit/service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: <%= cameledName %>', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('<%= scriptAppName %>')); 7 | 8 | // instantiate service 9 | var <%= cameledName %>; 10 | beforeEach(inject(function (_<%= cameledName %>Service_) { 11 | <%= cameledName %> = _<%= cameledName %>Service_; 12 | })); 13 | 14 | it('should do something', function () { 15 | expect(!!<%= cameledName %>).toBe(true); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /templates/mock/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "stat":"OK", 3 | "data":{<% 4 | if(mockFileCfg.mockCollection){ %> 5 | "currentPage": 1, 6 | "totalPages": 3, 7 | "totalCount": 30, 8 | "collection": [ 9 | { 10 | 11 | } 12 | ]<% 13 | } else { %> 14 | "model":{ 15 | 16 | }<% 17 | } %> 18 | } 19 | } -------------------------------------------------------------------------------- /templates/styles/page.less: -------------------------------------------------------------------------------- 1 | @import '_variable.less'; 2 | .page-<%= dashedName %>{ 3 | 4 | } -------------------------------------------------------------------------------- /templates/styles/simple/_variable.less: -------------------------------------------------------------------------------- 1 | @baseFont:"Hiragino sans GB","Microsoft Yahei","Helvetica Neue", Helvetica, STHeiTi, sans-serif; 2 | @globalColor:#337ab7; 3 | @white:#fff; 4 | @labelColor:#505050; 5 | -------------------------------------------------------------------------------- /templates/styles/simple/all.less: -------------------------------------------------------------------------------- 1 | @import 'common.less'; -------------------------------------------------------------------------------- /templates/styles/simple/common.less: -------------------------------------------------------------------------------- 1 | @import '_variable.less'; 2 | html{ 3 | width:100%; 4 | height:100%; 5 | } 6 | body{ 7 | width:100%; 8 | height:100%; 9 | font-size: 28px; 10 | *{ 11 | -webkit-overflow-scrolling: touch; 12 | } 13 | } 14 | //重置 15 | a{ 16 | color: @globalColor; 17 | &:hover, 18 | &:focus { 19 | color: @globalColor; 20 | text-decoration: none; 21 | outline: none; 22 | } 23 | } 24 | ul,ol{ 25 | margin:0; 26 | padding:0; 27 | li{ 28 | list-style-type: none; 29 | } 30 | } 31 | em,i,b{ 32 | font-style: normal; 33 | font-weight: normal; 34 | } 35 | img{ 36 | max-width: 100%; 37 | } 38 | label{ 39 | font-weight: normal; 40 | margin:0; 41 | } 42 | //子页面 43 | .dialog-subpage{ 44 | position: fixed; 45 | left:0; 46 | right:0; 47 | top:0; 48 | bottom:0; 49 | z-index: 1020; 50 | background: @white; 51 | overflow-x: hidden; 52 | overflow-y: auto; 53 | transform: translateX(100%) translateZ(0); 54 | //left:100%; 55 | transition:all ease .3s; 56 | >div,>section{ 57 | margin:0 !important; 58 | } 59 | } 60 | .dialog-subpage.bottom-top{ 61 | transform: translateY(100%) translateZ(0); 62 | } 63 | .dialog-subpage.slide-in{ 64 | transform: translateX(0) translateY(0); 65 | } 66 | //辅助类 67 | .mrg0 { 68 | margin: 0; 69 | } 70 | .mrgb0 { 71 | margin-bottom: 0px; 72 | } 73 | .mrgb5 { 74 | margin-bottom: 5px; 75 | } 76 | .mrgb10 { 77 | margin-bottom: 10px; 78 | } 79 | .mrgb20 { 80 | margin-bottom: 20px; 81 | } 82 | .mrgl20 { 83 | margin-left: 20px; 84 | } 85 | 86 | .mrgtb10 { 87 | margin-top: 10px; 88 | margin-bottom: 10px; 89 | } 90 | 91 | .mrgt5 { 92 | margin-top: 5px; 93 | } 94 | .mrgt10 { 95 | margin-top: 10px; 96 | } 97 | 98 | .mrgt20 { 99 | margin-top: 20px; 100 | } 101 | .pd10{ 102 | padding:10px; 103 | } 104 | .ellipsis { 105 | white-space: nowrap; 106 | text-overflow: ellipsis; 107 | overflow: hidden; 108 | } 109 | 110 | //flex布局相关 111 | .mui-flex { 112 | display: -webkit-box!important; 113 | display: -webkit-flex!important; 114 | display: -ms-flexbox!important; 115 | display: flex!important; 116 | -webkit-flex-wrap: wrap; 117 | -ms-flex-wrap: wrap; 118 | flex-wrap: wrap; 119 | &.vertical { 120 | -webkit-box-orient: vertical; 121 | -webkit-box-direction: normal; 122 | -webkit-flex-direction: column; 123 | -ms-flex-direction: column; 124 | flex-direction: column 125 | } 126 | &.vertical.reverse { 127 | -webkit-box-orient: vertical; 128 | -webkit-box-direction: reverse; 129 | -webkit-flex-direction: column-reverse; 130 | -ms-flex-direction: column-reverse; 131 | flex-direction: column-reverse 132 | } 133 | &.vertical .cell { 134 | width: auto 135 | } 136 | &.vertical>.cell>.inner { 137 | position: absolute; 138 | width: 100%; 139 | height: 100% 140 | } 141 | &.horizental { 142 | -webkit-box-orient: horizontal; 143 | -webkit-box-direction: normal; 144 | -webkit-flex-direction: row; 145 | -ms-flex-direction: row; 146 | flex-direction: row 147 | } 148 | &.reverse { 149 | -webkit-box-orient: horizontal; 150 | -webkit-box-direction: reverse; 151 | -webkit-flex-direction: row-reverse; 152 | -ms-flex-direction: row-reverse; 153 | flex-direction: row-reverse 154 | } 155 | &.justify-start { 156 | -webkit-box-pack: start; 157 | -webkit-justify-content: flex-start; 158 | -ms-flex-pack: start; 159 | justify-content: flex-start 160 | } 161 | &.justify-end { 162 | -webkit-box-pack: end; 163 | -webkit-justify-content: flex-end; 164 | -ms-flex-pack: end; 165 | justify-content: flex-end 166 | } 167 | &.justify-center { 168 | -webkit-box-pack: center; 169 | -webkit-justify-content: center; 170 | -ms-flex-pack: center; 171 | justify-content: center 172 | } 173 | &.justify-between { 174 | -webkit-box-pack: justify; 175 | -webkit-justify-content: space-between; 176 | -ms-flex-pack: justify; 177 | justify-content: space-between 178 | } 179 | &.justify-around { 180 | -webkit-justify-content: space-around; 181 | -ms-flex-pack: ~"distribute"; 182 | justify-content: space-around 183 | } 184 | &.align-start { 185 | -webkit-box-align: start; 186 | -webkit-align-items: flex-start; 187 | -ms-flex-align: start; 188 | align-items: flex-start 189 | } 190 | &.align-end { 191 | -webkit-box-align: end; 192 | -webkit-align-items: flex-end; 193 | -ms-flex-align: end; 194 | align-items: flex-end 195 | } 196 | &.align-center { 197 | -webkit-box-align: center; 198 | -webkit-align-items: center; 199 | -ms-flex-align: center; 200 | align-items: center 201 | } 202 | &.align-stretch { 203 | -webkit-box-align: stretch; 204 | -webkit-align-items: stretch; 205 | -ms-flex-align: stretch; 206 | align-items: stretch 207 | } 208 | &.align-stretch .cell { 209 | height: auto!important 210 | } 211 | &.center { 212 | -webkit-box-pack: center; 213 | -webkit-justify-content: center; 214 | -ms-flex-pack: center; 215 | justify-content: center; 216 | -webkit-box-align: center; 217 | -webkit-align-items: center; 218 | -ms-flex-align: center; 219 | align-items: center 220 | } 221 | &>.cell { 222 | -webkit-box-flex: 1; 223 | -webkit-flex: 1; 224 | -ms-flex: 1; 225 | flex: 1; 226 | width: 0; 227 | -webkit-flex-basis: 0; 228 | -ms-flex-preferred-size: 0; 229 | flex-basis: 0; 230 | max-width: 100%; 231 | display: block; 232 | padding: 0!important; 233 | position: relative 234 | } 235 | &>.cell.fixed { 236 | -webkit-box-flex: 0!important; 237 | -webkit-flex: none!important; 238 | -ms-flex: none!important; 239 | flex: none!important; 240 | width: auto 241 | } 242 | &>.cell.align-start { 243 | -webkit-align-self: flex-start; 244 | -ms-flex-item-align: start; 245 | align-self: flex-start 246 | } 247 | &>.cell.align-end { 248 | -webkit-align-self: flex-end; 249 | -ms-flex-item-align: end; 250 | align-self: flex-end 251 | } 252 | &>.cell.align-center { 253 | -webkit-align-self: center; 254 | -ms-flex-item-align: center; 255 | align-self: center 256 | } 257 | &>.cell.align-stretch { 258 | -webkit-box-align: stretch; 259 | -webkit-align-items: stretch; 260 | -ms-flex-align: stretch; 261 | align-items: stretch; 262 | height: auto!important 263 | } 264 | } 265 | 266 | //提示信息 267 | div.msg-mask,div.loading-mask{ 268 | position: fixed; 269 | top: 0; 270 | right: 0; 271 | bottom: 0; 272 | left: 0; 273 | padding:0 15px; 274 | z-index: 1100; 275 | background-color: rgba(0%,0%,0%,0) !important; 276 | display: none; 277 | .loading-wrapper{//loading 278 | //width: 200px; 279 | //height: 200px; 280 | padding: 30px 50px; 281 | border-radius: 12px; 282 | background-color: rgba(0, 0, 0, 0.8); 283 | text-align: center; 284 | color: #fff; 285 | i{ 286 | display: block; 287 | margin-bottom:10px; 288 | } 289 | } 290 | section{//msg 291 | //.flex-container; 292 | width: 440px; 293 | min-height:146px; 294 | padding:20px; 295 | //margin:350px auto 0 auto; 296 | border-radius: 12px; 297 | background-color: rgba(0,0,0,0.8); 298 | text-align: center; 299 | color:#fff; 300 | p{ 301 | margin: 0; 302 | } 303 | } 304 | } 305 | 306 | .global-mask{ 307 | position: fixed; 308 | top: 0; 309 | right: 0; 310 | bottom: 0; 311 | left: 0; 312 | //.flex-container; 313 | //padding:0 15px; 314 | z-index: 1100; 315 | background-color: rgba(0%,0%,0%,0); 316 | } 317 | 318 | .confirm-container{ 319 | width:440px; 320 | min-height: 198px; 321 | padding:50px 50px 20px; 322 | background-color: rgba(0,0,0,0.8); 323 | p{ 324 | margin-bottom: 30px; 325 | text-align: center; 326 | color: @white; 327 | } 328 | .buttons{ 329 | button{ 330 | display: inline-block; 331 | width:155px; 332 | height:60px; 333 | line-height:60px; 334 | border:0; 335 | background: @white; 336 | color:@labelColor; 337 | } 338 | button+button{ 339 | margin-left:27px; 340 | } 341 | } 342 | } 343 | .loading-mask.ng-hide,.msg-mask.ng-hide,.global-mask.ng-hide{ 344 | display: none !important; 345 | } 346 | 347 | //iframe不可见 348 | iframe{ 349 | display: none; 350 | } -------------------------------------------------------------------------------- /templates/styles/simple/sprites.css: -------------------------------------------------------------------------------- 1 | /*占坑,防止404*/ -------------------------------------------------------------------------------- /templates/views/_preinstall/_widgets/dialog-confirm/dialog-confirm.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{content}}

4 |
5 |
6 |
-------------------------------------------------------------------------------- /templates/views/_preinstall/_widgets/msg/msg.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 |
6 |
7 |
-------------------------------------------------------------------------------- /templates/views/view.html: -------------------------------------------------------------------------------- 1 | <% if(options['route']) { 2 | %>
3 |
4 |

'Allo, 'Allo!

5 |

6 | I'm Yeoman
7 |

8 |

Splendid!

9 |

this is the <%= name %> view. to be continued...

10 |
11 |
<% 12 | } else { 13 | %>

this is the <%= name %> view. to be continued...

<% } %> 14 | -------------------------------------------------------------------------------- /test/test-app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var assert = require('yeoman-generator').assert; 5 | var helpers = require('yeoman-generator').test; 6 | var os = require('os'); 7 | 8 | describe('ngstone:app', function () { 9 | before(function (done) { 10 | helpers.run(path.join(__dirname, '../app')) 11 | .inDir(path.join(os.tmpdir(), './temp-test')) 12 | .withOptions({ 'skip-install': true }) 13 | .withPrompt({ 14 | someOption: true 15 | }) 16 | .on('end', done); 17 | }); 18 | 19 | it('creates files', function () { 20 | assert.file([ 21 | 'bower.json', 22 | 'package.json', 23 | '.editorconfig', 24 | '.jshintrc' 25 | ]); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /view/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var SubGeneratorBase = require('../subgenerator-base'); 6 | 7 | module.exports = SubGeneratorBase.extend({ 8 | writing: function () { 9 | this.generateHtmlFile(this.name) 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /webdriver/chromedriver_mac32.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoneChen/generator-ngstone/0bfa6500448cca59ceec225696bb7384712ae949/webdriver/chromedriver_mac32.zip -------------------------------------------------------------------------------- /webdriver/chromedriver_win32.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoneChen/generator-ngstone/0bfa6500448cca59ceec225696bb7384712ae949/webdriver/chromedriver_win32.zip --------------------------------------------------------------------------------