用Express 框架创建草地鹨旅行社网站
来自: http://www.ccwebsite.com/create-a-meadowlark-travel-website-with-express-framework/
脚手架
脚手架并不是一个新想法,但很多人(包括我自己)都是通过Ruby才接触到这个概念的。这个想法很简单:大多数项目都需要一定数量的“套路化”代码,谁会想每次开始新项目时都重新写一次这些代码呢?对此有个简单的方法,那就是创建一个通用的项目骨架,每次开始新项目时,只需复制这个骨架,或者说是模板。
RoR把这个概念向前推进了一步,它提供了一个可以自动生成脚手架的程序。相对于从一堆模板中作出选择,这种方式的优点是可以生成更复杂的框架。
Express借鉴了RoR的这一做法,提供了一个生成脚手架的工具,从而可以让你开始一个新的Express项目。
尽管Express有可用的脚手架工具,但它目前并不能生成我推荐使用的框架。特别是它不支持我所选择的模板语言(Handlebars),也没有遵循我所偏好的命名规则(尽管这很容易解决)。
尽管我们不用这个脚手架工具,但我还是建议你看一下它:到那时,你就能够充分了解它生成的脚手架是否对你有用了。
套路化对最终发送到客户端的真正HTML也是有用的。我推荐非常出色的HTML5 Boilerplate( http://html5boilerplate.com/ ),它能生成一个很不错的空白HTML5网站。最近HTML5 Boilerplate又新增加了可定制的功能,其中一个定制选项包含推ter Bootstrap,这个是我高度推荐的前端框架。
草地鹨旅行社网站
本文以一个可运行的网站为例:假想的草地鹨旅行社网站,该旅行社是一家为到俄勒冈州旅游的人提供服务的公司。如果你对创建REST应用程序更感兴趣,不用担心,因为草地鹨旅行社网站除了作为功能性网站外,也提供REST服务。
初始步骤
先给你的项目创建一个新目录,这将作为项目的根目录。本文中,凡提到“项目目录”“程序目录”或“项目根路径”,指的都是这个目录。
提示:或许你会把Web程序文件跟项目相关的其他文件全都分开存放,比如会议纪要、文档等。因此,我建议你把项目根路径作为项目目录的子目录。比如,对于草地鹨旅行社网站而言,我会把项目放在~/projects/meadowlark,而项目根路径放在~/projects/meadowlark/site。
npm在package.json文件中管理项目的依赖项以及项目的元数据。要创建这个文件,最简单的办法是运行 npm init :它会问一系列的问题,然后为你生成一个package.json文件帮你起步(对于“入口点”的问题,用meadowlark.js或项目的名字作为答案)。
提示:如果你的package.json文件中没有指定一个存储库的URL,以及一个非空的README.md文件,那么你每次运行npm时都会看到警告信息。package.json文件中的元数据只有在发布到npm存储库时才是真正必要的,但为了消除npm的警告信息,做这些工作依然是值得的。
第一步是安装Express。运行下面这条npm命令:
npm install --save express
运行 npm install 会把指定名称的包安装到node_modules目录下。如果你用了 --save 选项,它还会更新package.json文件。因为node_modules随时都可以用npm重新生成,所以我们不会把这个目录保存在我们的代码库中。为了确保不把它添加到代码库中, 我们可以创建一个.gitignore文件:
# ignore packages installed by npm node_modules # put any other files you don't want to check in here, # such as .DS_Store (OSX), *.bak, etc.
接下来创建meadowlark.js文件,这是我们项目的入口。本文中将这个文件简单称为“程序文件”:
var express = require('express'); var app = express(); app.set('port', process.env.PORT || 3000); // 定制404页面 app.use(function(req, res){ res.type('text/plain'); res.status(404); res.send('404 - Not Found'); }); //定制500页面 app.use(function(err, req, res, next){ console.error(err.stack); res.type('text/plain'); res.status(500); res.send('500 - Server Error'); }); app.listen(app.get('port'), function(){ console.log( 'Express started on http://localhost:' + app.get('port') + '; press Ctrl-C to terminate.' ); });
提示:很多教程,甚至是Express的脚手架生成器会建议你把主文件命名为app.js(或者有时是index.js或server.js)。除非你用的托管服务或部署系统对程序主文件的名称有特定的要求,否则我认为这么做是没有道理的,我更倾向于按照项目命名主文件。凡是曾在编辑器里见过一堆index.html标签的人都会立刻明白这样做的好处。 npm init 默认是用index.js,如果要使用其他的主文件名,要记得修改package.json文件中的 main 属性。
现在你有了一个非常精简的Express服务器。你可以启动这个服务器(node meadowlark.js),然后访问http://localhost:3000。结果可能会让你失望,因为你还没给Express任何路由信息,所以它会返回一个404页面,表示你访问的页面不存在。
注释:注意我们指定程序端口的方式: app.set(port, process.env.PORT || 3000) 。这样我们可以在启动服务器前通过设置环境变量覆盖端口。如果你在运行这个案例时发现它监听的不是3000端口,检查一下是否设置了环境变量 PORT 。
提示:我高度推荐你安装一个能显示HTTP请求状态码和所有重定向的浏览器插件。这样在解决重定向问题或者不正确的状态码时会更加容易,它们经常被忽视。对于Chrome来说,Ayima的Redirect Path特别好用。在大多数浏览器中, 都能在开发者工具的网络部分看到状态码。
我们来给首页和关于页面加上路由。在404处理器之前加上两个新路由:
app.get('/', function(req, res){ res.type('text/plain'); res.send('Meadowlark Travel'); }); app.get('/about', function(req, res){ res.type('text/plain'); res.send('About Meadowlark Travel'); }); // 定制404页面 app.use(function(req, res, next){ res.type('text/plain'); res.status(404); res.send('404 - Not Found'); });
app.get 是我们添加路由的方法。在Express文档中写的是 app.VERB 。这并不意味着存在一个叫 VERB 的方法,它是用来指代HTTP动词的(最常见的是“get” 和“post”)。这个方法有两个参数:一个路径和一个函数。
路由就是由这个路径定义的。 app.VERB 帮我们做了很多工作:它默认忽略了大小写或反斜杠,并且在进行匹配时也不考虑查询字符串。所以针对关于页面的路由对于/about、/About、/about/、/about?foo=bar、/about/?foo=bar等路径都适用。
路由匹配上之后就会调用你提供的函数,并把请求和响应对象作为参数传给这个函数。现在我们只是返回了状态码为200的普通文本(Express默认的状态码是200,不用显式指定)。
我们这次使用的不是Node的 res.end ,而是换成了Express的扩展 res.send 。我们还用 res.set 和 res.status 替换了Node的 res.writeHead 。Express还提供了一个 res.type 方法,可以方便地设置响应头 Content-Type 。尽管仍然可以使用 res.writeHead 和 res.end ,但没有必要也不作推荐。
注意,我们对定制的404和500页面的处理与对普通页面的处理应有所区别:用的不是 app.get ,而是 app.use 。 app.use 是Express添加 中间件 的一种方法。你可以把它看作处理所有没有路由匹配路径的处理器。这里涉及一个非常重要的知识点:在Express中,路由和中间件的添加顺序至关重要。如果我们把404处理器放在所有路由上面,那首页和关于页面就不能用了,访问这些URL得到的都是404。现在我们的路由相当简单,但其实它们还能支持通配符,这会导致顺序上的问题。比如说,如果要给关于页面添加子页面,比如/about/contact和/about/directions会怎么样呢?下面这段代码是达不到预期效果的:
app.get('/about*',function(req,res){ // 发送内容.... }) app.get('/about/contact',function(req,res){ // 发送内容.... }) app.get('/about/directions',function(req,res){ // 发送内容.... })
本例中的 /about/contact 和 /about/directions 处理器永远无法匹配到这些路径,因为第一个处理器的路径中用了通配符: /about* 。
Express能根据回调函数中参数的个数区分404和500处理器。
你可以再次启动服务器,现在首页和关于页面都可以运行了。
截至目前我们所做的事情,即使不用Express也很容易完成,但Express所提供的一些功能并非那么显而易见。还记得如何规范化 req.url 来确定所请求的资源吗?我们必须手动剥离查询字符串和反斜杠,并转化为小写。而Express的路由器会自动帮我们处理好这些细节。尽管目前看起来这并非什么大不了的事情,但这只是Express路由器能力的冰山一角。
视图和布局
如果你熟知“模型-视图-控制器”模式,那你对视图这个概念应该不会感到陌生。视图本质上是要发送给用户的东西。对网站而言,视图通常就是HTML,尽管也会发送PNG或PDF,或者其他任何能被客户端渲染的东西。
视图与静态资源(比如图片或CSS文件)的区别是它不一定是静态的:HTML可以动态构建,为每个请求提供定制的页面。
Express支持多种不同的视图引擎,它们有不同层次的抽象。Express比较偏好的视图引擎是Jade(因为它也是TJ Holowaychuk开发的) 。Jade所采用的方式非常精简:你写的根本不像是HTML,因为没有尖括号和结束标签,这样可以少敲好多次键盘。然后,Jade引擎会将其转换成HTML。
Jade是非常吸引人的,但这种程度的抽象也是有代价的。如果你是一名前端开发人员,即便你实际上是用Jade编写视图,也必须理解HTML,并且有足够深入的认识。我认识的大多数前端开发人员都不喜欢他们主要的标记语言被抽象化处理。因此我推荐使用另外一个抽象程度较低的模板框架Handlebars。Handlebars(基于与语言无关的流行模板语言Mustache)不会试图对HTML进行抽象:你编写的是带特殊标签的HTML,Handlebars可以借此插入内容。
为了支持Handlebars,我们要用到Eric Ferraiuolo的 express3-handlebars 包(尽管名字中是express3,但这个包在Express 4.0中也可以使用)。在你的项目目录下执行:
npm install --save express3-handlebars
然后在创建app之后,把下面的代码加到meadowlark.js中:
var app = express(); // 设置handlebars视图引擎 var handlebars = require('express3-handlebars') .create({ defaultLayout:'main' }); app.engine('handlebars', handlebars.engine); app.set('view engine', 'handlebars');
这段代码创建了一个视图引擎,并对Express进行了配置,将其作为默认的视图引擎。接下来创建views目录,在其中创建一个子目录layouts。如果你是一位经验丰富的Web开发人员,可能已经熟悉 布局 的概念了(有时也被称为“母版页”)。在开发网站时,每个页面上肯定有一定数量的HTML是相同的,或者非常相近。在每个页面上重复写这些代码不仅非常繁琐,还会导致潜在的维护困境:如果你想在每个页面上做一些修改,那就要修改所有文件。布局可以解决这个问题,它为网站上的所有页面提供了一个通用的框架。
所以我们要给网站创建一个模板。接下来我们创建一个views/layouts/main.handlebars文件:
<!doctype html> <html> <head> <title>Meadowlark Travel</title> </head> <body> {{{body}}} </body> </html>
以上内容你未曾见过的可能只有 {{{body}}} 。这个表达式会被每个视图自己的HTML取代。在创建Handlebars实例时,我们指明了默认布局( defaultLayout:'main' )。这就意味着除非你特别指明,否则所有视图用的都是这个布局。
接下来我们给首页创建视图页面,views/home.handlebars:
<h1>Welcome to Meadowlark Travel</h1>
关于页面,views/about.handlebars:
<h1>About Meadowlark Travel</h1>
未找到页面,views/404.handlebars:
<h1>404 - Not Found</h1>
最后是服务器错误页面,views/500.handlebars:
<h1>500 - Server Error</h1>
提示:你或许想在编辑器中把.handlebars和.hbs (另外一种常见的Handlebars文件扩展名)跟HTML相关联,以便启用语法高亮和其他编辑器特性。如果是vim,你可以在~/.vimrc文件中加上一行 au BufNewFile,BufRead *.handlebars set file type=html 。其他编辑器请参考相关文档。
现在视图已经设置好了,接下来我们必须将使用这些视图的新路由替换旧路由:
app.get('/', function(req, res) { res.render('home'); }); app.get('/about', function(req, res) { res.render('about'); }); // 404 catch-all处理器(中间件) app.use(function(req, res, next){ res.status(404); res.render('404'); }); // 500错误处理器(中间件) app.use(function(err, req, res, next){ console.error(err.stack); res.status(500); res.render('500'); });
需要注意,我们已经不再指定内容类型和状态码了:视图引擎默认会返回 text/html 的内容类型和200的状态码。在catch-all处理器(提供定制的404页面)以及500处理器中,我们必须明确设定状态码。
如果你再次启动服务器检查首页和关于页面,将会看到那些视图已呈现出来。如果你检查源码,将会看到views/layouts/main.handlebars中的套路化HTML。
视图和静态文件
Express靠中间件处理静态文件和视图。只需了解中间件是一种模块化手段,它使得请求的处理更加容易。
static 中间件可以将一个或多个目录指派为包含静态资源的目录,其中的资源不经过任何特殊处理直接发送到客户端。你可以在其中放图片、CSS文件、客户端JavaScript文件之类的资源。
在项目目录下创建名为public的子目录 (因为这个目录中的所有文件都会直接对外开放,所以我们称这个目录为public)。接下来,你应该把 static 中间件加在所有路由之前:
app.use(express.static(__dirname + '/public'));
static 中间件相当于给你想要发送的所有静态文件创建了一个路由,渲染文件并发送给客户端。接下来我们在public下面创建一个子目录img,并把logo.png文件放在其中。
现在我们可以直接指向/img/logo.png (注意:路径中没有public,这个目录对客户端来说是隐形的), static 中间件会返回这个文件,并正确设定内容类型。接下来我们修改一下布局文件,以便让我们的logo出现在所有页面上:
<body> <header> <img src="/img/logo.png" alt="Meadowlark Travel Logo"> </header> {{{body}}} </body>
注释:`是HTML5中引入的元素,它出现在页面顶部,提供一些与内容有关的额外语义信息,比如logo、标题文本或导航等。
视图中的动态内容
视图并不只是一种传递静态HTML的复杂方式(尽管它们当然能做到)。视图真正的强大之处在于它可以包含动态信息。
比如在关于页面上发送“虚拟幸运饼干”。我们在meadowlark.js中定义一个幸运饼干数组:
var fortunes = [ "Conquer your fears or they will conquer you.", "Rivers need springs.", "Do not fear what you don't know.", "You will have a pleasant surprise.", "Whenever possible, keep it simple.", ];
修改视图(/views/about.handlebars)以显示幸运饼干:
<h1>About Meadowlark Travel</h1> <p>Your fortune for the day:</p> <blockquote>{{fortune}}</blockquote>
接下来修改路由/about,随机发送幸运饼干:
app.get('/about', function(req, res){ var randomFortune = fortunes[Math.floor(Math.random() * fortunes.length)]; res.render('about', { fortune: randomFortune }); );
重启服务器,加载/about页面,你会看到一个随机发放的幸运饼干。模板真的是非常有用。
小结
我们刚用Express创建了一个非常基本的网站。尽管简单,但这个网站包含了功能完备的网站所需的一切。
</div>