文章 – web百事通 https://www.askme-121.pw web互联网之家 Sat, 20 Apr 2024 05:44:01 +0000 zh-CN hourly 1 https://wordpress.org/?v=6.5.3 https://www.askme-121.pw/wp-content/uploads/2023/12/cropped-05ee702f-4b38-40f3-915f-c8fc68b10a91-32x32.png 文章 – web百事通 https://www.askme-121.pw 32 32 PHP-FPM是如何工作的? https://www.askme-121.pw/php-fpm/ https://www.askme-121.pw/php-fpm/#respond Sat, 20 Apr 2024 05:43:41 +0000 https://www.askme-121.pw/?p=518 首先了解一下几个知识点。
CGI:是 Web Server 与 Web Application 之间数据交换的一种协议。
FastCGI:同 CGI,是一种通信协议,但比 CGI 在效率上做了一些优化。
PHP-CGI:是 PHP (Web Application)对 Web Server 提供的 CGI 协议的接口程序。
PHP-FPM:是 PHP(Web Application)对 Web Server 提供的 FastCGI 协议的接口程序,额外还提供了相对智能一些任务管理。

CGI工作流程

1.如果客户端请求的是 index.html,那么Web Server会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。

2.当Web Server收到 index.php 这个请求后,会启动对应的 CGI 程序,这里就是PHP的解析器。接下来PHP解析器会解析php.ini文件,初始化执行环境,然后处理请求,再以CGI规定的格式返回处理后的结果,退出进程,Web server再把结果返回给浏览器。

FastCGI工作流程

1.如果客户端请求的是 index.html,那么Web Server会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。

2.当Web Server收到 index.php 这个请求后,FastCGI程序(FastCGI在启动时就初始化执行执行环境,每个CGI进程池各个CGI进程共享执行环境)在CGI进程池中选择一个CGI进程处理请求,再以CGI规定的格式返回处理后的结果,继续等待下一个请求。

PHP-FPM基本实现

PHP-FPM的实现就是创建一个master进程,在master进程中创建worker pool并让其监听socket,然后fork出多个子进程(work)。

这些子进程各自accept请求,子进程的处理非常简单,它在启动后阻塞在accept上,有请求到达后开始读取请求数据,读取完成后开始处理然后再返回。

在这期间是不会接收其它请求的,也就是说PHP-FPM的子进程同时只能响应一个请求,只有把这个请求处理完成后才会accept下一个请求

PHP-FPM的master进程与worker进程之间不会直接进行通信,master通过共享内存获取worker进程的信息。

比如worker进程当前状态、已处理请求数等,当master进程要杀掉一个worker进程时则通过发送信号的方式通知worker进程。

PHP-FPM可以同时监听多个端口,每个端口对应一个worker pool,而每个pool下对应多个worker进程

Worker工作流程

1.等待请求:worker进程阻塞在fcgi_accept_request()等待请求;

2.解析请求:fastcgi请求到达后被worker接收,然后开始接收并解析请求数据,直到request数据完全到达;

3.请求初始化:执行php_request_startup(),此阶段会调用每个扩展的:PHP_RINIT_FUNCTION()

4.编译、执行:由php_execute_script()完成PHP脚本的编译、执行;

5.关闭请求:请求完成后执行php_request_shutdown(),此阶段会调用每个扩展的:PHP_RSHUTDOWN_FUNCTION(),然后进入步骤(1)等待下一个请求。

Master进程管理

1.static: 这种方式比较简单,在启动时master按照pm.max_children配置fork出相应数量的worker进程,即worker进程数是固定不变的

2.dynamic: 动态进程管理,首先在fpm启动时按照pm.start_servers初始化一定数量的worker

运行期间如果master发现空闲worker数低于pm.min_spare_servers配置数(表示请求比较多,worker处理不过来了)则会fork worker进程,但总的worker数不能超过pm.max_children

如果master发现空闲worker数超过了pm.max_spare_servers(表示闲着的worker太多了)则会杀掉一些worker,避免占用过多资源,master通过这4个值来控制worker

3.ondemand: 这种方式一般很少用,在启动时不分配worker进程,等到有请求了后再通知master进程fork worker进程。

总的worker数不超过pm.max_children,处理完成后worker进程不会立即退出,当空闲时间超过pm.process_idle_timeout后再退出

PHP-FPM事件管理器

1.sp管道可读事件:这个事件是master用于处理信号的

2.fpm_pctl_perform_idle_server_maintenance_heartbeat():这是进程管理实现的主要事件。

master启动了一个定时器,每隔1s触发一次,主要用于dynamicondemand模式下的worker管理,master会定时检查各worker poolworker进程数,通过此定时器实现worker数量的控制

3.fpm_pctl_heartbeat():这个事件是用于限制worker处理单个请求最大耗时的,php-fpm.conf中有一个request_terminate_timeout的配置项,如果worker处理一个请求的总时长超过了这个值那么master将会向此worker进程发送kill -TERM信号杀掉worker进程,此配置单位为秒,默认值为0表示关闭此机制

4.fpm_pctl_on_socket_accept():ondemand模式下master监听的新请求到达的事件,因为ondemand模式下fpm启动时是不会预创建worker的,有请求时才会生成子进程,所以请求到达时需要通知master进程

]]>
https://www.askme-121.pw/php-fpm/feed/ 0
PHP开发api签名验证 https://www.askme-121.pw/php-api-sign/ https://www.askme-121.pw/php-api-sign/#respond Sat, 20 Apr 2024 04:17:40 +0000 https://www.askme-121.pw/?p=517 开发过程中,我们经常会与接口打交道,有的时候是调取别人网站的接口,有的时候是为他人提供自己网站的接口,但是在这调取的过程中都离不开签名验证。

我们在设计签名验证的时候,请注意要满足以下几点:

  • 可变性:每次的签名必须是不一样的。
  • 时效性:每次请求的时效,过期作废等。
  • 唯一性:每次的签名是唯一的。
  • 完整性:能够对传入数据进行验证,防止篡改。

这里介绍一种方式,是目前国内互联网公司常用的一种方式,其中淘宝的支付宝支付接口、淘宝开放平台接口、腾讯开放平台等应用的一种方式。

一、签名参数sign生成的方法

第1步: 将所有参数(注意是所有参数),除去sign本身,以及值是空的参数,按参数名字母升序排序。

第2步: 然后把排序后的参数按参数1值1参数2值2…参数n值n(这里的参数和值必须是传输参数的原始值,不能是经过处理的,如不能将”转成”后再拼接)的方式拼接成一个字符串。

第3步: 把分配给接入方的验证密钥key拼接在第2步得到的字符串前面。

第2步: 在上一步得到的字符串前面加上验证密钥key(这里的密钥key是接口提供方分配给接口接入方的),然后计算md5值,得到32位字符串,然后转成大写.

第4步: 计算第3步字符串的md5值(32位),然后转成大写,得到的字符串作为sign的值。

举例:

假设传输的数据是/interface.php?sign=sign_value&p2=v2& p1=v1&method=cancel&p3=&pn=vn(实际情况最好是通过post方式发送),其中sign参数对应的sign_value就是签名的值。

第一步,拼接字符串,首先去除sign参数本身,然后去除值是空的参数p3,剩下p2=v2&p1=v1&method=cancel& amp;pn=vn,然后按参数名字符升序排序,method=cancel&p1=v1&p2=v2&pn=vn.

