Write the Code. Change the World.

1月 19

从零开始构建 laravel 项目

包裹以下功能:

  1. web 页面(可能是商城,可能是论坛,可能是其他)
  2. 接口功能(一套接口(jwt),提供给 app,小程序 或其他)
  3. 后台功能 (不再使用 layui admin ,使用 ant design pro。权限控制,laravel 自己的权限控制很好,ant design pro 不熟,不知两边的权限能否融洽。就是只使用服务端的权限,不去耦合 antdesignpro 的权限)
  4. 额外。 微信授权、站点地图、elastsearch、定时任务、swoole等。

开始

开发环境依然是 homestead。虽然 docker 很火热, 但总用不好。还有,如果真是线上环境,宁愿一个一个去编译安装所用软件(个数不多)。因为不熟悉,所以不敢放心去使用。

homestead 配置域名:admin.com

laravel 版本为最新版本 6.8.*

# 创建 laravel 6.8 项目
composer create-project laravel/laravel --prefer-dist admin.com "6.8.*"

cd admin.com

# 去掉原本的 readme,添加简单的 readme
rm -rf README.md

echo 'laravel 6.8 initialize' >> Readme.md

# 初始化 git
git init

# 添加,并 commit
git add .

git commit -m 'laravel 6.8 initialize'

为了保证代码风格,安装使用 editorcofig 。配置文件 laravel 项目里边已经有了再稍微改一点点。如下,保存提交版本。

修改根目录下的 .editorconfig

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.yml]
indent_size = 2

[*.{js,html,blade.php,css,scss}]
indent_style = space
indent_size = 2

然后提交版本:

git add .
git commit -m '增加 editorconfig 配置'

修改时区以及语言版本

# vim config/app.php

'timezone' => 'Asia/Shanghai',

'locale' => 'zh-CN',

# git
git add .

git commit -m '设置时区以及默认语言版本'

添加辅助函数

# 在 app 目录下,添加 helpers.php 文件,辅助函数都放在该文件里。
touch app/helpers.php

# 修改 composer.json,让 autoload 的时候载入辅助函数

{
    ...

    "autoload": {
        "psr-4": {
            "App\\": "app/"
        },
        "classmap": [
            "database/seeds",
            "database/factories"
        ],
        "files": [
            "app/helpers.php"
        ]
    }
    ...
}

# 重新加载文件
composer dump-autoload

git提交

git add .

git commit -m '新增辅助函数文件'

现在 helpers.php 文件还是空的,但后续可以添加你需要的。比如 curl 等。

阅读全文 >>

1月 18

站点地图有助于 seo 优化,一般站点都会制作站点地图。 Laravel 项目,有制作站点地图的插件 Laravelium-sitemap。拿来用即可。

laravelium-sitemap wiki

看文档,根据不同的 laravel 版本安装不同的包。

了解点

  1. 可以直接展示站点地图或分包展示地图。也就是直接 xml 或 sitemap index。

做一做

composer require laravelium/sitemap

php artisan vendor:publish --provider="Laravelium\Sitemap\SitemapServiceProvider"
  1. 可以缓存。不是每次请求都要生一个。

  2. 可以添加图片,视频等资源。

实践产品

逻辑里边,两种展示方式都添加了。只是选择了直接使用 xml
https://www.yuepaibao.net/sitemap.xml

阅读全文 >>

1月 11

从零开始构建 node 项目

拥有 node,开始从零构建 node 项目吧

mkdir nodeservice

cd nodeservice

npm init

# 输入项目介绍,关键词,auth这些,一路回车,yes,成功创建 package.json 文件。
# 此时仅有该文件

然后按照需要,安装必须用到的包。少了,后边可以继续安装。

npm install express

npm install lodash

npm install moment

npm install mysql

npm install redis

npm install ioredis

npm install request

npm install socket.io

npm install socket.io-redis

npm install socketio-jwt

npm install async

npm install url

npm install jsonwebtoken

npm install bluebird

npm install china-time

npm install winston

npm install mqtt

然后创建 server.js

touch server.js

参考

https://blog.csdn.net/u013165804/article/details/80568626

https://www.jianshu.com/p/5690ebdd18fa

兜底node.js异常

unhandledRejection

阅读全文 >>

1月 11

为了早点体验到 ant design pro 的页面,注释了部分配置,显示出了 welcome 页面。可是当你刷新页面时候,到你面前的就是 404 。这里,就要先解决 404 问题。

