前端工作流程
若要获得更好的阅读体验请移驾http://willkan.github.io/blog/html/Workflow/
概述
为什么写这篇文章
接触前端一年了,瞎摸过来这么久一直很希望看到一个完整的前端构建流程,遗憾没找到相关文章,所以自己在做这个网页的时候顺便纪录下来自己从架构到部署的整个过程。
我一直希望有个网站可以让我用markdown写文章,并且可以在markdown中插入自己的demo(不是源码),于是就做了这么个项目让我可以轻松发布自己的文章。(文中并不会提到如何解析markdown,解析markdown可以了解下grunt-markdown
)
使用资源分析
网上前端资源非常丰富,做一个网页完全没必要重复造轮子,下面是我做这个网页用到的工具及其分析。
-
响应式css框架:Foundation 4 很多人听到响应式会第一时间想到Bootstrap,但在网上查询后我发现很多人再说Bootstrap在移动端的性能并不好,甚至会拖慢应用,很大一部分原因是因为Bootstrap只采用jQuery这么个在移动端上并不太适合的库。而Foundation4在移动端采用Zepto,并且采用mobile first概念(就是说首先为小屏幕设计,随着屏幕逐渐增大,层次越复杂,可以和"渐进增强"类比)。 Bootstrap和Foundation的详细对比大家可以参见twitter-bootstrap-vs-foundation-4-which-one-is-right-for-you(注意,这里比较的Bootstrap版本还比较老) 感觉Foundation 4 在国内使用群还比较少,所以教程还是参见官网吧。 但是有一点比较奇怪的是Foundation 4的官方Docs在我的移动设备上仍会卡顿。是html文件太大?还是Foundation 4的css太臃肿? 在之后真正进入开发时我会作出对比。
-
DOM操作库:Zepto 为什么选择Zepto?因为Zepto性能比其他DOM操作库在webkit上表现好太多了。而webkit又是IOS和Android上主力的浏览器核心。部分dom操作性能对比请查看http://jsperf.com/jqm3/103
-
模板解释引擎:HandleBars 其他模版引擎也可以,只是这里我比较喜欢Handlebars而已。模版解释的好处在于不再需要再写大量而不清晰的js插入dom结构语句。此外,我将回用Handlebars往markdown解析后的文件中插入demo。
-
前端模块化:seajs 要么requirejs,要么seajs,为啥选seajs,请查看seajs处的解释 架构工具:yeoman
-
提高开发效率和规范 移动端动画:采用css3动画实现页面切换而不是传统的setTimeOut调整dom的样式,
-
css预编译:Sass 因为用到了foundation 4,而foundaton 4 官方版本又拥有Sass版本,所以用上了Sass
-
测试:CasperJS 感觉上mocha,jasmine更偏向于本地纯js调试,而CasperJS这基于PhantomJS的工具则更适合浏览器调试
项目初始化
yeoman是什么
工具选好了之后,我们开始构建项目了。首先介绍一下yeoman,借用一下infoq上的介绍:
Yeoman是由Paul Irish、Addy Osmani、Sindre Sorhus、Mickael Daniel、Eric Bidelman和Yeoman社区共同开发的一个项目。它旨在为开发者提供一系列健壮的工具、程序库和工作流,帮助他们快速构建出漂亮、引人注目的Web应用。Yeoman拥有如下特性:
- 快速创建骨架应用程序——使用可自定义的模板(例如:HTML5、Boilerplate、Twitter Bootstrap等)、AMD(通过RequireJS)以及其他工具轻松地创建新项目的骨架。
- 自动编译CoffeeScrip和Compass——在做出变更的时候,Yeoman的LiveReload监视进程会自动编译源文件,并刷新浏览器,而不需要你手动执行。
- 自动完善你的脚本——所有脚本都会自动针对jshint(软件开发中的静态代码分析工具,用于检查JavaScript源代码是否符合编码规范)运行,从而确保它们遵循语言的最佳实践。
- 内建的预览服务器——你不需要启动自己的HTTP服务器。内建的服务器用一条命令就可以启动。
- 非常棒的图像优化——Yeoman使用OptPNG和JPEGTran对所有图像做了优化,从而你的用户可以花费更少时间下载资源,有更多时间来使用你的应用程序。
- 生成AppCache清单——Yeoman会为你生成应用程序缓存的清单,你只需要构建项目就好。
- “杀手级”的构建过程——你所做的工作不仅被精简到最少,让你更加专注,而且Yeoman还会优化所有图像文件和HTML文件、编译你的CoffeeScript和Compass文件、生成应用程序的缓存清单,如果你使用AMD,那么它还会通过r.js来传递这些模块。这会为你节省大量工作。
- 集成的包管理——Yeoman让你可以通过命令行(例如,yeoman搜索查询)轻松地查找新的包,安装并保持更新,而不需要你打开浏览器。
- 对ES6模块语法的支持——你可以使用最新的ECMAScript 6模块语法来编写模块。这还是一种实验性的特性,它会被转换成eS5,从而你可以在所有流行的浏览器中使用编写的代码。
- PhantomJS单元测试——你可以通过PhantomJS轻松地运行单元测试。当你创建新的应用程序的时候,它还会为你自动创建测试内容的骨架。
使用yeoman
安装yeoman
yeoman是基于node.js,首先得安装node.js,然后在命令行中安装yeoman
npm install -g yo grunt-cli bower
mac或linux下npm安装到全局需要root权限
使用yeoman
yo
yo用于构建项目的手足架。
刚安装完的yeoman(1.0版本)中是不含模版的,我先去找了个模版generator-backbone
npm install -g generator-backbone
然后使用该模版开始构建
mkdir myapps
cd myapps
yo bakcbone myapps
命令行提示是否使用twitter和coffeejs,该项目中都用不到,所以都不选
此时myapps的目录结构如下
但该架构并不完全满足我的需求,于是我进行了手动修改,修改后结构如下
yo的使用基本结束。
之后本文提到的路径都是myapps
的相对路径
bower
bower用于载入网页需要的组件
由于目录变更,我们需要修改bower的路径,bower的路径在.bowerrrc
文件中,将其内容改为
{
"directory": "app/public/bower_components"
}
bower.json
文件是用来描述需要载入的组件
grunt
grunt用于自动化,例如编译scss文件,编译coffe文件,部署等。
因为myapps的目录修改过,所以Gruntfile.js
中的相关路径也得修改。
后面的开发中会大量用到grunt,此处先提一下grunt的一些个人经验
-
yeoman预置的Gruntfile.js很聪明,已设置为从
package.json
读取grunt插件并注册,只需修改此文件并执行npm install
,即可注册grunt的插件。 -
在
grunt.initConfig()
中我们可以配置各种子任务,例如grunt.initConfig({ copy: { dist: { files: [{ expand: true, dot: true, cwd: '<%= yeoman.app %>', dest: '<%= yeoman.dist %>', src: [ '*.{ico,txt}', '.htaccess', 'images/{,*/}*.{webp,gif}' ] }] } } })
然后通过
grunt.registerTask()
来注册任务,例如grunt.registerTask('default', [ 'copy:dist', //执行copy下的dist子任务, 'copy' //执行copy下所有任务,执行顺序为定义时的顺序 ])
HTML和CSS框架
Foundation4
Foundation 4是一个CSS框架,有CSS版本和SCSSS版本,都可以在官网获得。
直接上我的html代码先
<div class="row">
<div class="small-12 large-9 columns">
<h1>
前端工作流程
</h1>
<hr/>
<article></article>
</div>
<button class="hide-for-medium hide-for-large nav-btn"></button>
<aside class="hide-for-small small-6 large-3 columns" data-magellan-expedition="fixed"></aside>
</div>
这里解释一下Foundation的栅栏表格。
columns
表示行,row
表示列-
.columns
元素必须得是.row
的后继元素 -
默认版本的Foundation中,把行分成12份,使用如下:
small-1
表示该列宽度占最近.row
祖先元素的1/12small-3
表示该列宽度占最近.row
祖先元素的3/12small-3 large-4
表示该列宽度在小屏幕中占3/12,在大屏幕中占4/12- 上述例子中的
small
,large
表示屏幕大小,可在_global.scss
中找到定义,可以在该文件中搜索关键字$small-screen
Foundation的css使用就解释到这,更多请查看API
然后来看看Foundation的js插件部分,foundation依赖于zepto或jQuery,引入其他插件前需要先引入foundation.js
若使用zepto请注意zepto是可定制的,foundation使用的是他自己定制的zepto,所以请使用foundation/js/vendor/zepto.js
SCSS
鉴于SCSS的方便,我采用了SCSS版本。这就带来了个问题,每次都要编译才能被浏览器解释。grunt这时候就起作用了。
首先说一下我SCSS的配置
- Foundation是通过bower载入的,所以Foundation的路径是
app/public/bower_components/foundation
- 自定义的SCSS我选择放在
app/resource/scss
中 - 编译后的CSS文件放在
app/public/style/scss
中 - 所有CSS文件由
app/public/style/main.css
通过@import
引入,html中只导入该css
然后就是grunt的配置了
- 首先得往系统中安装compass(需要ruby环境):
gem install compass
- yeoman构建项目时,
package.json
中已含有grunt-contrib-compass
插件,该插件可以对SCSS进行编译 -
然后可以开始配置
Gruntfile.js
:... grunt.initConfig({ ... //实时监控 watch: { ... compass: { files: ['<%= yeoman.app %>/resource/scss/{,*/}*.{scss,sass}'],//监控的文件路径 tasks: ['compass']//若监控文件改动,执行的任务 }, ... }, //compass配置 comapss: { //全局配置 options: { sassDir: '<%= yeoman.app %>/resource/scss', cssDir: 'app/public/styles/scss', imagesDir: '<%= yeoman.app %>/public/images', javascriptsDir: '<%= yeoman.app %>/public//scripts', fontsDir: '<%= yeoman.app %>/public/styles/fonts', //此处配置SCSS的引入源路径,我添加了foundation的SCSS路径和animate的SCSS路径 importPath: [ '<%= yeoman.app %>/public/bower_components', '<%= yeoman.app %>/public/bower_components/foundation/scss', '<%= yeoman.app %>/public/bower_components/animate.scss/source' ], relativeAssets: true }, //局部配置,覆盖同名项 dist: {}, server: { options: { debugInfo: true } } }, ... }); //往server任务中插入compass子任务,执行该任务命令为grunt server grunt.registerTask('server', [ ...//其他任务, 'compass:server', ...//其他任务 ]) ...
在命令行中执行grunt server,即可实现SCSS的实施编译
HTML模版
在HTML和CSS框架部分我们已经完成了HTML框架的构建。
下面我们开始使用HTML模版来编写网页需要模版。
HTML模版引擎的好处这里就不说了,在非常多的模版引擎中我选择了Handlebars,仅仅是因为我喜欢这个模版。
模版的编写就不说了,这部分主要还是描述如何模版文件的放置结构和实现模版的预编译。
首先来看看目录结构
- 模版文件我放置在
app/resource/templates
中, 后缀名是.hbs
- 自定义模版工具(就是helpers之类的东西)放置在
app/resource/templates/helpers
中 .hbs
这些文件编译生成的文件我放置在.tmp/templates
这临时文件夹中- 最后,把
.tmp/templates
中的文件和helpers连接在一起,生成.tmp/template.js
下面来看看预编译
-
首先,为什么要预编译?
模版文件并不能直接被js执行,需要经Handlebars解释成一个函数后才能被执行,也就是说浏览器得先执行模版解释,这对性能并不好的移动设备来说无疑带来了性能损耗。所以更好的做法是在服务器段就把这些模版文件预编译成可执行的函数。
-
那该怎么实现预编译呢?
grunt这时候又来了。
预编译Handlebars需要
grunt-contrib-handlebars
这个插件,载入方式还是在package.json
中添加该项,然后npm install
那接下来看看这插件的配置(
grunt.initConfig()中的配置
)handlebars: { compile: { options: { //函数所在命名空间 namespace: "Handlebars.templates", //函数名称格式 processName: function (filename) { return filename .replace(/app\/resource\/templates\//, '') .replace(/\.hbs$/, ''); } }, files: { //'生成文件':[需要编译的模版文件] '.tmp/templates/template.js': ['<%= yeoman.app %>/resource/templates/*.hbs'] } } }
上述代码实现下列转换(文件所在路径都是
app/resource/templates
)- a.hbs->Handlebars.templates.a
- b.hbs->Handlebars.templates.b
-
把helpers也合并进来
这里用到
grunt-contrib-concat
,注意文件连接顺序,应该是先连接helpers类文件,应为模版中可能会用到自定义的helper,只有先定义了helper,后面的模版函数才能正常执行 -
HTML引人预编译的文件
预编译带来的好处还有一个:我们不再需要引入完整的handlebars.js和
.hbs
这类模版文件,只需要引入handlebars.runtime.js(和handlebars.js相比,没有模版解释函数)和预编译生成的文件.tmp/template.js
-
实时编译监控
我们可以添加监控的文件和任务以实现
.hbs
这类文件的实时编译,这样我们只要直接修改模版文件,并不会因为预编译而给调试修改带来麻烦。
前端模块化
接下来让我们看看该如何组织js文件。
以前我们引入js文件的方式是:
<script src=""></script>
或者是用jQuery$.ajax()
但这些引入方式有一个致命缺陷就是文件依赖问题,我们更希望看到的是像java一样可以import
,于是一部分前端的前辈们就致力于解决模块化问题。
seajs(CMD规范)和requirejs(AMD规范)就是解决模块化问题比较好的两个库。
二者的详细对比我就不说了,大家可以看看SeaJS与RequireJS最大的区别。
虽然模块化的确很有用,但无可否认目前大多数的库还是传统的写法(外国也有一部分库已支持AMD),也就是说需要这些s库进行人工CMD化或AMD化。国际上的一些比较著名的库我还没找到原生支持CMD,都需要人工CMD化。
而我这次项目选择的是seajs,原因是二者在构建上都没有成熟的方案,seajs的生态圈更本地化(作者是玉伯大大),相比一堆英文的交流我更愿意用中文交流,也算是为国内前端生态圈尽一份力吧。
目录结构和seajs配置
我在这讲的主要是自己在使用seajs过程中遇到的问题和解决方案,想查看API者请移驾https://github.com/seajs/seajs/issues/266,如果玉伯大大看到这篇文章,容小弟稍微抱怨下用github作参考文档真心不太容易用。。。资料找起来还是挺费劲的。
目录结构
先规范一下scripts目录结构
以下描述的相对路径是app/public/scripts
sea-modules
放置的是通过spm(下面会提到)加载的库example
example/logic
放置的是自己编写的逻辑模块example/utils
放置的是CMD化后的开源js库example/templates
放置的是CMD化后的模版文件(例如前文提到的app目录下的.tmp/templates.js
,.tmp下的文件还未CMD化)example/static
放置的是已经CMD化的静态资源文件
seajs配置
注意seajs的路径解释规则,请查看模块标志
这里注意paths
中定义的路径就是其直接路径,并不会互相解释,例如
seajs.config({
//base默认为'app/public/scripts/sea-modules'
paths:{
scripts: '/public/scripts',
logic: 'scripts/logic' //此处并不会被解释为'/public/scripts/logic',而是'app/scripts/sea-modules/logic'
}
});
此外paths的配置还应注意不应以'/'结尾(仅限2.1.1版本),详情请见https://github.com/seajs/seajs/issues/926
这个问题是因为使用seajs还应注意一个初学者不太能发现的ID 和路径匹配原则
CMD化
seajs2.0开始推荐将所有js封装成CMD模块.
对于非CMD的js我们需要对其进行封装,我说一下封装foundation.js这个文件(依赖dom选择器)
define(function(require, exports, module){
var $ = require('$');//写入依赖,需要再seajs配置中配置alias的$
...//源代码
return Foundation;
})
而对于类似于模板预编译成的文件,我建议使用grunt对其进行封装,grunt-contrib-concat
支持对文件进行包装,这里不作描述
部署
经过上面的步骤,网站的组织已经比较清晰,再编辑完网页所有功能后,进入下一步----部署.
yeoman配置的Gruntfile.js
并不能适应所有自动化,我们需要对其进行适当修改.
先来看看我的grunt build
执行了哪些任务
grunt.registerTask('build', [
'clean:dist',//清空临时目录
'compass:dist',//编译SCSS
'markdown',//将markdown转换为handlebars模板格式
'handlebars:markdown',//预编译handlebars
'handlebars:compile',//预编译handlebars
'concat:template',//连接handlebars预编译后的文件以及助手
'wrap:template',//对连接后的文件进行CMD封装
'transport:seajs',//获取CMD模块ID
'concat:seajs',//连接CMD模块
'uglify:seajs',//压缩CMD模块连接而成的文件
'useminPrepare',
'imagemin',//压缩图像
'htmlmin',//压缩html文件
'cssmin',//压缩css
'copy',//将没处理过而网站又需要的文件拷贝过来
'rev',//为css和js添加版本号
'usemin'
]);
usemin
的作用请看官方说明
Replaces references to non-optimized scripts or stylesheets into a set of HTML files (or any templates/views).
屌丝翻译:替换HTML文件(或任意模板文件)的引用
举个例子,main.css
经处理后变成main-rev.css
,usemin就是把html中引用到main.css
的地方都改为main-rev.css
连接CMD模块
这里我只讲述文档比较少的transport和concat,这两个任务分别用到了grunt-cmd-transport
和grunt-cmd-concat
transport
对于为分配ID的CMD模块,我们首先使用grunt-cmd-transport
抽取其ID,先上代码
transport: {
seajs: {
options: {
alias: {
underscore: 'underscore',
backbone: 'backbone',
$: '$',
modernizr: 'modernizr'
},
paths: [
'app/public/scripts/example/static',
'app/public/scripts/sea-modules',
'.build'
]
},
files: [
{
cwd: 'app/public/scripts/example',
src: [
'static/{,*/,*/*/}*.js',
'templates/*.js',
'utils/*.js',
'logic/*.js'
],
dest: '.build'
}
]
}
}
-
解释一下options
-
alias
可以指向一个文件也可以指向自定义的别名,alias中的键值对(假设为key:value
)的表示需要处理的文件中若含有require('key')
则用require('value')
来代替 -
paths
是一个数组,表示需要用到的模块路径,例如处理的文件中需要require('logic')
,而logic模块并不在默认路径中,则需要在paths
中添加logic模块所在的路径(注意,此处应把file.dest
指向的路径也添加进去) -
idleading
我们可以用这属性给所有抽取的ID添加一个前缀
-
-
然后是抽取
- 抽取顺序和
file.src
这数组的顺序相同 - 若模块a依赖模块b,则必须先抽取b再抽取a
- 抽取到
file.dest
指向的路径下
- 抽取顺序和
-
模块ID的命名方式
ID名字 = options.idleading + 文件相对CWD路径
例如
假设无idleading,cwd为'app/build/scripts',dest为'.build',文件相对路径为'logic/a'则ID名字是'logic/a',抽取出来的目录结构是
.build |--logic |--a.js
concat
模块抽取完后就要开始进行连接.还是先上代码
concat: { seajs: { options: { relative: true, include: 'all', paths: [ 'app/public/scripts/sea-modules', 'app/public/scripts/example/static', '.build' ] }, files: { 'dist/public/scripts/main.js': ['.build/{,*/,*/*/}*.js'] } } }
options的
paths
解释和transport的options.paths
一样
部署优先策略
这是我自己应用的一个策略,也就是尽量保证一步部署,不再对部署后的文件上进行手工修改
usemin并不会对seajs的引用进行修改,为了实现部署优先,我们在index.html应采用下面方式
<!-- 引入seajs -->
<script src="public/scripts/sea-modules/seajs/seajs/2.1.1/sea.js"></script>
<!-- 引入seajs配置文件 -->
<script src="public/scripts/config.js"></script>
<!-- 引入seajs合并后的文件, 路径为合并后的文件相对路径 -->
<script src="public/scripts/main.js"></script>
<!-- 调用启动模块 -->
<script>
seajs.use(['modernizr','logic/load-markdown']);
</script>
解释一下引入合并后的文件的原因:
- 合并后的文件在开发调试时并不存在,调用该文件对开发不会造成任何影响
- 合并后的文件并不符合CMD模块,只是一系列CMD模块的定义
解释一下部署后的调用启动模块
- 引入合并后的文件并不会执行模块中任何函数
-
当引入合并后的文件时,会对一系列CMD模块进行define,define过程应该是这样的:
若define()的参数中含有ID,则先对ID进行路径解释,把解释后的路径添加到
seajs.cache
中 - seajs调用模块时会先去查看
seajs.cache
中是否含有解释后的路径,若有,则直接调用该路径对应的模块,若无则想服务器获取该模块 - seajs.use()调用模块的相对路径不是base,而是html所在路径.而只有调用的模块路径的解释路径和模块ID的解释路径相同才能正确调用, 为了部署优先,最好为启动模块设置顶级标识,应为顶级标识才能在
seajs.use
和require
时获得相同的解释路径
测试
我选择的测试工具是CasperJS.
基于PhantomJS, 也就是说CasperJS可以执行一系列浏览器上的动作, 应该就是所谓的UI测试吧,而这正是web最需要保证质量的事.
建议使用CasperJS 1.1版本(目前最新版本)
使用请参看API
对于刚迈入大四的我来说,也就是一两个月前接触过测试这一概念,而ui测试通俗点说就是: 点击了按钮a, 页面发生了响应. 而测试就是判断这响应是否符合测试设定的标准, 无论符不符合, 都返回测试结果.
后记
本来想描述自己的建网页流程,感觉写着写着有点变成流水账了…还丢了一坨代码上来…