第二步,然后做参数名和值的拼接,最后得到methodcancelp1v1p2v2pnvn
第三步,在上面拼接得到的字符串前加上验证密钥key,我们假设是abc,得到新的字符串abcmethodcancelp1v1p2v2pnvn

第四步,然后将这个字符串进行md5计算,假设得到的是abcdef,然后转为大写,得到ABCDEF这个值即为sign签名值。

注意,计算md5之前请确保接口与接入方的字符串编码一致,如统一使用utf-8编码或者GBK编码,如果编码方式不一致则计算出来的签名会校验失败。

二、签名验证方法

根据前面描述的签名参数sign生成的方法规则,计算得到参数的签名值,和参数中通知过来的sign对应的参数值进行对比,如果是一致的,那么就校验通过,如果不一致,说明参数被修改过。

三、下面直接看代码

<?php

// 设置一个公钥(key)和私钥(secret),公钥用于区分用户,私钥加密数据,不能公开
$key = "c4ca4238a0b923820dcc509a6f75849b";
$secret = "28c8edde3d61a0411511d3b1866f0636";

// 待发送的数据包
$data = array(
    'username' => 'abc@qq.com',
    'sex' => '1',
    'age' => '16',
    'addr' => 'guangzhou',
    'key' => $key,
    'timestamp' => time(),
);

// 获取sign
function getSign($secret, $data) {
    // 对数组的值按key排序
    ksort($data);
    // 生成url的形式
    $params = http_build_query($data);
    // 生成sign
    $sign = md5($params . $secret);
    return $sign;
}

// 发送的数据加上sign
$data['sign'] = getSign($secret, $data);

/**
 * 后台验证sign是否合法
 * @param  [type] $secret [description]
 * @param  [type] $data   [description]
 * @return [type]         [description]
 */
function verifySign($secret, $data) {
    // 验证参数中是否有签名
    if (!isset($data['sign']) || !$data['sign']) {
        echo '发送的数据签名不存在';
        die();
    }
    if (!isset($data['timestamp']) || !$data['timestamp']) {
        echo '发送的数据参数不合法';
        die();
    }
    // 验证请求, 10分钟失效
    if (time() - $data['timestamp'] > 600) {
        echo '验证失效, 请重新发送请求';
        die();
    }
    $sign = $data['sign'];
    unset($data['sign']);
    ksort($data);
    $params = http_build_query($data);
    // $secret是通过key在api的数据库中查询得到
    $sign2 = md5($params . $secret);
    if ($sign == $sign2) {
        die('验证通过');
    } else {
        die('请求不合法');
    }
}
]]>
https://www.askme-121.pw/php-api-sign/feed/ 0
用PHP实现SSO单点登录 https://www.askme-121.pw/php-sso/ https://www.askme-121.pw/php-sso/#respond Sat, 20 Apr 2024 02:33:31 +0000 https://www.askme-121.pw/?p=516 SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一,下面我们来看看吧。

简单讲一下 SSO 单点登录系统的接入的原理,前提是系统本身有完善的用户认证功能,即基本的用户登录功能,那实现起来就很方便了。

SSO 登录请求接口往往是接口加上一个回调地址,访问这个地址会跳转到回调地址并带上一个 ticket 参数,拿着这个 ticket 参数再请求接口可以获取到用户信息,如果存在用户则自动登录,不存在就新增用户并登录。

要使用 PHP 实现单点登录(Single Sign-On,SSO),可以借助一些标准和协议,如OAuth 2.0、OpenID Connect 或 SAML(Security Assertion Markup Language)。

下面是一个使用 OAuth 2.0 实现 SSO 的简单示例:

创建认证服务器(Authorization Server):该服务器负责处理用户认证和发放访问令牌。

// authorization_server.php

// 用户登录验证逻辑,验证用户名和密码是否正确
function validateUser($username, $password) {
    // 进行用户验证逻辑,返回验证结果
    // ...
}

// 发放访问令牌给授权成功的用户
function issueAccessToken($username) {
    // 生成访问令牌并返回
    // ...
}

// 处理用户登录请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = $_POST['username'];
    $password = $_POST['password'];

    // 验证用户名和密码
    if (validateUser($username, $password)) {
        // 发放访问令牌
        $accessToken = issueAccessToken($username);

        // 将访问令牌返回给客户端
        echo json_encode(['access_token' => $accessToken]);
        exit;
    } else {
        // 用户认证失败
        http_response_code(401);
        exit;
    }
}

创建客户端应用(Client Application):该应用将使用认证服务器发放的访问令牌来验证用户身份。

// client_application.php

// 验证访问令牌是否有效
function validateAccessToken($accessToken) {
    // 进行访问令牌验证逻辑,返回验证结果
    // ...
}

// 处理用户登录请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $accessToken = $_POST['access_token'];

    // 验证访问令牌
    if (validateAccessToken($accessToken)) {
        // 用户已验证通过,执行单点登录逻辑
        // ...
        echo 'Login successful';
        exit;
    } else {
        // 访问令牌验证失败
        http_response_code(401);
        exit;
    }
}

用户登录和单点登录流程:

  • 用户访问 Client Application,检查用户是否已登录。
  • 如果用户未登录,则跳转到认证服务器的登录页面。
  • 用户在认证服务器上提供用户名和密码进行登录认证。
  • 认证服务器验证用户名和密码,并发放访问令牌。
  • 用户被重定向回 Client Application,并将访问令牌发送到 Client Application。
  • Client Application 验证访问令牌的有效性,确认用户已成功登录,执行相应的单点登录逻辑。

请注意,上述示例只是一个简单的演示,实际的 SSO 实现可能更复杂,涉及到会话管理、安全性等方面的考虑。您可以根据具体的需求和使用的协议,进一步完善和扩展这个基本示例。

]]>
https://www.askme-121.pw/php-sso/feed/ 0
app开发框架有哪些 https://www.askme-121.pw/app/ https://www.askme-121.pw/app/#respond Mon, 18 Mar 2024 06:59:01 +0000 https://www.askme-121.pw/?p=514 APP开发框架是指在移动应用程序开发中,为了提高开发效率、降低开发成本和提高代码质量而使用的软件架构体系。下面将介绍一些常用的APP开发框架。

1. React Native

React Native是由Facebook开发的一种基于React的开源框架。它允许开发者使用JavaScript和React来构建原生移动应用程序。React Native可以让开发者快速构建高性能的移动应用程序,并且可以在不同平台上使用相同的代码。React Native通过使用原生组件来实现高性能,从而提高了应用程序的性能和响应能力。

2. Flutter

Flutter是Google开发的一种开源框架,它可以用于构建高性能的移动应用程序。Flutter使用Dart语言来编写代码,并且可以在iOS和Android平台上使用相同的代码。Flutter具有快速的开发周期、高质量的用户界面和出色的性能。Flutter使用自己的渲染引擎来实现高性能,从而提高了应用程序的性能和响应能力。

3. Ionic

Ionic是一个基于HTML5和CSS3的开源框架,可以用于构建跨平台的移动应用程序。Ionic使用AngularJS来构建应用程序,并且可以在不同平台上使用相同的代码。Ionic提供了大量的UI组件和工具来帮助开发者更快地构建应用程序。Ionic使用自己的渲染引擎来实现高性能,从而提高了应用程序的性能和响应能力。

4. Xamarin

