一个web项目的总结
来公司快一年了,除了日常APP内webview活动页面开发之外,最主要的工作就是重构了公司的web项目,包括PC站、Wap端、微信公众号和小程序。
除了前端开发工作之外,还负责后端路由和视图对接,以及一些业务功能的实现。现在回过头来整理一下整个项目,包括技术选型、开发环境搭建、项目分组等细节。
需求
重构有一个最重要的需求,即SEO优化。
旧版本的PC站采用的是angularJS,Wap站采用的是weex打包构建的web端项目,均是通过JavaScript动态生成页面内容,进行SEO优化工作比较困难。
由于公司的后台采用的是PHP,如果切换到Node然后使用SSR,维护成本比较高,因此决定采用原始的服务端渲染,后台框架使用Laravel。
数据接口
服务端渲染带来的一个问题是:由于需要返回视图,早期的部分纯数据接口不能直接使用了。
此外还需要添加一些来自于APP的新功能,如果重新写数据模型,需要花费大量时间精力,且后期需要维护两个地方(APP数据接口服务器和新的Web服务器)。
为了保持三端数据、逻辑同步,重写模型和业务逻辑的成本实在是太高了(加上我本来是写前端的~)。
这里的初期设计是:将web当做是跟iOS、Android客户端一样的一个新平台,统一请求后台接口服务器。
PHP进行客户端请求
大体流程是通过请求接口服务器返回的数据,渲染对应模板,然后返回给浏览器。采用的网络请求库是Guzzle
。
由于接口很多,因此按照业务模块对接口进行分组,并命名为XXXApiModel
此类形式,用于表示为网络接口模型。
网络请求效率
在开发过程中遇见的一个问题是网络请求效率。由于每个页面需要展示多个数据模块,这意味着可能需要进行多次接口调用。
然而Guzzle的默认网络请求是串行的*,这意味着页面打开时间是大于等于所有接口请求消耗时间总和的,这是一件完全不能容忍的事情。
庆幸的是通过Guzzle
提供了异步请求和Promise来实现并发请求。最后在项目中对Guzzle进行封装,实现了将某个控制器方法中的所有请求都进行并发处理的功能:
- 请求由不同的模型生成,最后统一发送,这里可以使用单例实现。
- 需要保证模型在控制器中的调用习惯(即同步代码),这是因为需要根据返回的数据处理部分业务逻辑,可以使用闭包和数据引用传递实现
最终实现的效果类似于
// 请求api数据接口
TestModel::getInstance()->test_1000_c($data['data1']);
TestModel::getInstance()->test_2000_c($data['data2']);
// 发送并发请求
TestModel::getInstance()->startAsync();
这在之前整理过一篇博文:使用Guzzle并发请求接口。如果在Node中,直接使用Promise.all
就可以了哈哈。
中转请求信息
部分接口,除了依赖于参数传递之外,还需要获取原始的网络请求信息,如Cookie、自定义Header等。
在项目中,通过Laravel的Request
对象获取浏览器的请求信息,然后对Guzzle进行封装,在请求接口时带上对应的数据即可。
项目分组
前面提到的数据接口,肯定是需要多个web平台(PC、Wap、小程序)共用的,在这一个大的web项目下,为了方便开发和后续维护,需要对这些平台进行分组。
路由分组
在项目中,通过Laravel的路由分组,结合PHP的命名空间,对不同端的web项目路进行分组,包括
- PC站
- Wap端
- 小程序
比如下面是PC站的路由
// pc.php
$pcRoute = function () {
// ... 定义路由
}
Route::group([
"namespace" => "Home",
"domain" => "www.xxx.la"
], $pcRoute);
下面是Wap端的路由
// mobile.php
$mobileRoute = function () {
// ... 定义路由
}
Route::group([
"namespace" => "Mobile",
"domain" => "m.xxx.la"
], $mobileRoute);
在RouteServiceProvider
中,引入对应的路由文件接口
protected function mapApiRoutes()
{
Route::middleware(['pc'])
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
protected function mapMobileRoutes()
{
Route::middleware('mobile')
->namespace($this->namespace)
->group(base_path('routes/mobile.php'));
}
另外对应的控制器命名空间,也按照不同的平台进行分组
- Pc
- Mobile
- Applet
- Common
公共代码
在对项目进行分组后发现,多个项目之间有不少公共的代码,
- 在后端第三方oAuth登录、获取验证码、购买接口等业务逻辑
- 前端比如公共的样式、工具库函数等
针对前后端的公共代码,处理方式也不尽相同
- 后台的公共代码,通过PHP的继承,提取父类控制器,然后在不同平台的子类控制器进行重载
- 前端的公共代码
- 样式表通过SCSS进行管理,拆分成多个样式单元,并在不同平台按需引入对应的公共样式模块(比如混合器、颜色变量等)
- 公共脚本遵循JavaScript模块化,最后通过webpack打包。相关流程在下面的开发环境搭建再提
至于接口相关的代码,原本设计就是公共的ApiModel
,因此肯定是最大程度上的共用。
这里遇见的一个问题是:由于项目的渐进式开发的,先开发PC站,然后是Wap站,接着处理微信公众号,最后是微信小程序。因此公共模块的提取是边开发边进行的,这导致前期有一些不太合理的代码设计。
比如Wap端和小程序的部分控制器功能基本相同,只是Wap端返回视图,而小程序返回JSON数据而已。这是接下来需要进行优化的地方,大概思路就是现在博客V0.4.0的样子,通过判断请求来源,在响应中间件中决定返回内容。
不过由于各个平台的业务侧重不同,因此将Wap端和小程序的控制器放在一起并不是一个明智之选,分组还是很有必要的。
中间件
之前写过一段时间的Node,因此对于中间件这个概念并不陌生,Laravel中的中间件,尽管实现略有差异,但基本思想是相同的,即将业务逻辑进行分层。
在项目中有不少使用中间件的地方,比如各个平台都有各自的名称中间件(如pc
、mobile
、applet
),还有一些公共的中间件,比如判断登录状态进入个人中心的personal
中间件。
印象比较深刻的是web数据统计,由于业务需要统计用户的渠道来源,实现的原理是在分享链接后到特定渠道的id值,然后保存到session中,并关联对应的用户。
在后续的迭代中,对这个功能做了很多扩展,之前将该功能封装在中间件中,在迭代的时候不用去修改对应的控制器方法,只需要专注特定的中间件即可,这对于代码维护还是很方便的。
前端开发环境
整个项目,最开始决定使用的打包工具是webpack,但是在开发流程中遇见了下面两个问题
直接开发blade模板
由于我还需要负责后端路由、控制器和视图,因此直接开发Laravel的blade视图模板,跳过了开发静态页面的步骤,
- 单个站点的页面不多,但公共样式较多,因此所有页面共享一个样式表
- 每个页面独立脚本文件
公司的开发环境是windows,由于直接开发模板,因此使用WAMP搭建的PHP本地开发环境,这意味着没必要在因此webpack-dev-server
搭建开发服务器,此外如HtmlWebpackPlugin
、ExtractTextPlugin
等插件也就不太适合了。
事实上我之前也折腾过通过HtmlWebpackPlugin
输出blade
模板(详细记录在这里),然后后来发现这个完全是瞎折腾,还是直接写PHP模板比较合适。
因此最后的选择是:基于gulp
进行环境搭建,使用gulp-webpack
进行脚本打包,使用gulp-sass
进行样式表打包。
由于存在多个平台,因此使用gulp-config.js
配置文件,设置设置env
参数选择对应的开发模块(参数可设为pc
,mobile
),决定最终的输出目录。
然后在对应的gulpfile.js
文件中读取gulp-config
.js配置文件,编写相关的任务即可,代码热更新使用的是gulp-watch
插件。
// gulp-config.js
module.exports = {
env: "mobile", // 用于切换工作环境
pc: {
folder: "assets",
webpack: function (PAGE_SCRIPT_PATH) {
return {
entry: {
// common : PAGE_SCRIPT_PATH + "/common.js",
// ... 其他页面
},
output: {
filename: "page/[name].js"
},
// 其他webpack配置...
}
}
},
mobile: {
folder: "mobile",
webpack: function (PAGE_SCRIPT_PATH) {
// 移动端webpack配置
}
}
};
小程序的前端开发使用了wepy
框架,因此直接使用wepy
集成好的开发环境(只是将默认less修改为了scss),因此位于另外一个独立分支上面。
位于CDN上面的依赖库
在开发中,大部分JavaScript依赖库都保存在CDN上面,如何统一管理这些CDN依赖也是一个比较重要的问题。如果在每个页面上都手动引入库文件,然后配置webpack的externals
,最后在文件中引入依赖,则后期很难进行维护。
关于webpack与CDN的问题,之前也整理过一篇文章。大体思路是通过模块加载器加载cdn依赖库,然后再通过webpack进行打包。
在项目中使用的是SystemJS
加载cdn模块
// base.js
let SystemJS = require("SystemJS");
SystemJS.config({
map: {
jquery: cdn('jquery/jquery-3.2.1.min.js'),
layer: cdn('layer/layer.js'), // layer弹窗
swiper: cdn('swiper/swiper.min.js'), // 轮播图swiper
swiper: cdn('vue/vue.min.js'), // vue
// 其他
},
depCache: {
layer: ['jquery'],
}
});
base.js
是所有页面公共的依赖脚本,每个页面还具有自己的独立页面脚本,负责当前页面的业务逻辑,手动引入也比较麻烦,因此写了一个加载器,根据localtion.href
判断当前页面,然后自动引入对应脚本
// base.js
let pageMap = {
// 这里配置当前页面对应的脚本路径,实际上是gulp-webpack输出的脚本文件
// 这里的实现不太合理,后续可能会重构
}
let page = (function (pageMap) {
let path = location.pathname,
page = pageMap[path];
// ... 路由正则检测
return page;
})(pageMap);
page && Promise.all([
SystemJS.import(page),
]).then(res=>{
// 公共代码
});
然后,只需要在具体的页面文件中,通过SystemJS.import
手动引入依赖模块
// home.js
Promise.all([
SystemJS.import("jquery"),
SystemJS.import("swiper"),
SystemJS.import("layer"),
]).then(([$, Swiper, layer]) => {
// 业务代码
})
实际上SystemJS的使用与RequireJS比较相似,但是模块的定义不用局限于AMD形式,这样子的迁移成本比较低,也方便后续维护。
模板管理
回到页面开发,blade
是一个非常棒的模板引擎(类似于JavaScript中的swig
),它提供了两个非常方便的功能
- 父模板继承,可以规划整站的基础布局,包括header、footer等,此外还可以统一加载样式表、公共脚本等。
- 子模板引入,可以用来提取公共的布局组件
通过这两个功能,开发模板变得十分轻松。
将子模板拆分成组件的另外一个好处是,在后续迁移到小程序的过程中,基本上不需要修改页面结构和样式,只需要替换标签为小程序的view
和text
即可。
由于PC站和Wap站的视图都位于/views
文件夹下,因此我们也需要对视图文件进行分组,这样方便修改和定位问题。
优化
SEO优化
由于早期旧网站对于SEO并不友好,导致百度快照更新周期十分长(大概二十多天一次),SEO的需求十分重要,由于我对于SEO并不是特别了解,因此在开发时也处于边摸索边尝试的阶段,主要从下面几个方面入手
TDK
TDK是SEO的一个缩写术语,即title
、describe
和keywords
。这也是SEO最基本的三个元素。
由于公司的业务主要是小说阅读,整个站点的页面可分为内容聚合页和内容详情页两大部分
- 内容聚合页包括搜索、排行榜等页面,这些页面适合堆砌小说分类关键字,提高整站的排名
- 内容详情页包括小说首页、章节阅读页,这些页面适合填充小说名称等关键字,提高单本小说的搜索几率
而具体的修改操作,则参考了起点、红袖添香等著名的小说阅读网站。
比如下面是小说阅读页的标题形式
小说标题_小说分类_作者名_站点名
针对不同的页面,可以动态生成最适合的描述和关键字,这样有助于整体的优化效果。
URL优化
当时采用Laravel的一个重要原因就是它支持自定义路由,除了前面提到的路由分组之外,自定义路由可以更方便地进行SEO优化。
较短的URL更适合SEO,此外在内容详情页,将路径中的/
转换成-
符号,进一步减少URL层次。
代码优化
在开发模板时注意页面结构层次,配置链接title、图片alt等属性。
历史链接
由于历史问题,旧网站的域名跟现在采用的域名并不太一样,导致百度搜索显示的域名还是旧的。
这里的处理方式是通过服务器设置304重定向,然后去站长平台提交死链,大概半个月后,收录了新的站点域名。
性能优化
性能优化方面,主要包括图片懒加载、精灵图、资源CDN、配置缓存等常规的web优化方案...
小结
虽然在项目中写了很多PHP,但对于数据库接触到的仍旧比较少,主要负责路由和控制器视图这块,因此也算是前端的工作。
就前端而言,主要实现了将前端开发环境嵌入视图开发流程中,以及处理模块加载、性能优化等工作。
就后台而言,完成整个项目,对于传统服务端渲染web项目有了更加丰富的经验,也认识到了MVC框架中各个层的作用和分离。Laravel是比较先进的后台开发框架,其中还有很多需要学习的东西,包括内部的原理和一些设计模式。
现在回过头看,整个项目还有很多不足的地方,早期项目分组和命名空间等也存在不合理的地方,踩过的坑都是经验哈哈哈,继续努力。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。