Ant Design Pro 使用的 Umi 支持两种路由方式:browserHistoryhashHistory。默认就是 browserHistory。这样漂亮好看。而默认就是这种方式。具体请看:

https://pro.ant.design/docs/deploy-cn#%E5%89%8D%E7%AB%AF%E8%B7%AF%E7%94%B1%E4%B8%8E%E6%9C%8D%E5%8A%A1%E7%AB%AF%E7%9A%84%E7%BB%93%E5%90%88

虽然这种方式漂亮好看。可对于解析认识是个问题。需要服务端进行处理。因为只使用过 nginx,这里就说下 nginx 下的解决方法。

找到对应的 nginx 的配置文件,并修改文件。

如果 ant design pro 编译后的文件直接在根目录下,可以在配置文件中增加

    location /  {
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

如果 ant design pro 编译后的文件不再根目录下(比如在 dashboard 下),可以在配置文件中增加

    location /dashboard  {
        index index.html index.htm;
        try_files $uri $uri/ /dashboard/index.html;
    }

然后保存重启 nginx ,再刷新页面,发现不再是 404 了。

nginx -s reload

homestead 中,虚拟机里的配置文件在 /etc/nginx/sites-available/ 下,可以单独修改,重启。但这样重启后,会再生。清理的脚步在本地的 Homesteadscripts/clear-nginx.sh 中。如果单独对某个域名处理,最好在本地的 scripts/site-types/ 下新建配置,然后 Homestead.yml 文件中,对具体域名指定配置。

多的话就不说了,具体还是 nginx 的修改配置。 至于 nginx 的 try_files 是什么意思了,可以了解下。

try_files 的核心目的就是替代。比如,

location /dashboard  {
    index index.html index.htm;
    try_files $uri $uri/ /index.php?q=$uri&$args;
}

以 根目录(对应站点的根目录)下的 dashboard 目录为参照,先查找第一个节点,看文件是否存在,如果不存在再看文件夹(有斜杠)是否存在,如果再不存在,就指向到第三个参数。

比如具体的 url 是这样的。

https://www.mlxiu.com/dashboard/love

它会先检查 love 这个文件是否存在,如果不存在 love 这个文件则检查 love 这个文件夹。当然,文件的后缀,上边已先列出。

具体看 nginx 的 try_files

https://blog.csdn.net/a519640026/article/details/9138329

https://www.cnblogs.com/boundless-sky/p/9459775.html

阅读全文 >>

1月 10

准备好基础框架后,就可以开始准备起接口了。准备最最基础的登录注册接口。

需要做以下处理:

  1. 准备 user 表的迁移文件。
  2. 准备 jwt
  3. 准备好路由以及文件结构
  4. 相关注册逻辑。

先准备 user 表的迁移文件

默认就有了 user 表的迁移,我们直接修改它就好。

$table->bigIncrements('id');
$table->string('account', 16)->unique();
$table->string('password');
$table->unsignedInteger('viewid')->unique();
$table->string('phone', 16)->unique();
$table->string('email')->nullable();
$table->string('nickname');
$table->string('avatar')->default('app/public/upload/image/avatar/default.jpg');
$table->string('signture')->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->rememberToken();
$table->nullableTimestamps();

# 执行迁移
php artisan migrate

创建 Models 目录。

# App 目录下创建 Models 文件夹,并将 User 模型移动到该文件夹下
mkdir App/Models
mv App/User.php App/Models/User.php

再修改 User 的命名空间,并将整个框架的 User 的命名空间变更。

安装 jwt

# 安装
composer require tymon/jwt-auth:1.0.0-rc.5

# 生成 jwt 的 secret
php artisan jwt:secret

jwt-auth 有两个重要的参数,可以在 .env 中进行设置
JWT_TTL 生成的 token 在多少分钟后过期,默认 60 分钟
JWT_REFRESH_TTL 生成的 token,在多少分钟内,可以刷新获取一个新 token,默认 20160 分钟,14 天。

这里需要理解一下 JWT 的过期和刷新机制,过期很好理解,超过了这个时间,token 就无效了。刷新时间一般比过期时间长,只要在这个刷新时间内,即使 token 过期了, 依然可以换取一个新的 token,以达到应用长期可用,不需要重新登录的目的。

# 编辑 .env 增加 jwt 的过期配置和刷新配置
vim .env
# 增加下边两个配置
JWT_TTL= 10080
JWT_REFRESH_TTL=20160

修改完善 User 模型,使其继承 Tymon\JWTAuth\Contracts\JWTSubject 接口,并实现接口的两个方法 getJWTIdentifier()getJWTCustomClaims()

于是 User 模型中的类容为:

<?php

namespace App\Models;

use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;


    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }

    protected $fillable = [
        'account', 'password', 'viewid', 'phone', 'email', 'nickname', 'avatar', 'signture'
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

我们启动 tinker,先创建一个用户,再尝试获取 jwt token。

# 启动 tinker
php artisan tinker

use App\Models\User;
use Hash;
use Auth;

$user = [
    'account' => '1367163xxxx',
    'password' => Hash::make('123456'),
    'phone' => '1367163xxxx',
    'nickname' => '七月羽歌',
    'signture' => '美的事物是永恒的喜悦'
];

# 创建用户
User::create($data);

# 获取用户
$user = User::first();

# 获取 token
Auth::guard('api')->login($user);

新建用户登录注册控制器

# 创建控制器
php artisan make:controller Api/AuthorizationsController

# 创建 request
php artisan make:request Api/AuthorizationRequest

然后编辑 request

<?php

namespace App\Http\Requests\Api;

use Illuminate\Foundation\Http\FormRequest;

class AuthorizationRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'account' => 'required|regex:/^1[1-9]\d{9}$/',
            'password' => 'required|alpha_dash|min:6'
        ];
    }
}