Xamarin是由Microsoft开发的一种跨平台移动应用程序开发框架。它允许开发者使用C#和.NET框架来构建原生移动应用程序。Xamarin可以在iOS、Android和Windows Phone平台上使用相同的代码。Xamarin使用自己的渲染引擎来实现高性能,从而提高了应用程序的性能和响应能力。

5. PhoneGap

PhoneGap是一个基于HTML、CSS和JavaScript的开源框架,可以用于构建跨平台的移动应用程序。PhoneGap使用Web技术来构建应用程序,并且可以在不同平台上使用相同的代码。PhoneGap使用自己的渲染引擎来实现高性能,从而提高了应用程序的性能和响应能力。

以上是一些常用的APP开发框架,每个框架都有自己的特点和适用范围。选择一个适合自己的框架可以提高开发效率和代码质量,从而更快地构建高性能的移动应用程序。

]]>
https://www.askme-121.pw/app/feed/ 0
uniapp中easycom用法详解 https://www.askme-121.pw/uniapp-easycom/ https://www.askme-121.pw/uniapp-easycom/#respond Sun, 17 Mar 2024 06:45:19 +0000 https://www.askme-121.pw/?p=513 Uniapp中的easycom是一种组件自动注册机制,可以让开发者更加方便地使用和管理组件。下面详细介绍下关于easycom使用方法。

什么是easycom

easycom是Uniapp框架提供的一种组件自动注册机制,它可以自动扫描指定目录下的所有组件,并注册到全局组件中。这意味着我们无需手动在components中引入组件,也无需在每个页面中单独引入组件,只需要在组件的目录下创建一个index.vue文件,就可以自动注册组件并在全局中使用了。

如何使用easycom?

使用easycom非常简单,只需要在项目根目录下的pages.json中配置easycom属性即可。例如:

{
  "easycom": {
    "autoscan": true,
    "custom": {
      "^cu-": "@/components/cu/"
    }
  }
}

其中,autoscan表示是否启用自动扫描功能,如果设置为true,则会自动扫描项目中所有符合规则的组件并注册到全局中。如果设置为false,则需要手动在components中引入组件。

custom是自定义规则,可以根据规则自动注册组件。例如上面的例子中,以cu-开头的组件会被自动注册到@/components/cu/目录下。

除了在pages.json中配置easycom属性外,还可以在单个页面的json文件中配置usingComponents属性来引用组件。例如:

{
  "usingComponents": {
    "cu-btn": "@/components/cu-btn/index"
  }
}

上面的例子中,cu-btn组件会被自动引入到当前页面中,无需手动在components中引入。

easycom的规则

easycom支持多种规则,可以自定义组件的目录和组件名。以下是常见的规则:

  • 目录规则:将组件放在components目录下,文件名为index.vue,则组件会自动注册到全局中。例如:components/my-component/index.vue会被自动注册为my-component组件。
  • 前缀规则:将组件放在任意目录下,文件名为index.vue,文件名以指定前缀开头,例如my-,则组件会自动注册到全局中。例如:components/my-component/index.vue会被自动注册为my-component组件。
  • 全路径规则:将组件放在任意目录下,文件名为index.vue,则可以在页面中使用全路径来引用组件,例如:@/components/my-component/index

easycom的注意事项

虽然easycom提供了方便的组件自动注册机制,但在使用easycom时,也有一些需要注意的事项:

  1. 组件命名必须是小写字母,使用短横线连接单词。例如:my-component
  2. 不同平台的组件可能有不同的实现方式,因此需要在pages.json中配置easycom属性时,需要根据平台分别配置。例如:
    {
    "easycom": {
    “nvue”: {
    “autoscan” true
    } ,
    “h5”: {
    “autoscan”: true
    }
    }
    }
  3.  如果有一些组件不需要自动注册,可以在组件目录下创建一个.easycomignore文件来忽略该组件的自动注册。例如:
    # 忽略my-component组件
    my-component/

    如果需要忽略某个目录下的所有组件,可以在.easycomignore文件中输入目录名即可。
  4. 如果需要自定义规则,可以在pages.json中配置custom属性。例如:
    {
    "easycom": {
    “autoscan”: true,
    “custom”: {
    “^my-“: “@/components/my/”
    }
    }
    }

    上面的例子中,以my-开头的组件会被自动注册到@/components/my/目录下。
  5. 如果需要在某个页面中引用组件,可以在页面的json文件中配置usingComponents属性。例如:
    { "usingComponents": { "my-component": "@/components/my-component/index" } }
    上面的例子中,my-component组件会被自动引入到当前页面中。

总的来说,easycom是Uniapp框架中非常方便的组件自动注册机制,可以大大简化组件的使用和管理。但是在使用时需要注意一些规则和注意事项,以保证组件能够正常注册和使用。

]]>
https://www.askme-121.pw/uniapp-easycom/feed/ 0
vue选项式vs组合式 https://www.askme-121.pw/vue-options-vs-composables/ https://www.askme-121.pw/vue-options-vs-composables/#respond Sun, 17 Mar 2024 05:54:18 +0000 https://www.askme-121.pw/?p=510 Vue中的选项式API和组合式API是两种不同的编写组件逻辑的方式。

选项式API(Options API):

  • 基于对象(data、methods、computed、watch等)的API。
  • 每个组件的选项是集中在一个地方的。
  • 可能会导致组件变得庞大而复杂。
// 选项式API示例
Vue.component('my-component', {
  data() {
    return {
      message: 'Hello, Vue!'
    };
  },
  methods: {
    updateMessage() {
      this.message = 'Updated message';
    }
  },
  template: '<div>{{ message }}</div>'
});

组合式API(Composition API):

  • 基于函数的API,使用setup函数。
  • 使用函数组合而非类继承。
  • 可以更灵活地使用Vue的响应式系统和其他API。
  • 需要Vue 3.x。
// 组合式API示例
Vue.component('my-component', {
  setup() {
    const message = Vue.ref('Hello, Vue!');
 
    function updateMessage() {
      message.value = 'Updated message';
    }
 
    return { message, updateMessage };
  },
  template: '<div>{{ message }} <button @click="updateMessage">Update</button></div>'
});

选项式API更适合于旧版Vue 2.x,而组合式API则是Vue 3.x推出的一个新特性,旨在提供更简洁、更易于理解的方式来组织组件逻辑。

伴随着新到的vue3,我们编写组件的书写方式也发生了变化。
除了底层的更新,编写方式的改变或许才是我们最能直观感受到的。

其实就是vue3多了一种名为组合式api(composables api)的写法,相对应传统的选项式api(options api)
组合式api简单来说就是使用setup方式编写组件。

传统的选项式api

来看看这种传统的写法:65行

<template>
  <div class="home" v-if="userInfo">
    <my-header/>
    用户详情:{{fullUname}},{{userInfo.age}}岁
  </div>
</template>
<script>
import MyHeader from '../components/my-header.vue';

export default {
  // 组件:公共头部
  components: { MyHeader },

  // 属性: 接受属性用户id
  props: {
    userId: {
      type: String,
      default: '2022-01-01'
    }
  },

  // 状态:用户信息
  data() {
    return {
      userInfo: null
    }
  },

  // 计算属性:给用户名加一个牛逼的前缀
  computed: {
    fullUname() {
      if(this.userInfo && this.userInfo.name){
        return '牛逼的' + this.userInfo.name;
      }
      return ''
    }
  },

  // 监听:用户id改变
  watch: {
    userId: {
      handler(newVal, oldVal) {
        console.log('用户id变化啦:'+newVal);
      },
      immediate: true
    }
  },

  // 方法:同步用户信息
  methods: {
    syncUserInfo(userId) {
      this.userInfo = {
        id: userId,
        name: '小明',
        age: 20
      };
    }
  },

  // 钩子:初始化
  mounted() {
    this.syncUserInfo(this.userId)
  }
}
</script>

