作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
使用form_alter控制表单元素的theme_wrappers 83
Content translation VS Entity translation 234
使用Entity translation模块翻译节点 235
使用Entity translation模块翻译分类术语 243
Translation Management Tool 272
我们来看登录页面HTML的中间部分,其实在内容和面包屑的之间,每个页面还有一张图片,对于这张图片,我们改如何处理呢?
有两种办法,一种是,把它放到一个区块里面,然后把区块放到主内容区域里面,也就是放在正文的上面。还有一种办法,就是将它处理成一个独立的区域,然后里面放置一个同样的区块;此时我们需要调整info文件里面区域的定义,调整page.tpl.php里面区域的输出。
如果在以前,我们可以采用后者,这是我比较喜欢的一种方式。不过在Drupal7中,我们可以采用前者。为什么呢?在Drupal6里面,我们向主内容区域添加区块,区块内容的显示,总是放在主内容的下面,但是在Drupal7里面,却是这样的:
主内容只是一个普通的区块,所以我们可以在区块管理界面,调整内容区域里面,区块之间的先后顺序,也就是说,我们可以将这里的图片区块放在主内容的上面。
导航到admin/structure/block,点击这里的添加区块链接,我们创建一个区块,这是区块的配置:
我们将文本格式选择为PHP code,区域选择SNT主题的内容区域。其它采用默认配置,保存区块。
现在我们访问user/login页面,或者访问一个节点页面,我们看到这样的内容:
我们发现了什么问题,就是页面的标题,还有tabs标签,全部显示在了图片区块的上面。解决办法是有的,就是我们不在page.tpl.php里面输出标题,把它放在节点模板里面。对于page.tpl.php里面的tabs变量,我们也可以去掉对它的输出。如果你觉得,这些都不是问题,都是可以接受的,那样这就可以了。
如果你觉得这些无法接受,标题和标签必须放在图片区块的下面。此时我们,可以添加一个新的区域。我们不妨把它命名为ziyebanner。步骤如下:
1) 修改snt.info文件,在区域定义里面,输入以下内容:
regions[ziyebanner] = Ziye banner
2) 修改page.tpl.php文件,这是修改后的对应代码:
<div class="ziye-left">
<?php if ($breadcrumb): ?>
<p class="daohang"><?php print $breadcrumb; ?></p>
<?php endif; ?>
<p><?php print render($page['ziyebanner']); ?> </p>
<div class="nei">
<?php print $messages; ?>
<?php if ($page['highlighted']): ?><div id="highlighted"><?php print render($page['highlighted']); ?></div><?php endif; ?>
<a id="main-content"></a>
<?php print render($title_prefix); ?>
<?php if ($title): ?><h1 class="title" id="page-title"><?php print $title; ?></h1><?php endif; ?>
<?php print render($title_suffix); ?>
<?php if ($tabs): ?><div class="tabs"><?php print render($tabs); ?></div><?php endif; ?>
<?php print render($page['help']); ?>
<?php if ($action_links): ?><ul class="action-links"><?php print render($action_links); ?></ul><?php endif; ?>
<?php print render($page['content']); ?>
<?php print $feed_icons; ?>
</div>
</div>
我们主要做的工作,就是输出了ziyebanner区域,还有就是在主内容区域外面加了一个<div class="nei">。
3) 现在清除缓存,在区块管理界面,就可以看到我们新增的ziyebanner区域了,我们把刚才创建的图片区块,放到这个区域。
保存区块设置,使用匿名用户访问user/login页面,刚才的问题已经不存在了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
表单覆写
我们现在来看,登录表单页面的覆写,美工所给的表单元素里面,还包含一个复选框“两周内自动登录”,对于这个功能,我们可以使用Remember me模块。
1) 安装、启用Remember me模块。有时候,我觉得Remember me模块,仅仅是起到了一个心理安慰作用,因为本身Drupal就会记住你的用户名/登录状态,下次访问通常不需要登录。但是很多网站,还是需要这个功能。
2) 启用后,访问登录页面,我们看到多了一个复选框“Remember me”,并且默认是选中的。
如何将这段文本,改造为我们想要的中文?有两种办法,一个就是使用form_alter这个钩子函数,修改对应表单元素的标签;第二个方法,就是将对应的英文翻译成我们想要的中文。我在实际的很多项目中,都采用后一种办法。我们导航到admin/config/regional/translate,点击翻译标签,进入页面admin/config/regional/translate/translate,在这里,输入“Remember me”,过滤。
点击文本右边的编辑链接,即可将“Remember me”翻译成“两周内自动登录”。
点击这里的“保存翻译”按钮。现在登录页面的内容,已经是我们想要的了。
我们现在来覆写这个表单页面,控制Drupal表单的输出,这是Drupal主题制作里面最富有技巧性质的。需要的知识比较多。我们这里的要求也比较简单,希望大家遇到了类似的问题,可以以我们的例子为参考,依葫芦画瓢,满足自己的需求即可。
1) 打开sites\all\themes\snt下面的template.php文件,向里面添加以下代码:
function snt_theme(&$existing, $typ
e, $theme, $path){
$hooks = array();
$hooks['user_login'] = array (
'template' => 'user-login',
'render element' => 'form',
'path' => drupal_get_path('theme','snt').'/templates',
//'preprocess functions' => array('snt_preprocess_user_login'),
);
return $hooks;
}
这段代码,实现了钩子hook_theme,在这里我们为user_login表单,声明了一个模板文件user-login.tpl.php,使用这个模板文件负责表单的输出,模板文件放在当前主题的templates目录里面。user_login是表单的ID,通过查看表单的HTML源代码,可以获得。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接着添加对应的预处理函数:
function snt_preprocess_user_login(&$variables) {
//$variables['intro_text'] = t('This is my awesome login form');
//$variables['form']['actions']['submit']['#attributes'] = array('class' => 'but');
$variables['name'] = drupal_render($variables['form']['name']);
$variables['pass'] = drupal_render($variables['form']['pass']);
$variables['submit'] = drupal_render($variables['form']['actions']['submit']);
//$variables['links'] = drupal_render($variables['form']['links']);
$variables['remember_me'] = drupal_render($variables['form']['remember_me']);
$variables['hidden'] = drupal_render_children($variables['form']);
}
在预处理函书里面,我们为模板文件提供变量,其实我们也可以不提供变量,在模板文件中直接输出表单元素。
1) 我们在sites\all\themes\snt\templates下面,创建user-login.tpl.php,里面放置以下内容:
<div class="login">
<table width="295" border="0" align="center">
<tr>
<td class="yhzc" colspan="3">用户登陆</td>
</tr>
<tr>
<td width="60" class="mima">账号:</td>
<td><label><?php print $name; ?></label></td>
<td class="hb"> </td>
</tr>
<tr>
<td class="mima">密码:</td>
<td><label><?php print $pass; ?></label></td>
<td class="hb"> </td>
</tr>
<tr>
<td class="mima"></td>
<td><?php print $remember_me;?></td>
<td class="hb"></td>
</tr>
<tr>
<td class="xib" colspan="3"><?php print $submit;?></td>
</tr>
</table>
<?php print $hidden ;?>
</div>
现在清除缓存,使用匿名用户访问user/login页面,我们看到的登录表单:
已经比较接近我们的需求了,只不过,还存在一些问题,比如表单元素的标签的重复输出,表单元素的描述不应该显示的问题,还有就是表单元素过长。
现在,如果使用Firebug查看页面源代码的话,我们发现,表单整体的HTML,已经被我们控制住了,但是表单元素的HTML,还没有控制住。
我们以用户名为例,我们的目标输出,是这个样子的:
<label><input class="wbk" type="text" name="textfield" id="textfield" /></label>
而实际,这里的输出,则有很多:
1) 我们在template.php文件添加以下代码:
function snt_form_alter(&$form, &$form_state, $form_id) {
if ($form_id == 'user_login') {
$form['name']['#theme_wrappers'] = array();
$form['pass']['#theme_wrappers'] = array();
$form['submit']['#theme_wrappers'] = array();
$form['remember_me']['#theme_wrappers'] = array();
}
}
现在样式干净了很多。
问题还是存在,表单元素过长了,我们以用户名表单元素为例,分析一下目标代码和当前输出。
目标代码:
<label><input class="wbk" type="text" name="textfield" id="textfield" /></label>
当前输出:
<label>
<input id="edit-name" class="form-text required" type="text" maxlength="60" size="60" value="" name="name" tabindex="1">
</label>
在这里,id和name我们是必须要要使用Drupal默认的了,不然的话,就没有办法正常工作了。我想,我们应该把目标代码里面的class="wbk"给加上。这是我们修改后的form_alter函数,粗体表示新增的。
function snt_form_alter(&$form, &$form_state, $form_id) {
if ($form_id == 'user_login') {
$form['name']['#theme_wrappers'] = array();
$form['pass']['#theme_wrappers'] = array();
$form['submit']['#theme_wrappers'] = array();
$form['remember_me']['#theme_wrappers'] = array();
$form['name']['#attributes'] = array('class' => array('wbk'));
$form['pass']['#attributes'] = array('class' => array('wbk'));
$form['actions']['submit']['#attributes'] = array('class' => array('anniu2'));
}
}
现在,样式正常了。
还有问题,登陆按钮,里面的文字重复显示了,因为背景图片里面已经包含了这两个字。我们在form_alter里面增加这么一行代码:
$form['actions']['submit']['#value'] = t('');
这样,就可以完全使用背景图片的样式了。
复选框后面的文本被去掉了,没有关系,我们直接在user-login.tpl.php里面,给加上来。这是修改后的对应代码:
<td><?php print $remember_me;?><span class="zidong">两周内自动登录</span></td>
现在,使用匿名用户访问登录页面,样式已经完全满足我们的需要了。
如何想去掉这个页面的标题/标签,也就是这部分:
我们可以这样,在sites\all\themes\snt\templates\override下面,创建一个文件夹page,然后将sites\all\themes\snt\templates下面的page.tpl.php复制过来,重命名为page--user--login.tpl.php。
打开page--user--login.tpl.php,找到这段代码:
<?php print render($title_prefix); ?>
<?php if ($title): ?><h1 class="title" id="page-title"><?php print $title; ?></h1><?php endif; ?>
<?php print render($title_suffix); ?>
<?php if ($tabs): ?><div class="tabs"><?php print render($tabs); ?></div><?php endif; ?>
将其删除掉。清楚缓存,现在的登录页面,已经和美工所给的HTML完全保持一致了。
对于表单的覆写,我们最多只能控制它的90%的输出,还是有一部分我们控制不了的,因为Drupal表单里面,有些东西是必须要要有的,不然的话就不能正常工作。但是,我们最需要覆写的输出,我们控制住了,而我们控制不了的那10%,对样式的影响,通常不大。
可能会有读者问,我是怎么知道的使用$form['name']['#attributes'],使用$form['name']['#theme_wrappers']的。我们可以在form_alter里面加上,我们熟悉这段代码:
print debug($form);
就可以查看表单的结构。
array (
'name' =>
array (
'#type' => 'textfield',
'#title' => '用户名',
'#size' => 60,
'#maxlength' => 60,
'#required' => true,
'#description' => '输入您在 SNT 的用户名。',
'#attributes' =>
array (
'tabindex' => 1,
),
),
'pass' =>
array (
'#type' => 'password',
'#title' => '密码',
'#description' => '输入与您用户名相匹配的密码。',
'#required' => true,
'#attributes' =>
array (
'tabindex' => 1,
),
),
'#validate' =>
array (
0 => 'user_login_name_validate',
1 => 'user_login_authenticate_validate',
2 => 'user_login_final_validate',
),
'remember_me' =>
array (
'#title' => '两周内自动登录',
'#type' => 'checkbox',
'#default_value' => 1,
'#attributes' =>
array (
'tabindex' => 1,
),
),
'actions' =>
array (
'#type' => 'actions',
'submit' =>
array (
'#type' => 'submit',
'#value' => '登录',
'#attributes' =>
array (
'tabindex' => 1,
),
),
),
'#form_id' => 'user_login',
'#type' => 'form',
'#build_id' => 'form-q3SaShBE9RJOLYFhQD5E9Dqm3dccXRyxB_EpYlZgyVQ',
'form_build_id' =>
array (
'#type' => 'hidden',
'#value' => 'form-q3SaShBE9RJOLYFhQD5E9Dqm3dccXRyxB_EpYlZgyVQ',
'#id' => 'form-q3SaShBE9RJOLYFhQD5E9Dqm3dccXRyxB_EpYlZgyVQ',
'#name' => 'form_build_id',
'#parents' =>
array (
0 => 'form_build_id',
),
),
'form_id' =>
array (
'#type' => 'hidden',
'#value' => 'user_login',
'#id' => 'edit-user-login',
'#parents' =>
array (
0 => 'form_id',
),
),
'#id' => 'user-login',
'#method' => 'post',
'#action' => '/snt2/user/login',
'#theme_wrappers' =>
array (
0 => 'form',
),
'#tree' => false,
'#parents' =>
array (
),
'#submit' =>
array (
0 => 'user_login_submit',
),
'#theme' =>
array (
0 => 'user_login',
),
)
在snt_preprocess_user_login里面,使用print debug($variables['form']);,也是同样的效果。
我们最后,看注册页面,这也是一个表单,更复杂一点,我们这里给出代码,和具体的实现。我们的目标样式:
1) 我们需要为用户添加一个手机字段,并将它显示在注册表单页面。导航到admin/config/people/accounts/fields,添加手机字段,字段配置。
属性 |
值 |
标签 |
手机 |
机读名称 |
field_telephone |
字段类型 |
文本 |
控件 |
文本字段 |
必填字段 |
选中 |
在用户注册表单中显示 |
选中 |
值的数量 |
1 |
其它采用默认配置即可。现在的注册表单,是这个样子的:
为了让用户能够在注册的时候,设置自己的密码,我们需要在admin/config/people/accounts页面,账号设置页面,配置一下,这里是我的配置:
现在注册页面,用户可以设置自己的密码了:
2) 我们开始写代码了,这里给出来的是我调试好的代码。在template.php的snt_theme函数里面,添加以下代码:
$hooks['user_register_form'] = array(
'template' => 'user-register-form',
'render element' => 'form',
'path' => drupal_get_path('theme','snt').'/templates',
//'preprocess functions' => array('snt_preprocess_user_register_form'),
);
添加预处理函数snt_preprocess_user_register_form:
function snt_preprocess_user_register_form(&$variables) {
//print debug($variables['form']);
$variables['form']['account']['pass']['pass1']['#theme_wrappers'] = array();
$variables['form']['account']['pass']['pass2']['#theme_wrappers'] = array();
$variables['form']['account']['pass']['pass1']['#attributes'] = array('class' => array('wbk'));
$variables['form']['account']['pass']['pass2']['#attributes'] = array('class' => array('wbk'));
}
添加函数snt_form_user_register_form_alter:
function snt_form_user_register_form_alter(&$form, &$form_state) {
//drupal_set_message('123456');
//print debug($form['account']['pass']['pass1']);
$form['account']['name']['#theme_wrappers'] = array();
$form['account']['mail']['#theme_wrappers'] = array();
$form['account']['pass']['#theme_wrappers'] = array();
//$form['account']['pass']['pass1']['#theme_wrappers'] = array();
//$form['account']['pass']['pass2']['#theme_wrappers'] = array();
$form['field_telephone']['und'][0]['value']['#theme_wrappers'] = array();
$form['account']['name']['#attributes'] = array('class' => array('wbk'));
$form['account']['mail']['#attributes'] = array('class' => array('wbk'));
//$form['account']['pass']['pass1']['#attributes'] = array('class' => array('wbk'));
$form['field_telephone']['und'][0]['value']['#attributes'] = array('class' => array('wbk'));
$form['actions']['submit']['#attributes'] = array('class' => array('anniu'));
$form['actions']['submit']['#value'] = t('');
// print debug($form['account']['pass']);
}
最后创建user-register-form.tpl.php文件,里面的代码如下:
<div class="zhuce">
<table width="489" border="0" align="center">
<tr>
<td class="yhzc" colspan="3">用户注册</td>
</tr>
<tr>
<td class="mima"><span>*</span>用户名:</td>
<td><label><?php print render($form['account']['name']); ?></label></td>
<td class="hb">账号不能为空</td>
</tr>
<tr>
<td class="mima"><span>*</span>密码:</td>
<td><label><?php print render($form['account']['pass']['pass1']); ?></label></td>
<td class="hb">密码不能为空</td>
</tr>
<tr>
<td class="mima"><span>*</span>确认密码:</td>
<td><label><?php print render($form['account']['pass']['pass2']); ?></label></td>
<td class="hb">确认密码不能为空</td>
</tr>
<tr>
<td class="mima"><span>*</span>手机:</td>
<td><label><?php print render($form['field_telephone']['und'][0]['value']); ?></label></td>
<td class="hb">手机号码不能为空</td>
</tr>
<tr>
<td class="mima"><span>*</span>电子邮件:</td>
<td><label><?php print render($form['account']['mail']); ?></label></td>
<td class="hb">电子邮箱地址不能为空</td>
</tr>
<tr>
<td class="xib" colspan="3"><label><?php print drupal_render($form['actions']); ?></label></td>
</tr>
</table>
<?php drupal_render_children($form)?>
</div>
这里面的代码,实现方式,与前面我们介绍的有所不同,我们没有在预处理函数里面定义变量,而是直接在模板里面输出对应的表单元素,注意这句代码的应用:
<?php print render($form['account']['name']); ?>
还有,就是密码确认表单元素,实际上由两部分组成pass1和pass2,不过在form_alter里面,我们获取不到,所以把对它们的修改放在了预处理函数里面了。
再有就是字段field_telephone的用法,需要特别的注意,对应的数组结构。
最终样式,完全和目标一致:
如何去掉这里的标题和标签呢?前面已经有个例子了,这里就不多说了。
我们来看首页的制作,以前我们对于首页的HTML做过分析的,截图在前面也有,可以回过头来看一下。使用Panels制作首页?这里不会讲解这个,这是比较高级的技术,我们讲最简单的办法。最简单的办法?是的,这是学习成本最低的办法,有点土哦。
办法是这样的,就是为首页单独的创建page--front.tpl.php文件,然后将首页区块里面的内容,保存到Drupal的区块里面,或者通过Views实现出来。然后在page--front.tpl.php文件,直接输出这些区块,或者Views。
我们再来分析一下首页和普通页面结构的对比,首先是首页的:
这个是普通页面的:
我们看到,首页的banner和jishu两个div,就对应于普通页面的ziye,我们首先这样做,在sites\all\themes\snt\templates\override\page下面创建page--front.tpl.php,直接复制现有的重命名即可,然后将ziye的内容,替换为banner和jishu两个div里面的静态内容。
这是替换后的样子。
我们替换后,需要将文件的编码格式转为UTF-8,不然看到的会是乱码。现在访问首页,已经可以看到内容了。
不过图片都没有显示出来,在复制过来的代码中,很多都是这个样子的:
<img src="images/003.gif" width="128" height="42" />
我们只需要把它替换为
<img src="<?php print base_path() . path_to_theme() ?>/images/003.gif" width="128" height="42" />
这样就可以了。凡是有图片的,我们都这样替换一遍。现在图片的显示都正常了。
我们知道,右上角的立即下载区块,我们已经放到了Drupal的区块里面了,我们这里实际上直接调用这个区块即可。此外,最新公告/最新新闻,我们也实现了。
我们在区块列表中找到banner-right这个区块,我们看到它的ID(Delta)为5。我们可以将首页对应的静态HTML部分删除掉,替换为这样的代码。
<?php
$block = module_invoke('block', 'block_view', 5);
print $block['content'];
?>
然后,刷新首页,这部分内容的显示,还是正常的。这段代码,就是在模板中的任何位置,输出区块的代码。module_invoke里面的第一个参数,是模块的机读名字,这里是block,第2个参数,是钩子,这里是block_view,第三个参数,是传递给钩子函数的,这里是区块的delta。
我们将<div id="jishu-right">和它里面的所有内容全部删除,在对应的位置,添加以下代码:
<?php print views_embed_view('news','block'); ?>
这样就把“最新公告/最新新闻”替换为我们Views的实现了。刷新首页,发现没有正常显示出来。
我仔细检查了一下,原来首页是<div id="jishu-right">,而其它页面则是<div id="jishu-right2">,就这一点差别。怎么办?我们可以将视图news里面的区块显示克隆一份,对于克隆出来的区块,我们使用同样的配置,同样的模板文件,只有一点不同,就是为它单独创建一个views-view--news--block-1.tpl.php,里面的内容也是复制过来的,只不过,我们将<div id="jishu-right2">替换为了<div id="jishu-right">。当然,对于附件显示,我们也需要将它附加到新建的区块上面来。
最后,我们将嵌入视图的代码修改为:
<?php print views_embed_view('news','block_1'); ?>
这样就正常了。
我们来看左边,幻灯下面的两段HTML:
<div id="jishu-left">下面包括<div class="tupo">和<div class="zhichi">,我们分别为它们两个创建两个自定义区块,然后在对应的位置输出对应的区块。这是创建好的区块,我们没有将它们放到任何区域:
这是替换后的代码:
<div id="jishu-left">
<?php
$block = module_invoke('block', 'block_view', 12);
print $block['content'];
?>
<?php
$block = module_invoke('block', 'block_view', 13);
print $block['content'];
?>
</div>
到目前为止,一切都好,只有一点小问题:
技术支持,这部分,没有显示全,出现了滚动条。我们的解决办法,是添加一段CSS代码,添加到sites\all\themes\snt\css下面的custom.css文件中:
#jishu #jishu-left .zhichi ul{
width:699px;
}
#jishu #jishu-left .zhichi{
overflow-x:hidden;
overflow-y:hidden;
}
这样就搞定了,有时候CSS覆写还是很方便的。
首页的幻灯,目前还是一片空白,没有显示出来:
幻灯是加载了JS的,怎么办?办法很简单。
在sites\all\themes\snt\templates\override下面创建文件夹html,然后将sites\all\themes\snt\templates下面的html.tpl.php复制过来,重命名为html--front.tpl.php,然后呢,将index.html里面head部分中的JS部分复制过来。这是我修改后的html--front.tpl.php:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN"
"http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php print $language->language; ?>" version="XHTML+RDFa 1.0" dir="<?php print $language->dir; ?>"<?php print $rdf_namespaces; ?>>
<head profile="<?php print $grddl_profile; ?>">
<?php print $head; ?>
<title><?php print $head_title; ?></title>
<?php print $styles; ?>
<?php print $scripts; ?>
<script src="<?php print base_path() . path_to_theme() ?>/js/jquery-1.4a2.min.js" type="text/javascript"></script>
<script src="<?php print base_path() . path_to_theme() ?>/js/jquery.KinSlideshow-1.2.1.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(function(){
$("#KinSlideshow").KinSlideshow({
moveStyle:"down",
intervalTime:8,
mouseEvent:"mouseover",
titleBar_height:40,
titleFont:{TitleFont_size:14,TitleFont_color:"#fdac11"}
});
})
</script>
<script type="text/javascript">
<!--
function MM_swapImgRestore() { //v3.0
var i,x,a=document.MM_sr; for(i=0;a&&i<a.length&&(x=a[i])&&x.oSrc;i++) x.src=x.oSrc;
}
function MM_preloadImages() { //v3.0
var d=document; if(d.images){ if(!d.MM_p) d.MM_p=new Array();
var i,j=d.MM_p.length,a=MM_preloadImages.arguments; for(i=0; i<a.length; i++)
if (a[i].indexOf("#")!=0){ d.MM_p[j]=new Image; d.MM_p[j++].src=a[i];}}
}
function MM_findObj(n, d) { //v4.01
var p,i,x; if(!d) d=document; if((p=n.indexOf("?"))>0&&parent.frames.length) {
d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);}
if(!(x=d[n])&&d.all) x=d.all[n]; for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
for(i=0;!x&&d.layers&&i<d.layers.length;i++) x=MM_findObj(n,d.layers[i].document);
if(!x && d.getElementById) x=d.getElementById(n); return x;
}
function MM_swapImage() { //v3.0
var i,j=0,x,a=MM_swapImage.arguments; document.MM_sr=new Array; for(i=0;i<(a.length-2);i+=3)
if ((x=MM_findObj(a[i]))!=null){document.MM_sr[j++]=x; if(!x.oSrc) x.oSrc=x.src; x.src=a[i+2];}
}
//-->
</script>
</head>
<body class="<?php print $classes; ?>" <?php print $attributes;?>>
<div id="skip-link">
<a href="#main-content" class="element-invisible element-focusable"><?php print t('Skip to main content'); ?></a>
</div>
<?php print $page_top; ?>
<?php print $page; ?>
<?php print $page_bottom; ?>
</body>
</html>
没有什么难的,代码直接复制过来了。清除缓存,刷新首页,幻灯的显示正常了。就这么简单。
如果需要的话,我们还可以将幻灯的HTML片段放到Drupal区块里面,然后在page--front.tpl.php里面动态的输出这个静态区块。
这样整个首页就制作完成了。
我们最后讲一点,创建一个内容类型“路由”(route),专门用来提供路径的,我以前经常把它叫做“节点占位符”。然后,创建一篇类型为“路由”(route)的节点,节点标题“首页占位符(请勿删除)”,正文里面不输入任何内容。创建后,我们看到这个节点的ID为13。
接着,我们导航到=admin/config/system/site-information,在这里,将站点首页设置为“node/13”。保存。
这样有什么好处呢? 当我们访问首页时,其实加载的是node/13这个节点,只不过我们没有显示这个节点的内容,我们显示的内容,都放到了page--front.tpl.php里面。如果我们不使用这种技术的话,首页加载的实际是node页面,这是节点的列表页面,加载的东西更多一些,性能损失更大一点。所以这样做的一个好处就是,稍微的提升性能。
第二个好处,首页我们可以使用page--front.tpl.php,如果一个站点有比较多的这样的动态复杂页面的话,我们又不想使用Panels,或者不会用,此时就可以使用这种办法,创建一个节点提供路经,然后覆写page--node--[nid].tpl.php,不输出内容区域,将对应的部分替换成想要的。
在实际的项目中,我经常采用这种技术,直到我后来熟悉了Panels模块。即便如此,这种办法,仍然实用。其中还有一个好处,就是简单易学,比较容易上手。
我们这里面介绍的很多办法,都不是特别标准,但是在实际的项目中,还经常用,能够解决实际问题的办法,就是好办法。我们的主题制作就介绍到这里,关于节点模板文件的覆写,可以参看Drupal实战一书中的主题制作部分,关于Panels的自定义布局,可以参看Drupal实战一书中制作首页一章。关于核心模板文件可用变量,可用模板建议,参看Think in Drupal第4集的主题模板文件一章。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们现在来做主题,做Drupal网站的时候,可以一开始就做主题,也可以配置一些工作以后再做主题。每个人的习惯是不一样的,我们还基于Zen来实现想要的功能。
我们将zen目录下的STARTERKIT复制到sites\all\themes下面,然后将其重命名为yaiyuan;将sites\all\themes\yaiyuan下面的STARTERKIT.info.txt重命名为yaiyuan.info,注意文件后缀名,对里面的信息做以下修改:
name = Yaiyuan
description = Yaiyuan theme.
打开template.php文件,将里面的STARTERKIT替换为yaiyuan;打开theme-settings. php文件,将里面的STARTERKIT替换为yaiyuan。这样我们的这个子主题就初步制作完成了。
导航到主题的管理界面,admin/appearance,将我们新建的这个主题启用并设置为默认主题。现在访问首页,看到样式还是乱的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
今天是2013年4月3日,我们的think in Drupal第5集正式开工了,从第4集到现在已经过去了3个月的时间了,中间事情很多,让我们耽误了一段时间,不过还好,我们还会继续下去的。
我们在第5集里面,主要讲解Drupal的主题制作,以及多语言网站建设。如果篇幅允许的话,可能再加点别的。开始我是这样计划的,做一个公司的网站,从设计,到转成HTML,再转成Drupal主题,然后再做出来网站,最后再做出来中英文版的。我们这里的多语言,就是指的中英文网站。
不过我找的美工比较忙,一直没有帮我制作HTML,不过我们的教程不会因此而停止的,还有,我朋友公司的静态HTML愿意提供给我作为素材,在此,我们对lonlife.net提供的素材表示感谢。制作Drupal主题的原理是一样的,我的意思是说,每个主题的外观都是不一样的,但是从静态HTML到Drupal主题的转换过程,大致是一样的。
我们这一集讲两个例子,一个就是从静态HTML,转为Drupal主题;另一个就是基于Omega,制作自己的子主题。从而也让大家看到两种制作Drupal主题的方式。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们先来看一下,第一个例子,从静态HTML,转为Drupal主题。首先来看做好的静态HTML,这是朋友网站的静态HTML。
我们看到,这里面包含6个静态HTML页面。Drupal主题制作的正常流程是这样的:
需要经过设计、切图、转Drupal主题。现在前面两步已经完成了,我们接下来要做的是把静态HTML转为Drupal主题。
现在回到首页,发现有了内容,不过这里的区块标题不是我们想要的:
我们在输入区块标题,区块描述的时候,两个地方都输入了。区块标题默认会显示出来的。有人可能会问,区块描述为什么是必填的?而区块标题则允许为空?
区块标题,是在页面上面显示出来的区块的标题,区块描述是在后台显示给管理员看的,一个是给普通用户看的,一个是给管理员看的。
在区块的配置页面,我们将区块标题置为空,就可以解决这个问题了。还有一个办法,就是在区块的模板文件里面,不输出区块标题变量。
我们这里顺带讲解一下hook_theme和theme_hook之间的区别。在Drupal7下,所有的主题函数,又被称为主题钩子函数,比如
theme_breadcrumb
theme_links
theme_menu_tree
这些函数,属于theme_hook的范畴,theme_后面跟的是具体的钩子。
hook_theme,我们举个例子,比如node_theme(),block_theme,这是它在节点、区块里面的具体实现,它是一个普通的钩子函数,和我们平时所用的hook_form_alter是一个概念,这个钩子函数是用来注册主题函数和模板文件的。在Drupal中使用的主题函数和模板文件都需要在这里注册一下,不然的话Drupal无法识别这些主题函数或者模板文件。
我们向template.php文件中添加以下代码:
function snt_menu_tree__main_menu($variables) {
return '<ul>' . $variables['tree'] . '</ul>';
}
注意我们这里的函数名字,我们是可以使用snt_menu_tree,不过这将作用于所有的菜单,我们只想作用于主菜单,所以在后面加了__main_menu,这是主题函数建议,和模板建议是一个概念。
对于模板建议,我们知道,模板建议规则,是在模板文件的预处理函数中,通过'theme_hook_suggestions'定义的。很多人可能会问,主题函数的建议,是在什么地方定义的?在Drupal6的时候,主题函数是没有建议这么一说的,只有模板建议,在Drupal7下面改进了一下,增加了主题函数的建议,不是每个主题函数都支持建议的,需要具体情况具体分析,这取决于不断地实践。对于theme_menu_tree,我们在menu_tree_output函数里面,代码的最后部分:
// Add the theme wrapper for outer markup.
// Allow menu-specific theme overrides.
$build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']['menu_name'], '-', '_');
theme_menu_tree的建议规则,就是在这里定义的,所以我们可以使用snt_menu_tree__main_menu这个函数,它只作用于主菜单。
现在清除缓存,访问首页,通过Firebug查看源代码:
ul标签上面的class属性已经被我们删除了,不过主导航的样式,还没有调整过来。我们现在覆写theme_menu_link,将这个函数从menu.inc中,复制到template.php中,我们来看一下默认的样子:
function theme_menu_link(array $variables) {
$element = $variables['element'];
$sub_menu = '';
if ($element['#below']) {
$sub_menu = drupal_render($element['#below']);
}
$output = l($element['#title'], $element['#href'], $element['#localized_options']);
return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
}
我们对它做相应的调整,这是调整后的代码:
function snt_menu_link__main_menu(array $variables) {
$element = $variables['element'];
$sub_menu = '';
if ($element['#below']) {
$sub_menu = drupal_render($element['#below']);
}
$output = l('<span>'.$element['#title'].'</span>', $element['#href'], array('html' => TRUE,));
return '<li>' . $output . $sub_menu . "</li>\n";
}
这里面有两处修改,首先是将函数名字改为了snt_menu_link__main_menu,这里面的__main_menu,和前面我们用的,一样,都是主题函数建议;其次,就是将li里面的属性输出,给删除了。
可能还会有人问,我是怎么知道这个主题函数的建议规则的?在menu_tree_output函数里面,中间部分,有这样两行代码:
// Allow menu-specific theme overrides.
$element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_');
这里定义了theme_menu_link的建议规则。
现在清除缓存,查看源代码,和目标输出已经完全一致了:
当然,此时的样式,也正常了:
对于下面的友情链接,我们也可以将它改造为菜单的形式,覆写的办法和这里一样,我们这里采用静态区块的形式就可以了。
我们将modules\block下面的block.tpl.php复制到sites\all\themes\snt\templates目录下面,同时我们将代码里面区块标题的输出给注释掉,这是调整后的代码:
<div id="<?php print $block_html_id; ?>" class="<?php print $classes; ?>"<?php print $attributes; ?>>
<?php print render($title_prefix); ?>
<?php if ($block->subject): ?>
<h2<?php print $title_attributes; ?>><?php //print $block->subject ?></h2>
<?php endif;?>
<?php print render($title_suffix); ?>
<div class="content"<?php print $content_attributes; ?>>
<?php print $content ?>
</div>
</div>
清除缓存数据,现在所有的区块标题,都不见了。
我们现在继续,我们看到图片的显示不正常,如图所示:
这是因为图片的路径不对,这是我们使用的html片段:
<img src="images/br.gif" width="253" height="8" />
<img src="images/snt.gif" width="217" height="26" />
<img src="images/xiazai.gif" width="222" height="76" />
我们现在需要将它们调整为这样的形式:
<img src="http://localhost/snt2/sites/all/themes/snt/images/br.gif" width="253" height="8" />
<img src="http://localhost/snt2/sites/all/themes/snt/images/snt.gif" width="217" height="26" />
<img src="http://localhost/snt2/sites/all/themes/snt/images/xiazai.gif" width="222" height="76" />
这是可以工作的,但是,如果我们这样将路径写死在里面的话,将来如果我们更换了域名,此时需要重新的调整这些地址,如果要调整的地方很多,也是非常费力的。
我们可以采用这样的方案,启用核心自带的PHP过滤器模块,在banner-right区块里面,将对应的代码修改为:
<img src="<?php print base_path() . path_to_theme(); ?>/images/br.gif" width="253" height="8" />
<img src="<?php print base_path() . path_to_theme(); ?>/images/snt.gif" width="217" height="26" />
<img src="<?php print base_path() . path_to_theme(); ?>/images/xiazai.gif" width="222" height="76" />
然后将区块正文的文本格式设置为PHP Code,保存。图片的显示就正常了:
注意这里面,这段代码的使用,<?php print base_path() . path_to_theme(); ?>,这是在主题制作、Drupal开发中经常使用的代码。
对于页脚的snt-left区块,我们也做类似的调整:
<img src="<?php print base_path() . path_to_theme(); ?>/images/snt2.gif" width="66" height="25" />
现在的主导航区块,还是静态的,如图所示:
我们想使用Drupal的主菜单来管理这里的内容,这样更方便一点。
我们导航到admin/structure/menu,在这里找到主菜单,进入它的菜单链接列表页面admin/structure/menu/manage/main-menu。
我们对默认的Home菜单链接进行编辑,将“Home”修改为“首页”,保存。然后依次添加菜单链接:下载、充值、我的账户、业务支持、新手指引。我们知道,在创建菜单链接的时候,必须提供一个有效的路径,所以我们为这些菜单链接默认使用路径“node/1”,这里面只有一个例外,就是“我的账户”,我们为它使用了路径“user”。开始的时候,我们可以随便的指定一个路径,在项目的后期,当我们有了真实可用的路径以后,我们再将它修改过来。通过拖拽,我们调整一下菜单链接之间的相对位置,这是调整后的样子。
现在导航到区块管理界面admin/structure/block,找到主菜单区块,我们将它放到Navigation区域,然后将原来的nav区块的区域设置为None,这样我们就使用了主菜单做我们的导航了。调整完了以后,记得点击下面的保存区块按钮。
我们现在访问首页,来观察一下这个导航样式的变化,这是我在Firefox下面看到的效果:
我们看到样式变了,我们可以通过调整CSS,调整成原来的样子,我们也可以覆写HTML输出,让Drupal的HTML输出和美工切图后的HTML保持一致,这里我们采用后者。
这是当前的HTML,我们看到,与美工所给的HTML相比,Drupal输出了太多我们不想要的HTML markup。我们现在逐个的去掉这些多余的输出。
我们在firefox下面打开美工所给的静态页面,通过Firebug查看对应部分的源代码:
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们的目标就是覆写Drupal的输出,使其与美工所给的输出保持一致。我们首先来看最外面的<div class="region region-nav">,这个div是在region.tpl.php里面输出的。我们首先找到默认的region.tpl.php文件,它位于modules\system目录下面,我们将它复制到sites\all\themes\snt\templates目录下面。我们来看一下这个模板文件:
<?php if ($content): ?>
<div class="<?php print $classes; ?>">
<?php print $content; ?>
</div>
<?php endif; ?>
我们这里只需要修改这个模板文件里面的内容,将div标签给删除即可,如果这样做的话,所有区域的div标签都会被删除,我们这里不这样做,首先完整的保留这个模板文件,然后我们新建一个模板建议文件,专门用于这里的导航区域。
我们在sites\all\themes\snt\templates下面新建一个文件夹override,里面用来放置模板建议文件,然后在这个文件夹下面,再创建一个子文件夹region,用来放置region.tpl.php的模板建议。
首先,这里除了templates的文件夹名字是固定的以外,里面子文件夹的名字,可以随便起,Drupal会自动的遍历templates里面的所有文件、文件夹、子文件夹。当然,我们需要起一个含义明确的名字,如果我们把所有的模板文件、模板建议文件,都直接放到sites\all\themes\snt\templates下面,这也是可以的;不过我们在实际的项目中,需要覆写的模板文件比较多,为了管理的方便,我们将模板建议文件统一放在了override下面;在override下面,又新建若干个文件夹,分别放置对应模板文件的覆写。
我们通过Google搜索一下预处理函数template_preprocess_region,我们可以在region.tpl.php模板文件中的注释里面找到这个函数名字。通常在搜索结果的第一条,就指向了api.drupal.org上面的对应代码了,我们点击对应的链接。
来看一下这个函数:
function template_preprocess_region(&$variables) {
// Create the $content variable that templates expect.
$variables['content'] = $variables['elements']['#children'];
$variables['region'] = $variables['elements']['#region'];
$variables['classes_array'][] = drupal_region_class($variables['region']);
$variables['theme_hook_suggestions'][] = 'region__' . $variables['region'];
}
注意代码里面的最后一行,表示它支持region--[region_name].tpl.php形式的模板建议。代码里面使用的是下划线,文件名字里面使用的是连字符。我们这里区域的名字,就是nav,这是在主题的snt.info文件中定义的。所以我们只需要创建region--nav.tpl.php就可以了。对于熟悉Drupal6主题开发的读者,这里需要注意,这里面是两个连字符--,不是一个,这是Drupal7与Drupal6之间的一个区别。
我们将region.tpl.php复制到sites\all\themes\snt\templates\override\region,然后将它重命名为region--nav.tpl.php,最后打开region--nav.tpl.php,删除里面的div代码,这是删除后的样子:
<?php if ($content): ?>
<?php print $content; ?>
<?php endif; ?>
其实这里面的if语句,在我们这里也没有什么特别的用出了,因此把if语句,也删除掉,这是删除后的代码:
<?php print $content; ?>
现在清除缓存,在浏览器中打开新标签页,访问http://localhost/snt2/,使用firebug查看页面源代码。我们看到,我们去掉region相关的<div>了。
但是此时,样式还没有任何变化。我们继续前进,我们把区块里面的多余HTML标签给删除掉。
我们在sites\all\themes\snt\templates\override下面创建一个文件夹block,将sites\all\themes\snt\templates里面的block.tpl.php复制过来。我们打开block.tpl.php,在注释里面找到函数template_preprocess_block,通过Google搜索这个函数,点击打开api.drupal.org上面的链接,找到该函数的代码,我们看这一部分:
这里的英文注视,说的就是下划线、连字符两者的用法,我们在模块的代码里面,也就是这里的预处理函书中,使用下划线,在文件名字里面使用连字符。如果在代码的'theme_hook_suggestions'数组里面使用了连字符,它就不会正常工作。我们看到,block.tpl.php支持以下形式的模板建议:
block--[region].tpl.php
block--[module].tpl.php
block--[module]--[delta].tpl.php
注意,这里需要解释一下的是这里的delta,delta是区块在当前模块里面的唯一标识,它在当前模块里面是唯一的,但是不是在所有区块里面都是唯一的。所以模块名,加上delta,合在一起,就是区块的唯一ID。
在我们这里,使用的区域为nav,模块名为system,delta为main-menu。我们导航到区块的管理界面,点击配置链接,进入区块的配置页面。此时的配置链接里面,就包含模块名和Delta信息:admin/structure/block/manage/system/main-menu/configure。
因此我们可以使用:
block--nav.tpl.php
block--system--main-menu.tpl.php
我们这里采用后者,将sites\all\themes\snt\templates\override\block下面的block.tpl.php重命名为block--system--main-menu.tpl.php,然后修改模板文件里面的代码,这是修改后的:
<?php print $content ?>
是的,就是这样,除了区块内容以外,我们没有输出任何的东西。这是我在实际的项目中,经常采用的方法。但是这种方式也有一种局限性,就是说,区块的配置链接没有了,所以当你使用的时候,需要注意一下,到底要不要输出区块的配置链接。区块的配置链接,是通过变量$title_suffix输出的。
我们现在清除缓存,使用Firebug查看源代码,现在干净了很多:
不过样式还没有没有变化:
与美工所给的输出相比的,我们的ul、li上面多了class属性,我们来看ul,Drupal默认的输出为<ul class="menu">,为了和美工所给的HTML保持一致,我们需要将它覆写为<ul>。在哪个模板文件里面覆写这里的输出呢?实际上这里的HTML是由Drupal的菜单系统控制,而且是由主题函数控制的。
打开Drupal核心的includes文件夹,在里面找到menu.inc文件,打开这个文件,在这里面可以找到函数theme_menu_tree和theme_menu_link,这两个主题函数用来控制菜单和菜单链接的HTML输出。我们需要对这两个函数进行覆写。
首先来看theme_menu_tree,代码如下:
function theme_menu_tree($variables) {
return '<ul class="menu">' . $variables['tree'] . '</ul>';
}
这里的class="menu",就是我们想要去除的。
我们回到sites\all\themes\snt文件夹下,在这里面创建文件template.php,这里的template.php,就相当于模块的.module文件,它对于Drupal的主题是非常重要的。我们在template.php文件中,可以做很多事情,比如:
1) 自定义函数,可以放到这里,然后在模板文件中调用,这样模板文件中,就不用放置太多的PHP逻辑代码。
2) 实现预处理、处理函数,为模板文件添加变量或者修改变量。
3) 实现钩子函数,比如hook_form_alter,hook_theme,在这一点上完全等同于.module文件,这是Drupal7对Drupal6的一个改进。
4) 实现主题函数,在当前主题下覆写默认的输出。
我们这里,当前用到的是第4条,实现主题函数的覆写。对于创建template.php文件,我仍然是喜欢复制一个已有的文件,然后修改。我这里是从网上书店里面复制过来的,里面包含了很多注释掉的代码。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们现在来看右上角的区块top right,在登录状态下,显示的内容仍然是:
我们配置这个静态区块,首先让这里的注册、登陆两个链接可以工作,我们修改区块里面的相应内容,这是修改后的样子:
<a href="<?php print base_path(); ?>user/register">注册</a></span> <span class="zd"><a href="<?php print base_path(); ?>user/login">登陆</a>
我们这里只使用了<?php print base_path(); ?>,后面紧跟着内部路径,两者之间没有使用“/”。
然后,我们把文本格式修改为PHP code。
在区块的可见性设置里面,点击“Roles”(角色)标签,选中匿名用户。
然后将区块描述修改为:
保存区块。
我们新建一个区块,将区块描述设置为“top-right authenticated user”,区块的正文设置为:
<p><span class="weibo"><a href="#">官方微博</a></span> | <span class="weibo"><a href="#">帮助中心</a></span> <span class="zd"> 欢迎 <?php global $user; print l($user->name, 'user') ?></span></p>
<p class="kefu">客服电话:400-680-6666</p>
这里,除了注册、登陆所在的地方被替换掉了以外,其它地方和匿名用户的区块保持一致。我们这里只看变动的地方:
<span class="zd"> 欢迎 <?php global $user; print l($user->name, 'user') ?></span>
对于路径,我们使用'user'即可,不需要使用'user/' . $user->uid,两者的效果是一样的。
文本格式我们继续使用PHP code。
对于区域设置,我们将它指定到同一个区域“top right”。
对于可见性设置,我们只让它显示给登录用户:
这是登录用户看到的区块:
和匿名用户看到的,已经有了变化。我们这里充分利用Drupal核心区块已有的可见性设置,很方便的解决了匿名用户、登录用户看到不同内容,这个常见问题。我们也可以创建一个区块,在一个区块里面,使用PHP代码搞定这样的问题,如果这样的话,也是可行的,不过PHP代码稍微复杂一点。借助于Drupal核心功能,轻松解决这个问题,何乐而不为呢?
我们拿到HTML以后,首先需要看一下,静态页面的样子,这里有6个页面,我们来看一下。首先是首页index.html页面。
这就是首页,我这里截了上下两个图,QQ的抓图,只能抓当前屏幕的,而页面比较长,所以只能分成两部分截图。
我们看到首页其实是比较复杂的,我们大致可以将其分为三部分,上中下三部分。导航条上面的是一部分,友情链接下面的是一部分,中间是一部分。虽然布局稍微复杂一点,不过还是很有中国特色的,很多中国的网站都是这样的风格。
接下来,我们使用浏览器打开下一个页面,newhand.html。我们来看一下这个页面的样子:
在这里我们看到,这个页面和首页相比,头部和尾部都是相同的,对于中间的部分,右边栏也是相同的。我在看这些界面的时候,在查看的过程中,大致的确定一下Drupal里面的区域。我假定你对Drupal的主题的基础知识已经有了解。
对于头部,我们大致确定三个区域,分别为:
和
还有:
而对于页脚,我们大致可以确定出来两个区域,分别为:
和
注意,我们这里只是大致的确定。就是从界面上面确定,我们后面还需要进一步的根据HTML代码,更准确的确定区域。
我们现在来看recharge.html:
再往下的内容,和前一个页面一致。只有中间的内容有变化,而中间部分又可以分为左右两部分,右边是一个边栏,里面的内容不变,只有左边的内容变。所以我们可以把右边部分确定成为一个Drupal区域,左边部分采用内容区域,这是一种方案。还有一种方案,把中间的这一大部分,处理成内容区域,左右两部分处理成Panels的布局。大家都知道,作者是比较喜欢Panels 这种方式,不过在这里面,我们首先讲解第一种方式。
第一种方式,就是完全基于Drupal核心的主题机制,这种方式的学习成本低,易于学习;Panels的方式,有好的一方面,但是也有坏的一方面,坏的一方面,就是你需要学习Panels模块,学习成本上来了,就是比第一种方式学习成本高。我建议刚开始接触Drupal,使用Drupal做网站的朋友,先采用简单的方式。先学会怎么用了以后,在发现了Drupal核心主题机制的种种限制以后,再去尝试Panels。这样可以降低项目的风险。
现在我们打开register.html,头部和尾部和前面的一样,只有中间部分不同:
打开support.html,头部和尾部还是一样,只有中间部分不同:
还有login.html页面,中间部分:
到现在为止,我们对于美工所给的HTML已经有了一个初步的印象。接下来,我们需要去阅读所给的这些HTML代码。是的,我们需要每个HTML页面都读一遍,分析里面结构。从里面找出相同的部分。
这是index.html的代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>SNT</title>
<link href="css/css.css" rel="stylesheet" type="text/css" />
<script src="js/jquery-1.4a2.min.js" type="text/javascript"></script>
<script src="js/jquery.KinSlideshow-1.2.1.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(function(){
$("#KinSlideshow").KinSlideshow({
moveStyle:"down",
intervalTime:8,
mouseEvent:"mouseover",
titleBar_height:40,
titleFont:{TitleFont_size:14,TitleFont_color:"#fdac11"}
});
})
</script>
<script type="text/javascript">
<!--
function MM_swapImgRestore() { //v3.0
var i,x,a=document.MM_sr; for(i=0;a&&i<a.length&&(x=a[i])&&x.oSrc;i++) x.src=x.oSrc;
}
function MM_preloadImages() { //v3.0
var d=document; if(d.images){ if(!d.MM_p) d.MM_p=new Array();
var i,j=d.MM_p.length,a=MM_preloadImages.arguments; for(i=0; i<a.length; i++)
if (a[i].indexOf("#")!=0){ d.MM_p[j]=new Image; d.MM_p[j++].src=a[i];}}
}
function MM_findObj(n, d) { //v4.01
var p,i,x; if(!d) d=document; if((p=n.indexOf("?"))>0&&parent.frames.length) {
d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);}
if(!(x=d[n])&&d.all) x=d.all[n]; for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
for(i=0;!x&&d.layers&&i<d.layers.length;i++) x=MM_findObj(n,d.layers[i].document);
if(!x && d.getElementById) x=d.getElementById(n); return x;
}
function MM_swapImage() { //v3.0
var i,j=0,x,a=MM_swapImage.arguments; document.MM_sr=new Array; for(i=0;i<(a.length-2);i+=3)
if ((x=MM_findObj(a[i]))!=null){document.MM_sr[j++]=x; if(!x.oSrc) x.oSrc=x.src; x.src=a[i+2];}
}
//-->
</script>
</head>
<body onload="MM_preloadImages('images/anniu.gif')">
<div id="main">
<div id="top">
<div id="top-left">
<div class="logo"><span>SNT</span>代理</div>
<div class="logoright">
<p>super network tunnel</p>
<p>突破所有网络限制</p>
</div>
</div>
<div id="top-right">
<p><span class="weibo"><a href="#">官方微博</a></span> | <span class="weibo"><a href="#">帮助中心</a></span> <span class="zd"><a href="#">注册</a></span> <span class="zd"><a href="#">登陆</a></span></p>
<p class="kefu">客服电话:400-680-6666</p>
</div>
</div>
<div id="nav">
<div id="nav-left"></div>
<div id="nav-middle">
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">下载</a></li>
<li><a href="#">充值</a></li>
<li><a href="#">我的账户</a></li>
<li><a href="#">业务支持</a></li>
<li><a href="#">新手指引</a></li>
</ul>
</div>
<div id="nav-right"></div>
</div>
<div id="banner">
<div id="banner-left">
<div id="KinSlideshow" style="visibility:hidden;">
<a href="1" target="_blank"><img src="images/banner1.gif" width="701" height="260" /></a>
<a href="2" target="_blank"><img src="images/banner2.gif" width="701" height="260" /></a>
<a href="3" target="_blank"><img src="images/banner3.gif" width="701" height="260" /></a>
</div>
</div>
<div id="banner-right">
<p><img src="images/br.gif" width="253" height="8" /></p>
<div class="br-middle">
<p class="xiazai"><img src="images/snt.gif" width="217" height="26" /></p>
<p class="banben">软件版本:2.36.116</p>
<p class="banben">软件大小:4.95M</p>
<p class="banben">适用平台:所有windows</p>
<p class="banben2">最近更新:2012-12-3</p><p><a href="#"><img src="images/xiazai.gif" width="222" height="76" /></a></p>
</div>
<div class="br-bottom"></div>
</div>
</div>
<div id="jishu">
<div id="jishu-left">
<div class="tupo">
<div class="tp-left">
<h2 class="blue">突破所有网络限制</h2>
<p><img src="images/01.gif" width="201" height="80" /></p>
<p>只要能开网页就能上QQ,玩游戏</p>
<p> 穿透各种防火墙 </p>
<p>突破一切网络限制</p>
<p> 让您尽情网上冲浪</p>
<p class="more"><a href="1" onmouseout="MM_swapImgRestore()" onmouseover="MM_swapImage('Image9','','images/anniu.gif',1)"><img src="images/anniu2.gif" name="Image9" width="133" height="28" border="0" id="Image9" /></a></p>
</div>
<div class="tp-left">
<h2 class="green">数据加密确保安全</h2>
<p><img src="images/02.gif" width="201" height="80" /></p>
<p>可选使用AES+RAS数据加密</p>
<p>确保所有通信数据安全</p>
<p>但速度会较慢</p>
<p>----------</p>
<p class="more"><a href="2" onmouseout="MM_swapImgRestore()" onmouseover="MM_swapImage('Image10','','images/anniu.gif',1)"><img src="images/anniu2.gif" name="Image10" width="133" height="28" border="0" id="Image10" /></a></p>
</div>
<div class="tp-left">
<h2 class="org"> 一键登陆使用方便</h2>
<p><img src="images/03.gif" width="201" height="80" /></p>
<p>使用方法:安装完成后</p>
<p> 输入用户名密码后即可登陆</p>
<p>登陆成功后,直接启动QQ或其它网游</p>
<p> 即可突破网络限制 </p>
<p class="more"><a href="3" onmouseout="MM_swapImgRestore()" onmouseover="MM_swapImage('Image11','','images/anniu.gif',1)"><img src="images/anniu2.gif" name="Image11" width="133" height="28" border="0" id="Image11" /></a></p>
</div>
</div>
<div class="zhichi">
<h2>技术支持</h2>
<ul>
<li>
<p><a href="#"><img src="images/001.gif" width="128" height="42" /></a></p>
<p><span><a href="#">诺基亚</a></span></p>
<p>提供最新的手机服务</p>
</li>
<li>
<p><a href="#"><img src="images/002.gif" width="128" height="42" /></a></p>
<p><span><a href="#">摩托罗拉</a></span></p>
<p>三大业务集团</p>
</li>
<li>
<p><a href="#"><img src="images/003.gif" width="128" height="42" /></a></p>
<p><span><a href="#">诺基亚</a></span></p>
<p>提供最新的手机服务</p>
</li>
<li>
<p><a href="#"><img src="images/002.gif" width="128" height="42" /></a></p>
<p><span><a href="#">诺基亚</a></span></p>
<p>提供最新的手机服务</p>
</li>
<li>
<p><a href="#"><img src="images/003.gif" width="128" height="42" /></a></p>
<p><span><a href="#">诺基亚</a></span></p>
<p>提供最新的手机服务</p>
</li>
</ul>
</div>
</div>
<div id="jishu-right">
<div class="news">
<h2>最新公告</h2>
<ul>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
</ul>
</div>
<div class="news">
<h2>最新新闻</h2>
<ul>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
</ul>
</div>
</div>
</div>
</div>
<div id="footer">
<div class="link">
<div class="link-left">
友情链接:
</div>
<div class="link-right">
<p><a href="#">多玩暗黑3专区</a> |<a href="#"> 盛世三国</a> |<a href="#"> 西游3</a> | <a href="#">多玩魔兽世界专区</a> | <a href="#">仙妮蕾德</a> | <a href="#">07073动漫网</a> | <a href="#">宅人网下载</a> |<a href="#"> 免费网站申请</a> | <a href="#">魔兽世界中文网</a> |<a href="#"> 神仙道</a> |<a href="#"> 178新手卡 </a>| <a href="#">三界游戏网</a></p>
</div>
</div>
<div class="ft"></div>
<div class="snt">
<div class="snt-left"><img src="images/snt2.gif" width="66" height="25" /></div>
<div class="snt-right">
<p><a href="#">返回首页</a> | <a href="#">关于我们</a> | <a href="#">隐私政策</a> <a href="#">|充值中心</a> | <a href="#">在线客服</a></p>
<p>版权所有:lonlife 网游加速器 豫ICP备11008357号</p>
</div>
</div>
</div>
</body>
</html>
比较长。我们来看login.html的代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>SNT</title>
<link href="css/css.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="main">
<div id="top">
<div id="top-left">
<div class="logo"><span>SNT</span>代理</div>
<div class="logoright">
<p>super network tunnel</p>
<p>突破所有网络限制</p>
</div>
</div>
<div id="top-right">
<p><span class="weibo"><a href="#">官方微博</a></span> | <span class="weibo"><a href="#">帮助中心</a></span> <span class="zd"><a href="#">注册</a></span> <span class="zd"><a href="#">登陆</a></span></p>
<p class="kefu">客服电话:400-680-6666</p>
</div>
</div>
<div id="nav">
<div id="nav-left"></div>
<div id="nav-middle">
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">下载</a></li>
<li><a href="#">充值</a></li>
<li><a href="#">我的账户</a></li>
<li><a href="#">业务支持</a></li>
<li><a href="#">新手指引</a></li>
</ul>
</div>
<div id="nav-right"></div>
</div>
<div id="ziye">
<div class="ziye-left">
<p class="daohang"><a href="#">首页</a> > <a href="#">业务支持</a></p>
<p><img src="images/ziyebanner.gif" width="701" height="109" /></p>
<div class="nei">
<div class="login">
<table width="295" border="0" align="center">
<tr>
<td class="yhzc" colspan="3">用户登陆</td>
</tr>
<tr>
<td width="60" class="mima">账号:</td>
<td><label>
<input class="wbk" type="text" name="textfield" id="textfield" />
</label></td>
<td class="hb"> </td>
</tr>
<tr>
<td class="mima">密码:</td>
<td><label>
<input class="wbk" type="text" name="textfield2" id="textfield2" />
</label></td>
<td class="hb"> </td>
</tr>
<tr>
<td class="mima"></td>
<td><input name="" type="checkbox" value="" /><span class="zidong">两周内自动登录</span></td>
<td class="hb"></td>
</tr>
<tr>
<td class="xib" colspan="3"><input name="" type="submit" class="anniu2" value="" /></td>
</tr>
</table>
</div>
</div>
</div>
<div class="ziye-right">
<div id="banner-right">
<p><img src="images/br.gif" width="253" height="8" /></p>
<div class="br-middle">
<p class="xiazai"><img src="images/snt.gif" width="217" height="26" /></p>
<p class="banben">软件版本:2.36.116</p>
<p class="banben">软件大小:4.95M</p>
<p class="banben">适用平台:所有windows</p>
<p class="banben2">最近更新:2012-12-3</p><p><a href="#"><img src="images/xiazai.gif" width="222" height="76" /></a></p>
</div>
<div class="br-bottom"></div>
</div>
<div id="jishu-right2">
<div class="news">
<h2>最新公告</h2>
<ul>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
</ul>
</div>
<div class="news">
<h2>最新新闻</h2>
<ul>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
<li><a href="#">SNT V2.7.3 20121106 正式版发布</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div id="footer">
<div class="link">
<div class="link-left">
友情链接:
</div>
<div class="link-right">
<p><a href="#">多玩暗黑3专区</a> |<a href="#"> 盛世三国</a> |<a href="#"> 西游3</a> | <a href="#">多玩魔兽世界专区</a> | <a href="#">仙妮蕾德</a> | <a href="#">07073动漫网</a> | <a href="#">宅人网下载</a> |<a href="#"> 免费网站申请</a> | <a href="#">魔兽世界中文网</a> |<a href="#"> 神仙道</a> |<a href="#"> 178新手卡 </a>| <a href="#">三界游戏网</a></p>
</div>
</div>
<div class="ft"></div>
<div class="snt">
<div class="snt-left"><img src="images/snt2.gif" width="66" height="25" /></div>
<div class="snt-right">
<p><a href="#">返回首页</a> | <a href="#">关于我们</a> | <a href="#">隐私政策</a> <a href="#">|充值中心</a> | <a href="#">在线客服</a></p>
<p>版权所有:lonlife 网游加速器 豫ICP备11008357号</p>
</div>
</div>
</div>
</body>
</html>
其它页面我就不粘贴代码了,它们的结构和login.html一致。我们接下来需要对这些页面的HTML代码进行分析,就是打开所有的页面。我这里使用的是nodepad++。你也可以使用类似的工具。
这是login.html的结构:
这是index.html的结构:
我们看到,在首页的中间部分,采用的是banner、jishu两个div的上下结构,而不是像其它页面的ziye-left、ziye-right的左右结构。希望读者读到这里的时候,认真的分析一下首页中间部分和其它页面的中间部分的不同之处。
我们这里是按照美工给什么,我们就输出什么的。但是我们会发现,美工在切图的时候,有的地方处理的有点小问题,它给我们程序员带来了更多的挑战。我们在这里只需要知道,首页并不是我们所想的左右结构,而是首先上下结构,然后左右结构。
我们这里先不去深究这一点,来分析一下top、nav、footer的结构。它们里面的内容在所有页面都是相同的,在实际项目当中,通常也都是这样的。
我们先来分析top的结构:
它里面包括top-left、top-right两个div;展开top-left,里面还是有结构的:
在这里面,我们看到,这个Logo,是一段文字,而不是一个图片,所以我们这里面,不需要使用Drupal页面模板里面的logo变量,我们只需要把它处理成为一个区块就可以了。我们这里面,在划分区域的时候,有三种方案:
1) 分一个区域:top。
2) 分两个区域:top-left、top-right。
3) 分三个区域:logo、logoright、top-right。
每个人的喜好不同,习惯也不一样。这三种方案,都是可行的,在这里面,我喜欢第三种方案。我喜欢多分几个区域,反正是固定不变的,区域分的越细,区块里面的HTML markup就越少。
我们来看nav的结构:
我们看到里面分为nav-left、nav-middle、nav-right,实际上这里的nav-left和nav-right里面是没有内容的,即便是把它们去掉,也不影响。所以我们这里只设置一个区域nav。
我们来看ziye的结构:
我们前期可以先把ziye-left处理成为content区域,也就是主内容区域。我在相当长的时间内,一直以为Drupal7里面,content区域是必须要有的,这在Drupal6下面是正确的,但是在Drupal7下,content区域已经不是必须的,我也是在前两天研究Drupal7主题的时候,才发现的。
我们看到ziye-right下面,分为banner-right、jishu-right2;而jishu-right2里面又包含两个news,其实我觉得,这里面的两个news,应该和banner-right并列才对,不知道美工在这里是怎么想的,我们总不能把banner-right、jishu-right2分成两个独立的区域吧。这里的HTML结构有点不合理的。
我们这里,把中间的部分,分成两个区域,content和ziye-right。后面我们会继续细化、调整的。
现在来看footer,它的HTML结构:
这里面,footer里面包含link、ft、snt;其中link里面包含link-left、link-right;ft里面为空;snt里面又分为snt-left、snt-right。区域的划分,也有多种方案:
1) 分一个区域:footer。
2) 分两个区域:link、snt。
3) 分三个区域:link、snt-left、snt-right。
4) 分四个区域:link-left、link-right、snt-left、snt-right。
这里面,我们采用第3种方案,也就是分三个区域的方案。这样我们普通页面的区域划分,就基本确定了下来,对于首页,我们暂时不去考虑它,因为我们后面会单独的为它制作页面模板。
实际上,对于这种静态HTML结构的分析,是必不可少的,尽管这里占了不小的篇幅。就是在转Drupal主题的前期,我们要充分的了解美工提供的静态HTML。我们现在继续。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们新建一个站点,把这个站点命名为snt1吧,因为我本地已经有了一个snt,是的,这个例子我已经给很多人讲了不止一次了。不过snt1已经用了,所以我们这里就用snt2了。安装的过程在这里就不说了,此外,简体中文也不装了。我们只讲主题制作。
现在导航到snt2\sites\all\themes目录下面,创建一个子文件夹snt,然后将我们的静态页面复制过来。把静态页面复制过来,只是为了方便。如图所示。
接下来,我们创建snt.info文件,实际上,我从来都是复制重命名的。就是将一个现有的主题的info文件复制过来,然后重命名为snt.info。由于前面我们已经初步的确定了区域。我是从bookstore.info复制过来的。
这是我做好的样子:
screenshot = screenshot.png
name = SNT
description = SNT 演示主题.
core = 7.x
stylesheets[all][] = css/css.css
stylesheets[all][] = css/custom.css
;scripts[] = js/script.js
regions[logo] = Logo
regions[logoright] = Logo right
regions[top_right] = Top right
regions[nav] = Navigation
regions[content] = Content
regions[ziye_right] = Ziye right
regions[link] = Link
regions[snt_left] = Snt left
regions[snt_right] = Snt right
; The page_top and page_bottom regions are hidden, which means they will not
; show up on the blocks administration page. But they are required in order for
; the html.tpl.php to work properly, so do not delete them.
regions[page_top] = Page top
regions[page_bottom] = Page bottom
前面的四个键值对,我就不多讲了。
stylesheets[all][]是用来指定CSS文件的,这里指定的CSS文件在当前主题的所有页面都会加载,我们这里只有一个CSS文件css/css.css,这是美工提供的CSS文件;在这里,我们在sites\all\themes\snt\css文件夹下面创建一个custom.css文件,这个文件里面放置我们程序员的CSS代码。这样的好处,就是说,程序员不需要去修改美工的CSS文件,我们在后面覆写主题模板文件的时候,很多时候,是需要调整CSS。将后期的CSS覆写代码,单独放在custom.css文件中,能够给我们带来管理上的方便。不然的话,程序员修改了CSS,美工又调整了自己的CSS,此时会遇到工作的合并问题。
scripts[]用来指定主题加载的JavaScript文件,我们这里前期不需要加载JS文件,只有在制作首页的时候,可能才会加载,所以我们保留了scripts[]这行代码,这里把它注释掉了,将来用的时候会比较方便。
再往下,就是我们设置的区域了,我们在前面分析静态HTML区域的时候,已经分析了要设置哪些区域,所以到这里就水到渠成了。
regions[logo] = Logo
regions[logoright] = Logo right
regions[top_right] = Top right
regions[nav] = Navigation
regions[content] = Content
regions[ziye_right] = Ziye right
regions[link] = Link
regions[snt_left] = Snt left
regions[snt_right] = Snt right
注意这里面,中括号里面,是没有加引号的;中括号里面的是机读名字,除了content以外,这里的机读名字,和静态HTML里面的ID或者Class是保持一致的;有一点需要说明一下,就是这里使用的是下划线,在静态HTML里面使用的是连字符;还有就是这里的机读名字,你想怎么起,就怎么起,只要符合PHP的变量规范就可以了,即便是content区域,也不是必须的,我们也可以将它替换掉,只不过我们这里沿用了传统的方式。右边是区域的用户可读名字,首字母这里大写了。
最后,两行代码:
regions[page_top] = Page top
regions[page_bottom] = Page bottom
这是必不可少的,隐藏的两个区域,在所有的Drupal主题里面都要定义,不过如果你不定义的话,问题可能也不大,自己可以尝试一下。有兴趣的读者,可以打开snt2\modules\system下面的html.tpl.php文件,里面有这么几行代码:
<?php print $page_top; ?>
<?php print $page; ?>
<?php print $page_bottom; ?>
这里的$page_top和$page_bottom两个变量,就是由前面我们定义的两个区域来负责的。
这样我们主题的info文件就创建好了,info文件里面还有很多其它可以使用的键值对,我们这里用不到,所以就不去讲解了。我们现在,在主题文件夹下面,创建一个templates文件夹,里面用来放置我们的模板文件。
接下来,我们将modules\system下面的html.tpl.php文件,复制到templates文件夹下面,如果不复制过来,默认仍然会使用modules\system下面的html.tpl.php,复制过来有个好处,将来覆写这个模板文件的时候方便。
我们来看一下这个模板文件:
/**
* @file
* Default theme implementation to display the basic html structure of a single
* Drupal page.
*
* Variables:
* - $css: An array of CSS files for the current page.
* - $language: (object) The language the site is being displayed in.
* $language->language contains its textual representation.
* $language->dir contains the language direction. It will either be 'ltr' or 'rtl'.
* - $rdf_namespaces: All the RDF namespace prefixes used in the HTML document.
* - $grddl_profile: A GRDDL profile allowing agents to extract the RDF data.
* - $head_title: A modified version of the page title, for use in the TITLE
* tag.
* - $head_title_array: (array) An associative array containing the string parts
* that were used to generate the $head_title variable, already prepared to be
* output as TITLE tag. The key/value pairs may contain one or more of the
* following, depending on conditions:
* - title: The title of the current page, if any.
* - name: The name of the site.
* - slogan: The slogan of the site, if any, and if there is no title.
* - $head: Markup for the HEAD section (including meta tags, keyword tags, and
* so on).
* - $styles: Style tags necessary to import all CSS files for the page.
* - $scripts: Script tags necessary to load the JavaScript files and settings
* for the page.
* - $page_top: Initial markup from any modules that have altered the
* page. This variable should always be output first, before all other dynamic
* content.
* - $page: The rendered page content.
* - $page_bottom: Final closing markup from any modules that have altered the
* page. This variable should always be output last, after all other dynamic
* content.
* - $classes String of classes that can be used to style contextually through
* CSS.
*
* @see template_preprocess()
* @see template_preprocess_html()
* @see template_process()
*
* @ingroup themeable
*/
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN"
"http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php print $language->language; ?>" version="XHTML+RDFa 1.0" dir="<?php print $language->dir; ?>"<?php print $rdf_namespaces; ?>>
<head profile="<?php print $grddl_profile; ?>">
<?php print $head; ?>
<title><?php print $head_title; ?></title>
<?php print $styles; ?>
<?php print $scripts; ?>
</head>
<body class="<?php print $classes; ?>" <?php print $attributes;?>>
<div id="skip-link">
<a href="#main-content" class="element-invisible element-focusable"><?php print t('Skip to main content'); ?></a>
</div>
<?php print $page_top; ?>
<?php print $page; ?>
<?php print $page_bottom; ?>
</body>
</html>
在这个模板文件的上面,是注释,核心自带的模板文件里面,都有注释的,注释里面给出了当前模板文件里面可用的变量;在注释的结尾处,给出了预处理函数、处理函数,我们这里使用的变量都是在这些预处理函数、处理函数里面定义的。
我们看到,html.tpl.php模板文件里面,主要负责head里面变量的输出,里面包含$head、$head_title、$styles、$scripts,后面的两个变量,分别负责样式、脚本的输出,我们在info文件里面定义的stylesheets和scripts就是通过这两个变量输出的。而在body里面,主要是输出$page,其它都是起辅助作用的。
在Drupal6里面,这些变量内容,其实是放在page.tpl.php里面,由于每个页面的这部分内容,基本上是比较固定的,所在Drupal7里面,就将其独立成一个单独的文件了,这样page.tpl.php里面的内容,就少了很多。
template_preprocess和template_process里面定义的变量,在所有的模板文件里面都可以使用。在里面主要定义了下面四个变量:
$variables['zebra'] = ($count[$hook] % 2) ? 'odd' : 'even';
$variables['id'] = $count[$hook]++;
// Tell all templates where they are located.
$variables['directory'] = path_to_theme();
// Initialize html class attribute for the current hook.
$variables['classes_array'] = array(drupal_html_class($hook));
此外,还有一些默认变量是通过_template_preprocess_default_variables定义,这些变量大致包括:
$variables = array(
'attributes_array' => array(),
'title_attributes_array' => array(),
'content_attributes_array' => array(),
'title_prefix' => array(),
'title_suffix' => array(),
'user' => $user,
'db_is_active' => !defined('MAINTENANCE_MODE'),
'is_admin' => FALSE,
'logged_in' => FALSE,
);
$variables['is_front'] = drupal_is_front_page();
在template_process里面,也可以定义变量,不过这些变量,都是直接来源于预处理函书中的,比如:
$variables['classes'] = implode(' ', $variables['classes_array']);
处理函数中的classes变量,就是直接根源于预处理函数中的classes_array,只不过将它从数组的形式转成了字符串。
template_preprocess_html定义了一些变量:
$variables['classes_array'][] = $variables['is_front'] ? 'front' : 'not-front';
$variables['rdf_namespaces'] = drupal_get_rdf_namespaces();
$variables['grddl_profile'] = 'http://www.w3.org/1999/xhtml/vocab';
$variables['language'] = $GLOBALS['language'];
$variables['language']->dir = $GLOBALS['language']->direction ? 'rtl' : 'ltr';
$variables['head_title_array'] = $head_title;
$variables['head_title'] = implode(' | ', $head_title);
$variables['theme_hook_suggestions'] = $suggestions;
我们注意到,好几个变量都没有在这里定义,此时我们可以尝试Google一下template_process_html,在api.drupal.org找到这个函数的定义,里面就有我们在模板里面用到的变量:
// Render page_top and page_bottom into top level variables.
$variables['page_top'] = drupal_render($variables['page']['page_top']);
$variables['page_bottom'] = drupal_render($variables['page']['page_bottom']);
// Place the rendered HTML for the page body into a top level variable.
$variables['page'] = $variables['page']['#children'];
$variables['page_bottom'] .= drupal_get_js('footer');
$variables['head'] = drupal_get_html_head();
$variables['css'] = drupal_add_css();
$variables['styles'] = drupal_get_css();
$variables['scripts'] = drupal_get_js();
我们在这里,只需要知道变量是在预处理函数、处理函数里面定义的即可,用到的时候再去查找相应的函数。还有一个问题,Drupal是怎么把模板文件和对应的变量合并成HTML片段的,经常有人问这个问题,这时通过theme_render_template函数完成的,我们在第4集的时候,简单的介绍过这个函数。
我们在html.tpl.php里面使用这么两句话输出CSS、JS文件:
<?php print $styles; ?>
<?php print $scripts; ?>
在实际的项目中,我们还经常这样用:
<?php print $styles; ?>
<link href="<?php print base_path().path_to_theme(); ?>/css/mycss.css" rel="stylesheet" type="text/css" />
<?php print $scripts; ?>
<script src="<?php print base_path().path_to_theme(); ?>/js/jquery-1.4a2.min.js" type="text/javascript"></script>
我们直接在模板文件里面输出了CSS、JS文件,而不是通过主题的info文件,这不是什么Drupal高手推荐的方式,但是却是在实际项目当中,用的最多的一种方式。另外,就是这里的base_path().path_to_theme(),这两个函数,我们在后面还会用到。我们这里也可以使用$base_path.$directory,效果是一样的,不过我更喜欢直接使用函数的形式。注意,变量的形式,只能用于模板文件中,而函数的形式,适用的范围更广泛一些。
接下来,我们将modules\system下面的page.tpl.php文件,复制到sites\all\themes\snt\templates文件夹下面。我建议每个学习Drupal的用户,都把这个默认模板文件看上几遍。
<?php
/**
* @file
* Default theme implementation to display a single Drupal page.
*
* The doctype, html, head and body tags are not in this template. Instead they
* can be found in the html.tpl.php template in this directory.
*
* Available variables:
*
* General utility variables:
* - $base_path: The base URL path of the Drupal installation. At the very
* least, this will always default to /.
* - $directory: The directory the template is located in, e.g. modules/system
* or themes/bartik.
* - $is_front: TRUE if the current page is the front page.
* - $logged_in: TRUE if the user is registered and signed in.
* - $is_admin: TRUE if the user has permission to access administration pages.
*
* Site identity:
* - $front_page: The URL of the front page. Use this instead of $base_path,
* when linking to the front page. This includes the language domain or
* prefix.
* - $logo: The path to the logo image, as defined in theme configuration.
* - $site_name: The name of the site, empty when display has been disabled
* in theme settings.
* - $site_slogan: The slogan of the site, empty when display has been disabled
* in theme settings.
*
* Navigation:
* - $main_menu (array): An array containing the Main menu links for the
* site, if they have been configured.
* - $secondary_menu (array): An array containing the Secondary menu links for
* the site, if they have been configured.
* - $breadcrumb: The breadcrumb trail for the current page.
*
* Page content (in order of occurrence in the default page.tpl.php):
* - $title_prefix (array): An array containing additional output populated by
* modules, intended to be displayed in front of the main title tag that
* appears in the template.
* - $title: The page title, for use in the actual HTML content.
* - $title_suffix (array): An array containing additional output populated by
* modules, intended to be displayed after the main title tag that appears in
* the template.
* - $messages: HTML for status and error messages. Should be displayed
* prominently.
* - $tabs (array): Tabs linking to any sub-pages beneath the current page
* (e.g., the view and edit tabs when displaying a node).
* - $action_links (array): Actions local to the page, such as 'Add menu' on the
* menu administration interface.
* - $feed_icons: A string of all feed icons for the current page.
* - $node: The node object, if there is an automatically-loaded node
* associated with the page, and the node ID is the second argument
* in the page's path (e.g. node/12345 and node/12345/revisions, but not
* comment/reply/12345).
*
* Regions:
* - $page['help']: Dynamic help text, mostly for admin pages.
* - $page['highlighted']: Items for the highlighted content region.
* - $page['content']: The main content of the current page.
* - $page['sidebar_first']: Items for the first sidebar.
* - $page['sidebar_second']: Items for the second sidebar.
* - $page['header']: Items for the header region.
* - $page['footer']: Items for the footer region.
*
* @see template_preprocess()
* @see template_preprocess_page()
* @see template_process()
* @see html.tpl.php
*
* @ingroup themeable
*/
?>
<div id="page-wrapper"><div id="page">
<div id="header"><div class="section clearfix">
<?php if ($logo): ?>
<a href="<?php print $front_page; ?>" title="<?php print t('Home'); ?>" rel="home" id="logo">
<img src="<?php print $logo; ?>" alt="<?php print t('Home'); ?>" />
</a>
<?php endif; ?>
<?php if ($site_name || $site_slogan): ?>
<div id="name-and-slogan">
<?php if ($site_name): ?>
<?php if ($title): ?>
<div id="site-name"><strong>
<a href="<?php print $front_page; ?>" title="<?php print t('Home'); ?>" rel="home"><span><?php print $site_name; ?></span></a>
</strong></div>
<?php else: /* Use h1 when the content title is empty */ ?>
<h1 id="site-name">
<a href="<?php print $front_page; ?>" title="<?php print t('Home'); ?>" rel="home"><span><?php print $site_name; ?></span></a>
</h1>
<?php endif; ?>
<?php endif; ?>
<?php if ($site_slogan): ?>
<div id="site-slogan"><?php print $site_slogan; ?></div>
<?php endif; ?>
</div> <!-- /#name-and-slogan -->
<?php endif; ?>
<?php print render($page['header']); ?>
</div></div> <!-- /.section, /#header -->
<?php if ($main_menu || $secondary_menu): ?>
<div id="navigation"><div class="section">
<?php print theme('links__system_main_menu', array('links' => $main_menu, 'attributes' => array('id' => 'main-menu', 'class' => array('links', 'inline', 'clearfix')), 'heading' => t('Main menu'))); ?>
<?php print theme('links__system_secondary_menu', array('links' => $secondary_menu, 'attributes' => array('id' => 'secondary-menu', 'class' => array('links', 'inline', 'clearfix')), 'heading' => t('Secondary menu'))); ?>
</div></div> <!-- /.section, /#navigation -->
<?php endif; ?>
<?php if ($breadcrumb): ?>
<div id="breadcrumb"><?php print $breadcrumb; ?></div>
<?php endif; ?>
<?php print $messages; ?>
<div id="main-wrapper"><div id="main" class="clearfix">
<div id="content" class="column"><div class="section">
<?php if ($page['highlighted']): ?><div id="highlighted"><?php print render($page['highlighted']); ?></div><?php endif; ?>
<a id="main-content"></a>
<?php print render($title_prefix); ?>
<?php if ($title): ?><h1 class="title" id="page-title"><?php print $title; ?></h1><?php endif; ?>
<?php print render($title_suffix); ?>
<?php if ($tabs): ?><div class="tabs"><?php print render($tabs); ?></div><?php endif; ?>
<?php print render($page['help']); ?>
<?php if ($action_links): ?><ul class="action-links"><?php print render($action_links); ?></ul><?php endif; ?>
<?php print render($page['content']); ?>
<?php print $feed_icons; ?>
</div></div> <!-- /.section, /#content -->
<?php if ($page['sidebar_first']): ?>
<div id="sidebar-first" class="column sidebar"><div class="section">
<?php print render($page['sidebar_first']); ?>
</div></div> <!-- /.section, /#sidebar-first -->
<?php endif; ?>
<?php if ($page['sidebar_second']): ?>
<div id="sidebar-second" class="column sidebar"><div class="section">
<?php print render($page['sidebar_second']); ?>
</div></div> <!-- /.section, /#sidebar-second -->
<?php endif; ?>
</div></div> <!-- /#main, /#main-wrapper -->
<div id="footer"><div class="section">
<?php print render($page['footer']); ?>
</div></div> <!-- /.section, /#footer -->
</div></div> <!-- /#page, /#page-wrapper -->
这仍然是HTML里面嵌套了对应的变量,这里面的变量很多,但是实际上,最常用的是区域变量,它们的调用方式都是一样的:
<?php print render($page['footer']); ?>
像page.tpl.php里面特有的变量,比如$logo、$breadcrumb,以及主导航链接,二级导航链接的输出,正在被区块的形式所取代。统一采用区块的形式,学习起来更易于掌握。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
现在我们要做的工作,是将静态HTML页面body里面的代码全部复制到page.tpl.php文件里面来,这里的HTML是从login.html里面复制过来的。我通常将这些静态的HTML放到注释的后面,原来代码的前面。我们要将里面的静态HTML,替换成对应的变量了。
这是没有替换前的样子:
我们里面的代码折叠了起来。
我们现在开始替换,首先看logo,原来是这样的:
<div class="logo"><span>SNT</span>代理</div>
我们看到logo是文字形式的,我们这里把它处理成为区域的形式了,这是替换后的样子:
<div class="logo"><?php print render($page['logo']); ?></div>
我们依次这样替换,这是第一轮替换后的样子;
我总是把主内容区域的输出,放在最后才替换。先易后难。我们现在来看主内容这块,首先是面包屑导航。
<p class="daohang"><a href="#">首页</a> > <a href="#">业务支持</a></p>
我们将其替换为:
<?php if ($breadcrumb): ?>
<p class="daohang"><?php print $breadcrumb; ?></p>
<?php endif; ?>
Drupal里面,常使用这样的if语句,来输出变量,这样在变量为空的时候,相关的HTML也就可以不输出了。我们来看一下核心的代码:
这里面输出了变量$messages,我们这里也把这个变量输出来,然后放到面包屑代码片段的下面。很多人做项目的时候,经常把这个给忘记了。
接下来,我们将这段HTML删除:
这就是主内容区域所在的位置。然后在同样的位置,将这段代码复制粘贴进来:
<?php if ($page['highlighted']): ?><div id="highlighted"><?php print render($page['highlighted']); ?></div><?php endif; ?>
<a id="main-content"></a>
<?php print render($title_prefix); ?>
<?php if ($title): ?><h1 class="title" id="page-title"><?php print $title; ?></h1><?php endif; ?>
<?php print render($title_suffix); ?>
<?php if ($tabs): ?><div class="tabs"><?php print render($tabs); ?></div><?php endif; ?>
<?php print render($page['help']); ?>
<?php if ($action_links): ?><ul class="action-links"><?php print render($action_links); ?></ul><?php endif; ?>
<?php print render($page['content']); ?>
<?php print $feed_icons; ?>
如果你对这些变量不熟悉的话,没有关系,我建议开始的时候把它们都输出出来,当我们发现信息显示的多了的时候,再删除相应的变量。$title是负责输出标题的;$title_prefix和$title_suffix是标题的前缀和后缀;$tabs用来输出标签,我们平时看到的查看、编辑标签,就是通过这个变量输出的;$action_links是动作链接,比如管理内容页面的添加内容链接,就是通过这个变量输出的;'highlighted'和'help'是两个辅助性质的区域,核心模板文件里面带的,我建议,我们这里把这两个区域也加上;$feed_icons是用来输出RSS订阅链接的。
我们现在,就在snt.info文件里面把'highlighted'和'help'这两个区域给加上:
regions[highlighted] = Highlighted
regions[help] = Help
到此为止,我们的这个page.tpl.php基本上就替换完了。现在我们可以把这个模板文件中的原来复制过来的代码给删除了,不然内容会重复的显示。删除完毕,保存page.tpl.php。
讲到page.tpl.php的时候,有人问过这样的问题,render的用法,以及与drupal_render之间的区别。其实我们可以看一下render的源代码,比较简单:
function render(&$element) {
if (is_array($element)) {
show($element);
return drupal_render($element);
}
else {
// Safe-guard for inappropriate use of render() on flat variables: return
// the variable as-is.
return $element;
}
}
如果是数组的话,就是用drupal_render输出,否则直接返回。在Drupal6的时候,对于页面回调,我们通常这样写代码:
$output = '';
$output .= '123456';
return $output;
但是在Drupal7下,标准的格式是这样的:
$render_array = array();
$render_array['#markup'] = '123456';
return $render_array;
返回的是数组的形式,不过在Drupal7下,也可以返回字符串的形式,这是对Drupal6原有用法的一个兼容,render函数其实就是负责这一兼容工作的,就是说,在返回字符串的情况下,也能正常工作。
现在我们就可以启用这个新作的主题了,导航到admin/appearance,在这里找到SNT主题:
我们将它启用并设置为默认主题。现在我们回到首页,样式是比较乱的:
我们添加一篇文章“联系我们”,我们看到的样子:
我们现在做一件事情,就是把头部、尾部、还有导航,还有右边栏的内容,把它们以静态区块的形式添加进来,这是我做Drupal主题的时候,采用的办法,就是说,先把样子显示出来,不至于在开始阶段,样子太乱,没有内容。
我们导航到admin/structure/block,在这里找到添加区块链接:
点击这个链接,进入区块的添加页面,这里面我们输入以下内容:
此外,对于文本格式,我们选择Full HTML:
对区域设置,我们将它放在Logo区域:
保存这个区块。这个时候,我们看到Logo区域里面还有很多其它区块,我们把这些无关的区块都去掉。我们添加了9个静态区块,里面的内容,就是我们在替换page.tpl.php时,替换掉的HTML。不要怕麻烦,以前我做Drupal主题的时候,都是这样,先放静态的,除了立即能够看到效果以外,还有一个好处,就是能够给我们确定一个目标,我们在后面,可以对这些静态区块逐一的改造升级。这是添加完的样子:
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
现在,我们来实现多语言,现在的默认语言为简体中文,我们还需要支持英文。实际上,原来默认的就是英文,只不过我们将它改为了简体中文。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
界面的多语言化
我们来回顾一下一个简单的问题,如何添加一个语言?这个比较简单,我们在添加简单中文的时候,就已经做过了。首先是启用Locale模块,接着导航到语言的管理界面,在这里点击添加语言链接。
在我们添加语言的时候,除了预定义好的语言以外,我们还可以自定义语言,实际上,在实际项目中,从来没有用过这个自定义语言。
点击右上角的“检测与选择”,进入页面admin/config/regional/language/configure,我们在这里可以配置,按照那种方式选择界面语言。
这里的检测方法包括:网址、会话、用户、浏览器、默认。我们这里使用的是默认。对于网址,它是有配置选项的:
会话的配置选项:
如果同时支持多个语言的话,我们通常都采用网址(路径前缀)的探测方法。我们后面会用到这个。
启用了简体中文以后,如何导入简体中文语言包?我们知道,Drupal的界面翻译,采用的是PO文件的格式,我们需要下载这些语言包,并将它们导入到Drupal中来。Drupal的翻译管理界面,位于admin/config/regional/translate。
在这里可以查看每个语言的翻译状态,我们看到简体中文的翻译率不足60%。
后台的翻译界面:
可以对界面语言进行翻译,在上面可以通过查找过滤功能,找到对应的字符串。找到以后,如果已经有了翻译,觉得翻译的不好的话,可以对它进行编辑,替换成我们自己想要的;如果尚未翻译的话,那么我们可以添加自己的翻译。
PO文件的导入界面:
可以导入PO文件,在导入的时候,需要选择语言,还需要选择导入的模式,这里有两种模式,一种是将原来的翻译覆盖掉,另一种是保持原有翻译不变,只添加新字符串,我们通常选择后者即可。
将本地的翻译,导出成PO文件,这个功能是有的,但是实际项目中很少用到。语言包可以在https://localize.drupal.org/translate下载到,简体中文的语言包位于https://localize.drupal.org/translate/languages/zh-hans。需要说明一下的是,简体中文小组的汉化工作比较混乱,我去年十月份汉化的Ubercart,到现在都没有人审批。
这段话写的很好,期待简体中文小组的汉化工作,在将来会得到改善。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
一个Drupal网站,通常会安装比较多的第3方模块,一个模块一个模块的导入汉化包,也是一件很麻烦的事情。很多时候,我都是这样的,只导入Drupal核心的汉化包,我很少导入第三方模块的。因为网站主要看的前台嘛。如果你对后台的界面的汉化,有更多的要求,而且还想自动化,此时有这样的一个模块,可以帮我们做这件事情。Localization update模块。
我们下载、安装、启用Localization update模块,我这里的版本是l10n_update-7.x-1.0-beta3。这个模块的作用,就是自动的帮我们下载这些语言包。模块启用后,直接导入该模块本身的语言包:
导入成功后:
在翻译的管理界面,多了一个“更新”标签:
点击这个标签,进入页面admin/config/regional/translate/update:
这里的状态大部分都是“Uninstalled translation available”,这说明了很多Drupal的第三方模块都没有最新的汉化包可用。这里以CKEditor模块为例,展开后:
右下角有一个下载链接,点击这个链接,就可以下载这个模块的PO文件。打开下载好的PO文件,发现里面的文本都已经翻译了。PO文件里面的内容大致如下:
在这个页面的下面,是一个更新模式选项和两个按钮:
点击这个更新翻译按钮:
我刚才还犯愁呢,没有想到点了这个按钮就自动导入了。这是我第一次使用这个模块。我能够手动做的,很少去装模块。这是更新成功后的样子:
汉化就这么简单。不过,美中不足:
不是100%的汉化了,只汉化了78%。不过很多都已经翻译了,翻译的还不错:
在语言的管理界面,多了一个“更新翻译”标签:
点击这个标签,进入页面admin/config/regional/language/update,这里是Localization update模块的配置选项:
这里面的简体中文,是从繁体中文中复制过来的,都没有转换一下,人真懒。如果我们在“保存下载的文件到”里面输入“sites/all/translations”,那么我们从服务器上下载下来的PO文件就会保存在这个目录下,默认为空时,不会保存这些文件的。
与界面翻译相关的还有Localization client模块,使用它可以在当前页面就能够完成翻译,并且能够将本地的翻译,上传到localize.drupal.org。
我们下载、安装、启用Localization client模块,我这里用的版本是l10n_client-7.x-1.2。启用后,在每个页面的下面,会有这个一个链接:
点击后,会展开更多内容:
左边是该页面上的所有界面文本,绿色的表示已经翻译好的,白色的表示尚未翻译的。双击一段白色的字符串,它就会在中间的“源”里面显示出来,比如这里的“Configure menu block.”,在右边的文本域中输入它的翻译,保存,就完成了翻译。这个比核心自带的翻译功能强大很多,方便很多。
如果我们在本地做了很多汉化工作,想将这些工作分享出来,学一下雷锋。在语言的管理界面,admin/config/regional/language,右边多了一个“分享”标签。
点击,进入页面,这是我的配置:
这并不意味着我们本地的汉化工作,可以自动的上传到服务器上了,我们需要进一的配置。在用户的编辑页面,有这么一个配置选项:
我们需要输入API密钥,点击右下角的链接https://localize.drupal.org/translate/remote/userkey/c060c2fd8de97f4b620b808390d89f0a, 如果你拥有drupal.org账号,并且已经登录的话,此时就会为你生成一个密钥:
将这个密钥复制过来,并保存。当我们在本地保存一个翻译时,系统就会尝试将这个翻译提交到服务器上:
遗憾的是,没有提交上,原因很可能是这个已经有人提交了,但是还没有被批准。又尝试了一下,提交成功:
我把Menu position模块的部分界面文本翻译了一下,并上传到了服务器上。如果所有的中国Drupal开发者,每人贡献10条翻译的话,汉化工作就会比较出色了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
问题出在了哪里?这些工作对于我们来说,费力不讨好,所以没有人愿意去做,即便是有人去做多语言网站,开发者关注的重点,也不是界面的翻译。重点在哪里?当然是内容的翻译。
Drupal核心自带了一个Content translation的模块,能够翻译节点;再加上i18n模块,这是Drupal多语言网站建设里面的一个标准解决方案。在Drupal6里面,这是标准方案,在Drupal7里面,也可以说是一个标准方案。不过在Drupal7里面,出现了另一种选择,这就是Entity translation模块,后者进入了Drupal8的内核,并在Drupal8取代了前者。所以,我们在搭建Drupal7下的多语言网站的时候,有两种选择。我们这里将会选用Entity translation模块和i18n模块做我们我们的解决方案。
Content translation的基本架构图
Entity translation的基本架构图
从上面的两个示意图中可以看出,Content translation是基于节点的,它为每种语言创建一个节点,并且维护不同语言节点之间的对应关系。Entity translation是基于字段的,在同一个节点内,同一字段在不同语言下,分别创建一个版本。
我们下载、安装、启用Entity translation模块,我这里使用的版本是entity_translation-7.x-1.0-beta3。我们知道,Drupal7里面,标题不是一个字段,而是一个属性,为了实现标题的翻译,我们还需要安装Title模块。我这里使用的版本是title-7.x-1.0-alpha7。
启用后,我们可以导航到Entity translation模块的配置界面,admin/config/regional/entity_translation。这里有三个配置选项;
一个是启用语言回退,一个是显示共享标签,最后一个是启用翻译工作流权限。我们这里采用默认的配置即可。
再往下是配置哪些实体类型可被翻译:
默认选择了节点,我们这里把分类术语也选中。我想知道Tittle模块是否支持分类术语的标签。选中分类保存后,下面多出来了一些与分类相关的配置选项。
展开后:
我们这里采用默认的即可。
除了这里的配置以外,在admin/config/regional/language/configure,语言的检测与选择页面,此时多出来了“内容语言检测”的配置选项。
在这里,我们启用网址、浏览器两种方法。对于网址的配置,我们采用路径前缀的方式即可。
我们回到内容类型的管理界面,编辑“关于我们”内容类型,进入页面zh-hans/admin/structure/types/manage/about-us。这个时候路径前缀已经显示出来了。在“发布选项”里面,有这么一个配置选项:
我们这里选中“Enabled, with field translation”,就是让这个内容类型支持多语言,通过字段翻译的方式。选中后,下面会显示出来进一步的配置选项:
隐藏内容翻译链接,这个我们采用默认的不选中就可以了。保存这里的配置。现在访问Entity translation的配置界面,我们可以看到“关于我们”内容类型的配置选项了,和前面分类术语的一样,只是默认值不同。
下面有四个配置选项,隐藏语言选择器,从可用语言中排除语言中立,一旦实体创建后就阻止修改语言属性,在翻译表单中隐藏共享元素。我们还是采用默认值。上面的默认语言,是这个节点类型的默认语言,可以与站点的默认语言不同。
现在,我们去编辑节点1,访问页面node/1/edit,此时在节点的编辑表单中,我们可以看到语言的下拉选择框:
这里有三个语言选项:
在右上角的标签中,多了一个“翻译”:
我们点击这个标签,进入页面node/1/translate:
这里显示的是“No translatable fields”,没有可翻译的字段,我想可能是这样的,原来我们保存节点的时候,字段的语言都是采用und的形式存储的,我们这里对原始内容编辑一下,保存,此时会有语言属性的。
我们导航到“关于我们”内容类型的管理字段页面,admin/structure/types/manage/about-us/fields:
Title的右边,有一个replace链接,点击这个链接,进入页面:
我们选中这个复选框,并点击保存设置按钮。这样标题就会被一个字段取代:
我们编辑这个字段,在字段实例的配置页面,我们看到这个配置选项:
这是这个字段的翻译设置,当前这个字段是可被翻译的,下面有一个“Disable translation”链接,可以用来禁用这个字段的翻译。
此时我们终于有了眉目,编辑Body字段,我们可以看到:
点击下面的“Enable translation”(启用翻译)链接。点击后,会看到这个配置选项:
我们采用默认的选中即可。点击这个页面的确认按钮。这样我们就为正文字段启用了翻译功能。
现在回到页面node/1/translate。
英文右边的操作,现在变为了“添加”,我们点击这个链接,进入页面en/node/1/edit/add/zh-hans/en。现在这里面是有内容的:
还是原来的,我们需要把它们替换为英文。
再往下是:
这个下拉选择框是被禁用了的,无法修改,原始语言为简体中文,我们这里实际情况下也不需要修改这个。
再往下是节点的配置选项:
很多配置选项后面都多了一个“(all languages)”,表明里面的配置是作用所有语言的。不过这里面有两个不是,其中一个是URL路径设置,这意味着我们可以为同一节点,在不同语言下设置不同的别名。
对于“翻译”,展开后,里面包含四个配置选项:
我们这里采用默认的即可。我们使用Google翻译,将标题和正文翻译一下,然后输入进来。
这是保存后的样子:
在正文的下面,有一个语言切换链接:
点击后,可以方便的切换到简体中文。在简体中文的版本下,有同样一个链接,可以用来切换到英文。
前面我们提到可以为单个语言分别设置路径别名,我们在保存英文翻译的时候,没有为node/1设置别名,此时英文的路径为en/node/1,并没有为它应用别名aboutus。我们还需要单独为它设置一下,不然出不来。
为其余的内容类型,重复前面的操作,直到所有内容类型都可被正确的翻译。
第一个问题是首页的产品区块的标题,在英文下没有正常显示:
这个原因是,我们默认使用的是Title,现在变成字段title_field了,我们在Views里面,需要添加字段title_field,来代替原来的。修改后,显示正常。
新闻标题显示不正常:
这个也是同样的问题。在Views里面,使用title_field替换原来的标题即可。需要注意的是,添加的时候,两个的名字相同。如果一次选不对的话,可以选两次,多试一下。
“Content: 标题 (title_field:language)”上面的这两个就是。
首页的“联系我们”区块的标题显示不正常:
它和左边的新闻区块,使用的是Mini panel,我们导航到admin/structure/mini-panels,对它进行编辑,
点击配置按钮,
在弹出的链接中点击设置链接:
原来我们这里覆写了标题。取消对复选框“覆写标题”的选中。保存。不过这样还不行。我最终这样解决的,将这个区块添加了两次。
第一个:
并将它限制在英文环境下显示:
第二个:
并将它限制在中文环境下显示:
现在,首页只有幻灯和企业愿景没有翻译。对于幻灯,我们使用的是节点类型,只需要将对应节点翻译一下即可。不过都是图片,我们这里就不翻译了。企业愿景,是一个自定义窗格内容,它本身不是一个区块。我们的解决办法,是创建两个企业愿景,一个用于中文,一个用于英文。下面截了几张图,第一个是内容窗格的配置链接:
这是点击了添加可见性规则后的配置表单,这里我选择了“用户:语言”:
将窗格的显示,限制在英文环境下:
保存后,显示正常了:
我将首页幻灯的第一张图片翻译了一下,上传了一张带有“English version”字样的,翻译后,英文版的图片没有显示出来,检查了一下幻灯视图,在设置中的“高级〉其它”部分:
上面的字段语言配置,默认是没有问题的。原因是这里我启用了缓存,去掉缓存,显示正常了。
这证明了幻灯也是可以翻译的。
前面,我们翻译联系我们的时候,这个节点上面的Webform表单还没有翻译。主要是表单元素的标签。不过只有几个,我们可以采用前面所使用的jQuery的方式,搞定这个问题。不过我觉得不是很好。搜索Webform + i18n,还真有现成的模块可用,这就是Webform Localization。
Webform Localization有两个版本,一个1.x,一个4.x,我们使用1.x的开发版。下载、安装、并启用。启用后,我首先来到“联系我们”的编辑页面,然后访问node/4/webform/,这里检查一下,看有没有与翻译相关的配置,还真被我找到了,在node/4/webform/configure,
Webform表单的多语言化,有两种方式,一种是使用一个表单,将表单元素的属性作为i18n字符串进行翻译;另一种是为每种语言创建一个表单,然后保持多个表单之间的同步。我们采用第一种方式:
其实,第二个复选框,是用于Content translation的。不过我们这里都选中了。现在,导航到admin/config/regional/translate/translate,这里我们将字符串限制在:
点击过滤按钮:
我们可以对这些字符串进行翻译了。翻译后,联系我们表单,在英文下正常:
当我们显示一个新闻节点的时候,新闻分类字段的标签,显示还是中文的形式,这个我们也可以在admin/config/regional/translate/translate对其进行翻译,首先将字符串限制在“字段”上,过滤:
此时会搜索到很多字段的标签:
我们对它们分别翻译一下。就解决了这个问题。
如果,你想将语言切换器改成下拉选择框的形式,可以安装Language Switcher Dropdown模块;如果想加上语言图标的话,可以安装Language icons。与多语言相关的模块,还有很多。我们这里就不逐一介绍了。这是安装Language icons模块后的效果:
整个网站,基本功能已经搭建完毕,不过还有很多需要完善的,与多语言相关的部分,我们暂时讲解到这里,希望大家读完以后,对于多语言网站的搭建过程,有个比较好的了解。而我们的Think in Drupal第5集,到这里也就结束了,感谢大家的支持。
最后加一句,我想说的是,前面通过jQuery解决的那个问题,后来我回去又想了想,终于想到新闻列表页面和产品列表页面的不同了。原来我为products页面添加了一个菜单位置规则(menu position rule),只不过这个规则后来被禁用了。我们为新闻添加一个同样的规则,并禁用,然后注释掉前面的JS代码。面包屑显示仍然正常。
发现页数不够272页,想了一下,这个模块还没有介绍,我们这里简单介绍一下。这是用来做翻译管理工具的模块,我这里使用的版本是tmgmt-7.x-1.0-beta1。这个模块依赖于entity模块,我们这里使用entity-7.x-1.1。这个模块的目的,是用来实现翻译自动化的,我们去翻译一个节点的时候,里面的内容能够预先的翻译好,比如使用Google翻译,这样我们就不用去Google翻译的页面复制粘贴了。不过令人遗憾的是,TMGMT Translator Google所依赖的Google翻译服务是收费的,没有免费版。还好,微软的提供免费的,我们拿微软的作为例子。我们使用TMGMT Translator Microsoft模块,这里使用的版本是tmgmt_microsoft-7.x-1.0-alpha2。为了启用子模块Translation Management UI,我们还需要安装VBO、Rules模块。我启用了这些模块:Entity Source、Entity Source User Interface、Microsoft Translator、Translation Management Core、Translation Management Field、Translation Management UI,以及它们所依赖的模块。
它的配置界面位于admin/config/regional/tmgmt_settings,包含两个配置选项:
流程设定,和性能设置。我们这里采用默认的即可。
另外一个配置页面位于/admin/config/regional/tmgmt_translator,这是翻译器设置:
这里只有一个翻译器,就是微软的这个。还是微软大方,提供免费的给我们用。我们对它进行编辑,在编辑页面,有微软翻译器的特有配置:
这里有一篇文档,http://blogs.msdn.com/b/translation/p/gettingstarted1.aspx,介绍了如何获取这些信息。
1, 我们需要在https://datamarket.azure.com上面注册一个账号。
2, 然后订阅微软的翻译器API,我这里定的是免费版的,定购的地址为:https://datamarket.azure.com/dataset/bing/microsofttranslator
3, 获取开发者密钥信息,访问https://datamarket.azure.com/,在我的账号里面,可以获取到密钥信息:
我们将这里获取的客户ID和密钥,填到Drupal里面对应的配置中来。
我们现在来创建一个静态页面节点:
我们对它进行翻译:
刚才在这里,我点击了添加链接,发现里面显示的还是中文。仔细看了一下这个页面,左边比以前多了一个复选框,下面好像多了一个“Request translation”按钮,我们选中“英文”,点击这个“请求翻译”按钮。
自动翻译好了,需要我们的审核。点击这里的“Needs review”链接,进入页面admin/tmgmt/items/1?destination=node/22/translate。我们在这里看到了:
对于这里的翻译,我们可以进一步的编辑。如果满意了,那么可以点击“Save as completed”(保存为已完成)。我们这里点击这个按钮。
这个节点的英文版已经翻译好了。省事吧。我们不用再把节点标题、正文,复制过去,翻译好了以后再复制回来,然后再保存了。只需要轻轻的点击几下按钮,就能自动完成翻译。
我们现在来看分类术语的翻译,导航到分类的管理界面,找到“新闻分类”词汇表,admin/structure/taxonomy/news_category,编辑它下面的第一个分类术语“公司新闻”:
我们看到,左上角有一个“翻译”标签,点击这个标签,进入页面taxonomy/term/4/translate。
这里英文版本的操作,仍然是“无可翻译的字段”。和前面遇到的情况一样。我们现在进入“新闻分类”词汇表的管理字段页面:
我们发现,“名称”和“描述”的右边都有一个“替换”链接,也就是我们可以把它替换成字段的形式。我们替换一下,这是替换后的样子:
注意,这里字段的机读名字分别为name_field和description_field,前面的节点标题替换后的为title_field。看来Title模块的功能,不仅仅支持节点标题,还支持分类术语的名称和描述。这真是一个不小的惊喜。
现在,对于“公司新闻”这个分类术语,我们可以为它添加英文翻译了:
点击这里的添加链接,在对应的名称、描述里面输入英文版本。我们这里只输入名称的英文版本即可:
我们重复这一操作,将新闻分类下面其余两个分类术语也翻译一下。将产品分类下面的分类术语也翻译一下。
不过令人遗憾的是,当我们创建新闻的英文版本时,新闻分类里面的分类术语,显示的还是简体中文:
保存后,才会显示英文的形式:
不过这里,字段的标签,没有被翻译过来。尽管有这么一个小问题,但是在显示的时候,没有问题,所以我觉得,分类术语的这种翻译方式,还是比较不错的。I18N模块里面,也有分类术语的子模块,如果你觉得这里的这种方式,对你来说有点问题的话,可以尝试一下I18N模块下面的分类术语翻译。
我们安装Title模块的时候,忘记说了,这个模块是有一个配置页面的,
就是创建一个bundle的时候,能够将实体的标题(名称)自动替换成字段的形式。对我们的影响不大。我们可以手动的完成。至于Label替换,这个到底怎么起作用的,我也没有弄清楚。我把它们选中以后,在英文版本下面,新闻分类显示的还是中文。
我们导航到区块的管理界面,找到“语言切换器(内容)”区块,将它放在主题的“标题”区域中,同时禁用区块标题。默认是这样显示的:
我们调整一下CSS:
.region-header{
float:right;
clear:none;
}
.region-header #block-locale-language-content .language-switcher-locale-url li{
display:block;
float:left;
margin-right:20px;
}
.region-header a {
text-decoration: none;
}
这是调整后的样子:
值得一提的是,如果把新闻翻译好了以后,在新闻列表页面的英文版本下,显示也是基本正常的,这是en/news/4路经下的样子:
除了节点和分类术语以外,我们还有很多工作要做,比如菜单、区块、系统变量,字段的标签,等等,需要翻译。
我们下载、安装、启用i18n模块,我这里使用的版本是i18n-7.x-1.9。这个模块还依赖于Variable模块,我们还需要先安装Variable模块,我这里使用的版本是variable-7.x-2.2。 i18n模块下面包含很多个子模块,从上到下:
我这里启用了Block languages、Field translation、Internationalization,这里的Contact translation是负责Contact这个核心模块的翻译的,不过我们这个站点没有用到这个模块。
这里,我启用了Menu translation模块;Multilingual content这个是基于content translation的,我们这里没有采用这种方案;Multilingual forum解决的是论坛模块的翻译,我们这里没有使用论坛模块;Multilingual select,这个可能有用,后面我们不妨启用测试一下,看是否能够解决我们前面所说的分类术语选择时的问题。
这里,启用了String translation;Synchronize translations是基于content translation的,我们这里没有采用这种方案;Taxonomy translation,我们已经解决了分类术语的翻译,如果你觉得我们前面的方式不够好的话,可以尝试一下这个模块;Path translation,用于翻译路经,有时候有用。
我们这里启用了Translation sets、Variable translation,前者为多个子模块所依赖,后者用于变量的翻译,很有用;Translation redirect主要用于SEO,我们这里对SEO的要求不高;User mail translation用于用户电子邮件的翻译,我们这个站点用处不大。
首先,我们来看站点名称的翻译,我们站点的名称为“亚艾元”,在英文环境下,我们想显示拼音的形式。怎么翻译呢?导航到admin/config/system/site-information,这是站点名称所在的配置界面:
在这个页面的上面有提示,这个表单中包含多语言的变量,我们将它切换到英文的环境下,admin/config/system/site-information?variable_realm_key_language=en,在这种情况下,输入拼音,保存:
此时,我又遇到了一个问题,那就是虽然我这里输入了变量的英文版本,但是在首页的中英文切换的过程中,站点名称始终是中文。我尝试了比较多的办法,比如分别使用路经:zh-hans/admin/config/system/site-information,和en/admin/config/system/site-information访问,然后输入对应的中文、英文版本,还是不行。
我Google 了一下,找到一篇文档,https://drupal.org/node/1113374,这里讲的就是变量翻译,我的配置没有任何问题。通过这篇文章,我了解到admin/config/regional/i18n的存在,这是i18n的配置界面,变量的多语言设置可以在admin/config/regional/i18n/variable完成。
然后我想到了在预处理函数中,搞定这个问题,在当前主题的template.php文件中添加这么一个函数:
function yaiyuan_preprocess_page(&$variables, $hook) {
global $language;
$lang_name = $language->language ;
$variables['site_name'] = (theme_get_setting('toggle_name') ? filter_xss_admin(i18n_variable_get('site_name', 'en')) : '');
drupal_set_message($lang_name);
}
注意,这里的$lang_name始终都是zh-hans,即便是我切换到了英文下面。我这里使用i18n_variable_get来替代默认的variable_get,以便支持多语言。
昨天晚上一直在想解决方案,如果我能够获取到当前语言,那么通过预处理函数的方式,就能够解决问题,Drupal核心自带的语言切换器区块对应的代码里面,应该有如何获取当前语言的代码。这样一想,感觉问题是可以通过预处理函数的方式解决的。
不过,我突然想到了一件事情,即便是使用en/admin/config/system/site-information访问后台页面的时候,后台显示的也是简体中文。我突然想到,Drupal7中,界面语言和内容语言的检测方法是分开配置的,界面语言的检测方法我们还是用的默认的,很可能是这里出了问题。
我们先沿着第一种方案继续前进,我们去配置“语言切换器(内容)”的时候,可以看出这个区块是由locale模块提供的,这样我们就可以顺藤摸瓜,找到对应的代码。
function locale_block_view($type) {
if (drupal_multilingual()) {
$path = drupal_is_front_page() ? '<front>' : $_GET['q'];
$links = language_negotiation_get_switch_links($type, $path);
这里面,我觉得比较关键是language_negotiation_get_switch_links,通过搜索,找到这个函数的源代码。在这个函数里面,有这么一段代码:
if (count(language_list()) >= 2) {
$language = language_initialize($type);
}
给了我很大的提示,这里的$type有多种情况,我们在前面分析核心源代码的时候,曾经提到过,打开bootstrap.inc文件,找到:
define('LANGUAGE_TYPE_CONTENT', 'language_content');
/**
* The type of language used to select the user interface.
*/
define('LANGUAGE_TYPE_INTERFACE', 'language');
/**
* The type of language used for URLs.
*/
define('LANGUAGE_TYPE_URL', 'language_url');
我们这里使用LANGUAGE_TYPE_CONTENT。根据这些信息,我最终使用下面的代码,解决了这个多语言问题:
function yaiyuan_preprocess_page(&$variables, $hook) {
//global $language;
//$lang_name = $language->language ;
$language = language_initialize(LANGUAGE_TYPE_CONTENT);
$variables['site_name'] = (theme_get_setting('toggle_name') ? filter_xss_admin(i18n_variable_get('site_name', $language->language)) : '');
//drupal_set_message($lang_name);
//print debug($language);
}
这里面,如何获取当前语言,我首先尝试了一下$language,总是不对。使用language_initialize(LANGUAGE_TYPE_CONTENT),则可以成功获取。注意我们这里获取的是当前内容的语言。不是当前的界面语言。此外,我对这个语言变量还debug了一下,查看了它的结构。它的结构是这个样子的:
stdClass::__set_state(array(
'language' => 'en',
'name' => 'English',
'native' => 'English',
'direction' => '0',
'enabled' => '1',
'plurals' => '0',
'formula' => '',
'domain' => '',
'prefix' => 'en',
'weight' => '0',
'javascript' => '',
'provider' => 'locale-url',
))
我们使用预处理函数的方式解决了问题。不过,总不能每次都这样解决吧。或许真的是界面语言的检测与选择的配置问题。导航到admin/config/regional/language/configure,对于界面语言的检测方法,我们与内容的保持一致。
保存这里的配置。注释掉,我们前面所写的预处理函数。站点名称的中英文切换,正常。当然,前面的工作,也不是没有白做,如果你跟着阅读了源代码,会对Drupal7多语言的背后机制有更深的理解。比如这个语言,它分为:内容、界面、路径。我们这里对这个概念有了更深的理解。我们前面的获取当前语言,其实是获取当前内容语言,而不是获取当前界面语言。
如果,我们这里是一个Logo,中文Logo和英文Logo不一样,怎么解决呢?导航到zh-hans/admin/config/regional/i18n/variable,
选中这里的主题设置复选框即可,这样主题的Logo就支持多语言了,分别为每个语言上传一个Logo,解决了Logo的多语言问题。
我们来看一下如何翻译主菜单,导航到主菜单的编辑页面,admin/structure/menu/manage/main-menu/edit。这里面有一个多语言的配置选项:
第一个选项,菜单项没有多语言选项,只有菜单本身可被翻译。菜单本身可被翻译?如何理解,看这个菜单右上角的标签,里面有一个“翻译”标签:
点击这个“翻译”标签,就可以对菜单进行翻译。只是我们这里没有必要。
第二个选项,翻译和本地化。注意,这里的翻译(translate)和本地化(Localize)之间是有区别的。如果一个菜单项,它具有语言属性,此时会允许翻译;如果它没有设置语言,则可被本地化。
第三个选项,固定语言,每个菜单项都有一个语言属性,它们只会显示在自己的语言下。
我们这里选择第二个选项,保存。我们现在对主菜单的菜单项,进行编辑:
点击“首页”菜单项右边的编辑链接。进入菜单项的编辑页面。
此时,右上角有了一个“翻译”标签,我们点击这个标签,就可以对这个菜单项进行翻译了。
点击操作里面的“翻译”链接,我们将菜单项的标题翻译一下:
翻译好了保存结果。这是翻译后的样子:
菜单项的编辑页面,还有一个语言选项:
我们可以设置,这个菜单项是属于哪个语言下的。这样的话,我们可以为每种语言都创建一个对应的菜单项,也能解决问题。
因为,我们这里面,中文、英文的节点ID是相同的,所以我们没有必要创建多个菜单项。直接对菜单项的标题进行翻译即可。
重复前面的操作,直到把主菜单里面剩余的菜单项都翻译完毕为止。需要说明一下的是,当我们翻译到“关于我们”下面的第一个子菜单项“公司历程”的时候,菜单项编辑页面的右上角没有“翻译”标签。如果我们直接输入对应的路径的话,此时会提示没有权限。这是因为,我们还没有翻译菜单项“公司历程”所对应的节点,我们将对应的节点翻译成英文后,这里的菜单项,也可以翻译了。
到最后,只剩下“联系我们”这个webform页面了,这也是一个普通的节点。我们首先让内容类型Webform支持字段翻译,将它的Title替换成字段的形式。这个时候,我们就可以将“联系我们”这个节点的标题和正文翻译成英文了。之后,就可以翻译对应的菜单项了。只不过这个页面的表单还无法翻译,主要是表单元素的标签,在英文环境下还是显示的中文。
我们现在先不管这个,到此,整个主菜单里面的所有菜单项,都被翻译成为了英文。注意,
注意,这里不仅仅是主菜单,左边的Menu block,以及面包屑都变成英文的。这说明了Menu block和Menu Position模块对多语言的支持是比较友好的。
不过,左下角的“联系我们”区块,仍然是中文的,我们来看一下,如何将它翻译成英文。点击它的配置链接,进入它的编辑页面en/admin/structure/block/manage/block/2/configure。在这里,可见性设置里面,多了一个语言选项。
这里有两种方式,第一种方式,是将这个区块标记为可翻译的,也就是选中“Make this block translatable”。选中后,下面的按钮多出来了一个:
我们可以点击“保存并翻译”按钮。
我们点击英文右边的翻译链接,来翻译这个区块:
遗憾的是,我们这里面只能翻译区块标题,不能翻译区块的正文,这是因为区块正文所使用的文本格式不支持多语言。如何让它支持多语言呢?导航到i18n的管理界面,点击右上角的“字符串”标签:
这里有两个配置选项:
和
对于源语言,我们使用默认的简体中文即可。对于可翻译的文本格式,我们这里选中所有的,保存。回到,刚才的区块的翻译页面,现在就可以翻译区块正文了:
在这里,输入对应的英文。注意,对于区块正文,应该保留原来的HTML标签。
我们可以创建两个区块,一个显示在中文下,一个显示在英文下,两个区块显示在同一个区域的同一位置。这也是一种常见的解决方案,我以前就非常喜欢这种方式。
我发现一个问题,面包屑没有正确显示:
具体原因不明。我把新闻、产品所有节点也都翻译了一遍,使用的Google翻译,没有人工校对。
现在,我们重复前面的操作,将“页脚菜单”、“版权信息”也翻译成英文。
令人遗憾的是,我们将所有的菜单项翻译后,在英文环境下,显示的还是中文。这是我的翻译:
原因未明。我们暂时先不管它。来看“版权所有”区块,我们对它进行配置,将它只显示在中文下:
接着,创建一个新的静态区块,是版权所有区块的英文版:
也放在页脚区域,这里只显示在英文环境下;
保存。这个区块的多语言,我们也解决了。来看页脚菜单区块,这个区块我们用的是核心自带的,我们前面讲到左边的菜单区块(menu block)的中英文显示是正常的,我们将这个页脚菜单导航替换成Menu block模块提供的方式显示。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
导航到区块的管理界面,创建一个新的menu block(菜单区块),做以下配置:
我们将这个区块放在页脚区域,测试一下。中英文显示正常了。把原来的核心自带的区块禁用。现在页脚也正常了。
我们没有必要去深究,为什么核心自带的对多语言支持不友好,只要解决问题就好。有时候换种方式,绕过去,可能会更好。
我们现在回到新闻列表页面,或者一个新闻节点页面,此时的面包屑是不正常的。我们前面已经提到过了。我想这里之所以显示“新闻”两个字,是因为Views里面的配置问题。我们编辑对应的Views,将标题设置为英文的形式:
现在,在英文环境下,面包屑显示正常了,但是在中文环境下,又不正常了。
可是,这个面包屑是基于菜单的,而我们的菜单项中,已经完成了“新闻”到“News”的翻译。经过测试,发现,新闻节点页面的面包屑,是基于菜单的;新闻列表页面,是基于Views的。
为了解决这个问题,我们下载、安装、启用Internationalization Views模块,我这里使用的版本是i18nviews-7.x-3.x-dev。这个模块只有开发版,不过Drupal7下的开发版的安装量也超过了1万。安装后,报了一个错:
这样的错误消息很多,我们这里不去管它。我最终成功解决这个问题,解决办法如下:
导航到admin/config/regional/translate/translate,在过滤条件中:
将搜索限制在视图上,也就是Views上。这个时候,会列出Views中的很多需要翻译的字符串:
这里的“新闻”,也就是news视图的标题,我们把它翻译一下:
翻译后,测试了一下,发现不行。我又导航到admin/config/regional/translate/i18n_string,在这里刷新了一下字符串:
这样新闻列表的面包屑,就显示正常了。
用同样的版本,解决产品列表的面包屑显示。需要说明的是,上面的刷新字符串在这里没有起作用,真正起作用的是清除缓存。
不过令人遗憾的是,新闻节点页面,在英文环境下的面包屑显示不正常:
但是产品节点的显示就正常。两者之间的配置,我记得没有任何不同。我对这个问题研究了很久,始终找不到解决办法。
我是这样解决的,解决的办法不是很地道。首先是编辑当前主题的info文件yaiyuan.info,取消注释这句代码:
scripts[] = js/script.js
接着向script.js这个文件中添加以下代码:
jQuery.noConflict();
(function($) {
$(function() {
$('.i18n-en .breadcrumb li a').each(function(index){
if($(this).attr('href') == '/yaiyuan1/en/news'){
//$(this).addClass('abc');
$(this).html('News');
}
});
});
})(jQuery);
这段JS的作用,就是将面包屑中“新闻”这个链接的文本替换为“News”。
有时候,问题实在解决不了的话,可以借助于jQuery, Hack一下,也没有关系。
现在发现,右边栏的样式是乱的:
最新公告、最新新闻,需要使用下面的滚动条,向右滚动才能显示出来,我们并没有覆写这里的输出。这里的输出,仍然是使用的默认区域、默认区块模板文件。
在sites\all\themes\snt\templates\override\block下,将block--system--main-menu.tpl.php复制一份,然后重命名为block--ziye-right.tpl.php,模板文件里面的内容不变:
<?php print $content ?>
然后清除缓存,样式恢复了正常。
如果要进一步的话,我们可以这样,在sites\all\themes\snt\templates\override\region下面,将region--nav.tpl.php复制一份,然后重命名为region--ziye-right.tpl.php,模板文件里面的内容不变:
<?php print $content ?>
有时候,我会把region.tpl.php、block.tpl.php模板文件里面的代码修改为:
<?php print $content ?>
其它部分全部删除。这样,就不会带有额外的输出了。
我们现在来将右边的这部分动态化,这里我们使用Views,来列出这里的最新新闻、最新公告。我们创建内容类型“新闻”,配置如下:
资源内容类型配置表
属性 |
值 |
名称 |
新闻 |
机读名称 |
news |
标题字段标签 |
标题 |
发布选项 |
已发表 |
展示设置 |
显示作者和日期信息 |
评论设置 |
开放/非主题式/30/回复表单在单独页面显示 |
菜单设置 |
无可用菜单 |
为了后面配置的方便,我们安装locale模块、启用简体中文,导入简体中文语言包、做一些其它常见配置。具体配置参看Think in Drupal的第2集(Drupal实战)。
接下来,我们创建10篇新闻,这里都是测试数据。现在测试数据已经创建好了。
接下来,我们创建Views,我们首先需要启用Views、Views UI模块。启用后,我们导航到Views的管理界面“首页 » 管理 » 结构 » Views”。
点击这里的“Add new view”链接,创建视图。这里是我的配置:
我们点击下面的继续并编辑按钮,进入Views的配置界面。然后点击右上角的保存按钮。这是我们配置好的样子:
现在导航到区块的管理界面“首页 » 管理 » 结构 » 区块”,我们找到通过Views创建的区块“View: news: Block”,然后将它放到区域“Ziye right”里面。
保存。然后访问前台页面,比如首页。看右边区块的样式。
我们看到这个样式,和目标还有不小的差距。我们现在来覆写Views的输出,让它与我们的静态HTML保持一致。我们来看一下对应的HTML:
我们这里将其与美工所给的HTML做一下对比:
这里都是Views的默认输出,很多人刚学Drupal时,抱怨最多的就是,Views的输出没有办法控制。实际上是这样的,是初学者,不懂得如何控制Views的输出。
我们现在就去覆写Views的模板文件,让Views的输出,和目标输出保持一致。来看第一个,通过字段的样式设置,控制字段的HTML输出。
我们编辑刚才创建的视图,admin/structure/views/view/news/edit,在这里点击字段里面的“内容: 标题”,此时会弹出字段的配置对话框。我们展开里面的样式配置,做一下配置:
这是我比较喜欢的配置。配置好了以后,保存。访问网站的首页,查看源代码,我们看一下这个区块的变化。
默认为:
配置后,变为:
这里删除了两层标签,一个div,一个span。
我们注意到,目标代码里面使用的是ul、li,所以我们可以调整一下views的格式,当前为Unformatted list:
我们点击“Unformatted list”链接,我们将其调整为“HTML list”:
点击下面的应用按钮,这样会弹出来的格式的配置对话框。
这里我们采用默认的即可。保存。访问首页,观察区块HTML的变化,现在变为了:
现在,我们这里也采用了ul、li标签,只不过li里面有多余的class,我们想去掉它,这样内部的输出就基本保持一致了。我们还看到了,这里面有个<div class="item-list">,这个"item-list"我好像有印象,在前面的格式设置里面,有对应的配置选项:
我们点击格式的配置链接,将这个配置选项置为空。保存。然后,观察一下对应HTML的变化。
这个<div class="item-list">竟然被整个的去掉了。
我们调整一下Row class和List class。
观察一下,HTML的变化:
注意这里的ul、li的class的变化,与Row class和List Class之间的对应关系,我们看到Row class作用于li标签,List Class作用于ul标签。
我们将Row class和List Class置为空。
在格式里面,我们当前的配置为:
我们点击Fields右边的设置,在弹出的对话框里面,看最上面的复选框。
如果我们取消了对这个复选框的选中,Views就会去掉默认的字段包装元素,我们去掉对它的选中。保存。观察HTML的变化:
没有任何变化。但是这并不意味着,这个复选框不起作用。有可能是我们在字段的样式设置里面,已经去除了字段包装元素的原因。
我们可以在字段的样式设置、视图的格式设置上面,多测试几下,观察一下HTML的变化,通过在这里配置样式,可以减轻我们的模板覆写工作。
通过Views的后台配置,我们可以控制字段层级的输出,甚至可以控制Row层级的输出,甚至再外面一层的输出。但是后台的配置,还是有局限性的。距离完全控制Views的输出,还有一点距离。不过没有关系,我们可以覆写Views的模板文件,在模板文件里面,搞定这些问题。
在Views的编辑页面,展开右边的高级选项,找到其它部分,最下面,找到“Theme”配置选项:
点击它右边的“Information”链接,这样就会弹出当前显示(Display)的主题信息对话框。其实我们在Think in Drupal第2集里面,讲过Views模板文件的覆写了。我们这里再重复一遍。
首先,我们需要弄清楚:Display output、Style output、 Row style ouput的含义,是什么意思?很多人经常问这个问题。我这里简单解释一下,Display output负责最外层的输出,Style output负责次一层的输出,Row style ouput负责最小单元的输出。这里面有一个从外到内的关系。这里的解释,可能不是很给力,你如果想搞明白的话,最好看看对应的模板文件,看看模板文件里面的HTML片段,然后和自己建立的views的输出,做一下对应。这样,你一下子就理解了,恍然大悟。我在这里解释半天,也不如你自己动手看看。
在Display output后面,跟随的是对应的模板文件的名字。第一个名字,是默认的模板文件名字,后面的,都是该模板文件的模板建议。黑体部分,表示当前起作用的模板文件名字。这里的名字,有个先后顺序的,越靠后的,越具体,优先级越高。“Style output”、 “Row style ouput”、“Field 内容: 标题 (ID: title)”后面的模板文件名字,也是这样的。
现在我们来覆写views-view.tpl.php,默认的模板文件位于sites\all\modules\views\theme目录下面,这里还包含了其它默认的views模板文件。
我们在sites\all\themes\snt\templates\override下面创建一个文件夹views,然后在新建的文件夹下面,再创建一个子文件夹news;将sites\all\modules\views\theme下面的views-view.tpl.php复制到sites\all\themes\snt\templates\override\views\news,并将文件名重命名为views-view--news--block.tpl.php,这个文件名,可以在Views的主题信息对话框中复制过来的。
我们打开views-view--news--block.tpl.php,现在的代码还是默认的:
<div class="<?php print $classes; ?>">
<?php print render($title_prefix); ?>
<?php if ($title): ?>
<?php print $title; ?>
<?php endif; ?>
<?php print render($title_suffix); ?>
<?php if ($header): ?>
<div class="view-header">
<?php print $header; ?>
</div>
<?php endif; ?>
<?php if ($exposed): ?>
<div class="view-filters">
<?php print $exposed; ?>
</div>
<?php endif; ?>
<?php if ($attachment_before): ?>
<div class="attachment attachment-before">
<?php print $attachment_before; ?>
</div>
<?php endif; ?>
<?php if ($rows): ?>
<div class="view-content">
<?php print $rows; ?>
</div>
<?php elseif ($empty): ?>
<div class="view-empty">
<?php print $empty; ?>
</div>
<?php endif; ?>
<?php if ($pager): ?>
<?php print $pager; ?>
<?php endif; ?>
<?php if ($attachment_after): ?>
<div class="attachment attachment-after">
<?php print $attachment_after; ?>
</div>
<?php endif; ?>
<?php if ($more): ?>
<?php print $more; ?>
<?php endif; ?>
<?php if ($footer): ?>
<div class="view-footer">
<?php print $footer; ?>
</div>
<?php endif; ?>
<?php if ($feed_icon): ?>
<div class="feed-icon">
<?php print $feed_icon; ?>
</div>
<?php endif; ?>
</div><?php /* class view */ ?>
这里面的第一行,<div class="<?php print $classes; ?>">,所负责的输出,就是我们区块最外面的div。
里面的这段代码:
<div class="view-content">
<?php print $rows; ?>
</div>
里面包含的<div class="view-content">,就对应区块HTML里面的:
最主要的是搞清楚这里面的对应关系。而目标HTML,在列表外面,也有两层DIV,只是ID、CLASS不同,我们这里不妨简单的调整一下。这是调整后的。
<div id="jishu-right2">
<?php print render($title_prefix); ?>
<?php if ($title): ?>
<?php print $title; ?>
<?php endif; ?>
<?php print render($title_suffix); ?>
<?php if ($header): ?>
<div class="view-header">
<?php print $header; ?>
</div>
<?php endif; ?>
<?php if ($exposed): ?>
<div class="view-filters">
<?php print $exposed; ?>
</div>
<?php endif; ?>
<?php if ($attachment_before): ?>
<div class="attachment attachment-before">
<?php print $attachment_before; ?>
</div>
<?php endif; ?>
<?php if ($rows): ?>
<div class="news">
<?php print $rows; ?>
</div>
<?php elseif ($empty): ?>
<div class="view-empty">
<?php print $empty; ?>
</div>
<?php endif; ?>
<?php if ($pager): ?>
<?php print $pager; ?>
<?php endif; ?>
<?php if ($attachment_after): ?>
<div class="attachment attachment-after">
<?php print $attachment_after; ?>
</div>
<?php endif; ?>
<?php if ($more): ?>
<?php print $more; ?>
<?php endif; ?>
<?php if ($footer): ?>
<div class="view-footer">
<?php print $footer; ?>
</div>
<?php endif; ?>
<?php if ($feed_icon): ?>
<div class="feed-icon">
<?php print $feed_icon; ?>
</div>
<?php endif; ?>
</div><?php /* class view */ ?>
现在,回到区块视图的主题对话框,找到下面的重新扫描模板文件按钮,点击这个按钮:
这是重新扫描后的样子:
我们看到,Display output起作用的模板文件,已经变成了views-view--news--block.tpl.php。现在访问首页,观察区块HTML输出的变化。
和目标已经非常接近了,而且现在的样式,也变了:
其实我觉得,li里面的class,对于样式,没有任何影响,不过我们这里也通过模板文件覆写的方式,将它替换成我们想要的。
对于最新公告,我们可以创建一个内容类型“公告”,然后创建对应的views区块,我们这里主要讲解的是主题制作,为了方便,我们就不再创建这个内容类型的,直接使用新闻的,然后将对应的标题改一下即可。我们这里这样操作:
1) 在视图的编辑页面admin/structure/views/view/news/edit,添加一个新的显示,显示类型选择“附件”。
2) 在新显示里面,找到附件设置:
我们点击Attach to右边的“Not defined”链接,在弹出的对话框中选择区块:
3) 在Global: Text area里面,我们将“最新新闻”修改为“最新公告”。
4) 在分页器设置里面,将显示的条目数量,修改为7。
现在,访问首页,观察样式的变化,“最新公告”已经显示出来的,但是样式和HTML都没有对上。
对应的HTML:
我们按照前面的办法,对附件的模板文件进行覆写。导航到我们刚才创建的目录sites\all\themes\snt\templates\override\views\news下面,创建文件views-view--news--attachment-1.tpl.php,里面的代码如下:
<?php if ($rows): ?>
<div class="news">
<h2><?php print $header; ?></h2>
<?php print $rows; ?>
</div>
<?php elseif ($empty): ?>
<div class="news">
<?php print $empty; ?>
</div>
<?php endif; ?>
创建文件views-view-list--news--attachment-1.tpl.php,里面的代码如下:
<?php print $wrapper_prefix; ?>
<?php if (!empty($title)) : ?>
<h3><?php print $title; ?></h3>
<?php endif; ?>
<?php print $list_type_prefix; ?>
<?php foreach ($rows as $id => $row): ?>
<li><?php print $row; ?></li>
<?php endforeach; ?>
<?php print $list_type_suffix; ?>
<?php print $wrapper_suffix; ?>
重新扫描模板文件,访问首页,样式已经完全达到了我们的要求:
只不过,HTML输出,还有一点不同,就是最新公告外面,包了一层<div class="attachment attachment-before">,你知道怎么去除它么?在views-view--news--block.tpl.php文件中去除对应的代码即可。
对于这个例子,我曾经这样给人演示过,视图的格式,选用“Unformatted list”,然后最新公告,采用区块的形式,在模板使用views_embed_view嵌套到最新新闻里面。此外还使用了模板覆写views-view-fields--news--block.tpl.php。
对应的代码,views-view--news--block.tpl.php的为:
<div id="jishu-right2">
<?php print views_embed_view('news','block_1'); ?>
<div class="news">
<?php if ($header): ?>
<h2> <?php print $header; ?></h2>
<?php endif; ?>
<?php if ($rows): ?>
<?php print $rows; ?>
<?php elseif ($empty): ?>
<?php print $empty; ?>
<?php endif; ?>
</div>
</div>
views-view--news--block-1.tpl.php的:
<div class="news">
<?php if ($header): ?>
<h2> <?php print $header; ?></h2>
<?php endif; ?>
<?php if ($rows): ?>
<?php print $rows; ?>
<?php elseif ($empty): ?>
<?php print $empty; ?>
<?php endif; ?>
</div>
views-view-unformatted--news--block.tpl文件的:
<ul>
<?php foreach ($rows as $id => $row): ?>
<li>
<?php print $row; ?>
</li>
<?php endforeach; ?>
</ul>
views-view-fields--news--block.tpl.php文件的:
<?php print $fields['title']->content ?>
这里将这些代码列出,供大家参考。同样的功能,解决办法可以有多个。对于views-view-fields.tpl.php,我们这里掌握这句代码即可:
<?php print $fields['field_name']->content ?>
这里的field_name就是主题信息对话框,对应字段的ID。
掌握这里的知识,就可以完全控制Views的输出了,没有多少难的。对于Views列表页面的覆写,和这里所讲的技术是一样的,希望大家看完以后,可以举一反三、触类旁通。现在,我们将右边的静态区块,去掉,因为动态的已经添加好了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
企业网站建设,它的复杂程度,远小于网上书店这个例子,而且这里的很多配置,在前面我们也都讲过了。但是,我还是想较为详细的记录整个过程,这样方便那些水平比较低的Drupal学习者,学习Drupal。在读者对象方面,我会优先满足Drupal水平比较低的读者群,优先为他们服务,这是出于市场的考虑,水平比较低的,更愿意购买我的资料;水平高的,我写的再好,他们也不买。
此外,在“联系我们”页面,我还想加上百度地图,在地图上,让用户看到公司的所在位置。以前都是使用Google地图的,不过毕竟Google退出了中国,所以这里决定使用百度的。Drupal官方网站是有两个Gmap模块的,不过一个百度的也没有,我觉得Drupal在中国发展,离不开与本土系统的集成,这些都不是一个人可以完成的。
进入百度地图页面,http://map.baidu.com,在这里找到链接“地图API”,点击这个链接,进入页面http://developer.baidu.com/map,这是百度地图的开发者主界面。
点击左边的“插件与工具”,找到“地图快速生成器”,点击这个链接。在这里,选好中心点,在对应的位置添加标注,保存标注。
操作完成后,现在获取对应的代码:
点击这里的“获取代码”按钮,我们就可以看到生成好的HTML源代码:
复制这些代码,直接将其粘贴到“联系我们”节点的正文字段里面,保存。现在地图就在Drupal中显示出来了。
我们这里,也可以这样做,分析一下复制过来的源代码,把JS的部分,放到html.tpl.php文件中,在正文字段中,只保留生成地图所用的div,即可。
我做网站的时候,根据需要,通常首先创建内容类型,这样可以方便的向里面添加内容。接下来,导航到admin/structure/types,我们创建内容类型“新闻”,配置如下:
名字 |
新闻 |
机读名字 |
news |
发布选项 |
已发表/多语言禁用 |
展示设置 |
显示 “作者和日期信息。” |
评论设置 |
关闭 |
菜单设置 |
无 |
在“新闻”的管理字段页面,admin/structure/types/manage/news/fields,为它添加一个已有字段“field_image”,注意,这两个地方的配置:
对于文件、图片字段,我们通常会为它设置一个文件目录,这样文件上传后,都会存储在这个目录下,这样的好处就是文件夹结构比较清晰。很多刚学Drupal的人不知道配置这个地方。
值的数量,我们这里设置为了“不限”。由于一个字段可以同时追加到多个实体上去,对于字段本身的配置,你在这里修改了,其它地方也会跟着改,这个需要注意。
导航到分类的管理界面,admin/structure/taxonomy,在这里添加一个新的词汇表,配置如下:
名字 |
新闻分类 |
机读名字 |
news_category |
然后向这个词汇表,添加三个分类术语,这是添加好的样子:
现在,回到admin/structure/types/manage/news/fields,为“新闻”内容类型,添加一个“新闻分类”字段,具体配置如下:
标签 |
新闻分类 |
机读名称 |
field_news_category |
字段类型 |
术语来源 |
控件 |
选择列表 |
词汇表 |
新闻分类 |
值的数量 |
1 |
创建内容类型“产品”,具体配置如下:
名字 |
产品 |
机读名字 |
product |
发布选项 |
已发表/多语言禁用 |
展示设置 |
不显示 “作者和日期信息。” |
评论设置 |
关闭 |
菜单设置 |
无 |
在“新闻”的管理字段页面,admin/structure/types/manage/news/fields,为它添加一个已有字段“field_image”。具体配置和前面新闻的一样。我们可以创建一个新的图片字段,也可以共用一个。
导航到分类的管理界面,admin/structure/taxonomy,在这里添加一个新的词汇表,配置如下:
名字 |
产品分类 |
机读名字 |
product_category |
然后向“产品分类”词汇表,添加三个分类术语,这是添加好的样子:
现在,回到admin/structure/types/manage/product/fields,为“产品”内容类型,添加一个“产品分类”字段,具体配置如下:
标签 |
产品分类 |
机读名称 |
field_product_category |
字段类型 |
术语来源 |
控件 |
选择列表 |
词汇表 |
产品分类 |
值的数量 |
1 |
创建内容类型“解决方案”,具体配置如下:
名字 |
解决方案 |
机读名字 |
solution |
发布选项 |
已发表/多语言禁用 |
展示设置 |
不显示 “作者和日期信息。” |
评论设置 |
关闭 |
菜单设置 |
主菜单 |
创建内容类型“服务”,具体配置如下:
名字 |
服务 |
机读名字 |
service |
发布选项 |
已发表/多语言禁用 |
展示设置 |
不显示 “作者和日期信息。” |
评论设置 |
关闭 |
菜单设置 |
无 |
创建内容类型“成功案例”,具体配置如下:
名字 |
成功案例 |
机读名字 |
portfolio |
发布选项 |
已发表/多语言禁用 |
展示设置 |
不显示 “作者和日期信息。” |
评论设置 |
关闭 |
菜单设置 |
无 |
对于内容类型上面的字段,我们在后面的项目建设中,还可以进一步的完善。
进一步的添加内容,添加新闻,产品,解决方案。这是我添加的内容:
创建新闻列表页面
我们这里采用了内容摘要的形式,而没有采用字段的形式。在Views里面,字段的形式,是我更喜欢的,这个后面根据实际需要可以调整。我们在这里,最主要的是生成一个页面,有内容显示出来。我们这里使用了页面显示(page display)。
对于新闻列表,我通常采用这样的解决办法:
news
news/category/[tid]
这个时候,可以创建两个页面显示,一个负责所有的,一个负责具体分类下的。不过我们也可以采用这样的办法,只创建news一个页面显示,然后向它传递tid参数,这个时候只有一个内部路径,下面的路径都是有效的:
news
news/[tid]
这是两种不同的解决方式,注意两者之间的区别。除此以外,还有基于Panels的解决方式,我们在Drupal实战一书中,有了详细的介绍。
现在,我们向创建的视图里面添加一个上下文过滤器(contextual filter),在Drupal7里面,分类术语是以字段的形式出现的,新闻下面有一个新闻分类字段,我们按照这个分类字段过滤就可以了。找到这个上下文过滤器:
选中,并点击应用按钮。注意,在接下来的对话框中,有这么几个配置选项:
我们知道,新闻有三个分类:公司新闻、国内新闻、国际新闻。当显示对应的页面时,我们想让标题也显示成对应的。这个时候,我们可以使用这里的配置选项“覆写标题”(Override title)。这是我的配置:
%1表示第一个参数,%2表示第二个参数。
我们将这个视图的标题从“news”改为“新闻”,保存视图。
现在访问news页面,就可以看到内容了:
如果我们访问路径“news/4”,我们将会看到:
页面标题,在这里显示出来的是对应的tid,而不是对应的分类术语名称,这个以前的时候好像介绍过,也是很多人容易犯的小错误。
我们导航到视图的编辑页面admin/structure/views/view/news/edit/page,找到上下文过滤器,添加一个新的上下文过滤器:
在这里,“内容: Has taxonomy term ID”和“内容: Has taxonomy term ID (with depth)”都可以解决我们这里的问题,我更喜欢带有深度的这个。“内容: Has taxonomy term ID depth modifier”,这个是深度修正器,我很少用这个。
这里选中“内容: Has taxonomy term ID (with depth)”,点击应用按钮。这是我喜欢的配置:
我们将前面添加的上下文过滤器删除,只保留我们新增的这一个。保存视图。
现在访问news/4,此时将会得到:
此时的页面标题显示正确。
我们这里,注意体会这两个上下文过滤器之间的区别,一个不行换用另一个即可。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们这里顺带讲解Views里面的一个基本配置,这个配置在前面的文章中没有介绍过。我们访问drupal.org,在它的首页,新闻部分,是这个样子的:
第一条,显示标题、时间、内容摘要,其余三条只显示标题。这是实际项目中,经常遇到的一个需求,使用Panels就可以很好的解决这个问题,创建两个view,一个取第一条,另一个取2,3,4条,两个加在一块,就可以解决这里的问题了。这是解决这个问题的基本思路,就是把它分成两个view。我们这里,给大家演示一下,具体的解决办法。
这是我的初始配置,保存。
这是预览的效果,此时只列出来了三条,因为我们只有三条测试数据。
我们先做一个区块显示,列出只包含标题的最新新闻。接下来,我们添加一个新的显示,这次,我们对于显示类型,我们选择附件。
对于这个附件显示,我们首先为它添加更多的字段,注意这里只作用当前显示,这是添加后的样子:
这里面,对于body字段,我们覆写了它的输出,截取150字显示,只显示纯文本。不显示字段标签。对于Post date字段,这是我喜欢的一种配置:
对于“内容:链接”字段,我们这里主要配置了“Text to display”(要显示的文本):
在分页器的配置里面,我是这样配置的:
实时的预览效果:
在“附件设置”里面,找到“追加到”(Attach to)配置选项。
当前是未定义状态,我们修改一下,将它追加到区块显示上:
附件的位置默认是“Before”(在前面),除此以外,还有另外两个配置选项:
我们这里,使用“Before”就可以了。现在切换到区块显示,实时预览的效果:
只不过,这里面,第一条新闻重复显示了。现在我们对区块显示的配置做出调整,这是原来的分页器的配置选项。
这是我修改后的:
此时,区块只负责显示3条,并且跳过第一条。这里,跳过的这条,就是附件所负责显示的那条。保存这里的配置,这是保存后的实时预览:
这个就是我们想要的。这里面,注意附件的用法,特别注意的是分页器里面的Offset的用法。Offset是跳过的意思:
我们继续前进,按照同样的办法,创建产品列表视图,我这里是克隆现有的news视图,然后重命名修改而成。
现在,导航到菜单的管理界面,找到主菜单,我们可以为它添加更多地菜单项了。
我们这里主要添加产品新闻相关的菜单项,并调整了顺序。比如添加公司新闻这个菜单项的时候,输入路径时,要对准了,具体是哪个tid,不要写错了。
现在,菜单显示出来,还是一级的,没有显示二级子项。很多时候,我们想显示动态下拉菜单,这个时候有两个模块可供选择,一个是Nice menu模块,另一个就是Superfish模块,我们这里采用后者。在Drupal7下,Superfish模块更流行一些。
我这里下载的版本是superfish-7.x-1.9。这个模块,依赖于一个jQuery库,下载地址:https://github.com/mehrpadin/Superfish-for-Drupal/zipball/master。在sites\all下面创建libraries目录,然后将下载的jQuery库解压缩,然后复制到sites\all\libraries\superfish目录下。
将superfish模块解压缩后复制到sites\all\modules目录下。现在启用superfish模块。启用后,导航到区块管理界面admin/structure/block,可以看到:
这是superfish模块提供的四个默认区块,我们这里使用第一个。点击它的配置链接,进入对应的区块配置页面,这里的配置选项很多。
我们在这里,可以选择使用哪个菜单。如果菜单比较大的话,我们可以选择一个菜单项。菜单深度,-1表示所有,我们采用默认的即可。“Take "Expanded" option into effect.”,这个采用默认的不选中即可,可能很多人对于这个“展开”选项不是很了解,对于一个菜单链接,它有这么一个配置选项,你编辑一个菜单链接的时候,会找到的:
我们这里不考虑核心的“展开”选项即可。
区块配置里面,接下来是superfish设置,里面的设置项很多:
我们首先看到的是菜单类型,它分三种:水平、垂直、导航条。我们采用水平即可。对于样式,我们暂时先采用默认的“无”即可,这里面自带了多个样式可供选择,选择“无”的话,我们可以自定义样式。
再往下是有关速度的两个配置选项:
我们采用默认的配置即可。
再往下是Path class和Path levels:
对于拿不准的配置选项,我们使用默认的即可。往下继续看:
“Slide-in effect”就是鼠标移到包含子菜单项的菜单项上时,隐藏的子菜单项显示出来所用的效果,包括垂直、水平、对话框,如果安装了jQuery Easing 插件的话,这里将会包含更多的效果选项。Auto-arrows应该表示自动加上箭头;Drop shadows应该表示下拉阴影,从字面意思理解。下面还有一个“more options”,展开后有更多两个配置选项。
再往下,还有:
我们这里就不逐个展开一一介绍了。采用默认的即可。我们把这个区块放到Bartik的Featured区域,保存。预览一下效果先:
功能有了,我们需要把原来的主菜单替换成现在的superfish区块。这又涉及到主题制作了。
我们在Drupal实战一书里面,安装过CKEditor模块,我们这里仍然使用这个模块,只不过对于文件上传,我们使用IMCE模块。这里简单的重复一下安装过程,我这次使用的版本是ckeditor-7.x-1.13,下载解压缩后,放到sites\all\modules目录下,这里原来已经有了一个,我们替换为最新的。接下来,去http://ckeditor.com/download下载最新的JS,我这里下载的是ckeditor_4.2_full,将它解压缩,复制到sites\all\libraries目录下。导航到模块的管理界面,启用CKEditor模块。现在,创建内容时,就可以使用所见即所得编辑器了。
此时,还有文件上传的问题,以前我们曾经使用过CKFinder,但是这个是收费的,破解也是有办法的,不过有时候还是需要替代的解决办法,其中一个是使用Insert模块,另一个就是使用IMCE模块。关于Insert模块,我曾经写过一篇教程,有兴趣的可以参考一下http://www.thinkindrupal.com/node/4993。我们这里使用IMCE。我这里使用的版本是imce-7.x-1.7。解压缩后,将其放到sites\all\modules目录下。启用后,我们可以导航到admin/config/media/imce,对它进行配置。
它默认包含两个配置好的profile,一个是专为用户1准备的,另一个应该是为普通用户准备的。如果这两个不够用的话,可以新建一个。
我们可以按照用户角色,为它分配profile,对于管理员,我们可以为它分配User-1;对于注册用户,可以为它分配Sample profile;对于匿名用户,可以不分配。实际上,对于一个企业站,特别是小企业站,里面只有一个用户。
我们编辑User-1,进入页面admin/config/media/imce/profile/edit/1,此时可以看到很多的配置选项。
这里可以设置单个文件的大下,目录的大小限制,单个用户的文件总大小。以前曾经有朋友问过我类似的问题,就是为每个用户分配一个文件夹,这个文件夹是有大小限制的,和这里的很类似。
再往下,可以配置文件的扩展名,允许上传哪些文件;文件的最大尺寸限制;一个操作可以作用于多少个文件。我们这里采用默认的即可。
目录,对于用户1,默认使用跟目录,这个我们可以改一下,将它改为:
定义文件夹名字的时候,除了%uid可用以外,还可以使用PHP,比如“php: return 'users/'.$user->name;”,就定义了目录“users/USER-NAME”。我觉得这里可以改进一下,支持token,这样会更方便一些。
再往下,是定义好的缩略图,但是我不知道这和核心自带的图片样式是否有直接关系。
除了Profile的设置以外,在admin/config/media/imce页面,这里还有IMCE的常见设置:
一般情况下,我们采用默认的即可,我觉得这里比较有用的是绝对路径这个配置选项。
我们讲了这么多,现在还是无法上传图片的,我们把这个问题解决了,导航到admin/config/content/ckeditor,找到:
我们对“Advanced”进行编辑,在编辑页面,找到文件浏览器设置,展开后,可以看到
我们这里,在文件浏览器类型里面选择IMCE,保存即可。为CKEditor的Full profile做同样的操作。
如果使用IMCE,在所见即所得编辑器里面,点击图片按钮,我们将会看到这么一个弹出框:
点击这里的浏览服务器按钮,有一个探出框:
在这里,我们可以上传、编辑、删除、插入图片。
有一个小问题,是关于文本格式的,默认总是使用Filtered HTML,怎么才能将它改为Full HTML呢?
以前在Drupal6的时候,有这样一个模块Better Formats可以解决这个问题。在Drupal7下,为了解决这个问题,我首先想到的就是这个模块。但是后来,通过实践,我发现,不装这个模块,也可以配置出来。这里介绍一下我的解决办法,导航到admin/config/content/formats,在这里可以看到:
注意,这里,我们是可以通过拖拽调整文本格式之间的相对位置的。这是调整后的样子:
调整后,保存即可。这样“Full HTML”,就成为了默认的文本格式了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
有时候,我们创建区块的时候,想使用“Full HTML”,但是又不想使用CKEditor,因为CKEditor会帮我们做一些我们不想要的工作。此时,我们可以创建一个新的文本格式:
不为它启用任何的过滤器:
如果,我们不在CKEditor的配置界面,做相应的配置的话,当我们选用“区块专用Full HTML”文本格式的时候,就不会加载CKEditor。
这个在实际项目中,也是很有用的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们导航到admin/structure/types,在这里添加一个新的内容类型“关于我们”,我喜欢为菜单上的每个栏目创建一个内容类型。具体配置如下:
关于我们 |
|
机读名字 |
about_us |
发布选项 |
已发表/多语言禁用 |
展示设置 |
不显示 “作者和日期信息。” |
评论设置 |
关闭 |
菜单设置 |
主菜单 |
对于字段,我们使用默认的即可,现在导航到node/add/about-us,创建第一篇节点。在正文、标题里面输出相应的内容;文本格式选用Full HTML;
为这个节点指定一个菜单链接,菜单链接标题设置为“关于我们”;
将它的URL别名设置为“aboutus”。OK,保存。
这是保存后的效果:
是的,这是真实的内容,我们在搭建一个真实的网站。接下来,创建“我们的团队”节点,还是使用“关于我们”内容类型。对于这个节点,它的配置,和前面的基本一样。注意在设置菜单链接的时候,我们把它的上级菜单项设置为了“关于我们”(node/1)。
“我们的团队”,现在还只是一个静态页面,将来可以进一步的改进,比较加上团队成员的照片,采用更好的样式。如果将来公司发展起来了,人员比较多,可以专门的创建一个内容类型,比如说“员工(staff)”,用来列出员工的详细信息,而这个“我们的团队”页面,则可以替换成一个列表页面。这些都是后话。
这是创建好的样子。
创建“公司历程节点”:
接下来创建“联系我们”节点,这个节点里面包含一个表单。我们可以考虑使用webform模块,我这里使用的版本是webform-7.x-3.19。安装启用好了webform模块以后,我们就可以导航到node/add/webform,添加内容了。
首先,我们输入标题“联系我们”,正文里面输入以下内容:
“
<h3>地址:</h3>
北京市海淀区上地十街辉煌国际4号楼1308室。
地铁13号线,西二旗站下车,A口出,即辉煌国际大厦。
<h3>联系人:崔克俊 </h3>
Mobile: 13681445347
QQ: 372948992
”
文本格式选择“Full HTML”。指定一个菜单链接,并将上级菜单项设置为“关于我们”。这样就进入了webform的配置界面。
在这里面,添加一个表单组件(component),就像添加一个字段一样方便,所不同的是,这里的表单元素组件,不是字段。我们输入以下信息:
然后,点击“添加”按钮。就进入了这个字段的配置界面。
这里的标签,就是我们在前面输入的“您的姓名”;这里的Field Key,就是这个表单元素的机读名字,系统会根据标签尝试自动生成这个机读名字,我们这里输入的是中文,而且没有装“transliteration”模块,所以我们需要在这里手动的输入Field Key,我这里输入了“name”;默认值,就是这个表单元素的默认值,我们这里不设置默认值,留空。
在输入默认值的下面,有“Token values”(令牌值)字段集。展开,可以看到详细信息:
有时候,会用到这些。不过平时很少用。
再往下,是表单元素的描述,我们这里仍然留空,这里也有“Token values”可用。
再往下,是表单元素的验证规则:
这里面,有3个配置选项,必填、唯一、最大长度。我们这里只设置了必填。如果你觉得这里的验证规则不够用的话,可以下载安装Webform validation模块,我所写的Field validation模块,最初就是从这个模块迁移过来的。我们在实际的项目中,是用过Webform validation模块的。
最后是“显示”配置选项,上面的三个选项为:
这里面包括三项:宽度、前缀、后缀。从字面意思,就可以看出它的用途了。不过,对于表单元素的宽度控制,我比较喜欢使用CSS的方式,而不是在这里配置。
后面三项:
标签显示,里面包含“位于上方”、“行内”、“无”三个配置选项,配置过Drupal字段显示的用户,对这三个选项应该不会陌生,和字段里面的配置类似;禁用,就是使得这个字段无法编辑,有时候我们想设置一个默认值,而这个默认值又不想让用户修改的时候,这个时候非常有用;私有,只有具有结果访问权限的用户,才能查看私有字段。
我们保存这些配置,即可。接下来,添加“邮箱”,配置如下:
标签 |
邮箱 |
Field key |
|
类型 |
|
必填 |
选中 |
标签显示 |
行内 |
其它采用默认即可。
添加“标题”,配置如下:
标签 |
标题 |
Field key |
subject |
类型 |
Textfield |
必填 |
选中 |
标签显示 |
行内 |
添加“正文”,配置如下:
标签 |
正文 |
Field key |
body |
类型 |
Textfield |
必填 |
选中 |
Resizable(可调整大小) |
选中 |
隐藏标签 |
选中 |
其它采用默认即可。
这是添加好的样子:
前台的显示效果:
我们点击webform标签下面的“电子邮件”子标签,进入页面node/4/webform/emails。
我们在这里输入以下信息:
也就是输入站点的接收邮箱地址,这里使用了我自己的邮箱。点击添加按钮。进入页面node/4/webform/emails/new%3Foption%3Dcustom%26email%3Dg089h515r806%2540gmail.com。这里可以进一步的配置。
可以配置email的标题,这是我的配置:
可以配置发件人的邮箱地址,这是我的配置:
可以配置发件人的名字,这是我的配置:
当然,还可以配置邮件的正文,这是默认的模板:
这是我的配置:
有时候,收件人不仅仅有一个,所以这里可以添加更多的收件人地址。比如抄送给发件人。
现在,点击“Webform”标签下的“Form settings”子标签,进入页面node/4/webform/configure。
首先看到的是确认消息设置,这是我的配置:
上面的文本域中,可以输入具体的确认消息,无非是一些感谢的话;下面是文本格式,我们这里选择了Full HTML;再往下是重定向的地址,默认为确认页面,也可以自定义URL,当然,还可以在当前页面显示,如果在当前页面显示的话,确认消息是以Drupal消息的形式显示出来的。
再往下,是提交配置选项:
我们可以控制总提交次数,单位时间内可以有多少次提交,我们这里使用了默认的不限次数;也可以控制单个用户的单位时间内的总提交次数,我们这里使用了默认的不限;还可以设置这个表单的状态,开放还是关闭,这里当然是开放了,如果是一个问卷调查,有时间限制的,到了特定的时间,我们可以选择关闭表单。
再往下是控制哪些角色的用户可以提交这个表单:
我们这里选择了“匿名用户”和“注册用户”,这表示所有的用户都可以提交表单。
最后,是高级设置,我们点击这个字段集,展开里面的内容:
“Available as block”,表示将这个表单以区块的形式显示。
“Show complete form in teaser”,表示在节点摘要中显示完整的表单。
“Show "Save draft" button”,表示显示“保存草稿”按钮。
“Automatically save as draft between pages”,表示在多步表单中自动保存为草稿。
最后是提交按钮文本设置,我们这里输入“提交”两个字即可。很多人,使用Webform的时候,经常想修改这个提交按钮上的文本,但是不知道在哪里修改。在这里修改,记住了。另外一个常用的功能,就是以区块的形式显示,这个也经常用。
我们首先,来看这个企业站。这个网站,就是为我自己的公司做的,所以做好做坏都一样,没有特别大的压力。做网站,首先需要有个需求,你这个网站,有哪些页面组成,每个页面的结构,最好写个文档。然后由设计人员做出设计图,然后再由美工人员做出静态页面,最后将它们转到Drupal上来。
有个软件Axure,用来画网站的原型图,比较方便,我安装了一个试用版,画了几个原型图,这样设计人员、美工人员以这些原型图为参考,做静态页面的话,就比较方便了。
主要内容包括:产品列表、产品详细信息、新闻列表、新闻详细信息、联系我们、解决方案、首页等页面。将来根据需要会有一些的调整,总体来讲比较简单。
我们先来看首页,它主要包含以下内容:
(1),菜单是可以下拉的,鼠标移上,显示子菜单项。
(2),首页主导航下面是一个幻灯效果,4张图片轮播,包括一个THINK in Drupal中文资料的图片、Drupal培训班的图片、开源模块的图片、一个有关网上书店系统的图片。
(3),在下面是产品列表,三个推荐到首页的产品,包括图片,简单的描述。
(4)再往下,左边是新闻列表,右边是两个静态区块。
(5),最下面是页脚导航链接、页脚的版权信息等等。
我开始是这样想的,找个美工,做出静态页面,以这个为例子,写主题制作的内容,这样更完美一些,不过我找的美工朋友一直比较忙,没有时间。后来,我就以一个现成的例子来讲解主题制作,所以我们这里,就不用从静态页面转Drupal主题了。
我在实验的过程中,尝试了多个Drupal主题,比如omega、bootstrap、corporateclean、simplecorp。但是,我最终决定,仍然使用Zen,基于Zen实现我们的主题制作,学会多个,不如学精一个。虽然有很多人说,Omega是最有前途的,bootstrap是最炫的,corporateclean是最漂亮的拿来就可以用的;但是它们也是有缺点的,Omega的用户量毕竟没有Zen的多,bootstrap不支持IE7和IE8,在国外可以,在国内这是不行的,corporateclean里面的很多都是写死在里面的。一招鲜,吃遍天。
最有前途的Omega
干净漂亮的corporateclean
如果我们去阅读一下Zen的文档的话,发现里面有很多东西我们都没有用到,比如对HTML5的支持、对响应式的支持、对手机的支持,等等。从功能上来说,我觉得Zen和Omega差不多。我之所以不喜欢Omega的原因,就是它里面添加了一个新的概念section,没有具体的用过,所以干脆不用这个。做项目是需要控制风险的。
产品列表页面的原型图:
注意:上面的绿线,是我截图的时候,不小心加上去的,应该没有这条线的。上半部分和下半部分是连在一起的,我截图的时候,屏幕太小,只能分开截取。后面都是这样的,分成上下两部分。
这里面,左边是一个产品分类导航链接,包括三个分类:Think in Drupal、Drupal培训班、开源模块。我把自己写的模块也算作产品了,虽然不赚钱,但是也是自己的作品。将来这个产品分类有可能会调整。左边的这个区块,就是主菜单里面“产品”菜单项的子菜单项。我们这里面,可以考虑使用menu block模块实现这个功能。右边是一个产品列表,每个产品项都有三部分组成,图片、标题、摘要描述。
这里面,左边仍然是一个产品分类导航链接;右边是产品详细,里面包括产品的标题、图片、正文。如果有多张图片的话,可以加个幻灯效果。
联系我们页面的原型图:
在这个页面的左边,是“关于我们”下面的子菜单项,包括公司历程、团队建设、联系我们等等。右边是联系信息,联系信息下面,是一个表单,用户可以通过这个表单,发送邮件信息。这里,我们可以考虑使用webform模块。
除此之外,还有解决方案,新闻列表页面、新闻详细页面,我们采用相同的结构即可。这个企业网站搭建好了以后,然后再多语言化,也就是支持英文版的。
我下载了一个新的Drupal,解压缩后,放到了htdocs下面的yaiyuan1目录下面,这个名字后面加了一个1,是因为我前面已经做过一些测试用了,前面使用了yaiyuan这个名字。创建一个名为yaiyuan1的数据库,将Drupal安装起来。这个过程我们都熟悉。
接下来是,一些常见的配置,这些配置在Drupal实战一书里面都有介绍。比如,配置时区、配置日期格式、配置文件系统,特别是临时文件目录。
还有,就是安装Locale模块,然后启用简体中文,将简体中文设置为默认语言,导入简体中文的语言包。
我们将zen目录下的STARTERKIT复制到sites\all\themes下面,然后将其重命名为yaiyuan;将sites\all\themes\yaiyuan下面的STARTERKIT.info.txt重命名为yaiyuan.info,注意文件后缀名,对里面的信息做以下修改:
name = Yaiyuan
description = Yaiyuan theme.
打开template.php文件,将里面的STARTERKIT替换为yaiyuan;打开theme-settings. php文件,将里面的STARTERKIT替换为yaiyuan。这样我们的这个子主题就初步制作完成了。
导航到主题的管理界面,admin/appearance,将我们新建的这个主题启用并设置为默认主题。现在访问首页,看到样式还是乱的。
创建内容类型“首页幻灯”,并为它添加字段“幻灯图片”和“指向链接”,和Drupal实战里面的一样。
接着,安装Views slideshow模块。这个模块依赖于libraries模块,我们首先下载、安装、启用libraries模块,我这里使用的版本是libraries-7.x-2.1。接着安装Views Slideshow和它的子模块Views Slideshow: Cycle,这里使用的版本是views_slideshow-7.x-3.0。下载jquery.cycle,将其复制到了sites\all\libraries\jquery.cycle目录下面。
其实,我们这里涉及到一个软件复用问题,我们这里面的配置,在别的网站上已经配置好了,如何将它复制过来呢?这样,我们新建一个站点的时候,就不需要重复这里的配置了。在Drupal7下,最佳的解决方案是使用Features模块,比如这里的首页幻灯,我们就可以将它导出成feature。这样,新建一个Drupal站点的时候,通过安装这个Feature,所有的配置都会帮我们安装好了。对于内容类型的导入、导出,还有一个替代方案,就是使用Bundle copy,这个也不错,与Features模块相比,比较轻量级。
我们现在从网上书店中,将首页幻灯的视图导出:
导出后的样子:
我们将文本域中的代码复制下来,进入yaiyuan1站点的Views管理界面,这里有一个导入链接:
点击这个链接,进入页面admin/structure/views/import,我们将刚才复制的代码粘贴过来即可:
之后点击导入按钮。进入已导入视图的编辑界面,点击视图的保存按钮。导入过程完成。原来的视图中,图片的显示是使用了图片样式的,而这个样式我们这里没有定义,所以我们这里采用了默认的原始图片,这个没有关系。我们现在定义这个图片样式。
以前,我们在网上书店的例子中,是这样定义图片样式的,660_250,这种方式有这种方式的好处,就是说一看就知道图片的大小。我们这里换种方式。导航到图像样式的管理页面admin/config/media/image-styles,在这里添加一个样式frontbanner,专门用于首页幻灯。添加好这个样式后,我们暂时不为它添加任何效果。对于前面视图中,幻灯图片的显示,我们就可以为它使用这个图片样式了。
我们这里顺带介绍一下Features模块的用法。下载、安装、启用Features模块,当前使用版本为features-7.x-2.0-rc1。启用后,导航到admin/structure/features,这是Features模块的管理界面:
当前还没有任何已经创建的features,我们可以点击右上角的“创建特性”(create feature)链接,进入页面admin/structure/features/create。
在名称里面输入“frontbanner”,版本里面输入“7.x-1.0-beta1”,在右边的组件中,为内容类型、views、图像样式选择frontbanner对应的部分:
此时,Feature会自动的为我们选好字段、字段实例、依赖关系:
点击左边的“Download feature”(下载特性)按钮,我们就可以下载这个特性了。下载后,我们得到这样一个文件:
将它解压缩,我们得到一个完整的模块:
我们现在可以将这个模块安装到我们的站点上了,或者当一个站点需要幻灯时,我们就可以直接安装这个模块。
Features模块,能够很好的解决将配置信息从数据库中导出成Drupal代码,方便功能在不同站点之间的迁移。现在,在Features模块的基础之上,又出现了Apps模块,现在的很多发行版都基于Apps的概念。
我们继续前进,下载、安装、启用Panels模块,我用的版本是panels-7.x-3.3.zip。我同时下载、安装了panelizer模块,使用的版本是panelizer-7.x-3.1。Panelizer能够做很多Display suite模块做的工作,有机会的话,会简单的介绍一下Panelizer。我们这次启用以下模块:Page manager, Views content panes, Mini panels, Panelizer, Panels。注意Page manager, Views content panes是Ctools模块里面的子模块。
我们创建一个Panels页面,名字、路径都设置为frontpage,同时把这个页面设置为首页;布局采用1列通栏布局;禁用Drupal区块/区域;内容里面先后添加:首页幻灯、企业愿景、产品推荐、新闻等。
我已经做好了,先来看幻灯的效果:
首先是为图片样式frontbanner添加了一个缩放并裁切的效果,大小开始为950x300,后来调整为960x300。然后就是调整CSS,这里参考的是京东的效果。
#content .view-frontbanner .views-slideshow-controls-bottom {
float: right;
margin-top: -35px;
margin-right: 10px;
position: relative;
width: auto;
z-index: 9;
}
#content .view-frontbanner .views-slideshow-controls-bottom .views-slideshow-pager-fields {
float: right;
width: auto;
}
#content .view-frontbanner .views-slideshow-controls-bottom .views-slideshow-pager-field-item {
display: block;
float: left;
height: 20px;
margin-right: 10px;
padding: 0;
width: 20px;
}
#content .view-frontbanner .views-slideshow-controls-bottom .views-content-counter {
background: none repeat scroll 0 0 #999999;
border-radius: 12px 12px 12px 12px;
color: #FFFFFF;
cursor: default;
display: inline-block;
text-align: center;
height: 22px;
line-height: 22px;
width: 22px;
margin-right:20px;
margin-bottom: 10px;
}
#content .view-frontbanner .views-slideshow-controls-bottom .active .views-content-counter{
background: none repeat scroll 0 0 #E4393C;
}
主要是把背景设置为了圆形,这里面没有使用背景图片,使用的是border-radius。
再往下是企业愿景,这里使用了Panels的自定义内容功能,没有使用静态区块;还使用了Panel窗格自带的圆角样式:
可能部分读者不知道“自定义内容”在哪里,为Panels区域添加内容的时候,弹出框的左边下面部分的这个链接就是:
再往下是首页的产品列表,我在products视图里面创建了一个区块显示,用来显示首页的内容。我们先来看效果:
这是对应的视图显示:
这里使用了我们常见的Views字段,格式为Grid(格子),一行显示两个,总共显示4个。注意字段之间的先后顺序。我把图像放在了前面,其次是标题,最后是正文,这主要是为了调整CSS时方便;对于正文的输出,我们截取前面50个字;此外,我们还加了一个过滤条件,把产品分类中的开源模块部分过滤掉了。对应的CSS如下:
.view-display-id-block_1 td {
padding-right:20px;
}
.view-display-id-block_1 td .views-field-field-image{
float:left;
margin-right:10px;
}
.view-display-id-block_1 td .views-field-field-image img{
border: 1px solid #E0E0E0;
}
再往下是首页的新闻和联系我们:
我把这两部分放在了一个Mini Panel里面了。一个Mini Panel就是一个区块,只不过对于这个区块,我们也可以进一步的细化它的布局。
这是Mini panel的配置:
对于左边的这个新闻区块,我们使用的是test news这个视图,稍微做了一下调整,主要就是隐藏了时间、read more这两个字段,调整了一下正文截取的长度,这里取80个字。
我后来对主导航菜单的样式做了进一步的调整:
#navigation .sf-menu.sf-style-simple.sf-vertical li, #navigation .sf-menu.sf-style-simple.sf-horizontal li li, #navigation .sf-menu.sf-style-simple.sf-navbar li li li {
border-bottom: 1px solid #E0E0E0;
}
#navigation .sf-menu.sf-style-simple.sf-horizontal > li:first-child {
border-bottom-left-radius: 0px;
border-top-left-radius: 0px;
}
#navigation .sf-menu.sf-style-simple.sf-horizontal > li:last-child, #navigation .sf-menu.sf-style-simple.sf-horizontal li li > ul > li.firstandlast, #navigation .sf-menu.sf-style-simple.sf-vertical li li > ul > li.firstandlast, #navigation .sf-menu.sf-style-simple.sf-navbar li li li > ul > li.firstandlast {
border-bottom-right-radius: 0px;
border-top-right-radius: 0px;
}
#navigation .sf-menu.sf-style-simple li {
background-image : none;
height:45px;
}
主要是因为菜单项部分的背景图片高了一小点,把下面的幻灯图片遮住了一点。通过调整解决了这个问题。我有一个客户告诉我说,做Drupal开发,主要就是调CSS比较麻烦。说得很有道理。
最后说一个问题,前面我们在联系我们页面加了一个百度地图不是,现在这个地图显示不出来了:
原来可以显示,现在显示不了了。怎么办?我们可以把主题切换到默认的Bartik上去,结果可以正确显示。遇到问题,我们首先清除缓存,其次可能就是切换主题了。那现在怎么解决呢?需要在主题层解决。不过对于这个问题,我们不必深究。再变通一下,使用QQ截图功能,把生成的地图转成图片的形式,我们这里通过图片来显示地理位置,同样可以解决问题。
这样,我们的这个企业站,基本上就做完了。还有几个功能,比如客户、成功案例没有实现。此外产品列表页面,产品详细页面也需要美化一下,这些从技术上来讲,都已经讲过了。我们接下来,把精力放在多语言功能的实现上面来。
我们两次输出了主菜单,可以把默认的输出给禁用。导航到默认主题的配置界面,admin/appearance/settings/yaiyuan,找到配置选项:
将这两个禁用,保存。这样就只显示一次了。
我们配置这个区块,将区块的标题设置为“<none>”,这样就可以不显示“Main menu”两个字了。此外,将这个区块,放到“Navigation bar”区域里面。
导航到默认主题的配置界面,admin/appearance/settings/yaiyuan,找到配置选项:
将Logo禁用,我不想显示“禅”。现在整个界面,还是比较朴素的。
我们要调CSS样式。首先来调这个主菜单的样式,我们前面提到Superfish自带了,多个样式,我们配置这个superfish区块,将它的样式设置为“Simple”,保存。效果如下:
这是我最满意的一个效果了。有多满意,90%的都是我想要的。我是怎么知道使用“Simple”这个样式的呢?我是一个一个的测试出来的,它自带了这么多的样式:
我是从上到下,逐一测试后,感觉Simple这个样式,最符合我们这里的需要。除了一些常用的和我用过的模块以外,对于其余的模块,我并不比大家高明多少,很多时候就是这样,对于所有的配置选项,逐一测试,然后从中选出来自己认为最合适的。
此外,我觉得Spring这个样式,也还不错,只不过不是我这里想要的。
打开yaiyuan.info文件,找到CSS的部分,向里面追加这么一段话:
stylesheets[all][] = css/custom.css
然后在sites\all\themes\yaiyuan\css下面创建文件custom.css。我们将我们覆写的CSS代码都放在这个文件中。向里面添加以下代码:
#main #navigation #superfish-1 {
margin-left:300px;
}
#main #navigation{
background: url("../images/simple-background-active.png") repeat-x scroll left top #EEEEEE;
border-bottom-right-radius: 8px;
border-top-right-radius: 8px;
border-bottom-left-radius: 8px;
border-top-left-radius: 8px;
-moz-border-bottom-colors: none;
-moz-border-left-colors: none;
-moz-border-right-colors: none;
-moz-border-top-colors: none;
border-color: #E0E0E0 #E0E0E0 #E0E0E0 #FFFFFF;
border-image: none;
border-style: solid;
border-width: 1px;
height:51px;
}
我们还需要把simple-background-active.png图片,从sites\all\libraries\superfish\style\simple\images目录下,复制到sites\all\themes\yaiyuan\images。我原来还以为这个图片是放在modules目录下面的,找了一下没有找到,通过Firebug,查看CSS代码的出处,才找到了这里。
其实,我就是想让主导航居中显示,同时为整个导航条加个背景。也就是在默认的基础上,再美化一点而已。我第一次见到border-bottom-right-radius的用法,以前真不知道可以这样设置圆角。这是调整后的样子。
做到这里的时候,主题的响应式已经被我破坏掉了,所以我们还采用固定宽度布局,960,这种最常见的布局。
打开yaiyuan.info文件,找到CSS里面的这段代码:
stylesheets[all][] = css/layouts/responsive-sidebars.css
将它注释掉,然后在它的下面,加上:
stylesheets[all][] = css/layouts/fixed-width.css
然后将主导航的margin-left从300改为250。先这样吧,后面会继续调整样式。
我们来看另一个功能,当我们访问产品页面products,希望左边显示一个区块,这个区块里面列出产品分类的链接。就是把这部分内容:
同时显示在左边栏。这个是一个非常常见的需求,在很多企业站里面,随处可见。Drupal里面,有一个现成的模块,可以做这件事。这就是Menu Block模块。
我们下载、安装、启用这个模块,我这里使用的版本是menu_block-7.x-2.3。启用后,导航到区块的管理界面admin/structure/block,此时会看到这样的一个链接:
我们点击“Add menu block”链接,创建一个菜单区块。
这是菜单区块的基本配置选项,里面包括选用哪个菜单,开始的层级,最大深度。如果点击上面的高级选项标签,将会显示更多的配置选项,比如:
对于这些,我们采用默认的即可。我们将这个新建的区块放在第一边栏区域里面,保存。
这里的“主菜单 (levels 1+)”就是我们新建的菜单区块。我们把这个区域里面的其它区块禁用,我们不想把它们显示在这里。现在,访问产品页面products,左边是这个样子。
和预期的有区别。我们对这个区块进行再配置,将开始层级改为2:
这下好了:
访问新闻页面,我们得到:
访问“关于我们”页面,访问“解决方案”页面,左边都做相应的变化。这就是我们想要的,只不过样式需要调整。
以前的时候,我是怎么解决这个问题的呢?为产品、新闻、关于我们、解决方案,分别创建一个菜单,然后将这个菜单对应的区块放在左边栏,根据区块的可见性设置,控制每个区块的显示,也能够实现同样的效果。但是都没有我们这里的这种方式简洁。
如果,我们多点几下的话,还是会发现更多一些的问题的。比如我们访问产品分类页面products/1,products/2,products/3,这些都是没有问题的,左边都会显示。
如果,我们点击进入一个产品节点,或者一个新闻节点的时候,左边的这个区块消失了。左边栏(第一边栏)没有了。
这个不是我们想要的,如果采用前面我们所说的土办法,没有任何问题,不过现在就不行了。怎么解决这个问题呢?此时我们可以尝试一下Menu Position模块。
我们下载、安装、启用Menu Position模块,我这里使用的版本是menu_position-7.x-1.1。安装后,我们可以导航到它的管理界面admin/structure/menu-position。
我们点击这里的添加链接,来添加一个“菜单位置规则”(menu position rule)。这样就进入了页面admin/structure/menu-position/add:
首先看到的是管理标题和父菜单项。
接着看到的是条件设置,也就是在什么条件下激活上面选中的菜单项。这里又分5种情况,内容类型、页面、用户角色、语言、分类。
我们先测试一个简单的,在管理标题里面输入“产品”,父菜单项选择“产品”,内容类型里面选中“产品”,保存这个规则。现在访问一个产品节点,左边的区块显示了出来:
只不过,除了显示出来以外,还多显示了一些信息,竟然把节点标题也显示在左边的区块里面了。
我们回到admin/structure/menu-position,看右上角,有两个标签链接:
点击这里的“设置”标签,进入页面admin/structure/menu-position/settings:
这里只有这么一个配置,里面包含三个选项:把当前页面的标题插入到菜单树中;把规则的父菜单项标记为“active”;不为任何菜单项标记“active”。默认选中的是第一项,我们把它改为第二项,保存。这样访问刚才的页面,问题解决了:
如果细心一下,可以使用Firebug查看一下页面源代码,此时我们可以看到,主菜单里面的“产品”菜单项,里面多了一个类“active”。
这是Menu position模块追加进来的。我们这里只是没有为active定义样式而已,在实际项目中,访问一个产品节点页面,激活产品列表页面对应的菜单项,这个需求也很常见,Menu position模块就是用来解决这个问题的。
我们现在再进一步,当访问节点“Drupal培训班--------Drupal主题制作”(node/9)时,我们不去激活“产品”菜单项,而是去激活它的子菜单项“Drupal培训班”(products/2)。
我们创建一个新的menu position规则,在管理标题中输入“Drupal培训班”,在父菜单项中选择“Drupal培训班”,内容类型中选择“产品”,我们这里除了内容类型以外,还设置一下分类,具体配置如下:
设置好了以后,保存。重新访问node/9页面。没有任何变化,和原来一样。这个时候我猜测,是前面创建的规则在起作用。所以可以把它禁用(或者删除)了先。
在规则列表中,有“启用”这么一列,取消选中,保存即可。现在访问node/9,一切正常了:
通过Firebug可以看到这个菜单项的链接上面,有了active:
这里值得一提的是,面包屑也正常了,就是我们想要的。我们已经知道具体的实现方法了。接下来,重复这一过程。将产品下面剩余的两个分类和新闻下面的三个分类,都创建类似的规则。这是添加好的样子:
经测试,新闻节点页面,左边区块正常,面包屑正常:
这里顺便说一下,北大图书馆的网站,也是使用的Drupal,左边区块也是使用的Menu Block,但是他们没有使用menu position模块,比如他们的图书馆新闻节点页面,就无法显示左边区块。
我点击多个页面,所有的面包屑都是正确设置了的,这个时候,我们就不需要去安装Custom breadcrums, path breadcrumbs这样的模块了。
在当前主题的配置页面,admin/appearance/settings/yaiyuan,里面有面包屑的配置选项:
这是主题层提供的配置选项,包括是否显示面包屑,面包屑的分隔符,在面包屑中显示首页链接,在面包屑的最后,追加一个分隔符,在面包屑的结尾追加内容标题。我们把最后这个复选框选中,保存。
做企业网站的时候,比如对于产品列表页面,很多客户有这样的需求,想控制列表中的产品的位置,能够随心所欲的控制先后顺序,把重要的产品显示在前面。比如对于公司的团队成员,如果每个成员对应一个节点页面的话,那么对于成员的列表页面,这个先后顺序也是很重要的,大领导排在前面,小领导排在后面。这个时候,就无法按照节点的发布日期、修改日期排序了。
这一问题的解决方案很多,常见的有Weight模块、DraggableViews模块、Nodequeue模块。我们这里面介绍Nodequeue模块,我在实际的项目中,使用过这个模块,而且这个模块的安装量比前面两个要高出一些。
我们下载、安装、启用Nodequeue模块,我这里使用的版本是nodequeue-7.x-2.0-beta1。启用后,我们导航到它的管理界面admin/structure/nodequeue,
点击这里的添加链接“Add simple queue”,进入页面,admin/structure/nodequeue/add/nodequeue,这是我的配置:
其它采用默认配置。保存。现在,当我们访问一个产品节点的时候,就会看到一个nodequeue标签:
而在这个节点的下面,我们可以看到这样的一个链接:
点击这个链接,就会刷新这个页面,当前节点就会被添加到我们创建的队列中来。此时,这里的链接就变成了:
现在点击“Nodequeue”标签,进入页面node/6/nodequeue,我们会看到:
也可以在这个标签页面下面,把节点添加到队列中或者从队列中删除。我们把所有的产品节点,都添加到我们创建的产品队列中来。现在查看这个产品队列。我们看到:
此时,可以通过拖动,来调整节点之间的相对位置,这个我们就比较熟悉了。此外,在这个页面,还有下面的这些按钮:
Reverse表示反向的意思,shuffle表示随机的意思。
现在,导航到Views的管理界面,我们可以看到与Nodequeue相关的视图。
每当创建一个队列的时候,系统会自动的创建一个视图。我们编辑一下这个视图,看一下它有什么特别的配置。有两个地方需要注意:
首先是关联关系这里,加了一个从节点到队列的关联关系。其次是排序标准:
明白了这两点以后,我们完全可以不使用这个视图,而采用修改原有视图的办法。我们编辑视图products,为它添加关联关系“Nodequeue: Queue”,做以下配置:
为它添加排序标准“Nodequeue: Position”,并按照升序排列,删除原来的排序标准“内容: Post date (desc) ”。保存视图。现在产品列表页面中,产品就会按照我们队列中的先后位置排序了。
每当创建一个队列的时候,会自动的创建一个视图,如果你觉得自动创建的这些视图没有什么用处的话,我们可以取消这个自动创建视图的功能。在Nodequeue的配置页面,admin/structure/nodequeue/settings:
取消对最后一个复选框的选中即可。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
现在,我们简单的调整一下左边区块的样式,向custom.css里面添加以下CSS代码:
#main #block-menu-block-1 h2{
background: none repeat scroll 0 0 #E9E9E9;
border-bottom: 1px solid #D5D5D5;
border-top: 1px solid #F2F2F2;
font-size: 100%;
font-weight: bold;
text-align: center;
}
#main #block-menu-block-1 ul li {
list-style-image: none;
list-style-type: none;
}
#main #block-menu-block-1 ul li a {
color: #222222;
text-decoration: none;
}
#main #block-menu-block-1 ul li a.active {
color: #0041A0;
}
调整后的效果:
不是很专业,但是比没有样式的时候,好了很多。
对面包屑和标题的样式,也做一下调整,添加以下CSS代码:
#main #content .breadcrumb{
margin-top: 20px;
margin-left: 20px;
margin-bottom: 10px;
}
#main #content #page-title{
text-align: center;
margin-top: 20px;
margin-bottom: 20px;
}
#main a {
text-decoration: none;
}
调整后,效果如下:
现在,页脚区域还是比较单薄了,除了“Powered by Drupal”以外,什么都没有。我们首先创建一个“Footer menu”菜单,并为它添加以下菜单项:
接着,导航到区块管理界面,添加一个静态区块“版权所有”,并将它放到页脚区域里面。将“Footer menu”菜单对应的区块,也放到页脚区域中。将“Powered by Drupal”区块禁用。去除“Footer menu”和“版权所有”的区块标题。现在是这个样子的:
而且是居左显示。我们需要在样式上做出调整。向custom.css文件中添加以下代码:
#footer{
background-color: #F6F6F2;
font-size: 0.92307em;
line-height: 1.5em;
padding: 30px 0 20px;
}
#footer .block{
color: #666666;
text-align: center;
}
#footer ul.menu {
margin-bottom: 10px;
overflow: hidden;
}
#footer ul {
clear: both;
list-style: none outside none;
text-align: center;
padding: 0 0 0 0px;
}
#footer li {
display: inline;
}
#footer a {
font-size: 14px;
margin: 0 10px;
text-decoration: none;
}
#footer li.leaf {
list-style-image: none;
list-style-type: none;
margin: 0;
padding: 0;
}
此外,对于版权所有区块,我们为它加上两个<p>标签。
上面的那段CSS代码其实是从网上书店那个例子中复制过来的。最后,样式基本符合预期的要求:
说到站点地图了,其实我们可以创建一个普通的page页面,手工的编辑好站点地图后,放进去。还有一个办法,是安装Site map模块,由这个模块生成站点地图页面。生成好了以后,我们可以将这个页面上面的内容复制下来,然后粘贴到一个静态页面中来,这样我们就可以禁用这个模块了。
我们下载、安装、启用Site map模块,我这里使用的版本是site_map-7.x-1.0。导航到admin/config/search/sitemap,这是我的配置:
其余的采用默认配置。Site map模块能够基于菜单、分类生成站点地图,在生成站点地图时,我们可以选择包含哪些菜单、哪些分类,对于分类的话,可以进一步的控制。我们这里是基于主菜单生成的站点地图。这个时候访问页面sitemap,就能够看到站点地图了。我们的站点地图是根据主菜单生成的。显示出来的效果是这样的:
在这个页面的左边,没有区块,所以通栏只有主内容,有点单薄。此外,上面的“Main menu”也不是我们想要的。现在,我们可以将这个页面的链接内容复制下来,创建一个page类型的节点,复制过去,保存。然后将站点地图这个菜单链接的URL指向新建节点。禁用Site map模块。
我们创建“联系我们”区块,将它放在第一边栏里面,区块的正文中输入以下信息:
保存后,调整一下CSS代码:
#main #block-block-2 {
border-right: 1px dashed #B4BDC4;
margin-top: 20px;
}
#main #block-block-2 h2{
background: none repeat scroll 0 0 #E9E9E9;
border-bottom: 1px solid #D5D5D5;
border-top: 1px solid #F2F2F2;
font-size: 100%;
font-weight: bold;
text-align: center;
}
#main #block-block-2 ul li {
font-size:12px;
}
这是调整后的样子:
这个基本满意了,我看到站点名字“亚艾元”三个字比较小,我们这里也没有使用Logo图片,不够显眼。这里通过CSS将字体调的更大一点:
#header #site-name {
font-size: 4em;
margin-left:20px;
}
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
整个网站,我已经基本满意了,唯一的不足是字体的配置比较灰,这个不影响功能。除此以外,还差什么呢?首页还没有做呢。是的,做网站的时候,可以先做首页,也可以放在后面。首页用什么?我还是用Panels。幻灯用什么?我还是用Views slideshow。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
很多人,刚学Drupal的时候,常常会问这样的问题,Drupal是怎么生成这个页面的。比如,我们创建了第一个Drupal节点页面,“关于我们”,很多人,想知道这个页面是怎么组装起来的。想知道大致的流程。
问这个问题的人很多,所以每当我给人讲解Drupal模块开发的时候,首先带领学生分析的,就是这个节点页面的组装过程。有的人听着听着,就睡着了,而有的人则会听得津津有味。我们这里所讲的这些东西,就是这样,对你使用Drupal没有直接的帮助,但是能够帮助你更好的理解Drupal背后的机理,帮你建立学习Drupal的信心。
这就是我们创建的第一个节点页面,除了节点本身,还有导航链接,左边栏的区块,页脚的区块,这些东西共同的组成了一个页面。这个节点的路径为node/1,我们也可以使用别名aboutus。
我们来看阶段7,也就是完成阶段,对应的代码:
case DRUPAL_BOOTSTRAP_FULL:
require_once DRUPAL_ROOT . '/includes/common.inc';
_drupal_bootstrap_full();
break;
在这里,首先加载includes/common.inc文件,接着将这个阶段的工作,委托给了_drupal_bootstrap_full函数,注意这个函数位于common.inc文件中。我们打开这个文件,找到这个函数的定义:
function _drupal_bootstrap_full() {
static $called = FALSE;
if ($called) {
return;
}
$called = TRUE;
require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc');
require_once DRUPAL_ROOT . '/includes/theme.inc';
require_once DRUPAL_ROOT . '/includes/pager.inc';
require_once DRUPAL_ROOT . '/' . variable_get('menu_inc', 'includes/menu.inc');
require_once DRUPAL_ROOT . '/includes/tablesort.inc';
require_once DRUPAL_ROOT . '/includes/file.inc';
require_once DRUPAL_ROOT . '/includes/unicode.inc';
require_once DRUPAL_ROOT . '/includes/image.inc';
require_once DRUPAL_ROOT . '/includes/form.inc';
require_once DRUPAL_ROOT . '/includes/mail.inc';
require_once DRUPAL_ROOT . '/includes/actions.inc';
require_once DRUPAL_ROOT . '/includes/ajax.inc';
require_once DRUPAL_ROOT . '/includes/token.inc';
require_once DRUPAL_ROOT . '/includes/errors.inc';
// Detect string handling method
unicode_check();
// Undo magic quotes
fix_gpc_magic();
// Load all enabled modules
module_load_all();
// Make sure all stream wrappers are registered.
file_get_stream_wrappers();
$test_info = &$GLOBALS['drupal_test_info'];
if (!empty($test_info['in_child_site'])) {
// Running inside the simpletest child site, log fatal errors to test
// specific file directory.
ini_set('log_errors', 1);
ini_set('error_log', 'public://error.log');
}
// Initialize $_GET['q'] prior to invoking hook_init().
drupal_path_initialize();
// Let all modules take action before the menu system handles the request.
// We do not want this while running update.php.
if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
// Prior to invoking hook_init(), initialize the theme (potentially a custom
// one for this page), so that:
// - Modules with hook_init() implementations that call theme() or
// theme_get_registry() don't initialize the incorrect theme.
// - The theme can have hook_*_alter() implementations affect page building
// (e.g., hook_form_alter(), hook_node_view_alter(), hook_page_alter()),
// ahead of when rendering starts.
menu_set_custom_theme();
drupal_theme_initialize();
module_invoke_all('init');
}
}
这个函数里面做的第一个工作,就是加载文件,path.inc、theme.inc、pager.inc、menu.inc、tablesort.inc、file.inc、unicode.inc、image.inc、form.inc、mail.inc、actions.inc、ajax.inc、token.inc、errors.inc就是在这个时候加载的。
unicode_check用来判断字符串处理方法,Drupal能够处理各种字符,这里需要检查一下PHP对unicode的支持。fix_gpc_magic,这个函数比较陌生,我后来查看函数的具体定义,和相关的文档,才弄明白的这个函数的作用,这个函数最终调用的是stripslashes,它的作用是去除转义反斜线,比如:
(\') 转换为(')。
双反斜线(\\)被转换为单个反斜线(\)。
module_load_all负责加载所有的module文件。file_get_stream_wrappers负责注册Drupal的流包装器(stream wrappers)。
$test_info这段代码,与simpletest有关,我们这里没有用到,所以跳过。
drupal_path_initialize负责路径的初始化,具体工作就是正确的设置$_GET['q']。
最后设置当前的主题,初始化主题,触发hook_init钩子。到此,整个引导指令就全部完成了,Drupal已经启动起来了。
Drupal启动以后,接下来做什么?我们回到index.php文件,接下来执行的是menu_execute_active_handler函数,就执行了这么一个函数,就完了。我们现在要做的是分析这个函数,看看在这个函数里面,Drupal具体做了什么。这个函数以menu开头,所以我推测它位于includes里面menu.inc文件中。我们也可以使用Google,搜索一下这个函数,这样会印证我前面的猜测。其实第一次的时候,我也不知道,也是Google出来的。
打开menu.inc文件,找到menu_execute_active_handler函数,阅读这个函数的源代码。
/**
* Execute the page callback associated with the current path.
*
* @param $path
* The drupal path whose handler is to be be executed. If set to NULL, then
* the current path is used.
* @param $deliver
* (optional) A boolean to indicate whether the content should be sent to the
* browser using the appropriate delivery callback (TRUE) or whether to return
* the result to the caller (FALSE).
*/
function menu_execute_active_handler($path = NULL, $deliver = TRUE) {
// Check if site is offline.
$page_callback_result = _menu_site_is_offline() ? MENU_SITE_OFFLINE : MENU_SITE_ONLINE;
// Allow other modules to change the site status but not the path because that
// would not change the global variable. hook_url_inbound_alter() can be used
// to change the path. Code later will not use the $read_only_path variable.
$read_only_path = !empty($path) ? $path : $_GET['q'];
drupal_alter('menu_site_status', $page_callback_result, $read_only_path);
// Only continue if the site status is not set.
if ($page_callback_result == MENU_SITE_ONLINE) {
if ($router_item = menu_get_item($path)) {
if ($router_item['access']) {
if ($router_item['include_file']) {
require_once DRUPAL_ROOT . '/' . $router_item['include_file'];
}
$page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
}
else {
$page_callback_result = MENU_ACCESS_DENIED;
}
}
else {
$page_callback_result = MENU_NOT_FOUND;
}
}
// Deliver the result of the page callback to the browser, or if requested,
// return it raw, so calling code can do more processing.
if ($deliver) {
$default_delivery_callback = (isset($router_item) && $router_item) ? $router_item['delivery_callback'] : NULL;
drupal_deliver_page($page_callback_result, $default_delivery_callback);
}
else {
return $page_callback_result;
}
}
这个函数,执行当前路径的对应页面回调,向浏览器返回具体的页面。这个函数包含两个参数$path和$deliver,不过这两个参数都是可选的;在index.php里面调用这个函数的时候,也没有传递参数,所以这两个参数现在是默认值。
第一行代码,是检查当前站点是否是在线状态。Drupal支持离线状态,什么时候用到离线状态,网站升级的时候,通常把站点配置为离线状态。离线配置地址位于admin/config/development/maintenance。
我们这里访问的node/1,当前状态为在线状态。_menu_site_is_offline是一个帮助函数,用来检查站点是否离线。
接下来的代码:
drupal_alter('menu_site_status', $page_callback_result, $read_only_path)
用来允许其它模块修改站点状态,这里面只允许修改站点状态,不允许修改当前路径。如果要修改路径的话,可以使用hook_url_inbound_alter。
再往下是if语句,我们的站点当前为在线,所以进入了if语句里面,里面还是一个if语句:
if ($router_item = menu_get_item($path)) {
在我们这里,$path的值为NULL,menu_get_item会获取当前路径对应的菜单项。当前路径为node/1,对应的菜单项node/%,这个是在node_menu里面定义的,我们打开node.module文件,找到node_menu,找到node/%对应的菜单项。对应的定义如下:
$items['node/%node'] = array(
'title callback' => 'node_page_title',
'title arguments' => array(1),
// The page callback also invokes drupal_set_title() in case
// the menu router's title is overridden by a menu link.
'page callback' => 'node_page_view',
'page arguments' => array(1),
'access callback' => 'node_access',
'access arguments' => array('view', 1),
);
menu_get_item里面返回的信息,和hook_menu里面定义的菜单项,存在着一一对应的关系,当然menu_get_item里面包含更多一些属性。
此时,$router_item不为空,所以我们继续往下执行。接下来检查路由项的访问权限,看当前用户是否有权限访问这个页面。
if ($router_item['access'])
我们这里是有权限的,所以继续执行里面的代码:
if ($router_item['include_file']) {
require_once DRUPAL_ROOT . '/' . $router_item['include_file'];
}
$page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
$router_item['include_file']指的是什么?我们看node_menu,找到下面的代码:
$items['node/%node/edit'] = array(
'title' => 'Edit',
'page callback' => 'node_page_edit',
'page arguments' => array(1),
'access callback' => 'node_access',
'access arguments' => array('update', 1),
'weight' => 0,
'type' => MENU_LOCAL_TASK,
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
'file' => 'node.pages.inc',
);
$items['node/%node/delete'] = array(
'title' => 'Delete',
'page callback' => 'drupal_get_form',
'page arguments' => array('node_delete_confirm', 1),
'access callback' => 'node_access',
'access arguments' => array('delete', 1),
'weight' => 1,
'type' => MENU_LOCAL_TASK,
'context' => MENU_CONTEXT_INLINE,
'file' => 'node.pages.inc',
);
这里的’file’键,就对应于$router_item['include_file'],如果路由里面指定了这个文件,Drupal在执行回调函数之前,会尝试加载这个文件。比如我们访问node/1/edit,对应的菜单项为'node/%node/edit',在菜单项的定义里面,存在'file' => 'node.pages.inc',当程序执行到这里的时候,Drupal就会尝试加载文件node.pages.inc。
这样做有什么好处?好处,就是将页面回调函数的逻辑代码,从module文件中分离出来,使得module文件尽可能的小。Drupal是很吃内存的,其中的一个重要的原因,就是Drupal启动后,会加载所有的module文件,如果每个module文件都比较小的话,那么消耗的内存就比较小。我们在think in Drupal的第一集里面,介绍过这个问题,我们今天通过阅读Drupal核心的源代码,进一步了解到,核心的具体实现办法。
下面来看这句抽象的代码:
$page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
首先我们需要知道,$router_item['page_callback']和$router_item['page_arguments']的值在当前情况下是什么。现在的路径为node/1。它对应的:
$router_item['page_callback'] : node_page_view
$router_item['page_arguments'] : array(1)
注意,这里的array(1),表示arg(1),Drupal向node_page_view传递参数的时候,会做以下工作:
$nid = arg(1);
$node = node_load($nid);
对于node/1/edit,arg(0)就是node,arg(1)就是1,arg(2)就是edit,依次类推。这个参数的传递过程,是Drupal菜单系统的一种特有机制,我们了解就可以了,知道这里传递过来的是$node对象。
我们把前面抽象的函数,换成具体的情况:
$page_callback_result = call_user_func_array(‘node_page_view’, array($node));
call_user_func_array是一个PHP函数,我刚开始看到这个函数的时候,感到头大。查了一下文档,明白了它是用来动态的调用函数的。上面的这行代码,就等价于:
call_user_func_array(‘node_page_view’, array($node)) = node_page_view($node);
实际上,就是这样的:
$page_callback_result = node_page_view($node);
这是当路径为node/1时的情况。当路径为node/1/edit的时候,执行到这里,实际情况是这样的:
$page_callback_result = node_page_edit($node);
这里具体执行的这个回调函数,是动态的,路径不一样,对应的回调函数也不一样。
现在让我们来看一下node_page_view返回了什么,我们在node.module里面找到这个函数。
/**
* Menu callback: Displays a single node.
*
* @param $node
* The node object.
*
* @return
* A page array suitable for use by drupal_render().
*
* @see node_menu()
*/
function node_page_view($node) {
// If there is a menu link to this node, the link becomes the last part
// of the active trail, and the link name becomes the page title.
// Thus, we must explicitly set the page title to be the node title.
drupal_set_title($node->title);
$uri = entity_uri('node', $node);
// Set the node path as the canonical URL to prevent duplicate content.
drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE);
// Set the non-aliased path as a default shortlink.
drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
return node_show($node);
}
这里面具体复杂的是node_show($node),我们来看node_show的定义:
/**
* Generates an array which displays a node detail page.
*
* @param $node
* A node object.
* @param $message
* A flag which sets a page title relevant to the revision being viewed.
*
* @return
* A $page element suitable for use by drupal_render().
*/
function node_show($node, $message = FALSE) {
if ($message) {
drupal_set_title(t('Revision of %title from %date', array('%title' => $node->title, '%date' => format_date($node->revision_timestamp))), PASS_THROUGH);
}
// For markup consistency with other pages, use node_view_multiple() rather than node_view().
$nodes = node_view_multiple(array($node->nid => $node), 'full');
// Update the history table, stating that this user viewed this node.
node_tag_new($node);
return $nodes;
}
这里返回的是一个数组,Drupal将具体的实现又交给了node_view_multiple,只不过当前只有一个节点。我们来看node_view_multiple:
/**
* Constructs a drupal_render() style array from an array of loaded nodes.
*
* @param $nodes
* An array of nodes as returned by node_load_multiple().
* @param $view_mode
* View mode, e.g. 'full', 'teaser'...
* @param $weight
* An integer representing the weight of the first node in the list.
* @param $langcode
* (optional) A language code to use for rendering. Defaults to NULL which is
* the global content language of the current request.
*
* @return
* An array in the format expected by drupal_render().
*/
function node_view_multiple($nodes, $view_mode = 'teaser', $weight = 0, $langcode = NULL) {
field_attach_prepare_view('node', $nodes, $view_mode, $langcode);
entity_prepare_view('node', $nodes, $langcode);
$build = array();
foreach ($nodes as $node) {
$build['nodes'][$node->nid] = node_view($node, $view_mode, $langcode);
$build['nodes'][$node->nid]['#weight'] = $weight;
$weight++;
}
$build['nodes']['#sorted'] = TRUE;
return $build;
}
这里返回的是一个数组,包含节点的数组,可以使用drupal_render呈现这个数组。而这里,单个节点的构件,则是由node_view完成的。
/**
* Generates an array for rendering the given node.
*
* @param $node
* A node object.
* @param $view_mode
* View mode, e.g. 'full', 'teaser'...
* @param $langcode
* (optional) A language code to use for rendering. Defaults to the global
* content language of the current request.
*
* @return
* An array as expected by drupal_render().
*/
function node_view($node, $view_mode = 'full', $langcode = NULL) {
if (!isset($langcode)) {
$langcode = $GLOBALS['language_content']->language;
}
// Populate $node->content with a render() array.
node_build_content($node, $view_mode, $langcode);
$build = $node->content;
// We don't need duplicate rendering info in node->content.
unset($node->content);
$build += array(
'#theme' => 'node',
'#node' => $node,
'#view_mode' => $view_mode,
'#language' => $langcode,
);
// Add contextual links for this node, except when the node is already being
// displayed on its own page. Modules may alter this behavior (for example,
// to restrict contextual links to certain view modes) by implementing
// hook_node_view_alter().
if (!empty($node->nid) && !($view_mode == 'full' && node_is_page($node))) {
$build['#contextual_links']['node'] = array('node', array($node->nid));
}
// Allow modules to modify the structured node.
$type = 'node';
drupal_alter(array('node_view', 'entity_view'), $build, $type);
return $build;
}
这里我们可以看一下,这个数组的具体结构,如粗体代码所示。node_build_content,这个我们就不往下追踪了,有兴趣的可以继续往下分析下去。
曾经有人在这里,问过我一个这样的问题,我怎么没有看到SQL语句啊?我们前面讲了,在向node_page_view传递参数之前,调用了node_load函数,将节点ID转为了节点对象。我们来看一下node_load函数的定义。
/**
* Loads a node object from the database.
*
* @param $nid
* The node ID.
* @param $vid
* The revision ID.
* @param $reset
* Whether to reset the node_load_multiple cache.
*
* @return
* A fully-populated node object, or FALSE if the node is not found.
*/
function node_load($nid = NULL, $vid = NULL, $reset = FALSE) {
$nids = (isset($nid) ? array($nid) : array());
$conditions = (isset($vid) ? array('vid' => $vid) : array());
$node = node_load_multiple($nids, $conditions, $reset);
return $node ? reset($node) : FALSE;
}
它将单个节点的加载委托给了node_load_multiple,只不过这里传递过来一个节点ID而已。我们往下看node_load_multiple。
/**
* Loads node entities from the database.
*
* This function should be used whenever you need to load more than one node
* from the database. Nodes are loaded into memory and will not require database
* access if loaded again during the same page request.
*
* @see entity_load()
* @see EntityFieldQuery
*
* @param $nids
* An array of node IDs.
* @param $conditions
* (deprecated) An associative array of conditions on the {node}
* table, where the keys are the database fields and the values are the
* values those fields must have. Instead, it is preferable to use
* EntityFieldQuery to retrieve a list of entity IDs loadable by
* this function.
* @param $reset
* Whether to reset the internal node_load cache.
*
* @return
* An array of node objects indexed by nid.
*
* @todo Remove $conditions in Drupal 8.
*/
function node_load_multiple($nids = array(), $conditions = array(), $reset = FALSE) {
return entity_load('node', $nids, $conditions, $reset);
}
node_load_multiple又将具体的实现,委托给了entity_load。我们通过Google搜索一下entity_load,发现这个函数位于includes/common.inc里面,这让人有点意外。Fago对这一点就提出了批评,说entity的函数放的哪都是,Drupal8在这方面做了改进,Fago的有关实体的想法进入了内核。
不过这里面仍然没有SQL语句。我们在这个api.drupal.org上,在代码的上面,有这么一部分:
我们点击DrupalDefaultEntityController,进入页面https://api.drupal.org/api/drupal/includes%21entity.inc/class/DrupalDefaultEntityController/7,找到:
点击这里的DrupalDefaultEntityController::load。阅读这个成员函数,在代码里面找到:
我们看到SQL语句的构建,是由DrupalDefaultEntityController::buildQuery完成的,我们点击这个成员函数,查看对应的源代码,这里面,我们看到了SQL语句:
protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
$query = db_select($this->entityInfo['base table'], 'base');
$query->addTag($this->entityType . '_load_multiple');
if ($revision_id) {
$query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(':revisionId' => $revision_id));
}
elseif ($this->revisionKey) {
$query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
}
// Add fields from the {entity} table.
$entity_fields = $this->entityInfo['schema_fields_sql']['base table'];
if ($this->revisionKey) {
// Add all fields from the {entity_revision} table.
$entity_revision_fields = drupal_map_assoc($this->entityInfo['schema_fields_sql']['revision table']);
// The id field is provided by entity, so remove it.
unset($entity_revision_fields[$this->idKey]);
// Remove all fields from the base table that are also fields by the same
// name in the revision table.
$entity_field_keys = array_flip($entity_fields);
foreach ($entity_revision_fields as $key => $name) {
if (isset($entity_field_keys[$name])) {
unset($entity_fields[$entity_field_keys[$name]]);
}
}
$query->fields('revision', $entity_revision_fields);
}
$query->fields('base', $entity_fields);
if ($ids) {
$query->condition("base.{$this->idKey}", $ids, 'IN');
}
if ($conditions) {
foreach ($conditions as $field => $value) {
$query->condition('base.' . $field, $value);
}
}
return $query;
}
这段代码仍然很抽象,从这里面,我们可以看出,Drupal在PHP的基础之上,为我们封装了很多层。
现在,包含节点对象的可呈现数组,已经返回来了。这个页面是怎么构件出来的呢?让我们回到函数menu_execute_active_handler上面来。
$page_callback_result = MENU_ACCESS_DENIED;
$page_callback_result = MENU_NOT_FOUND;
这两种特殊情况我们就不分析了。往下看。
if ($deliver) {
$default_delivery_callback = (isset($router_item) && $router_item) ? $router_item['delivery_callback'] : NULL;
drupal_deliver_page($page_callback_result, $default_delivery_callback);
}
else {
return $page_callback_result;
}
我们没有向menu_execute_active_handler传递参数,所以这里的$deliver为TRUE,将会执行if语句里面的代码。
我们在node_menu里面,并没有定义'delivery callback',所以这里的$default_delivery_callback将会使用Drupal的默认值。$page_callback_result就是刚才返回的包含节点对象的呈现数组。现在,让我们来看一下drupal_deliver_page,是怎么将这个数组转换成整个页面,并返回给浏览器的。
function drupal_deliver_page($page_callback_result, $default_delivery_callback = NULL) {
if (!isset($default_delivery_callback) && ($router_item = menu_get_item())) {
$default_delivery_callback = $router_item['delivery_callback'];
}
$delivery_callback = !empty($default_delivery_callback) ? $default_delivery_callback : 'drupal_deliver_html_page';
// Give modules a chance to alter the delivery callback used, based on
// request-time context (e.g., HTTP request headers).
drupal_alter('page_delivery_callback', $delivery_callback);
if (function_exists($delivery_callback)) {
$delivery_callback($page_callback_result);
}
else {
// If a delivery callback is specified, but doesn't exist as a function,
// something is wrong, but don't print anything, since it's not known
// what format the response needs to be in.
watchdog('delivery callback not found', 'callback %callback not found: %q.', array('%callback' => $delivery_callback, '%q' => $_GET['q']), WATCHDOG_ERROR);
}
}
实际上我们并没有在任何地方设置$delivery_callback,所以将会使用默认的'drupal_deliver_html_page'。这里的
drupal_alter('page_delivery_callback', $delivery_callback);
用来允许第三方模块修改这里的$delivery_callback,但是实际上,我基本上没有见过哪个第三方模块实现了这个钩子函数。Drupal提供了太多的钩子函数,有些可能从来没有被使用过。
function_exists负责检查函数是否存在,drupal_deliver_html_page这个函数是存在的,所以这里实际调用的是:
drupal_deliver_html_page($page_callback_result);
为什么不直接调用drupal_deliver_html_page呢?我们要的就是html页面啊。Drupal核心的开发者是这样考虑的,这个地方留个接口,这样第三方模块就可以实现,比如说:
drupal_deliver_json_page
drupal_deliver_xml_page
也就是说,Drupal不仅仅支持HTML,返回有可能还是json、xml等其它格式。我们这里只用到了html,所以让我们来看一下默认的具体实现。
/**
* Packages and sends the result of a page callback to the browser as HTML.
*
* @param $page_callback_result
* The result of a page callback. Can be one of:
* - NULL: to indicate no content.
* - An integer menu status constant: to indicate an error condition.
* - A string of HTML content.
* - A renderable array of content.
*
* @see drupal_deliver_page()
*/
function drupal_deliver_html_page($page_callback_result) {
// Emit the correct charset HTTP header, but not if the page callback
// result is NULL, since that likely indicates that it printed something
// in which case, no further headers may be sent, and not if code running
// for this page request has already set the content type header.
if (isset($page_callback_result) && is_null(drupal_get_http_header('Content-Type'))) {
drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
}
// Send appropriate HTTP-Header for browsers and search engines.
global $language;
drupal_add_http_header('Content-Language', $language->language);
// Menu status constants are integers; page content is a string or array.
if (is_int($page_callback_result)) {
// @todo: Break these up into separate functions?
switch ($page_callback_result) {
case MENU_NOT_FOUND:
// Print a 404 page.
drupal_add_http_header('Status', '404 Not Found');
watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
// Check for and return a fast 404 page if configured.
drupal_fast_404();
// Keep old path for reference, and to allow forms to redirect to it.
if (!isset($_GET['destination'])) {
$_GET['destination'] = $_GET['q'];
}
$path = drupal_get_normal_path(variable_get('site_404', ''));
if ($path && $path != $_GET['q']) {
// Custom 404 handler. Set the active item in case there are tabs to
// display, or other dependencies on the path.
menu_set_active_item($path);
$return = menu_execute_active_handler($path, FALSE);
}
if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
// Standard 404 handler.
drupal_set_title(t('Page not found'));
$return = t('The requested page "@path" could not be found.', array('@path' => request_uri()));
}
drupal_set_page_content($return);
$page = element_info('page');
print drupal_render_page($page);
break;
case MENU_ACCESS_DENIED:
// Print a 403 page.
drupal_add_http_header('Status', '403 Forbidden');
watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
// Keep old path for reference, and to allow forms to redirect to it.
if (!isset($_GET['destination'])) {
$_GET['destination'] = $_GET['q'];
}
$path = drupal_get_normal_path(variable_get('site_403', ''));
if ($path && $path != $_GET['q']) {
// Custom 403 handler. Set the active item in case there are tabs to
// display or other dependencies on the path.
menu_set_active_item($path);
$return = menu_execute_active_handler($path, FALSE);
}
if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
// Standard 403 handler.
drupal_set_title(t('Access denied'));
$return = t('You are not authorized to access this page.');
}
print drupal_render_page($return);
break;
case MENU_SITE_OFFLINE:
// Print a 503 page.
drupal_maintenance_theme();
drupal_add_http_header('Status', '503 Service unavailable');
drupal_set_title(t('Site under maintenance'));
print theme('maintenance_page', array('content' => filter_xss_admin(variable_get('maintenance_mode_message',
t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))))));
break;
}
}
elseif (isset($page_callback_result)) {
// Print anything besides a menu constant, assuming it's not NULL or
// undefined.
print drupal_render_page($page_callback_result);
}
// Perform end-of-request tasks.
drupal_page_footer();
}
最前面的两端代码,是用来添加http header的(drupal_add_http_header),我们不用去管它。
再往下是对$page_callback_result做了整数判断。
if (is_int($page_callback_result)) {
什么时候会是整数呢,当MENU_NOT_FOUND、MENU_ACCESS_DENIED、MENU_SITE_OFFLINE的时候,会是整数。我们这里面返回的是数组,不是整数,我们这里也就跳过了这三种特殊情况,继续往下看代码。
elseif (isset($page_callback_result)) {
// Print anything besides a menu constant, assuming it's not NULL or
// undefined.
print drupal_render_page($page_callback_result);
}
// Perform end-of-request tasks.
drupal_page_footer();
这是我们这里实际执行的语句。用来合成页面的是drupal_render_page函数。这个时候,传递过来的$page_callback_result,只是一个包含节点对象的呈现数组,节点外面的区域、区块是怎么加进来的呢?这是很多初学者的疑问。让我们来看这个函数的定义:
/**
* Renders the page, including all theming.
*
* @param $page
* A string or array representing the content of a page. The array consists of
* the following keys:
* - #type: Value is always 'page'. This pushes the theming through
* page.tpl.php (required).
* - #show_messages: Suppress drupal_get_message() items. Used by Batch
* API (optional).
*
* @see hook_page_alter()
* @see element_info()
*/
function drupal_render_page($page) {
$main_content_display = &drupal_static('system_main_content_added', FALSE);
// Allow menu callbacks to return strings or arbitrary arrays to render.
// If the array returned is not of #type page directly, we need to fill
// in the page with defaults.
if (is_string($page) || (is_array($page) && (!isset($page['#type']) || ($page['#type'] != 'page')))) {
drupal_set_page_content($page);
$page = element_info('page');
}
// Modules can add elements to $page as needed in hook_page_build().
foreach (module_implements('page_build') as $module) {
$function = $module . '_page_build';
$function($page);
}
// Modules alter the $page as needed. Blocks are populated into regions like
// 'sidebar_first', 'footer', etc.
drupal_alter('page', $page);
// If no module has taken care of the main content, add it to the page now.
// This allows the site to still be usable even if no modules that
// control page regions (for example, the Block module) are enabled.
if (!$main_content_display) {
$page['content']['system_main'] = drupal_set_page_content();
}
return drupal_render($page);
}
这个时候会组装$page这个数组,开始的时候,只包含节点部分,注意往下执行的时候,粗体部分的代码,定义了hook_page_build钩子函数,允许其它模块向$page数组追加内容。难道block模块实现了这个钩子函数?
让我们来试一下,打开block.module文件,搜索block_page_build,还真找到了。真聪明,你猜对了。我们来看一下block模块的具体实现。
/**
* Implements hook_page_build().
*
* Renders blocks into their regions.
*/
function block_page_build(&$page) {
global $theme;
// The theme system might not yet be initialized. We need $theme.
drupal_theme_initialize();
// Fetch a list of regions for the current theme.
$all_regions = system_region_list($theme);
$item = menu_get_item();
if ($item['path'] != 'admin/structure/block/demo/' . $theme) {
// Load all region content assigned via blocks.
foreach (array_keys($all_regions) as $region) {
// Assign blocks to region.
if ($blocks = block_get_blocks_by_region($region)) {
$page[$region] = $blocks;
}
}
// Once we've finished attaching all blocks to the page, clear the static
// cache to allow modules to alter the block list differently in different
// contexts. For example, any code that triggers hook_page_build() more
// than once in the same page request may need to alter the block list
// differently each time, so that only certain parts of the page are
// actually built. We do not clear the cache any earlier than this, though,
// because it is used each time block_get_blocks_by_region() gets called
// above.
drupal_static_reset('block_list');
}
else {
// Append region description if we are rendering the regions demo page.
$item = menu_get_item();
if ($item['path'] == 'admin/structure/block/demo/' . $theme) {
$visible_regions = array_keys(system_region_list($theme, REGIONS_VISIBLE));
foreach ($visible_regions as $region) {
$description = '<div class="block-region">' . $all_regions[$region] . '</div>';
$page[$region]['block_description'] = array(
'#markup' => $description,
'#weight' => 15,
);
}
$page['page_top']['backlink'] = array(
'#type' => 'link',
'#title' => t('Exit block region demonstration'),
'#href' => 'admin/structure/block' . (variable_get('theme_default', 'bartik') == $theme ? '' : '/list/' . $theme),
// Add the "overlay-restore" class to indicate this link should restore
// the context in which the region demonstration page was opened.
'#options' => array('attributes' => array('class' => array('block-demo-backlink', 'overlay-restore'))),
'#weight' => -10,
);
}
}
}
Block模块,在这里会找到当前主题的所有的区域,对于每个区域,会加载这个区域里面的所有的当前可用的区块。以区域的机读名字为键,把区域里面的内容追加到$page数组上来。
代码里面的:
if ($item['path'] == 'admin/structure/block/demo/' . $theme) {
这是一种非常特殊的情况,只有在区块的管理界面,显示一个主题的演示区域的时候,才会用到,所以里面的代码,我们这里不用深究。
现在,整个$page数组已经构建完成了,Drupal是怎么把它转为HTML页面的呢?注意这里的这个数组的类型是page,这个和表单元素数组,是一样的。Drupal6里面的表单数组,在Drupal7下,概念上做了进一步的扩充,除了表单元素以外,区块、节点、页面都是以呈现数组的形式出现。所有的呈现数组,经过drupal_render函数处理,就会转换成对应HTML形式。我们来看一下drupal_render的定义。
function drupal_render(&$elements) {
// Early-return nothing if user does not have access.
if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
return;
}
// Do not print elements twice.
if (!empty($elements['#printed'])) {
return;
}
// Try to fetch the element's markup from cache and return.
if (isset($elements['#cache'])) {
$cached_output = drupal_render_cache_get($elements);
if ($cached_output !== FALSE) {
return $cached_output;
}
}
// If #markup is set, ensure #type is set. This allows to specify just #markup
// on an element without setting #type.
if (isset($elements['#markup']) && !isset($elements['#type'])) {
$elements['#type'] = 'markup';
}
// If the default values for this element have not been loaded yet, populate
// them.
if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
$elements += element_info($elements['#type']);
}
// Make any final changes to the element before it is rendered. This means
// that the $element or the children can be altered or corrected before the
// element is rendered into the final text.
if (isset($elements['#pre_render'])) {
foreach ($elements['#pre_render'] as $function) {
if (function_exists($function)) {
$elements = $function($elements);
}
}
}
// Allow #pre_render to abort rendering.
if (!empty($elements['#printed'])) {
return;
}
// Get the children of the element, sorted by weight.
$children = element_children($elements, TRUE);
// Initialize this element's #children, unless a #pre_render callback already
// preset #children.
if (!isset($elements['#children'])) {
$elements['#children'] = '';
}
// Call the element's #theme function if it is set. Then any children of the
// element have to be rendered there.
if (isset($elements['#theme'])) {
$elements['#children'] = theme($elements['#theme'], $elements);
}
// If #theme was not set and the element has children, render them now.
// This is the same process as drupal_render_children() but is inlined
// for speed.
if ($elements['#children'] == '') {
foreach ($children as $key) {
$elements['#children'] .= drupal_render($elements[$key]);
}
}
// Let the theme functions in #theme_wrappers add markup around the rendered
// children.
if (isset($elements['#theme_wrappers'])) {
foreach ($elements['#theme_wrappers'] as $theme_wrapper) {
$elements['#children'] = theme($theme_wrapper, $elements);
}
}
// Filter the outputted content and make any last changes before the
// content is sent to the browser. The changes are made on $content
// which allows the output'ed text to be filtered.
if (isset($elements['#post_render'])) {
foreach ($elements['#post_render'] as $function) {
if (function_exists($function)) {
$elements['#children'] = $function($elements['#children'], $elements);
}
}
}
// Add any JavaScript state information associated with the element.
if (!empty($elements['#states'])) {
drupal_process_states($elements);
}
// Add additional libraries, CSS, JavaScript an other custom
// attached data associated with this element.
if (!empty($elements['#attached'])) {
drupal_process_attached($elements);
}
$prefix = isset($elements['#prefix']) ? $elements['#prefix'] : '';
$suffix = isset($elements['#suffix']) ? $elements['#suffix'] : '';
$output = $prefix . $elements['#children'] . $suffix;
// Cache the processed element if #cache is set.
if (isset($elements['#cache'])) {
drupal_render_cache_set($output, $elements);
}
$elements['#printed'] = TRUE;
return $output;
}
这里面注意缓存的相关代码,Drupal缓存无处不在。另外需要注意的是,呈现数组,就是一个树状结构,一个枝干下面会有子枝干,子枝干下面又有子枝干,一直递归下去,最后是树叶。将呈现数组转为HTML的时候,也是这样递归调用的,最先转成HTML的,就是最小的单元组成部分,慢慢地合成,最后整个呈现数组彻底转为HTML。
当Drupal执行完HTTP请求后,调用drupal_page_footer。
/**
* Performs end-of-request tasks.
*
* This function sets the page cache if appropriate, and allows modules to
* react to the closing of the page by calling hook_exit().
*/
function drupal_page_footer() {
global $user;
module_invoke_all('exit');
// Commit the user session, if needed.
drupal_session_commit();
if (variable_get('cache', 0) && ($cache = drupal_page_set_cache())) {
drupal_serve_page_from_cache($cache);
}
else {
ob_flush();
}
_registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE);
drupal_cache_system_paths();
module_implements_write_cache();
system_run_automated_cron();
}
这里提供了一个hook_exit钩子函数,允许第三方模块在HTTP请求结束时,与Drupal进行交互。然后就是会话提交,页面缓存设置。这里的ob_flush,用来刷新输出缓冲区,可以看作引导指令页面头部阶段ob_start()的一个回应。
再往下面就是注册表检查、缓存、自动运行定时任务。我们看到,定时任务的运行,是在HTTP请求结束后,运行的,这样就不会影响返回当前页面的性能。
通过阅读Drupal核心的代码,我们弄明白了,Drupal一个普通节点页面的生成过程。如果你还有不明白的地方,可以多看几遍,把每个函数都认真的读一遍,遇到不懂的函数,查一下文档。
你是不是觉得,你已经学会了?还是觉得,我想知道背后发生了什么?四行代码的背后,Drupal都做了什么。这四行代码里面,前面两行代码,都是做的准备工作,里面没有什么弯弯绕绕,很好理解。我们来看第三行,drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL),打开includes目录下面的bootstrap.inc文件,找到drupal_bootstrap函数。
* Ensures Drupal is bootstrapped to the specified phase.
*
* In order to bootstrap Drupal from another PHP script, you can use this code:
* @code
* define('DRUPAL_ROOT', '/path/to/drupal');
* require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
* drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
* @endcode
*
* @param $phase
* A constant telling which phase to bootstrap to. When you bootstrap to a
* particular phase, all earlier phases are run automatically. Possible
* values:
* - DRUPAL_BOOTSTRAP_CONFIGURATION: Initializes configuration.
* - DRUPAL_BOOTSTRAP_PAGE_CACHE: Tries to serve a cached page.
* - DRUPAL_BOOTSTRAP_DATABASE: Initializes the database layer.
* - DRUPAL_BOOTSTRAP_VARIABLES: Initializes the variable system.
* - DRUPAL_BOOTSTRAP_SESSION: Initializes session handling.
* - DRUPAL_BOOTSTRAP_PAGE_HEADER: Sets up the page header.
* - DRUPAL_BOOTSTRAP_LANGUAGE: Finds out the language of the page.
* - DRUPAL_BOOTSTRAP_FULL: Fully loads Drupal. Validates and fixes input
* data.
* @param $new_phase
* A boolean, set to FALSE if calling drupal_bootstrap from inside a
* function called from drupal_bootstrap (recursion).
*
* @return
* The most recently completed phase.
*/
function drupal_bootstrap($phase = NULL, $new_phase = TRUE) {
// Not drupal_static(), because does not depend on any run-time information.
static $phases = array(
DRUPAL_BOOTSTRAP_CONFIGURATION,
DRUPAL_BOOTSTRAP_PAGE_CACHE,
DRUPAL_BOOTSTRAP_DATABASE,
DRUPAL_BOOTSTRAP_VARIABLES,
DRUPAL_BOOTSTRAP_SESSION,
DRUPAL_BOOTSTRAP_PAGE_HEADER,
DRUPAL_BOOTSTRAP_LANGUAGE,
DRUPAL_BOOTSTRAP_FULL,
);
// Not drupal_static(), because the only legitimate API to control this is to
// call drupal_bootstrap() with a new phase parameter.
static $final_phase;
// Not drupal_static(), because it's impossible to roll back to an earlier
// bootstrap state.
static $stored_phase = -1;
// When not recursing, store the phase name so it's not forgotten while
// recursing.
if ($new_phase) {
$final_phase = $phase;
}
if (isset($phase)) {
// Call a phase if it has not been called before and is below the requested
// phase.
while ($phases && $phase > $stored_phase && $final_phase > $stored_phase) {
$current_phase = array_shift($phases);
// This function is re-entrant. Only update the completed phase when the
// current call actually resulted in a progress in the bootstrap process.
if ($current_phase > $stored_phase) {
$stored_phase = $current_phase;
}
switch ($current_phase) {
case DRUPAL_BOOTSTRAP_CONFIGURATION:
_drupal_bootstrap_configuration();
break;
case DRUPAL_BOOTSTRAP_PAGE_CACHE:
_drupal_bootstrap_page_cache();
break;
case DRUPAL_BOOTSTRAP_DATABASE:
_drupal_bootstrap_database();
break;
case DRUPAL_BOOTSTRAP_VARIABLES:
_drupal_bootstrap_variables();
break;
case DRUPAL_BOOTSTRAP_SESSION:
require_once DRUPAL_ROOT . '/' . variable_get('session_inc', 'includes/session.inc');
drupal_session_initialize();
break;
case DRUPAL_BOOTSTRAP_PAGE_HEADER:
_drupal_bootstrap_page_header();
break;
case DRUPAL_BOOTSTRAP_LANGUAGE:
drupal_language_initialize();
break;
case DRUPAL_BOOTSTRAP_FULL:
require_once DRUPAL_ROOT . '/includes/common.inc';
_drupal_bootstrap_full();
break;
}
}
}
return $stored_phase;
}
在函数的注释里面,我们看到,Drupal的引导指令分为多个阶段,DRUPAL_BOOTSTRAP_FULL是最后一个阶段,启动Drupal引导指令的时候,可以指定具体的阶段,而不是仅仅限于最后这个阶段。
如果要在别的PHP脚本中,启动Drupal引导指令的话,可以使用下面的代码:
define('DRUPAL_ROOT', '/path/to/drupal');
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
我们来看函数里面的代码,首先是:
static $phases = array(
DRUPAL_BOOTSTRAP_CONFIGURATION,
DRUPAL_BOOTSTRAP_PAGE_CACHE,
DRUPAL_BOOTSTRAP_DATABASE,
DRUPAL_BOOTSTRAP_VARIABLES,
DRUPAL_BOOTSTRAP_SESSION,
DRUPAL_BOOTSTRAP_PAGE_HEADER,
DRUPAL_BOOTSTRAP_LANGUAGE,
DRUPAL_BOOTSTRAP_FULL,
);
这里面列出来了,引导指令的所有阶段,我们在think in Drupal的第一集里面,介绍过这些阶段。我们看到,这些阶段都是使用的大写字母,表示这是定义好的常量。在bootstrap.inc文件中,有这样的代码。
/**
* First bootstrap phase: initialize configuration.
*/
define('DRUPAL_BOOTSTRAP_CONFIGURATION', 0);
/**
* Second bootstrap phase: try to serve a cached page.
*/
define('DRUPAL_BOOTSTRAP_PAGE_CACHE', 1);
/**
* Third bootstrap phase: initialize database layer.
*/
define('DRUPAL_BOOTSTRAP_DATABASE', 2);
/**
* Fourth bootstrap phase: initialize the variable system.
*/
define('DRUPAL_BOOTSTRAP_VARIABLES', 3);
/**
* Fifth bootstrap phase: initialize session handling.
*/
define('DRUPAL_BOOTSTRAP_SESSION', 4);
/**
* Sixth bootstrap phase: set up the page header.
*/
define('DRUPAL_BOOTSTRAP_PAGE_HEADER', 5);
/**
* Seventh bootstrap phase: find out language of the page.
*/
define('DRUPAL_BOOTSTRAP_LANGUAGE', 6);
/**
* Final bootstrap phase: Drupal is fully loaded; validate and fix input data.
*/
define('DRUPAL_BOOTSTRAP_FULL', 7);
我们看到这些常量,从上到下,分别对应于0、1、2、3、4、5、6、7。引导指令共分为8个阶段。当我们明白了这些常量就是整数的时候,理解下面的这段代码,就很简单了。
while ($phases && $phase > $stored_phase && $final_phase > $stored_phase) {
这句话的意思使用,如果传过来的阶段为7,那么系统将会从0开始,1,2,3,4,5,6这样一直执行下去,直到执行到阶段7为止。如果传过来的阶段为3,那么将会执行0,1,2,3。就是说,从0开始,一直执行到传递过来阶段为止。引导指令是按照先后顺序,依次执行的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们来看阶段0,也就是配置阶段,对应的代码:
case DRUPAL_BOOTSTRAP_CONFIGURATION:
_drupal_bootstrap_configuration();
break;
我们看到,Drupal将这个阶段要做的工作,委托给了_drupal_bootstrap_configuration函数。这个函数就位于bootstrap.inc文件中,我们通过文本查找,很快就找到了这个函数的定义:
/**
* Sets up the script environment and loads settings.php.
*/
function _drupal_bootstrap_configuration() {
// Set the Drupal custom error handler.
set_error_handler('_drupal_error_handler');
set_exception_handler('_drupal_exception_handler');
drupal_environment_initialize();
// Start a page timer:
timer_start('page');
// Initialize the configuration, including variables from settings.php.
drupal_settings_initialize();
}
在这个函数里面,做了这样几件事情,设置Drupal自定义的错误处理器;Drupal环境的初始化;初始化配置,加载settings.php文件中的变量。timer_start这个函数,我也没有弄明白是做什么的,这里的字面意思是说,启动一个页面计时器。对于这个阶段,我们只需要知道,这里面做了环境初始化和加载settings.php文件两样工作就可以了。
我们来看阶段1,也就是页面缓存阶段,对应的代码:
case DRUPAL_BOOTSTRAP_PAGE_CACHE:
_drupal_bootstrap_page_cache();
break;
Drupal将这个阶段要做的工作,委托给了_drupal_bootstrap_page_cache函数。这个函数也位于bootstrap.inc文件中,我们通过文本查找,很快就找到了这个函数的定义:
/**
* Attempts to serve a page from the cache.
*/
function _drupal_bootstrap_page_cache() {
global $user;
// Allow specifying special cache handlers in settings.php, like
// using memcached or files for storing cache information.
require_once DRUPAL_ROOT . '/includes/cache.inc';
foreach (variable_get('cache_backends', array()) as $include) {
require_once DRUPAL_ROOT . '/' . $include;
}
// Check for a cache mode force from settings.php.
if (variable_get('page_cache_without_database')) {
$cache_enabled = TRUE;
}
else {
drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES, FALSE);
$cache_enabled = variable_get('cache');
}
drupal_block_denied(ip_address());
// If there is no session cookie and cache is enabled (or forced), try
// to serve a cached page.
if (!isset($_COOKIE[session_name()]) && $cache_enabled) {
// Make sure there is a user object because its timestamp will be
// checked, hook_boot might check for anonymous user etc.
$user = drupal_anonymous_user();
// Get the page from the cache.
$cache = drupal_page_get_cache();
// If there is a cached page, display it.
if (is_object($cache)) {
header('X-Drupal-Cache: HIT');
// Restore the metadata cached with the page.
$_GET['q'] = $cache->data['path'];
drupal_set_title($cache->data['title'], PASS_THROUGH);
date_default_timezone_set(drupal_get_user_timezone());
// If the skipping of the bootstrap hooks is not enforced, call
// hook_boot.
if (variable_get('page_cache_invoke_hooks', TRUE)) {
bootstrap_invoke_all('boot');
}
drupal_serve_page_from_cache($cache);
// If the skipping of the bootstrap hooks is not enforced, call
// hook_exit.
if (variable_get('page_cache_invoke_hooks', TRUE)) {
bootstrap_invoke_all('exit');
}
// We are done.
exit;
}
else {
header('X-Drupal-Cache: MISS');
}
}
}
很多人经常抱怨Drupal的性能,但是我们看到,在Drupal引导指令的第2个阶段,就支持了页面缓存。这段代码的意思是说,如果存在缓存:
$user = drupal_anonymous_user();
// Get the page from the cache.
$cache = drupal_page_get_cache();
// If there is a cached page, display it.
if (is_object($cache)) {
那么就把缓存页面,返回给客户端:
drupal_serve_page_from_cache($cache);
除此之外,还做了一些其它工作,从缓存中恢复元数据,设置默认时区,当然,(有可能)还触发了两个钩子函数:
bootstrap_invoke_all('boot');
bootstrap_invoke_all('exit');
hook_boot和hook_exit有可能在这里触发。触发钩子函数的途径很多,比如module_invoke_all、module_invoke、drupal_alter、module_implements等等。bootstrap_invoke_all是用来在引导指令阶段就触发钩子函数。
在这个函数里面,我们来看前面几句代码:
require_once DRUPAL_ROOT . '/includes/cache.inc';
foreach (variable_get('cache_backends', array()) as $include) {
require_once DRUPAL_ROOT . '/' . $include;
}
这段代码的含义是,加载includes/cache.inc文件;如果cache_backends不为空的话,那么尝试加载对应include文件。这段代码,我以前很难理解,后来在Drupal实战一书里面,使用memcached模块的时候,才明白这里的用法。我们在使用memcached模块的时候,需要修改settings.php文件,对应的配置如下:
$conf['cache_backends'][] = 'sites/all/modules/memcache/memcache.inc';
$conf['cache_default_class'] = 'MemCacheDrupal';
$conf['cache_class_cache_form'] = 'DrupalDatabaseCache';
这里面有$conf['cache_backends'][],它就对应于页面缓存阶段的variable_get('cache_backends'', array())。
我们看到,Drupal与其它缓存技术的集成,是不需要去修改Drupal核心代码的,Drupal核心本身就支持这些潜在的缓存技术。当我们了解到Drupal提供的各种缓存后,我们对于Drupal的性能问题,就没有必要再去担心什么了。
我们来看阶段2,也就是数据库阶段,对应的代码:
case DRUPAL_BOOTSTRAP_DATABASE:
_drupal_bootstrap_database();
break;
Drupal将这个阶段要做的工作,委托给了__drupal_bootstrap_database函数。这个函数就位于bootstrap.inc文件中,我们通过文本查找,很快就找到了这个函数的定义:
/**
* Initializes the database system and registers autoload functions.
*/
function _drupal_bootstrap_database() {
// Redirect the user to the installation script if Drupal has not been
// installed yet (i.e., if no $databases array has been defined in the
// settings.php file) and we are not already installing.
if (empty($GLOBALS['databases']) && !drupal_installation_attempted()) {
include_once DRUPAL_ROOT . '/includes/install.inc';
install_goto('install.php');
}
// The user agent header is used to pass a database prefix in the request when
// running tests. However, for security reasons, it is imperative that we
// validate we ourselves made the request.
if ($test_prefix = drupal_valid_test_ua()) {
// Set the test run id for use in other parts of Drupal.
$test_info = &$GLOBALS['drupal_test_info'];
$test_info['test_run_id'] = $test_prefix;
$test_info['in_child_site'] = TRUE;
foreach ($GLOBALS['databases']['default'] as &$value) {
// Extract the current default database prefix.
if (!isset($value['prefix'])) {
$current_prefix = '';
}
elseif (is_array($value['prefix'])) {
$current_prefix = $value['prefix']['default'];
}
else {
$current_prefix = $value['prefix'];
}
// Remove the current database prefix and replace it by our own.
$value['prefix'] = array(
'default' => $current_prefix . $test_prefix,
);
}
}
// Initialize the database system. Note that the connection
// won't be initialized until it is actually requested.
require_once DRUPAL_ROOT . '/includes/database/database.inc';
// Register autoload functions so that we can access classes and interfaces.
// The database autoload routine comes first so that we can load the database
// system without hitting the database. That is especially important during
// the install or upgrade process.
spl_autoload_register('drupal_autoload_class');
spl_autoload_register('drupal_autoload_interface');
}
这个函数代码里面的第一部分,当数据库为空,当前尚未安装Drupal的时候,此时做了重定向,重定向到了install.php页面。
if (empty($GLOBALS['databases']) && !drupal_installation_attempted()) {
include_once DRUPAL_ROOT . '/includes/install.inc';
install_goto('install.php');
}
这个函数代码里面的第二部分,表示如果当前为单元测试环境下,要做的工作,我们这里不是单元测试环境,所以不需要考虑这段代码的含义:
if ($test_prefix = drupal_valid_test_ua()) {
…
}
再往下,是加载includes/database/database.inc文件,这样就可以建立数据库连接了。最后是自动加载(缓加载)。
spl_autoload_register('drupal_autoload_class');
spl_autoload_register('drupal_autoload_interface');
我们这里看到,Drupal7只支持类、接口的缓加载,不支持函数的缓加载。我第一集里面讲过这个问题,今天看到的是对应的代码部分。
我们来看阶段3,也就是变量阶段,对应的代码:
case DRUPAL_BOOTSTRAP_VARIABLES:
_drupal_bootstrap_variables();
break;
Drupal将这个阶段要做的工作,委托给了_drupal_bootstrap_variables函数。这个函数就位于bootstrap.inc文件中,我们通过文本查找,很快就找到了这个函数的定义:
/**
* Loads system variables and all enabled bootstrap modules.
*/
function _drupal_bootstrap_variables() {
global $conf;
// Initialize the lock system.
require_once DRUPAL_ROOT . '/' . variable_get('lock_inc', 'includes/lock.inc');
lock_initialize();
// Load variables from the database, but do not overwrite variables set in settings.php.
$conf = variable_initialize(isset($conf) ? $conf : array());
// Load bootstrap modules.
require_once DRUPAL_ROOT . '/includes/module.inc';
module_load_all(TRUE);
}
首先是加载includes/lock.inc文件,并初始化锁系统。接着是变量的初始化,我们知道,Drupal中很多配置变量都是存放在variable表里面,这里的初始化,将会把这个表中的数据全部加载出来的,是的,整张表的数据。我们平时做开发的时候,不要在变量里面存储大数据,这是我们要注意的。
当我看到lock_initialize的时候,我当时想,我只知道Drupal里面有锁机制,但是从来不知道具体的用处。我今天在写文档的时候,顺着查看了一下API函数,也就是查看了一下variable_initialize函数,一下明白了锁机制的用法,同时也明白了为什么这里需要加载includes/lock.inc文件。
function variable_initialize($conf = array()) {
// NOTE: caching the variables improves performance by 20% when serving
// cached pages.
if ($cached = cache_get('variables', 'cache_bootstrap')) {
$variables = $cached->data;
}
else {
// Cache miss. Avoid a stampede.
$name = 'variable_init';
if (!lock_acquire($name, 1)) {
// Another request is building the variable cache.
// Wait, then re-run this function.
lock_wait($name);
return variable_initialize($conf);
}
else {
// Proceed with variable rebuild.
$variables = array_map('unserialize', db_query('SELECT name, value FROM {variable}')->fetchAllKeyed());
cache_set('variables', $variables, 'cache_bootstrap');
lock_release($name);
}
}
foreach ($conf as $name => $value) {
$variables[$name] = $value;
}
return $variables;
}
db_query负责将变量从数据库表中取出,array_map('unserialize',..)负责反序列化。lock_acquire尝试获取锁,获取失败就等待一下lock_wait;获取成功后,就释放锁lock_release。此外,我们看到,所有的变量都存放在一个大的数组里面,名值对。
module_load_all(TRUE),这句代码,负责加载引导指令阶段,需要加载的module文件。我们知道,Drupal启动后,会加载所有的module文件。不过在启动的过程中,也就是在引导指令阶段,就有一部分module文件被优先加载了进来。使用phpmyadmin打开Drupal的数据库,找到system表,打开这个表,我们看到这个表里面包含了bootstrap一列。
凡是这里bootstrap的值为1的模块,都会在module_load_all(TRUE)的时候加载进来。哪些模块的bootstrap的值为1呢?
默认只有这么两个模块。我当时还想着比如system、user模块会被加载进来,没有想到是这两个模块。
我们来看阶段4,也就是会话阶段,对应的代码:
case DRUPAL_BOOTSTRAP_SESSION:
require_once DRUPAL_ROOT . '/' . variable_get('session_inc', 'includes/session.inc');
drupal_session_initialize();
break;
在这里,首先加载includes/session.inc 文件,然后调用drupal_session_initialize函数。drupal_session_initialize函数就位于includes/session.inc文件中,我们通过文本查找,很快就找到了这个函数的定义:
/**
* Initializes the session handler, starting a session if needed.
*/
function drupal_session_initialize() {
global $user, $is_https;
session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection');
// We use !empty() in the following check to ensure that blank session IDs
// are not valid.
if (!empty($_COOKIE[session_name()]) || ($is_https && variable_get('https', FALSE) && !empty($_COOKIE[substr(session_name(), 1)]))) {
// If a session cookie exists, initialize the session. Otherwise the
// session is only started on demand in drupal_session_commit(), making
// anonymous users not use a session cookie unless something is stored in
// $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
drupal_session_start();
if (!empty($user->uid) || !empty($_SESSION)) {
drupal_page_is_cacheable(FALSE);
}
}
else {
// Set a session identifier for this request. This is necessary because
// we lazily start sessions at the end of this request, and some
// processes (like drupal_get_token()) needs to know the future
// session ID in advance.
$GLOBALS['lazy_session'] = TRUE;
$user = drupal_anonymous_user();
// Less random sessions (which are much faster to generate) are used for
// anonymous users than are generated in drupal_session_regenerate() when
// a user becomes authenticated.
session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE)));
if ($is_https && variable_get('https', FALSE)) {
$insecure_session_name = substr(session_name(), 1);
$session_id = drupal_hash_base64(uniqid(mt_rand(), TRUE));
$_COOKIE[$insecure_session_name] = $session_id;
}
}
date_default_timezone_set(drupal_get_user_timezone());
}
session_set_save_handler用来初始化会话处理器,包括会话的打开、关闭、读取、写入、销毁、垃圾收集。接下来检查会话是否存在,如果会话已经存在,那么启动会话;如果会话不存在,那么生成会话。我们这里看到函数drupal_hash_base64,这是Drupal的加密算法,采用base64的哈希算法。uniqid用来生成一个唯一的ID号。session_id用来设置会话的ID号。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
case DRUPAL_BOOTSTRAP_PAGE_HEADER:
_drupal_bootstrap_page_header();
break;
在这里,Drupal将这个阶段的工作委托给了_drupal_bootstrap_page_header函数。我们通过文本查找,很快就找到了这个函数的定义:
/**
* Invokes hook_boot(), initializes locking system, and sends HTTP headers.
*/
function _drupal_bootstrap_page_header() {
bootstrap_invoke_all('boot');
if (!drupal_is_cli()) {
ob_start();
drupal_page_header();
}
}
在这个函数里,首先是触发hook_boot;接着使用drupal_is_cli判断一下,当前脚本是否是运行在命令行环境下。我们这里当然不是。如果不是的话,使用ob_start来打开输出缓冲区,这是服务器端和浏览器端交互时,常见的PHP用法,接着使用drupal_page_header输出页面头部信息。什么是页面头部信息,我们来看一下这个帮助函数:
function drupal_page_header() {
$headers_sent = &drupal_static(__FUNCTION__, FALSE);
if ($headers_sent) {
return TRUE;
}
$headers_sent = TRUE;
$default_headers = array(
'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT',
'Last-Modified' => gmdate(DATE_RFC1123, REQUEST_TIME),
'Cache-Control' => 'no-cache, must-revalidate, post-check=0, pre-check=0',
'ETag' => '"' . REQUEST_TIME . '"',
);
drupal_send_headers($default_headers);
}
这里面的'Expires'、'Last-Modified'、'Cache-Control'、'ETag'就是页面头部信息,对于登陆用户,Drupal总是将缓存设置为no-cache,这样保证登陆用户每次请求总能看到最新的页面。
我们来看阶段6,也就是语言阶段,对应的代码:
case DRUPAL_BOOTSTRAP_LANGUAGE:
drupal_language_initialize();
break;
在这里,Drupal将这个阶段的工作委托给了drupal_language_initialize函数。我们通过文本查找,很快就找到了这个函数的定义:
/**
* Initializes all the defined language types.
*/
function drupal_language_initialize() {
$types = language_types();
// Ensure the language is correctly returned, even without multilanguage
// support. Also make sure we have a $language fallback, in case a language
// negotiation callback needs to do a full bootstrap.
// Useful for eg. XML/HTML 'lang' attributes.
$default = language_default();
foreach ($types as $type) {
$GLOBALS[$type] = $default;
}
if (drupal_multilingual()) {
include_once DRUPAL_ROOT . '/includes/language.inc';
foreach ($types as $type) {
$GLOBALS[$type] = language_initialize($type);
}
// Allow modules to react on language system initialization in multilingual
// environments.
bootstrap_invoke_all('language_init');
}
}
在这里,language_types函数用来获取所有的语言类型;language_default函数用来获取默认语言。这里的语言类型包括三类:
/**
* The type of language used to define the content language.
*/
define('LANGUAGE_TYPE_CONTENT', 'language_content');
/**
* The type of language used to select the user interface.
*/
define('LANGUAGE_TYPE_INTERFACE', 'language');
/**
* The type of language used for URLs.
*/
define('LANGUAGE_TYPE_URL', 'language_url');
我刚开始以为,这里是获取所有的语言呢。后来往下细看了一下,原来的想法不对。注意这里是语言类型,它里面包括三种类型,界面、内容、URL。drupal_multilingual用来判断是否是多语言,判断的标准是启用的语言数量是否大于1,对于中文用户来说,我们通常启用简体中文,以及默认的英文,所以这里的判断应该为真。再往下就是加载includes/language.inc,为每种语言类型初始化。最后触发hook_language_init,给第三方模块一个交互的机会。
当我们访问node/1这个路径的时候,会发生什么呢?首先我们看到的路径,通常是这样的形式:
此时,我们是启用了简洁URL的,如果没有启用的话,可以配置,网上有比较多的教程。不启用简洁URL的路径,则是这样的:
http://www.example.com/?q=node/1
是的,与前者相比,多了一个“?q=”。
这是我们看到的路径,也是Apache看到的路径。可能有人会问,难道Drupal看到的路径不是这个样子的?是的,Drupal看到的路径不是这个样子。那么可能还会有人继续问,Drupal看到的路径是什么?Drupal看到的路径是这个样子的:
http://www.example.com/index.php?q=node/1
别名模式下:
http://www.example.com/index.php?q=aboutus
我们在本地做一个实验,分别访问路径:
http://localhost/snt6/index.php?q=node/1
http://localhost/snt6/?q=node/1
我们看到了什么?我们看到的内容是完全一样的。aboutus和node/1之间是别名关系,可以说是一一对应的关系,Drupal在数据库里面,有张表,用来存储这样的对应关系。当我们访问路径aboutus的时候,它自动的就找到了对应的内部路径node/1。这个我们比较好理解。对于很多人,他刚开始的时候,并不知道Drupal看到的路径是:
http://www.example.com/index.php?q=node/1
很多人可能有个误区,感觉我们看到的路径,就是Drupal看到的路径。这样理解是有问题的。
当我们使用
http://www.example.com/?q=node/1
访问页面时,Apache帮我们做了转换,它将这个路径转为了:
http://www.example.com/index.php?q=node/1
在简洁URL下,apache将路径http://www.example.com/node/1最终也会转为:
http://www.example.com/index.php?q=node/1
有人可能会问,你是怎么知道这个转换的?在Drupal的根目录下,有这样一个文件.htaccess。我们打开这个文件,里面有这行代码:
# Pass all requests not referring directly to files in the filesystem to
# index.php. Clean URLs are handled in drupal_environment_initialize().
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteRule ^ index.php [L]
这段代码就是做这个转换的,这段代码里面的RewriteCond %{REQUEST_FILENAME} !-f是什么意思?这个我也不知道。我们只需要知道,这里做了一个路径转换。仅此而已。
我们看到的,几乎所有的Drupal页面,入口程序都是index.php。当然也有例外,比如安装Drupal时,会使用install.php,更新Drupal时,会使用update.php。例外的情况,我们这里就不分析了,我们来分析一下正常的情况。
我们打开Drupal根目录下的index.php程序,我们看到了非常简洁的代码:
<?php
/**
* @file
* The PHP page that serves all page requests on a Drupal installation.
*
* The routines here dispatch control to the appropriate handler, which then
* prints the appropriate page.
*
* All Drupal code is released under the GNU General Public License.
* See COPYRIGHT.txt and LICENSE.txt.
*/
/**
* Root directory of Drupal installation.
*/
define('DRUPAL_ROOT', getcwd());
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
menu_execute_active_handler();
老外写代码,通常是注释比代码长,这个文件也不例外。上面的注释说,Drupal所有的页面,都走这个入口程序。路由系统,将会把控制权分发给对应的处理器,然后输出对应的页面。我们往下看代码,第一行:
define('DRUPAL_ROOT', getcwd());
这里面定义了一个常量,DRUPAL_ROOT。getcwd,用来获取当前文件所在的目录,比如我们这里就是D:\xampp\htdocs\snt6。
第二行代码:
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
这段代码的作用,是加载bootstrap.inc文件。
第三行代码:
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
这段代码的作用,是启动Drupal,说的更完整一点就是启用Drupal的完整引导指令。drupal_bootstrap这个函数,位于bootstrap.inc文件中,我们前面加载这个文件,就是为了执行这个函数。这里的引导指令,是每个Drupal页面,都要走过的流程。这就有点类似于,我们打开计算机的时候,点了启动按钮,点过之后,计算机需要一个启动的过程,这个电脑的开机启动过程,就类似于Drupal的引导指令。
第四行代码:
menu_execute_active_handler();
这个函数做了什么?连个参数都没有。前面Drupal已经启动了,跑了起来,这个函数的作用是这样的,根据Drupal看到的路径,找到对应的回调函数,调用对应的回调函数,然后将返回的内容,组装成HTML页面,返回给浏览器。也就是英文注释里面所讲的。
这就是Drupal的组装过程,非常简单。与其它系统不一样,Drupal只有一个入口程序,我见过的一些系统都是这样的:
http://www.example.com/foroum.php
http://www.example.com/login.php
http://www.example.com/user/register.php
只有Drupal,是一个入口程序。
index.php文件的逻辑结构