控制器代码

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\AuthorizationRequest;
use Illuminate\Auth\AuthenticationException;
use Auth;

class AuthorizationsController extends Controller
{
    public function login(AuthorizationRequest $request)
    {
        $data = $request->only(['account', 'password']);

        if (!$token = Auth::guard('api')->attempt($data)) {
            throw new AuthenticationException('用户名或密码错误');
        }

        return $this->respondWithToken($token)->setStatusCode(200);
    }

    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'Bearer',
            'expires_in' => auth('api')->factory()->getTTL() * 60
        ]);
    }
}

路由

Route::namespace('Api')->group(function () {
    Route::post('authorizations/login', 'AuthorizationsController@login')->name('api.authorizations.login');
});

然后使用 postman 测试下

登录好了,下一步就处理 ant design pro 框架。构建基础的功能以及去掉不需要的功能,美化页面。

有用的参考

如果多端,多表授权可以点这里

https://www.jianshu.com/p/344c5e540eaa

https://www.cnblogs.com/freely/p/10874297.html

https://www.cnblogs.com/lihuijuan/p/11242976.html

阅读全文 >>

1月 09

laravel 好用, ant design pro 不仅好用还好看。那么,laravel + ant design pro 做出来的项目也会很好用,很好看。

创建初始化 laravel 项目以及 ant design pro 项目

先创建 laravel 项目

# 创建 最新版本的 laravel 项目,并且指定项目文件名为 lreact.com
composer create-project laravel/laravel=6.* --prefer-dist lreact.com

# 删除原本的 readme
rm -rf README.md

# 使用新的 readme,简洁
echo '### Laravel 6.* initialize' >> Readme.md

# 初始化 git 仓库
git init
git add .
git commit -m 'Laravel 6.* initialize'

再创建 ant design pro 项目

在 `laravel` 的 `resources` 目录下,新建文件夹 `antdesign`
cd resources

mkdir antdesign

npm create umi
# 依次选择 ant design pro -> javascript

# 当前版本 v4

git add .

git commit -m 'ant design pro v4 initialize'

npm install

安装完成后,需要稍微修改,并打包

部署请参考 https://pro.ant.design/docs/deploy-cn

# 修改输出目录(antdesign 目录下)
vim config/config.js

# 添加指定目录 base 以及 publicPath