先进的组合式api

来看看这种先进的写法:48行

<template>
  <div class="home" v-if="userInfo">
    <my-header />
    用户详情:{{ fullUname }},{{ userInfo.age }}岁
  </div>
</template>
<script setup>// <script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。
import { ref, onMounted, watch, computed } from 'vue';
import MyHeader from '../components/my-header.vue';

// 属性: 接受属性用户id
const props = defineProps({
  userId: {
    type: String,
    default: '2022-01-01'
  }
})

// 状态:用户信息
const userInfo = ref(null);

// 计算属性:给用户名加一个牛逼的前缀
const fullUname = computed(() => {
  if (userInfo.value && userInfo.value.name) {
    return '牛逼的' + userInfo.value.name;
  }
  return ''
})

// 监听:用户id改变
watch((newVal, oldVal) => {
  console.log('用户id变化啦:' + newVal);
}, { immediate: true })

// 方法:同步用户信息
const syncUserInfo = (userId) => {
  userInfo.value = {
    id: userId,
    name: '小明',
    age: 20
  };
}

// 钩子:初始化
onMounted(() => {
  syncUserInfo(props.userId)
})
</script>
]]>
https://www.askme-121.pw/vue-options-vs-composables/feed/ 0
Vue中配置使用process.env详解 https://www.askme-121.pw/vue-process-env/ https://www.askme-121.pw/vue-process-env/#respond Thu, 07 Mar 2024 12:13:05 +0000 https://www.askme-121.pw/?p=507 process是 nodejs 下的一个全局变量,它存储着 nodejs 中进程有关的信息。process.env 是 Node.js 中的一个环境对象,其中保存着系统的环境的变量信息,可使用 Node.js 命令行工具直接进行查看。

而 NODE_ENV 就是其中的一个环境变量。这个变量主要用于标识当前的环境(生产环境,开发环境)。默认是没有这个环境变量的,需要自己手动配置。不同系统有不同的环境变量配置方式,在这里就不多加赘述。

NODE_ENV 变量只能在系统中配置吗?其实不然。在 Vue 项目中, Vue 提供了自己的配置方式。这就要涉及到 Vue CLI 中模式的概念了。 Vue CLI 文档说明了这个问题。

也就是说,在 Vue 中, NODE_ENV 可以通过 .env 文件或者 .env.[mode] 文件配置。配置过后,运行 Vue CLI 指令( npm run dev(serve) ,npm run build )时,就会将该模式下的NODE_ENV载入其中了。而这些命令,都有自己的默认模式:

  • npm run dev(serve) ,其实是运行了 vue-cli service serve ,默认模式为 development 。可以在 .env.development 文件下修改该模式的 NODE_ENV 。
  • npm run build ,其实运行了 vue-cli service build ,默认模式为 production 。可以在 .env.production 文件下修改该模式的 NODE_ENV 。

除了以上的修改方式外,也可以在命令后直接使用 –mode 参数手动指定模式。当然,每个模式配置的变量也不只有 NODE_ENV , 也可以通过配置其他的变量简化工作流程。

模式的应用

有了模式的概念,就可以根据不同的环境配置模式,就不用每次打包时都去更改 vue.config.js 文件了。比如在测试环境和生产环境, publicPath参数 (部署应用包时的基本 URL) 可能不同。遇到这种情况就可以在 vue.config.js 文件中,将 publicPath 参数设置为:publicPath: process.env.BASE_URL
设置之后,再在各个 .env.[mode] 文件下对 BASE_URL 进行配置就行了,这样就避免了每次修改配置文件的尴尬。其他的配置也是同理。
Tips: 即使不是生产环境,也可以将模式设置为 production ,这样可以获得 webpack 默认的打包优化。

process.env使用

1、在nodejs中使用

1、安装

npm install dotenv

2、根目录下创建 .env 文件

HOST = localhost
PORT = 8080

3、入口文件中引入 dotenv 并使用

require("dotenv").config({path: '.env'})
console.log(process.env.HOST); // localhost
console.log(process.env.PORT); // 8080

2、在vue中使用

在使用脚手架创建项目的时候,会自动安装dotenv,可以从package-lock.json中找到配置。在main.js入口文件中打印console.log(process.env);可以看出,默认的模式是development即开发模式。

也就是说,在Vue中, NODE_ENV 可以通过 .env 文件或者.env.[mode]文件配置。配置过后,运行 Vue CLI 指令( npm run dev(serve) ,npm run build )时,就会将该模式下的NODE_ENV载入其中了。而这些命令,都有自己的默认模式:

  • npm run dev(serve) ,其实是运行了 vue-cli service serve ,默认模式为 development 。可以在 .env.development 文件下修改该模式的 NODE_ENV 。
  • npm run build ,其实运行了 vue-cli service build ,默认模式为 production 。可以在 .env.production 文件下修改该模式的 NODE_ENV 。

注意:只有 NODE_ENVBASE_URL 和以 VUE_APP_ 开头的变量将通过 webpack.DefinePlugin 静态地嵌入到客户端侧的代码中。这是为了避免意外公开机器上可能具有相同名称的私钥。

NODE_ENV = development
VUE_APP_BASE_API = 'http://localhost:8099/'

注意:.env 环境文件是通过运行 vue-cli-service 命令载入的,因此环境文件发生变化,你需要重启服务。除了以上的修改方式外,也可以在命令后直接使用--mode参数手动指定模式。

当运行 vue-cli-service 命令时,所有的环境变量都从对应的环境文件中载入。如果文件内部不包含 NODE_ENV 变量,它的值将取决于模式,例如,在 production 模式下被设置为 “production”,在 test 模式下被设置为 “test”,默认则是 “development”。

NODE_ENV 将决定您的应用运行的模式,是开发,生产还是测试,因此也决定了创建哪种 webpack 配置。

例如通过将 NODE_ENV 设置为 “test”,Vue CLI 会创建一个优化过后的,并且旨在用于单元测试的 webpack 配置,它并不会处理图片以及一些对单元测试非必需的其他资源。

同理,NODE_ENV=development 创建一个 webpack 配置,该配置启用热更新,不会对资源进行 hash 也不会打出 vendor bundles,目的是为了在开发的时候能够快速重新构建。

当你运行 vue-cli-service build 命令时,无论你要部署到哪个环境,应该始终把 NODE_ENV 设置为 “production” 来获取可用于部署的应用程序。

示例配置

我们现在有三个配置文件,分别如下:

#.env.development
NODE_ENV=development
VUE_APP_AXIOS_BASEURL=http://xxxx
#.env.preview 测试环境的配置
NODE_ENV=production
VUE_APP_AXIOS_BASEURL=http://xxxx
#.env.production
NODE_ENV=production
VUE_APP_AXIOS_BASEURL=http://xxxx

在 axios 中使用

import axios from "axios";
const conf = {
  baseURL: process.env.VUE_APP_AXIOS_BASEURL,
};
return axios.create(conf);

package.json 配置

