这一章主要讲解PHP开发PV模块。前面花了6天时间,将PHP的方方面面都学了个遍,那么现在就来验证一下学习成果。
一、创建项目
我们的项目是前后端分离的,前端使用的是react框架,这里默认你会使用react框架。
创建服务端PHP项目
首先按照第四天所学,我们创建一个新laravel项目。
进入vagrant虚拟机的映射目录,创建一个名为blog的laravel项目。
由于网络问题,这个过程需要花费一些时间,我们可以先继续做别的事情。
修改Homestead.yaml配置文件。在sites选项中添加一个映射。
sites
修改hosts文件,添加一个路由映射。
192.168.10.10 blog.test
等待项目创建完成,重启homestead使配置生效,在Homestead根目录下执行vagrant reload --provision
创建前端React项目
为了简化学习成本,我们使用create-react-app来创建react项目。
这里假设你已经安装好了npm、yarn和create-react-app环境。
运行craete-react-app blog-ui
命令,创建一个名为blog-ui的react项目。
推荐使用VSCode来编写前端代码。
二、业务需求分析
PV是什么?
PV(Page View)访问量, 即页面浏览量或点击量,衡量网站用户访问的网页数量;在一定统计周期内用户每打开或刷新一个页面就记录1次,多次打开或刷新同一页面则浏览量累计。
我们我们根据以上内容总结一下:
-
页面打开或刷新,PV+1
-
同一IP用户,在一定周期内不会多次统计PV量,防止刷PV。
那么我们需要做什么呢?
由于我的博客并不打算开放注册功能,所以采用ip来记录用户。
打开文章时,发送一个 http get 请求,这个请求只需要附带文章 id,而用户 ip 则通过后端来获取。后端将这一条数据记录到 redis 中,并从 redis 中取出 PV 数,作为返回值给请求方。前端接收到后展示在页面上。再每隔 30 秒从 redis 读取点赞数据写入数据库中做持久化存储。
你可能会有疑问,为什么不在获取文章数据的接口中写PV逻辑呢?这样做的唯一好处是能减少1个请求。坏处是无论在什么场景下,只要访问文章数据,都会记录PV。比如后台管理系统里面,访问获取文章数据的接口,仍然会记录PV,这样会让接口变得高度耦合,无法达到正常的复用。
针对这种情况,业界的解决方案是微服务,通过组合基础API服务来实现各种高级API服务。我们这里没有必要做得这么复杂。
三、数据格式设计
redis支持5种数据结构,分别为String、List、Set、Hash、Zset。
我们当前的业务需求可以使用最简单的String存储。predis有对应的api让我们像操作number一样来递增和递减这个String值。
四、后端代码编写
创建数据库
CREATE DATABASE IF NOT EXISTS blog DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
创建表
CREATE TABLE `blog`.`Untitled` ( `id` int(11) NOT NULL AUTO_INCREMENT, `count` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
修改.env文件的数据库配置
DB_DATABASE=blog
在PHP项目中安装predis
composer require predis/predis
在RouteServiceProvider.php中添加版本前缀
protected function mapApiRoutes() { Route::prefix('api/v1') ->middleware('api') ->namespace($this->namespace) ->group(base_path('routes/api.php')); }
创建一个controller
php artisan make:controller PVController --invokable
在里面写记录PV的逻辑。
在redis中维护2个值,一个是通过每个ip和文章id组合的用于检测是否在30秒内的值,另一个是每篇博客的PV值。
public function __invoke(Request $request, string $id) { $ip = $request->ip(); $timeout_key = $ip . '@' . $id; $blog_pv_id = 'pv@' . $id; if (Redis::exists($timeout_key)) { if (time() - Redis::get($timeout_key) > 30) { if (Redis::exists($blog_pv_id)) { Redis::incr($blog_pv_id); Redis::set($timeout_key, time()); } else { Redis::set($blog_pv_id, 1); } } } else { Redis::set($timeout_key, time()); } return Redis::get($blog_pv_id); }
添加路由
Route::get('getPV/{id}', 'PVController');
至此,主要的逻辑代码已经完成。可以从浏览器访问,或者通过专业测试工具来测试这个接口。
五、前端代码编写
首先编写前端代码,安装一下路由和axios。
yarn add react-router-dom axios
在package.json文件中添加配置进行代理,解决一下跨域问题。
"proxy": "http://blog.test/api/v1"
在src下创建pages目录,在pages目录中创建Home页面和Blog页面
Home页面内容:
import React from "react"; import { Link } from "react-router-dom"; export default function() { return ( <div> 欢迎来到我的网站 <BlogList></BlogList> </div> ); } // 这里模拟一下数据 function BlogList() { return ( <ul> <li> <Link to="/blog/1">博客1</Link> </li> <li> <Link to="/blog/2">博客2</Link> </li> </ul> ); }
Blog页面内容:
import React from "react"; import PV from "../components/PV"; export default function(props) { const id = props.match.params.id; return ( <div> i'm blog {id} <PV id={id}></PV> </div> ); }
在src下创建components目录,在components目录下创建PV组件
PV组件内容:
import React, { Component } from "react"; import Axios from "axios"; async function getPV(id) { return await Axios.get(`/getPV/${id}`); } export default class PV extends Component { state = { count: 0 }; componentDidMount() { getPV(this.props.id).then(res => { setState({ count: res.data }); }); } render() { return <div>阅读: {this.state.count}</div>; } }
在根目录创建router.js文件
import React from "react"; import { BrowserRouter, Route } from "react-router-dom"; import Home from "./pages/Home"; import Blog from "./pages/Blog"; export default function() { return ( <BrowserRouter> <Route path="/" exact component={Home}></Route> <Route path="/blog/:id" component={Blog}></Route> </BrowserRouter> ); }
修改App.js文件。
import React from "react"; import Router from "./router"; function App() { return <Router></Router>; } export default App;
运行yarn start
启动
访问http://localhost:3000/,点击进入一个博客.就能看到浏览量了。
六、redis定时同步到Mysql
创建定时任务命令。在app/Console下创建Commands文件夹。从中创建PVSynchronization.php文件
内容如下:
<?php namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redis; class PVSynchronization extends Command { // 自定义命令 protected $signature = 'pv-synchronization'; // 命令说明 protected $description = 'Timing from PV volume of redis synchronization blog to MySQL'; // 该命令对应的执行代码 public function handle() { $keys = Redis::keys('pv@*'); foreach ($keys as $key) { // 这里截取掉前17个字符是因为 predis存储的键,自带 laravel_database_ 前缀,需要截掉 $p_redis_key = substr($key, 17, mb_strlen($key)); $count = Redis::get($p_redis_key); // 这里截取前3个字符是 pv@ DB::update('update pv set id = ?, count = ?', [substr($p_redis_key, 3, mb_strlen($p_redis_key)), $count]); } } }
注册定时任务
在app/Console/Commands/Kernel.php文件中,找到schedule方法,把我们定义的任务加入进去
protected function schedule(Schedule $schedule) { // everyMinute 表示每 1 分钟执行一次。 $schedule->command('pv-synchronization')->everyMinute(); }
运行 php artisan list
命令来检测是否注册成功,看到我们自己定义的命令就意味着注册成功了。
最后,运行php artisan schedule:run
,就可以让这个定时任务运行起来。