export default {
  base: '/dashboard/',
  publicPath: '/dashboard/',
  plugins,
  hash: false,

# 编译
npm run build

# 将打包好的文件复制到 laravel 的 public 目录
mv dist ../../public/dashboard

# 访问站点
https://www.lrect.com/dashboard

能正常访问,但是授权登录错误。先到这来。

如果你不想每次都移来移去,可以在 config/config.js 里追加配置 outputPath
参考

于是,配置为

export default {
  base: '/dashboard/',
  publicPath: '/dashboard/',
  outputPath: '../../public/dashboard',
  plugins,
  hash: false,

再打包。文件就会打包到 laravel 项目下的 public 下的 dashboard 目录下。

修改 request

ant design pro 的请求都放在 services 下。在这里你可以定义和修改请求。但,这里的请求用的是 umi-request, 而这里对 umi-request 的引用放在 utils/request.js 中。这里你可以添加 header,加入拦截器等等。请看 umi-request 文档。

https://github.com/umijs/umi-request/blob/master/README_zh-CN.md

这里添加空的拦截器(request,response),还有添加请求前缀。完整的 request.js 如下。后边,会对拦截器进一步完善。

/**
 * request 网络请求工具
 * 更详细的 api 文档: https://github.com/umijs/umi-request
 */
import { extend } from 'umi-request';
import { notification } from 'antd';
const codeMessage = {
  200: '服务器成功返回请求的数据。',
  201: '新建或修改数据成功。',
  202: '一个请求已经进入后台排队(异步任务)。',
  204: '删除数据成功。',
  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
  401: '用户没有权限(令牌、用户名、密码错误)。',
  403: '用户得到授权,但是访问是被禁止的。',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
  406: '请求的格式不可得。',
  410: '请求的资源被永久删除,且不会再得到的。',
  422: '当创建一个对象时,发生一个验证错误。',
  500: '服务器发生错误,请检查服务器。',
  502: '网关错误。',
  503: '服务不可用,服务器暂时过载或维护。',
  504: '网关超时。',
};
/**
 * 异常处理程序
 */

const errorHandler = error => {
  const { response } = error;

  if (response && response.status) {
    const errorText = codeMessage[response.status] || response.statusText;
    const { status, url } = response;
    notification.error({
      message: `请求错误 ${status}: ${url}`,
      description: errorText,
    });
  } else if (!response) {
    notification.error({
      description: '您的网络发生异常,无法连接服务器',
      message: '网络异常',
    });
  }

  return response;
};

/**
 * 配置request请求时的默认参数
 */
const request = extend({
  prefix: '/dashboard',
  errorHandler,
  // 默认错误处理
  credentials: 'include', // 默认请求是否带上cookie
});

// request拦截器, 改变url 或 options.
request.interceptors.request.use(async (url, options) => {
  const headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  };

  return (
    {
      url: url,
      options: { ...options, headers: headers },
    }
  );
})

// response拦截器, 处理response
request.interceptors.response.use((response, options) => {
  return response;
});

export default request;

到目前为止,输出目录,接口前缀,都是手动写的。分别写在了三个地方。如果想修改,必须得每个地方改一次。如果定义一个常量,直接使用常量不就好了么。下边进行常量定义。不过,定义了常量,那就必须保证你的需求里这三个地方的值是一样的。

定义常量,方便输出以及接口调用

在 config/config.js 中,定义常量 BASE , 设置好值。

const BASE = '/dashboard';

然后在 define 中进行注册该值。只有注册了,其他 js 才能访问的到,并且不需要加 window,请看这里文档。
https://pro.ant.design/docs/environment-variables-cn#%E5%A4%84%E7%90%86%E5%9C%A8-lint-%E4%B8%AD%E7%9A%84%E6%8A%A5%E9%94%99

于是 config/config.js 是这样

const BASE = '/dashboard';
…
export default {
  base: BASE,
  publicPath: `${BASE}/`,
  outputPath: `../../public/${BASE}/`,
  plugins,
  hash: false,
  targets: {
    ie: 11,
  },

…

utils/request.js 是这样

const request = extend({
  prefix: BASE,
  errorHandler,
  // 默认错误处理
  credentials: 'include', // 默认请求是否带上cookie
});

好了,提交 git 暂时先到这一步。

git add .

git commit -m '定义BASE作为项的输出路径和Api前缀'

最后又多一句,还是可以使用 npm start 进行奔跑的。

可参考

https://pro.ant.design/docs/getting-started-cn

https://www.yuque.com/yuxuanbeishui/zog1rm

https://learnku.com/articles/32218

https://umijs.org/zh/config/#uglifyjsoptions

https://www.codercto.com/a/26106.html

阅读全文 >>

1月 03

socket.io 默认支持断开重连。可经常断开长连也很不好。短时间内(比如一分钟)断开重连这种现象,很容易观察到,并且可以观察到时间都很准时,每经过某个时间就会断开然后重连。

遇到这个现象,该怎么处理呢。如果熟悉 socket.io 的机制,可能想到心跳问题。这里先跳过。

遇到这个现象 ,可能你会找服务端的原因。是 nginx 出问题了,还是实现 socket.io 的服务出现问题了。(socket.io 是 websocket 的包装版)

  1. 观察 nginx,因为 nginx 的代码最少。发现 nginx timeout 都注释掉了。
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout: Nginx will close the connection if the proxied server does not send data to Nginx in 60 seconds; At the same time, this close behavior is also affected by heartbeat setting of Swoole.
# proxy_read_timeout 60s;
  1. 分析框架源码,这个是最漫长,和可能性大的。毕竟用到别人的框架。这里使用 swoole + laravels + websocket 的 socket.io 的封装。找了好久也没找到问题。

  2. 找不到具体原因,然后去看 socket.io 的文档。才发现,心跳没发送是一个原因。

https://socket.io/docs/server-api/#new-Server-httpServer-options

重述具体现象

前端页面连接好 socket.io 后,如果前端不发送任何消息,60 秒内,服务端就会断开服务。然后重连。如果在 60 秒之内,你有发送消息,这个断开时间就会延长。直到你不发消息后的 60 秒。那如果这样,利用 socket.io 的心跳不就解决了。可是,从始至终,都没看见过发送过心跳。原来是服务端设置的心跳时间太长了,远远超过了 60 秒。也就是心跳还没来得及发,服务端就已经断开了,前端就已经又重连了。既然这样,那控制心跳时间小于 60 秒不就好了。

既然是一种方法,那么前端 socket.io 怎么发送这个心跳呢。

前端连接好服务端后,服务端会推送一个消息下来,这个消息包裹以下信息:

0{"sid":"NWUwZWY4NzBiYjk1OQ==","upgrades":[],"pingInterval":44000,"pingTimeout":36000}

就是以 pingInterval 这个频率来发送心跳。 pingInterval 要大于 pingTimeout。 这样就可以很直接的看到心跳的间隔。修改服务端代码,onOpen 的时候,推送这个消息过来就好,并 pingInterval 是正确的值。

心跳就是 ping pong。一来一回,建立起来就好。

末了

socket.io 是 websocket 的封装版,就是在 websocket 上加入了约定好的算法以及计算,来实现 socket.io 的这种连接方式。

上边虽然解决了不再一分钟断开又重连。可是这个心跳频率还是有点高。因为上边提到的一分钟就断开,导致没办法把心跳设置大一点。这个问题需要处理。还是得去了解源码,进一步分析吧。

阅读全文 >>

12月 25

了解 model.exports export 等等

https://blog.vini123.com/187

了解各种工具包

lodash 。Lodash 通过降低 array、number、objects、string 等等的使用难度从而让 JavaScript 变得更简单。

https://www.lodashjs.com/

moment 。 时间格式处理工具。

http://momentjs.cn/

jsonwebtoken 。用于生产及交验 token

https://github.com/auth0/node-jsonwebtoken

https://blog.csdn.net/qq_37398213/article/details/81606824

https://cloud.tencent.com/developer/article/1431793

url 。 url 模块提供了一些实用函数,用于URL处理与解析。

https://www.cnblogs.com/fengch/p/8610196.html

http://nodejs.cn/api/url.html

yeast 。根据时间戳生成唯一字符串 (做聊天的时候,生成房间 id 可以使用)

https://github.com/unshiftio/yeast

[https://blog.csdn.net/themagickeyjianan/article/details/78588257](https://blog.csdn.net/themagickeyjianan/article/details/78588257)

https://www.javascriptcn.com/read-53551.html

ioredis 。 ioredis 是一个用于 Node.js/io.js 的 Redis 客户端,强健、功能强大且全面。

https://www.oschina.net/p/ioredis
https://blog.csdn.net/qq_33589252/article/details/85535890

ws 。 ws websocket 库, node.js 的 websocket 的实现。

https://www.npmjs.com/package/ws

https://blog.csdn.net/LiMubai_CN/article/details/81844156

os 。 os 模块提供了与操作系统相关的实用方法和属性。

http://nodejs.cn/api/os.html#os_os

http://nodejs.cn/api/os/os_networkinterfaces.html

mysql 。 mysql 连接池

https://www.jianshu.com/p/2239a4df6ed5

bluebird 。bluebird 实现更强大的 Promise

https://www.ibm.com/developerworks/cn/web/wa-lo-use-bluebird-implements-power-promise/index.html

http://bluebirdjs.com/docs/getting-started.html

https://blog.csdn.net/qq_24884955/article/details/84564778

阅读全文 >>