{
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build --mode preview",
    "build:release": "vue-cli-service build"
  }
}

启动方式

npm run serve # 默认 dev
npm run build # 测试环境
npm run build:release # 正式环境
]]>
https://www.askme-121.pw/vue-process-env/feed/ 0
uniapp-x uts 介绍 https://www.askme-121.pw/uniapp-x-uts/ https://www.askme-121.pw/uniapp-x-uts/#respond Wed, 06 Mar 2024 12:55:38 +0000 https://www.askme-121.pw/?p=506 uts 是什么

uts,全称 uni type script,是一门跨平台的、高性能的、强类型的现代编程语言。

它可以被编译为不同平台的编程语言,如:

  • web平台,编译为JavaScript
  • Android平台,编译为Kotlin
  • iOS平台,编译Swift

uts 采用了与 ts 基本一致的语法规范,支持绝大部分 ES6 API。但为了跨端,uts进行了一些约束和特定平台的增补。

过去在js引擎下运行支持的语法,大部分在uts的处理下也可以平滑的在kotlin和swift中使用。但有一些无法抹平,需要使用条件编译。和uni-app的条件编译类似,uts也支持条件编译。写在条件编译里的,可以调用平台特有的扩展语法。

用途和关系

uts是一门语言。也仅是一门语言,不包含ui框架。uvue是DCloud提供的跨平台的、基于vue语法的ui框架。uts相当于js,uvue相当于html和css。它们类似于v8和webkit的关系、类似于dart和flutter的关系。

uts这门语言,有2个用途:

  • 开发uni-app 和 uni-app x 的原生扩展插件:因为uts可以调用所有原生能力。
  • uts和uvue一起组合,开发原生级的项目,也就是 uni-app x 项目

uni-app x 开发App时,输出的是纯原生的App(Android上就是kotlin的app),里面没有js引擎和webview。也就是说,uts可以在uni-app中使用,也可以在uni-app x中使用。

在uni-app中,主编程语言是js。uts可以开发原生插件,包括API插件和组件插件。
在uni-app x中,主编程语言是uts。不管是应用逻辑还是扩展插件,均使用uts编程,没有js。

]]>
https://www.askme-121.pw/uniapp-x-uts/feed/ 0
Swoole协程与Go协程有哪些区别? https://www.askme-121.pw/swoole-go/ https://www.askme-121.pw/swoole-go/#respond Mon, 29 Jan 2024 11:23:23 +0000 https://www.askme-121.pw/?p=504 一、进程、线程、协程

进程是什么?

进程就是应用程序的启动实例。
例如:打开一个软件,就是开启了一个进程。
进程拥有代码和打开的文件资源,数据资源,独立的内存空间。

线程是什么?

线程属于进程,是程序的执行者。
一个进程至少包含一个主线程,也可以有更多的子线程。
线程有两种调度策略,一是:分时调度,二是:抢占式调度。

协程是什么?

协程是轻量级线程, 协程的创建、切换、挂起、销毁全部为内存操作,消耗是非常低的。
1 协程是属于线程,协程是在线程里执行的。
2 协程的调度是用户手动切换的,所以又叫用户空间线程。
3 协程的调度策略是:协作式调度。

为什么要用协程

目前主流语言基本上都选择了多线程作为并发设施,与线程相关的概念就是抢占式多任务(Preemptive multitasking),而与协程相关的是协作式多任务。其实不管是进程还是线程,每次阻塞、切换都需要陷入系统调用(system call),先让CPU跑操作系统的调度程序,然后再由调度程序决定该跑哪一个进程(线程)。而且由于抢占式调度执行顺序无法确定的特点,使用线程时需要非常小心地处理同步问题,而协程完全不存在这个问题(事件驱动和异步程序也有同样的优点)。因为协程是用户自己来编写调度逻辑的,对于我们的CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。

协程相对于多线程的优点

多线程编程是比较困难的, 因为调度程序任何时候都能中断线程, 必须记住保留锁, 去保护程序中重要部分, 防止多线程在执行的过程中断。而协程默认会做好全方位保护, 以防止中断。我们必须显示产出才能让程序的余下部分运行。对协程来说, 无需保留锁, 而在多个线程之间同步操作, 协程自身就会同步, 因为在任意时刻, 只有一个协程运行。总结下大概下面几点:

  • 无需系统内核的上下文切换,减小开销;
  • 无需原子操作锁定及同步的开销,不用担心资源共享的问题;
  • 单线程即可实现高并发,单核 CPU 即便支持上万的协程都不是问题,

所以很适合用于高并发处理,尤其是在应用在网络爬虫中。

二、Swoole 协程

swoole协程为什么是单线程

在swoole中,因为协程的切换是串行的,在同一个时间点只能运行一个协程,一个协程正在运行时,其他协程会停止工作,所以swoole的协程是基于单线程的。

swoole协程的调度方式是什么

线程的调度方式为系统调度,常用的调度策略有分时调度、抢占调度。说白就是线程的调度完全不受自己控制,协程的调度方式为协作式调度,不受内核控制由自由策略调度切换。上述说了协程是用户态的,所以所谓的协作式调度直接可以理解为是程序员写的调度方式,也就是我想怎么调度就怎么调度,而不用通过系统内核被调度。

Swoole 的协程客户端必须在协程的上下文环境中使用。

// 第一种情况:Request 回调本身是协程环境
$server->on('Request', function($request, $response) {
    // 创建 Mysql 协程客户端
    $mysql = new Swoole\Coroutine\MySQL();
    $mysql->connect([]);
    $mysql->query();
});

// 第二种情况:WorkerStart 回调不是协程环境
$server->on('WorkerStart', function() {
    // 需要先声明一个协程环境,才能使用协程客户端
    go(function(){
        // 创建 Mysql 协程客户端
        $mysql = new Swoole\Coroutine\MySQL();
        $mysql->connect([]);
        $mysql->query();
    });
});

Swoole 的协程是基于单线程的, 无法利用多核CPU,同一时间只有一个在调度。

// 启动 4 个协程
$n = 4;
for ($i = 0; $i < $n; $i++) {
    go(function () use ($i) {
        // 模拟 IO 等待
        Co::sleep(1);
        echo microtime(true) . ": hello $i " . PHP_EOL;
    });
};
echo "hello main \n";

// 每次输出的结果都是一样
$ php test.php 
hello main 
1558749158.0913: hello 0 
1558749158.0915: hello 3 
1558749158.0915: hello 2 
1558749158.0915: hello 1

Swoole 协程使用示例及详解

// 创建一个 Http 服务
$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);

// 调用 onRequest 事件回调函数时,底层会调用 C 函数 coro_create 创建一个协程,
// 同时保存这个时间点的 CPU 寄存器状态和 ZendVM stack 信息。
$server->on('Request', function($request, $response) {
    // 创建一个 Mysql 的协程客户端
    $mysql = new Swoole\Coroutine\MySQL();

    // 调用 mysql->connect 时发生 IO 操作,底层会调用 C 函数 coro_save 保存当前协程的状态,
    // 包括 Zend VM 上下文以及协程描述的信息,并调用 coro_yield 让出程序控制权,当前的请求会挂起。
    // 当协程让出控制权之后,会继续进入 EventLoop 处理其他事件,这时 Swoole 会继续去处理其他客户端发来的 Request。
    $res = $mysql->connect([
        'host'     => '127.0.0.1',
        'user'     => 'root',
        'password' => 'root',
        'database' => 'test'
    ]);

    // IO 事件完成后,MySQL 连接成功或失败,底层调用 C 函数 coro_resume 恢复对应的协程,恢复 ZendVM 上下文,继续向下执行 PHP 代码。
    if ($res == false) {
        $response->end("MySQL connect fail");
        return;
    }

    // mysql->query 的执行过程和 mysql->connect 一致,也会进行一次协程切换调度
    $ret = $mysql->query('show tables', 2);

    // 所有操作完成后,调用 end 方法返回结果,并销毁此协程。
    $response->end('swoole response is ok, result='.var_export($ret, true));
});

// 启动服务
$server->start();

三、Go 的协程 goroutine

1. goroutine 是轻量级的线程,Go 语言从语言层面就支持原生协程。
2. Go 协程与线程相比,开销非常小。
3. Go 协程的堆栈开销只用2KB,它可以根据程序的需要增大和缩小,
而线程必须指定堆栈的大小,并且堆栈的大小都是固定的。
4. goroutine 是通过 GPM 调度模型实现的。
M: 表示内核级线程,一个 M 就是一个线程,goroutine 跑在 M 之上的。
G: 表示一个 goroutine,它有自己的栈。
P: 全称是 Processor,处理器。它主要用来执行 goroutine 的,同时它也维护了一个 goroutine 队列。Go 在 runtime、系统调用等多个方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或进行系统调用时,会主动把当前协程的 CPU 转让出去,让其他协程调度执行。

Go 语言原生层面就支持协层,不需要声明协程环境。

package main

import "fmt"

func main() {
    // 直接通过 Go 关键字,就可以启动一个协程。
    go func() {
        fmt.Println("Hello Go!")
    }()
}

Go 协程是基于多线程的,可以利用多核 CPU,同一时间可能会有多个协程在执行。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 设置这个参数,可以模拟单线程与 Swoole 的协程做比较
    // 如果这个参数设置成 1,则每次输出的结果都一样。
    // runtime.GOMAXPROCS(1)

    // 启动 4 个协程
    var i int64
    for i = 0; i < 4; i++ {
        go func(i int64) {
            // 模拟 IO 等待
            time.Sleep(1 * time.Second)
            fmt.Printf("hello %d \n", i)
        }(i)
    }

    fmt.Println("hello main")

    // 等待其他的协程执行完,如果不等待的话,
    // main 执行完退出后,其他的协程也会相继退出。
    time.Sleep(10 * time.Second)
}

// 第一次输出的结果
$ go run test.go
hello main
hello 2 
hello 1 
hello 0 
hello 3 

// 第二次输出的结果
$ go run test.go
hello main
hello 2 
hello 0 
hello 3 
hello 1 

// 依次类推,每次输出的结果都不一样

Go 协程使用示例及详解

package main

import (
    "fmt"
    "github.com/jinzhu/gorm"
    "net/http"
    "time"
)
import _ "github.com/go-sql-driver/mysql"

func main() {
    dsn := fmt.Sprintf("%v:%v@(%v:%v)/%v?charset=utf8&parseTime=True&loc=Local",
        "root",
        "root",
        "127.0.0.1",
        "3306",
        "fastadmin",
    )
    db, err := gorm.Open("mysql", dsn)
    if err != nil {
        fmt.Printf("mysql connection failure, error: (%v)", err.Error())
        return
    }
    db.DB().SetMaxIdleConns(10)  // 设置连接池
    db.DB().SetMaxOpenConns(100) // 设置与数据库建立连接的最大数目
    db.DB().SetConnMaxLifetime(time.Second * 7)

    http.HandleFunc("/test", func(writer http.ResponseWriter, request *http.Request) {
        // http Request 是在协程中处理的
        // 在 Go 源码 src/net/http/server.go:2851 行处 `go c.serve(ctx)` 给每个请求启动了一个协程
        var name string
        row := db.Table("fa_auth_rule").Where("id = ?", 1).Select("name").Row()
        err = row.Scan(&name)
        if err != nil {
            fmt.Printf("error: %v", err)
            return
        }
        fmt.Printf("name: %v \n", name)
    })
    http.ListenAndServe("0.0.0.0:8001", nil)
}

四、案例分析

背景:在我们的积分策略服务系统中,使用到了 mongodb 存储,但是 swoole 没有提供 mongodb 协程客户端。那么这种场景下,在连接及操作 Mongodb 时会发生同步阻塞,无法发生协程切换,导致整个进程都会阻塞。在这段时间内,进程将无法再处理新的请求,这使得系统的并发性大大降低。

使用同步的 mongodb 客户端

$server->on('Request', function($request, $response) {
    // swoole 没有提供协程客户端,那么只能使用同步客户端
    // 这种情况下,进程阻塞,无法切换协程
    $m = new MongoClient();    // 连接到mongodb
    $db = $m->test;            // 选择一个数据库
    $collection = $db->runoob; // 选择集合
    // 更新文档
    $collection->update(array("title"=>"MongoDB"), array('$set'=>array("title"=>"Swoole")));
    $cursor = $collection->find();
    foreach ($cursor as $document) {
        echo $document["title"] . "\n";
    }
}}

通过使用 Server->taskCo 来异步化对 mongodb 的操作

$server->on('Task', function (swoole_server $serv, $task_id, $worker_id, $data) {
    $m = new MongoClient();    // 连接到mongodb
    $db = $m->test;            // 选择一个数据库
    $collection = $db->runoob; // 选择集合
    // 更新文档
    $collection->update(array("title"=>"MongoDB"), array('$set'=>array("title"=>"Swoole")));
    $cursor = $collection->find();
    foreach ($cursor as $document) {
        $data = $document["title"];
    }
    return $data;
});

$server->on('Request', function ($request, $response) use ($server) {
    // 通过 $server->taskCo() 把对 mongodb 的操作,投递到异步 task 中。
    // 投递到异步 task 后,将发生协程切换,可以继续处理其他的请求,提供并发能力。
    $tasks[] = "hello world";
    $result = $server->taskCo($tasks, 0.5);
    $response->end('Test End, Result: '.var_export($result, true));
});

上面两种使用方式就是 Swoole 中常用的方法了。
那么我们在 Go 中怎么处理这种同步的问题呢 ?

实际上在 Go 语言中就不用担心这个问题了,如我们之前所说到的,
Go 在语言层面就已经支持协程了,只要是发生 IO 操作,网络请求都会发生协程切换。这也就是 Go 语言天生以来就支持高并发的原因了。

package main

import (
    "fmt"
    "gopkg.in/mgo.v2"
    "net/http"
)

func main() {
    http.HandleFunc("/test", func(writer http.ResponseWriter, request *http.Request) {
        session, err := mgo.Dial("127.0.0.1:27017")
        if err != nil {
            fmt.Printf("Error: %v \n", err)
            return
        }
        session.SetMode(mgo.Monotonic, true)
        c := session.DB("test").C("runoob")
        fmt.Printf("Connect %v \n", c)
    })
    http.ListenAndServe("0.0.0.0:8001", nil)
}

并行:同一时刻,同一个 CPU 只能执行同一个任务,要同时执行多个任务,就需要有多个 CPU。

并发:CPU 切换时间任务非常快,就会感觉到有很多任务在同时执行。

五、协程 CPU 密集场景调度

我们上面说到都是基于 IO 密集场景的调度。
那么如果是 CPU 密集型的场景,应该怎么处理呢?在 Swoole v4.3.2 版本中,已经支持了协程 CPU 密集场景的调度。想要支持 CPU 密集调度,需要在编译时增加编译选项 --enable-scheduler-tick 开启 tick 调度器。其次还需要我们手动声明 declare(tick=N) 语法功能来实现协程调度。

<?php
declare(ticks=1000);

$max_msec = 10;
Swoole\Coroutine::set([
    'max_exec_msec' => $max_msec,
]);

$s = microtime(1);
echo "start\n";
$flag = 1;
go(function () use (&$flag, $max_msec){
    echo "coro 1 start to loop for $max_msec msec\n";
    $i = 0;
    while($flag) {
        $i ++;
    }
    echo "coro 1 can exit\n";
});

$t = microtime(1);
$u = $t-$s;
echo "shedule use time ".round($u * 1000, 5)." ms\n";
go(function () use (&$flag){
    echo "coro 2 set flag = false\n";
    $flag = false;
});
echo "end\n";

// 输出结果
start
coro 1 start to loop for 10 msec
shedule use time 10.2849 ms
coro 2 set flag = false
end
coro 1 can exit

Go 在 CPU 密集运算时,有可能导致协程无法抢占 CPU 会一直挂起。这时候就需要显示的调用代码 runtime.Gosched() 挂起当前协程,让出 CPU 给其他的协程。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 如果设置单线程,则第一个协程无法让出时间片
    // 第二个协程一直得不到时间片,阻塞等待。
    // runtime.GOMAXPROCS(1)

    flag := true

    go func() {
        fmt.Printf("coroutine one start \n")
        i := 0
        for flag {
            i++
            // 如果加了这行代码,协程可以让时间片
            // 这个因为 fmt.Printf 是内联函数,这是种特殊情况
            // fmt.Printf("i: %d \n", i)
        }
        fmt.Printf("coroutine one exit \n")
    }()

    go func() {
        fmt.Printf("coroutine two start \n")
        flag = false
        fmt.Printf("coroutine two exit \n")
    }()

    time.Sleep(5 * time.Second)
    fmt.Printf("end \n")
}

// 输出结果
coroutine one start 
coroutine two start 
coroutine two exit 
coroutine one exit 
end

注:time.sleep() 模拟 IO 操作,for i++ 模拟 CPU 密集运算。

总结

  • 协程是轻量级的线程,开销很小。
  • Swoole 的协程客户端需要在协程的上下文环境中使用。
  • 在 Swoole v4.3.2 版本之后,已经支持协程 CPU 密集场景调度。
  • Go 语言层面就已经完全支持协程了。
]]>
https://www.askme-121.pw/swoole-go/feed/ 0
静态化API是什么?用Swoole如何去实现呢? https://www.askme-121.pw/static-api/ https://www.askme-121.pw/static-api/#respond Mon, 29 Jan 2024 06:25:32 +0000 https://www.askme-121.pw/?p=503 什么是静态化API?
静态化API可以理解成把一些接口的数据存储在服务器本地。常用的是存成json文件,也可以是放在swoole的table中,总之是用户不从数据库直接读取数据,而是从本地加载的方式来大幅提高性能,因为很多系统的性能瓶颈是在数据库的位置。

解决方案
方案1 easySwoole + crontab
方案2 easySwoole定时器
方案3 Swoole table
方案4 Redis

实现
这里做的分页的场景,不包含分页的源码,只从拿到了分页的数据看定时生成json和获取json的部分

原始的方法 – 读取mysql

这是原始的方法,每个用户访问都会去数据库里面读取一次,每一次分页也会访问数据库,会造成大量的资源开销。

public function lists0(){
    $condition = [];
    if(!empty($this->params['cat_id'])){
        $condition['cat_id'] = intval($this->params['cat_id']);
    }

    try {
        $videoModel = new VideoModel();
        $data = $videoModel->getVideoData($condition, $this->params['page'], $this->params['size']);
    } catch (\Exception $e) {
        //$e->getMessage();
        return $this->writeJson(Status::CODE_BAD_REQUEST,"服务异常");
    }

    if(!empty($data['lists'])){
        foreach ($data['lists'] as &$list){
            $list['create_time'] = date("Ymd H:i:s",$list['create_time']);
            $list['video_duration'] = gmstrftime("%H:%M:%S",$list["video_duration"]);
        }
    }

    return $this->writeJson(Status::CODE_OK,'OK',$data);
}

知识点:

1 获取以秒为单位的时间长度使用gmstrftime(“%H:%M:%S”,$list[“video_duration”])
2 在做模型的时候写一个基类,把连接数据库的工作放在这个基类的构造方法当中,这样每次实例化的时候就自动连接了,提高代码的复用性

<?php


namespace App\Model;

use EasySwoole\Core\Component\Di;

class Base
{
    public $db = "";

    public function __construct()
    {
        if(empty($this->tableName)){
            throw new \Exception("table error");
        }
        $db = Di::getInstance()->get("MYSQL");
        if($db instanceof \MysqliDb){
            $this->db = $db;
        }else{
            throw new \Exception("db error");
        }
    }

    public function add($data){
        if(empty($data) || !is_array($data)){
            return false;
        }
        return $this->db->insert($this->tableName,$data);
    }

}

方案一、方案二 使用easySwoole定时器以及contab的生成静态化API

这是直接读取静态化API的方法,其实就是读文件然后返回给前端

/**
 * 第二套方法 - 直接读取静态化json数据
 * @return bool
 */
public function lists(){
    $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
    $videoFile = EASYSWOOLE_ROOT."/webroot/video/json/".$catId.".json";
    $videoData = is_file($videoFile) ? file_get_contents($videoFile) : [];
    var_dump($videoData);
    $videoData = !empty($videoData) ? json_decode($videoData,true) : [];

    $count = count($videoData);
    // var_dump($videoData);
    return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
}

这里的getPagingData使用了array_slice来切割数组来做分页。

/**
 * 获取分页信息
 * @param $count
 * @param $data
 * @return array
 */
public function getPagingData($count, $data){
    $totalPage = ceil($count / $this->params['size']);
    $data = $data ?? [];
    $data = array_slice($data, $this->params['from'], $this->params['size']);
    return [
        'total_page' => $totalPage,
        'page_size' => $this->params['page'],
        'count' => intval($count),
        'list' => $data
    ];

}

定时生成Json文件代码 – 核心部分 全局事件easySwooleEvent->mainServiceCreate中执行,这样easySwoole启动了之后就会执行,这里使用的原生的crontab,只能够精确到分

$cacheVideoObj = new VideoCache();
// 使用cronTab处理定时任务
// 这里是闭包 要use $cacheVideoObj之后才能获取,实例化不放在function中是为了防止每次都实例化浪费资源
CronTab::getInstance()
    ->addRule("test_bing_crontab", '*/1 * * * *', function () use($cacheVideoObj) {
        $cacheVideoObj->setIndexVideo();
    });

也可以使用easySwoole的定时器来实现也是放在mainServiceCreate中执行,这里的代码一定要注意要注册一个onWorkerStart。
然后指定一个进程去执行这个定时任务注意一下这里闭包里面又有一个闭包,外面的变量要use两次。
一定要注意easwoole定时器的使用,这里的坑比较多一定要注意,优势是swoole的定时器可以到毫秒级而contab只能到分级

// easySwoole自带定时器
$register->add(EventRegister::onWorkerStart,function (\swoole_server $server,$workerId)use($cacheVideoObj){
    //让第一个进程去执行,否则每个进程都会执行
    if($workerId == 0){
        Timer::loop(1000*2,function ()use ($cacheVideoObj){
            $cacheVideoObj->setIndexVideo();
        });
    }
});

方案三 Swoole table的解决方案

swoole_table一个基于共享内存和锁实现的超高性能,并发数据结构。用于解决多进程/多线程数据共享和同步加锁问题。性能十分强悍。使用起来有一点像是缓存。这里不再是生成一个json文件去读取了,读取table中数据给前端的方法,注意要use cache这个类,这里直接使用get就可以根据key获取到table中的数据,注意这个是一个单例模式,因此需要getInstance

public function lists(){
    $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
    $videoFile = EASYSWOOLE_ROOT."/webroot/video/json/".$catId.".json";

    //  第三套方案 table
    $videoData = Cache::getInstance()->get("index_video_data_cat_id".$catId);
    $videoData = !empty($videoData) ? $videoData : [];

    $count = count($videoData);;
    return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
}

设置直接set(key,data)就可以了,注意这里的代码不是很严谨,比如要判断一下是不是设置成功了,没有设置成功发短信之类的,也要处理一下空值的场景,这里只是做一个演示。

<?php

namespace App\Lib\Cache;

use App\Model\Video as VideoModel;
use EasySwoole\Config;
use EasySwoole\Core\Component\Cache\Cache;

class Video
{
    public function setIndexVideo()
    {
        $catIds = array_keys(Config::getInstance()->getConf("category"));
        array_unshift($catIds, 0);

        $modelObj = new VideoModel();

        foreach ($catIds as $catId) {
            $condition = [];
            if (!empty($catId)) {
                $condition['cat_id'] = $catId;
            }

            try {
                $data = $modelObj->getVideoCacheData($condition);
            } catch (\Exception $e) {
                // 短信报警
                $data = [];
            }
            if (empty($data)) {

            }
            foreach ($data as &$list) {
                $list['create_time'] = date("Ymd H:i:s", $list["create_time"]);
                $list["video_duration"] = gmstrftime("%H:%M:%s", $list["video_duration"]);
            }
            // 由于table存在内存所以重启服务器时数据会丢失,要将配置中的PERSISTENT_TIME设置为1进行落盘操作
            Cache::getInstance()->set("index_video_data_cat_id".$catId, $data);
        }
    }
}

注意,一定要去config里面开启

'PERSISTENT_TIME'=>1//如果需要定时数据落地,请设置对应的时间周期,单位为秒

否则会在重启服务的时候没有数据,因为每次启动的时候swoole table会清空,要等到定时去set table的时候才会有数据,因此要开启数据落盘,这样会生成两个文件:

每次启动的时候由于还没有执行定时任务,就会先读取这两个落盘的文件中的数据,防止服务启动时等待table生成造成业务中断。

方案四 Redis

用redis来做数据缓存,每次从缓存里面度读先重写一下set方法,更加严谨一点

/**
 * 重写set方法 处理一下失效时间以及数组转json
 * @param $key
 * @param $value
 * @param $time
 * @return bool|string
 */
public function set($key, $value, $time){
    if(empty($key)){
        return '';
    }
    if(is_array($value)){
        $value = json_encode($value);
    }
    if(!$time){
        return $this->redis->set($key,$value);
    }
    return $this->redis->setex($key, $time, $value);
}

使用起来很简单啦,在之前的代码中

Di::getInstance()->get("REDIS")->set("index_video_data_cat_id".$catId, $data);

然后取出的数据的部分

public function lists(){
    $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
    //redis
    $videoData = Di::getInstance()->get("REDIS")->get("index_video_data_cat_id".$catId);
    $videoData = !empty($videoData) ? json_decode($videoData,true) : [];
    $count = count($videoData);
    return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
}

高度封装

只需要在配置文件中进行配置即可选择相应方法。设置静态化API和获取静态化API的方法

<?php


namespace App\Lib\Cache;

use App\Model\Video as VideoModel;
use EasySwoole\Config;
use EasySwoole\Core\Component\Cache\Cache;
use EasySwoole\Core\Component\Di;
use EasySwoole\Core\Http\Message\Status;

class Video
{
    /**
     * 设置静态API的方法
     * @throws \Exception
     */
    public function setIndexVideo()
    {
        $catIds = array_keys(Config::getInstance()->getConf("category"));
        array_unshift($catIds, 0);

        // 获取配置
        try {
            $cacheType = Config::getInstance()->getConf("base.indexCacheType");
        } catch (\Exception $e) {
            return $this->writeJson(Status::CODE_BAD_REQUEST,"请求失败");
        }

        $modelObj = new VideoModel();

        foreach ($catIds as $catId) {
            $condition = [];
            if (!empty($catId)) {
                $condition['cat_id'] = $catId;
            }

            try {
                $data = $modelObj->getVideoCacheData($condition);
            } catch (\Exception $e) {
                // 短信报警
                $data = [];
            }
            if (empty($data)) {

            }

            foreach ($data as &$list) {
                $list['create_time'] = date("Ymd H:i:s", $list["create_time"]);
                $list["video_duration"] = gmstrftime("%H:%M:%s", $list["video_duration"]);
            }

            switch ($cacheType) {
                case 'file':
                    $res = file_put_contents($this->getVideoCatIdFile($catId), json_encode($data));
                    break;
                case 'table':
                    $res = Cache::getInstance()->set($this->getCatKey($catId), $data);
                    break;
                case 'redis':
                    $res = Di::getInstance()->get("REDIS")->set($this->getCatKey($catId), $data);
                    break;
                default:
                    throw new \Exception("请求不合法");
                    break;
            }

            if(empty($res)){
            //    记录日志
            //    报警

            }

        }

    }

    /**
     * 获取数据的方法
     * @param $catId
     * @return array|bool|mixed|null|string
     * @throws \Exception
     */
    public function getCache($catId){
        $cacheType = Config::getInstance()->getConf("base.indexCacheType");
        switch ($cacheType){
            case 'file':
                $videoFile = $this->getVideoCatIdFile($catId);
                $videoData = is_file($videoFile) ? file_get_contents($videoFile) : [];
                $videoData = !empty($videoData) ? json_decode($videoData,true) : [];
                break;
            case 'table':
                $videoData = Cache::getInstance()->get($this->getCatKey($catId));
                $videoData = !empty($videoData) ? $videoData : [];
                break;
            case 'redis':
                $videoData = Di::getInstance()->get("REDIS")->get($this->getCatKey($catId));
                $videoData = !empty($videoData) ? json_decode($videoData,true) : [];
                break;
            default:
                throw new \Exception("请求不合法");
                break;
        }
        return $videoData;
    }

    public function getVideoCatIdFile($catId = 0){
        return EASYSWOOLE_ROOT . "/webroot/video/json/" . $catId . ".json";
    }


    public function getCatKey($catId = 0){
        return "index_video_data_cat_id".$catId;
    }
}

只需修改配置文件

<?php

return [
    "indexCacheType" => "redis" // redis file table
];

控制器获取数据给前端

public function lists(){
    $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
    $videoData = (new VideoCache())->getCache($catId);
    $count = count($videoData);
    return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
}
]]>
https://www.askme-121.pw/static-api/feed/ 0